Class: VivlioStarter::CLI::UnifiedIndexManager

Inherits:
Object
  • Object
show all
Defined in:
lib/vivlio_starter/cli/index/unified_index_manager.rb

Constant Summary collapse

ASCII_SHORT_TERM_PATTERN =

R9: [用語](読みなし)記法で ASCII 可視文字のみ 2 文字以下の語は登録しない。 単位 [eV] [Hz] やフラグ解説 [g] が索引語として誤登録されるのを防ぐ ([g] は pattern /\bg\b/ となり本文中の英字 g 全部にタグが付く)。 意図的に登録したい場合は読み付き [eV|いーぶい] を使う。

/\A[\x21-\x7E]{1,2}\z/

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeUnifiedIndexManager

Returns a new instance of UnifiedIndexManager.



53
54
55
56
57
58
59
# File 'lib/vivlio_starter/cli/index/unified_index_manager.rb', line 53

def initialize
  @terms_manager = UnifiedTermsManager.new
  @queue_manager = ReviewQueueManager.new
  @markdown_generator = ReviewMarkdownGenerator.new
  @config = load_index_config
  @glossary_config = load_glossary_config
end

Instance Attribute Details

#markdown_generatorObject (readonly)

Returns the value of attribute markdown_generator.



51
52
53
# File 'lib/vivlio_starter/cli/index/unified_index_manager.rb', line 51

def markdown_generator
  @markdown_generator
end

#queue_managerObject (readonly)

Returns the value of attribute queue_manager.



51
52
53
# File 'lib/vivlio_starter/cli/index/unified_index_manager.rb', line 51

def queue_manager
  @queue_manager
end

#terms_managerObject (readonly)

Returns the value of attribute terms_manager.



51
52
53
# File 'lib/vivlio_starter/cli/index/unified_index_manager.rb', line 51

def terms_manager
  @terms_manager
end

Instance Method Details

#apply_markdown_review!Object

Markdownから承認・リジェクトを適用 仕様: vs index:apply は辞書を更新するだけで、索引ページの生成は行わない (生成はビルドパイプラインの責務)



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/vivlio_starter/cli/index/unified_index_manager.rb', line 162

def apply_markdown_review!
  unless @markdown_generator.exists?
    Common.log_warn('_index_glossary_review.md が見つかりません')
    Common.log_info('先に vs index:auto を実行してください')
    return
  end

  # --- Phase: 索引処理 ---
  index_approved = @markdown_generator.parse_index_approved
  index_rejected = @markdown_generator.parse_index_rejected

  # --- Phase: 用語集処理 ---
  glossary_approved = @markdown_generator.parse_glossary_approved
  glossary_rejected = @markdown_generator.parse_glossary_rejected

  # --- Phase: 共通処理 ---
  both_rejected = @markdown_generator.parse_rejected
  unreject = @markdown_generator.parse_unreject
  yomi_changes = @markdown_generator.parse_yomi_changes
  main_references = @markdown_generator.parse_main_references

  changes_made = false
  index_count = 0
  glossary_count = 0

  # --- Phase: 索引承認 ---
  if index_approved.any?
    @terms_manager.merge_terms!(index_approved, flags: 'i', source: 'auto_extracted')
    index_count = index_approved.size
    changes_made = true
  end

  # --- Phase: 用語集承認 ---
  if glossary_approved.any?
    validate_glossary_definitions!(glossary_approved)
    @terms_manager.merge_terms!(glossary_approved, flags: 'g', source: 'review')
    glossary_count = glossary_approved.size
    changes_made = true
  end

  # [ig] → [i] に変更された場合: g フラグを除去
  glossary_approved_names = glossary_approved.map { it['term'] }
  index_only = index_approved.reject { glossary_approved_names.include?(it['term']) }
  index_only.each do |term|
    next unless @terms_manager.glossary_term_names.include?(term['term'])

    @terms_manager.remove_flag!(term['term'], 'g')
    Common.log_info("用語集フラグを除去しました(索引のみ): #{term['term']}")
    changes_made = true
  end

  # --- Phase: 索引のみリジェクト([-i]) ---
  if index_rejected.any?
    index_rejected.each { @terms_manager.remove_flag!(it['term'], 'i') }
    @queue_manager.save_rejected_terms(index_rejected)
    changes_made = true
  end

  # --- Phase: 用語集のみリジェクト([-g]) ---
  if glossary_rejected.any?
    glossary_rejected.each { @terms_manager.remove_flag!(it['term'], 'g') }
    @queue_manager.save_rejected_terms(glossary_rejected)
    changes_made = true
  end

  # --- Phase: 両方リジェクト([r]) ---
  if both_rejected.any?
    both_rejected.each { @terms_manager.remove_term!(it['term']) }
    @queue_manager.save_rejected_terms(both_rejected)
    changes_made = true
  end

  # --- Phase: リジェクト解除 + 直接登録 ---
  if unreject.any?
    unreject.each do |entry|
      @queue_manager.unreject_term_by_name!(entry['term'])
      flag = entry['flag'] || 'i'
      term_data = { 'term' => entry['term'], 'yomi' => entry['yomi'] }
      flags = case flag
              when 'i', 'x' then 'i'
              when 'g' then 'g'
              when 'ig', 'gi' then 'ig'
              else 'i'
              end
      @terms_manager.merge_terms!([term_data], flags:, source: 'unreject')
      index_count += 1 if flags.include?('i')
      glossary_count += 1 if flags.include?('g')
      Common.log_info("リジェクト解除 → [#{flag}] 登録: #{entry['term']}")
    end
    changes_made = true
  end

  # --- Phase: 読み変更 ---
  if yomi_changes.any?
    @terms_manager.update_yomi!(yomi_changes)
    changes_made = true
  end

  # --- Phase: 主要参照 ---
  changes_made = true if apply_main_references!(main_references)

  # --- Phase: 孤立データ除去 ---
  index_approved_names = index_approved.map { it['term'] }
  glossary_approved_names_all = glossary_approved.map { it['term'] }
  unreject_index_names = unreject.select { %w[i x ig gi].include?(it['flag']) }.map { it['term'] }
  unreject_glossary_names = unreject.select { %w[g ig gi].include?(it['flag']) }.map { it['term'] }

  # 明示的にリジェクトされた用語は孤立除去の対象外
  # ([-i] で i を除去した後に残る g を誤って除去しないため)
  explicitly_rejected = (index_rejected + glossary_rejected + both_rejected).map { it['term'] }.uniq

  # 索引フラグの孤立除去
  stale_index = @terms_manager.index_term_names - index_approved_names - unreject_index_names - explicitly_rejected
  stale_index.each do |term_name|
    @terms_manager.remove_flag!(term_name, 'i')
    Common.log_info("索引フラグを除去: #{term_name}")
    changes_made = true
  end

  # 用語集フラグの孤立除去
  stale_glossary = @terms_manager.glossary_term_names - glossary_approved_names_all - unreject_glossary_names - explicitly_rejected
  stale_glossary.each do |term_name|
    @terms_manager.remove_flag!(term_name, 'g')
    Common.log_info("用語集フラグを除去: #{term_name}")
    changes_made = true
  end

  # --- Phase: Section 4 同期処理 ---
  rejected_section_all = @markdown_generator.parse_rejected_section_all
  unreject_names = unreject.map { it['term'] }

  confirmed_rejected = rejected_section_all.select { ['', ' '].include?(it['flag']) }
                                           .reject { unreject_names.include?(it['term']) }

  if confirmed_rejected.any?
    rejected_count = 0
    confirmed_rejected.each do |entry|
      term_name = entry['term']
      next unless @terms_manager.term_names.include?(term_name)

      @terms_manager.remove_term!(term_name)
      Common.log_info("除外済みリストに基づき登録を解除: #{term_name}")
      rejected_count += 1
    end

    @queue_manager.save_rejected_terms(confirmed_rejected)
    changes_made = true if rejected_count.positive? || confirmed_rejected.any?
  end

  if changes_made
    rejected_total = both_rejected.size + (confirmed_rejected&.size || 0)
    # 何をどれだけ適用したかを既定ログレベルでも 1 行で報告する
    Common.log_result("辞書を更新しました(索引 #{index_count} 件・用語集 #{glossary_count} 件・" \
                      "リジェクト #{rejected_total} 件)", status: :success)
    Common.log_info("読み変更: #{yomi_changes.size}") if yomi_changes.any?
    Common.log_info('ページ生成は vs build 実行時に行われます')
  else
    Common.log_warn('変更がありませんでした')
    Common.log_info('_index_glossary_review.md でフラグを編集してください')
  end

  # _index_glossary_review.md は残す(再編集の可能性があるため)
  # vs build の clean 処理で削除される
  changes_made
end

#auto_process!(chapters) ⇒ Object

全自動索引候補抽出 → _index_review.md 生成

Parameters:

  • chapters (Array<String>)

    対象章のリスト



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/vivlio_starter/cli/index/unified_index_manager.rb', line 78

def auto_process!(chapters)
  auto_discovery = @config.fetch(:auto_discovery, true)

  Common.log_action('索引の自動処理を開始します...')

  # R8: 辞書へ書いた登録内容を種別ごとに集め、既定ログレベルで要約表示する
  dictionary_writes = {}

  # 1. 手動マークアップを検出して統合辞書に登録
  manual_terms = extract_manual_markup_terms(chapters)
  if manual_terms.any?
    added = @terms_manager.merge_terms!(manual_terms, flags: 'i', source: 'manual_markup')
    dictionary_writes['手動マークアップ'] = added if added.any?
    Common.log_info("手動マークアップから #{manual_terms.size} 件の用語を登録しました")
  end

  # auto_discovery が無効の場合、自動候補抽出をスキップ
  unless auto_discovery
    @terms_manager.record_scanned_chapters!(chapters)
    report_dictionary_writes(dictionary_writes)
    Common.log_info('auto_discovery: false のため、自動候補抽出をスキップします')
    Common.log_info('手動マークアップのみが索引に反映されます')
    return
  end

  # 2. 候補抽出
  candidates = extract_candidates(chapters)
  Common.log_info("候補抽出: #{candidates.size}")

  # 3. 既に辞書にある語とリジェクト済みの語を落とす(=選べる候補だけ残す)
  selectable, rejected_count_in_candidates = selectable_candidates(candidates)

  # 4. 登録語と同じ土俵で並べ、推奨候補/一般候補/見直し候補に分ける。
  #    スコアの絶対値では切らない——閾値は書籍の規模で意味が変わるうえ、
  #    「目安に達しているから推奨は 0 件」という誤った判断を生む(§3.4-1)。
  bands = build_bands(@terms_manager.index_terms, selectable, current_estimate(chapters), @extractor)
  by_name = selectable.to_h { [it['term'], it] }
  high_candidates = review_entries(bands&.recommended, by_name)
  low_candidates = review_entries(bands&.general, by_name)

  # 5. 自動承認は既定で行わない。旧既定(スコア 300 以上を無条件登録)が
  #    「頻出の一般語ばかりが辞書に入る」現状を作った張本人である。
  auto_approved = @config[:auto_approve] == true ? high_candidates : []
  if auto_approved.any?
    added = @terms_manager.merge_terms!(auto_approved, flags: 'i', source: 'auto_extracted')
    dictionary_writes['自動承認'] = added if added.any?
  end

  # 6. 登録済み用語(索引+用語集すべて)に文脈を付与。
  #    見直し候補(順位が目安語数の外に出た登録語)も渡し、レビューで一覧できるようにする
  terms_with_context = enrich_terms_with_context(
    @terms_manager.load_terms, chapters,
    scores: candidate_scores_by_name(selectable, candidates),
    review_terms: bands&.review&.map(&:term)&.to_set || Set[]
  )

  # 7. リジェクト済み用語に文脈とスコアを付与
  # candidatesからスコアを復元できるように渡す
  rejected_with_context = enrich_rejected_with_context(candidates)

  # 8. _index_review.md を生成
  @markdown_generator.generate!(
    terms: terms_with_context,
    high_candidates: high_candidates,
    low_candidates: low_candidates,
    rejected: rejected_with_context
  )

  # 9. 走査した章集合を辞書へ記録(R7: ビルド時の章追加検知に使う)
  @terms_manager.record_scanned_chapters!(chapters)

  # 10. 結果レポート
  report_dictionary_writes(dictionary_writes)
  # 現況と候補の分布は vs index:plan と同じ画面を出す(§6.3)。
  # 辞書を書き換えた後なので、登録語数は更新後の値になる。
  @terms_manager.clear_cache!
  build_plan_reporter(chapters, selectable, extractor: @extractor).render
  report_auto_results(auto_approved, high_candidates, low_candidates,
                      rejected_count_in_candidates, rejected_with_context.size)
end

#build_glossary!Object

用語集ページを生成(後方互換 - 単独呼び出し用)



414
415
416
417
418
419
420
421
422
# File 'lib/vivlio_starter/cli/index/unified_index_manager.rb', line 414

def build_glossary!
  return unless glossary_enabled?

  @terms_manager.clear_cache!
  glossary = @terms_manager.glossary_terms
  builder = UnifiedPageBuilder.new(glossary_config: @glossary_config)
  result = builder.build_glossary!(glossary)
  Common.log_success('用語集ページを生成しました') if result
end

#build_index!(chapters) ⇒ Object

索引・用語集ページを生成(内部用 - vs build から呼ばれる)

Parameters:

  • chapters (Array<String>)

    対象章のリスト



374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
# File 'lib/vivlio_starter/cli/index/unified_index_manager.rb', line 374

def build_index!(chapters)
  Common.log_action('索引・用語集ページを生成しています...')

  # 本文スキャン(索引タグ付け+用語集リンク生成)
  scanner = IndexMatchScanner.new(defer_warnings: true)
  scanner.scan_all_chapters!(chapters, read_only: false)

  # UnifiedPageBuilder で索引+用語集を生成
  builder = UnifiedPageBuilder.new(glossary_config: @glossary_config)

  # 索引ページ生成
  builder.build_index!
  report_reference_style(builder)

  # 用語集ページ生成(glossary_enabled かつ g フラグの用語がある場合)
  # スキャンは辞書を書かない(R1)ためリロード不要
  if glossary_enabled?
    glossary = @terms_manager.glossary_terms
    builder.build_glossary!(glossary)
    warn_unmatched_glossary_terms(glossary, scanner.glossary_backlinks, chapters)
  end

  Common.log_success('索引・用語集ページの生成が完了しました')

  # R7: 索引候補の抽出(vs index:auto)が未実施の章を検出して案内
  warn_unscanned_chapters(chapters)
  # 主要参照が未指定で広く散らばっている語を要約 1 行で促す
  warn_missing_main_references(chapters)

  return unless scanner.config_missing || scanner.no_matches

  IndexCommands.add_post_build_message(IndexCommands::INDEX_TERMS_MISSING_MESSAGE)
  # 画面に 🟡 を出す以上は集計にも載せる(出ているのに「良好」と総括しない)。
  PreProcessCommands::IssueRegistry.record(
    severity: :warn, category: :index,
    message: '索引語辞書がありません(vs index:auto → vs index:apply で作成できます)'
  )
end

#full_catalog_scope?(build_targets) ⇒ Boolean

走査対象が catalog の全章を覆っているか(R4 の前提条件)。 catalog を読めない場合は従来どおり検査する(判定材料が無いのに黙るのは危険)。

Parameters:

  • build_targets (Array<String>)

    走査した章の basename

Returns:

  • (Boolean)


555
556
557
558
559
560
561
562
# File 'lib/vivlio_starter/cli/index/unified_index_manager.rb', line 555

def full_catalog_scope?(build_targets)
  catalog = Build::CatalogLoader.load_existing_basenames
  return true if catalog.empty?

  (catalog - build_targets).empty?
rescue StandardError
  true
end

#glossary_enabled?Boolean

用語集機能が有効か

Returns:

  • (Boolean)


425
426
427
# File 'lib/vivlio_starter/cli/index/unified_index_manager.rb', line 425

def glossary_enabled?
  @glossary_config[:enabled] == true
end

#list_rejected_termsObject

リジェクト済み候補の一覧表示



565
566
567
# File 'lib/vivlio_starter/cli/index/unified_index_manager.rb', line 565

def list_rejected_terms
  @queue_manager.list_rejected_terms
end

#plan!(chapters) ⇒ Object

索引の下見(vs index:plan)。現況と候補の分布を表示するだけで、 辞書もレビューファイルも書かない。表示は auto_process! と同一にする ——見え方が実行ごとに変わると、下見の意味がなくなる(§6.2)。

Parameters:

  • chapters (Array<String>)

    対象章のリスト



65
66
67
68
69
70
71
72
73
74
# File 'lib/vivlio_starter/cli/index/unified_index_manager.rb', line 65

def plan!(chapters)
  Common.log_action('索引の現況を確認しています...')
  candidates = @config.fetch(:auto_discovery, true) ? extract_candidates(chapters) : []
  # auto_process! と同じく「選べる候補」だけを渡す。生の候補には登録済みの語も
  # 棄却済みの語も混ざっており、そのまま出すと下見だけが多く見え、しかも
  # 著者が自分で外した語を推奨してしまう(実測: 2,880 件 対 2,644 件)。
  selectable, = selectable_candidates(candidates)
  build_plan_reporter(chapters, selectable, extractor: @extractor).render(dry_run: true)
  0
end

#report_reference_style(builder) ⇒ Object

参照を絞ったことをビルド末尾で報告する(R6・no silent caps)。

索引が短くなった理由が設定にあると分からないと、著者は「索引語が 消えた」と読む。何語をどの設定で絞ったかまで書く。

Parameters:



456
457
458
459
460
461
462
463
464
465
466
467
# File 'lib/vivlio_starter/cli/index/unified_index_manager.rb', line 456

def report_reference_style(builder)
  limitation = builder.reference_limitation
  return unless limitation.any?

  message = if limitation.style == 'main_only'
              "💡 索引を主要参照のみで組みました(index.reference_style: main_only・#{limitation.size} 語)"
            else
              "💡 索引の副次参照を #{limitation.size} 語で #{limitation.limit} 件までに絞りました" \
              "(index.max_sub_references: #{limitation.limit}"
            end
  IndexCommands.add_post_build_message(message)
end

#reset_rejected!Object

リジェクト履歴をクリア



576
577
578
# File 'lib/vivlio_starter/cli/index/unified_index_manager.rb', line 576

def reset_rejected!
  @queue_manager.reset_rejected!
end

#strip_markdown(text) ⇒ Object

Markdown装飾を除去してプレーンテキストを取得



360
361
362
363
364
365
366
367
368
369
370
# File 'lib/vivlio_starter/cli/index/unified_index_manager.rb', line 360

def strip_markdown(text)
  text
    .gsub(/\*\*(.+?)\*\*/, '\1')  # **bold**
    .gsub(/\*(.+?)\*/, '\1')      # *italic*
    .gsub(/`(.+?)`/, '\1')        # `code`
    .gsub(/\[(.+?)\]\(.+?\)/, '\1') # [link](url)
    .gsub(/^#+\s*/, '')           # # heading
    .gsub(/^\s*[-*]\s+/, '')      # - list item
    .gsub(/\n+/, ' ')             # newlines to space
    .strip
end

#unreject_term!(term_or_number) ⇒ Object

リジェクト解除

Parameters:

  • term_or_number (String)

    用語名または番号



571
572
573
# File 'lib/vivlio_starter/cli/index/unified_index_manager.rb', line 571

def unreject_term!(term_or_number)
  @queue_manager.unreject_term!(term_or_number)
end

#validate_glossary_definitions!(terms) ⇒ Object

用語集の説明文バリデーション require_definition: true の場合、説明文が空ならエラー max_definition_length を超過している場合は警告



331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
# File 'lib/vivlio_starter/cli/index/unified_index_manager.rb', line 331

def validate_glossary_definitions!(terms)
  max_length = @glossary_config[:max_definition_length]

  # 説明文の長さチェック(Markdown装飾を除去した文字数)
  terms.each do |term|
    definition = term['definition'].to_s
    next if definition.strip.empty?

    plain_text = strip_markdown(definition)
    next unless plain_text.length > max_length

    Common.log_warn(
      "用語「#{term['term']}」の説明文が #{max_length} 文字を超過しています " \
      "(#{plain_text.length} 文字)"
    )
  end

  return unless @glossary_config[:require_definition]

  missing = terms.select { it['definition'].to_s.strip.empty? }
  return if missing.empty?

  missing.each do |term|
    Common.log_error("用語「#{term['term']}」に説明文がありません")
  end
  raise "用語集の説明文が必須ですが、#{missing.size}件の用語に説明文がありません"
end

#warn_missing_main_references(chapters) ⇒ Object

広く散らばっているのに主要参照が未指定の索引語を促す(R7)。

要約 1 行だけにする。実測で該当は 30〜38 語あり、語ごとに 1 行ずつ出すと ビルドログが埋まる。しかもそこから修正には進めない——直す場は レビューファイルなので、導線はそちらへ向ける(語ごとの候補は R2 が出す)。

部分ビルドでは黙る。出現章数の比率は全章を走査したときにしか意味を持たず、 1 章だけを対象にすると全語が「広い」判定になる (warn_unmatched_glossary_terms と同じ立場)。 reference_style: all で機能を切っている著者にも促さない。

Parameters:

  • chapters (Array<String>)

    ビルド対象章



480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
# File 'lib/vivlio_starter/cli/index/unified_index_manager.rb', line 480

def warn_missing_main_references(chapters)
  return if main_reference_disabled?

  build_targets = chapters.map { File.basename(it.to_s, '.md') }
  return unless full_catalog_scope?(build_targets)

  pending = @terms_manager.index_terms.reject { it['main'] }
  return if pending.empty?

  spreads = IndexCommands::TermSpread.measure(pending, chapters)
  wide = IndexCommands::TermSpread.common_terms(spreads, ratio: MAIN_REFERENCE_HINT_RATIO)
  return if wide.empty?

  IndexCommands.add_post_build_message(
    "🟡 主要参照が未指定の索引語が #{wide.size} 語あります(索引が引きにくくなります)\n" \
    '🟡  vs index:auto を実行すると、章の候補付きでレビューファイルに一覧できます'
  )
  # 特定の章の欠陥ではないので chapter は付けない。件数は語ごとではなく
  # **1 件**だけ積む——30 件積むと章別サマリーが索引の話で埋まる。
  PreProcessCommands::IssueRegistry.record(
    severity: :warn, category: :index,
    message: "主要参照が未指定の索引語が #{wide.size} 語あります(vs index:auto で候補を確認できます)"
  )
end

#warn_unmatched_glossary_terms(glossary_terms, glossary_backlinks, chapters) ⇒ Object

R4: ビルド対象章に 1 回も出現しない用語集語を警告する(掲載自体は維持)。 catalog 未登録の原稿に出現があるならその章名を添え、どこにも無ければその旨を伝える。 除外したい場合の判断(-g フラグ)は著者に委ねる——定義は書籍の語彙資産のため。

判定材料は「今回のスキャン結果」なので、章を絞った実行では出現しないのが当然になる (1 章だけを対象にすると用語集語がほぼ全滅して誤検知の山になる)。 全章を走査したときだけ意味を持つ検査なので、部分実行では黙る。 詳細 → preflight-glossary-warning-scope-report.md

Parameters:

  • glossary_terms (Array<Hash>)

    用語集対象の用語

  • glossary_backlinks (Hash{String => Array})

    今回のスキャンで出現した語 → 出現箇所

  • chapters (Array<String>)

    ビルド対象章



516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
# File 'lib/vivlio_starter/cli/index/unified_index_manager.rb', line 516

def warn_unmatched_glossary_terms(glossary_terms, glossary_backlinks, chapters)
  build_targets = chapters.map { File.basename(it.to_s, '.md') }
  return unless full_catalog_scope?(build_targets)

  missing = glossary_terms.reject { glossary_backlinks.key?(it['term']) }
  return if missing.empty?

  all_contents = Dir.glob(File.join(Common::CONTENTS_DIR, '*.md')).to_h do |path|
    [File.basename(path, '.md'), File.read(path, encoding: 'utf-8')]
  end

  missing.each do |term|
    name = term['term']
    found = all_contents.keys.select { all_contents[it].include?(name) }
    outside = found - build_targets
    hint = if outside.any?
             # 走査は catalog 全章を覆っている(上のガード)ため、ここに来るのは
             # contents/ にはあるが catalog.yml に載っていない原稿に限られる
             "catalog 未登録の #{outside.join(', ')} に出現"
           elsif found.any?
             "#{found.join(', ')} に文字列出現はあるがリンク化されていません(コード内・タグ内等)"
           else
             '原稿のどこにも出現しません(語の変更・削除?)'
           end
    Common.log_warn("用語集語がビルド対象章に出現しません: #{name}#{hint}")
    # 章別サマリーへブリッジする。全章走査時にしか到達しないので(上のガード)
    # ここに来る指摘は「辞書に残った死語」=原稿とのズレであり、集計に載せる価値がある。
    # 特定の章の欠陥ではないため chapter は付けない(章別サマリーには出ず、
    # 最終行の総括にだけ効く。直し方は上のメッセージが言い切っている)。
    PreProcessCommands::IssueRegistry.record(
      severity: :warn, category: :index,
      message: "用語集語が原稿に出現しません: #{name}"
    )
  end
end

#warn_unscanned_chapters(chapters) ⇒ Object

R7: ビルド対象のうち index:auto が未走査の章があれば、ビルド末尾の案内へ積む。 旧辞書(scanned_chapters キーなし)では判定しない——誤警告を避けるため

Parameters:

  • chapters (Array<String>)

    ビルド対象章



432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
# File 'lib/vivlio_starter/cli/index/unified_index_manager.rb', line 432

def warn_unscanned_chapters(chapters)
  scanned = @terms_manager.scanned_chapters
  return if scanned.nil?

  unscanned = chapters.map { File.basename(it.to_s, '.md') } - scanned
  return if unscanned.empty?

  IndexCommands.add_post_build_message(
    "🟡 索引候補の抽出が未実施の章があります: #{unscanned.join(', ')} → vs index:auto を実行してください"
  )
  # 章別サマリーへブリッジする。どの章が未走査かは特定できているので章ごとに積む。
  unscanned.each do |chapter|
    PreProcessCommands::IssueRegistry.record(
      chapter: chapter, severity: :warn, category: :index,
      message: '索引候補の抽出が未実施です(vs index:auto を実行してください)'
    )
  end
end