Module: VivlioStarter::CLI::DoctorCommands::ConfigSalvager
- Defined in:
- lib/vivlio_starter/cli/doctor/config_salvager.rb
Overview
破損した設定ファイルからのサルベージ復元(機能 D / best-effort)
Defined Under Namespace
Classes: Result
Constant Summary collapse
- BOOK_SALVAGE_KEYS =
行スキャンで救出を試みる book.yml のトップレベル単一行スカラー。 複数行ブロックスカラー(legal.* 等)は境界判定できないため対象外(spec §3D.3)。
%i[main_title subtitle series release publisher contact author name].freeze
- BOOK_PLACEHOLDERS =
scaffold テンプレートのプレースホルダと salvage キーの対応
{ '{{MAIN_TITLE}}' => :main_title, '{{SUBTITLE}}' => :subtitle, '{{AUTHOR}}' => :author, '{{PUBLISHER}}' => :publisher, '{{PROJECT_NAME}}' => :name }.freeze
- BOOK_DEFAULTS =
救出値が無い場合の既定値(vs new の DEFAULT_ANSWERS と同じ非対話既定)
{ main_title: '新しい本', subtitle: '', author: '', publisher: '' }.freeze
- CATALOG_SECTIONS =
catalog.yml のセクションと章種別の対応(出力順を兼ねる)
{ preface: 'PREFACE', chapter: 'CHAPTERS', appendix: 'APPENDICES', postface: 'POSTFACE' }.freeze
Class Method Summary collapse
-
.chapter_entries_from_contents ⇒ Object
contents/ の章ファイルを [basename, kind] の配列で返す(章番号昇順)。 アンダースコア始まりのシステムページと、章番号レンジ外のファイルは除外する。.
-
.default_book_value(key) ⇒ Object
救出値が無いキーの既定値。プロジェクト名はカレントディレクトリ名を使う (vs new がディレクトリ名を既定のプロジェクト名にするのと同じ流儀).
-
.extract_book_scalars(corrupt_content) ⇒ Object
破損内容を行単位でスキャンし、救出対象キーの値を集める。 破損行は単にマッチしないだけで処理は継続する(取りこぼし容認)。.
-
.kind_for(number) ⇒ Object
章番号→セクション判定は TokenResolver の定義を再利用する(判定ロジックを複製しない).
-
.render_book_yml(scaffold_path, salvaged = {}) ⇒ String
scaffold の book.yml テンプレートを展開する。salvaged の値を優先し、 無いキーは既定値で埋める(機能 A の「欠落 book.yml の素の復元」でも使用)。.
-
.replace_scalar_line(content, key, value) ⇒ Object
key: "..."行の値部分だけを救出値で置き換える(コメントは保持). -
.salvage(path, corrupt_content, scaffold_path) ⇒ Result?
破損ファイルから復元内容を生成する。 救出できない・対象外のファイルなら nil(素の scaffold 復元へフォールバック)。.
-
.salvage_book(corrupt_content, scaffold_path) ⇒ Object
破損 book.yml から単一行スカラーを抽出し、テンプレートへ書き戻す。 1 件も救出できなければ nil(素の scaffold 復元へ)。.
-
.salvage_catalog ⇒ Result?
破損した catalog.yml は解析せず、contents/*.md の命名規約 (NN-slug.md)と章番号レンジから目録を組み立て直す。 部タイトルと意図的な除外(コメントアウト)は catalog.yml にしか 存在しないため構造上復元できない(notes で利用者に明示する)。.
-
.scalar_value_from(line, key) ⇒ String?
1 行から
key: value形式の値を取り出す。 引用符付き(" / ')と裸の値に対応し、行末コメントは無視する。 閉じ引用符の無い行(破損行)はマッチさせない。. -
.unescape_double_quoted(raw) ⇒ Object
double-quoted 値のエスケープを復元する(best-effort: 主要シーケンスのみ).
-
.yaml_escape(value) ⇒ Object
YAML double-quoted string 内で安全になるよう値をエスケープする (NewCommands.yaml_escape_double_quoted と同等の最小実装).
Class Method Details
.chapter_entries_from_contents ⇒ Object
contents/ の章ファイルを [basename, kind] の配列で返す(章番号昇順)。 アンダースコア始まりのシステムページと、章番号レンジ外のファイルは除外する。
125 126 127 128 129 130 131 132 |
# File 'lib/vivlio_starter/cli/doctor/config_salvager.rb', line 125 def chapter_entries_from_contents Dir.glob(File.join(Common::CONTENTS_DIR, '*.md')) .map { File.basename(it, '.md') } .reject { it.start_with?('_') } .filter_map { |base| (number = base[/\A(\d+)(?=[-_]|\z)/, 1]) && [base, number.to_i] } .sort_by { |base, number| [number, base] } .filter_map { |base, number| (kind = kind_for(number)) && [base, kind] } end |
.default_book_value(key) ⇒ Object
救出値が無いキーの既定値。プロジェクト名はカレントディレクトリ名を使う (vs new がディレクトリ名を既定のプロジェクト名にするのと同じ流儀)
188 189 190 |
# File 'lib/vivlio_starter/cli/doctor/config_salvager.rb', line 188 def default_book_value(key) key == :name ? File.basename(Dir.pwd) : BOOK_DEFAULTS.fetch(key, '') end |
.extract_book_scalars(corrupt_content) ⇒ Object
破損内容を行単位でスキャンし、救出対象キーの値を集める。 破損行は単にマッチしないだけで処理は継続する(取りこぼし容認)。
158 159 160 161 162 163 164 165 166 167 168 |
# File 'lib/vivlio_starter/cli/doctor/config_salvager.rb', line 158 def extract_book_scalars(corrupt_content) BOOK_SALVAGE_KEYS.each_with_object({}) do |key, found| corrupt_content.each_line do |line| next unless (value = scalar_value_from(line, key)) # プレースホルダが残っている値は「利用者の入力」ではないため救出しない found[key] = value unless value.empty? || value.include?('{{') break end end end |
.kind_for(number) ⇒ Object
章番号→セクション判定は TokenResolver の定義を再利用する(判定ロジックを複製しない)
135 136 137 |
# File 'lib/vivlio_starter/cli/doctor/config_salvager.rb', line 135 def kind_for(number) TokenResolver::Resolver::KIND_RANGES.find { |_, range| range.cover?(number) }&.first end |
.render_book_yml(scaffold_path, salvaged = {}) ⇒ String
scaffold の book.yml テンプレートを展開する。salvaged の値を優先し、 無いキーは既定値で埋める(機能 A の「欠落 book.yml の素の復元」でも使用)。
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
# File 'lib/vivlio_starter/cli/doctor/config_salvager.rb', line 81 def render_book_yml(scaffold_path, salvaged = {}) # --- Phase: プレースホルダ展開 --- # 単一パス置換で値中の別プレースホルダ文字列の二重展開を防ぐ(vs new と同じ手法) substitutions = BOOK_PLACEHOLDERS.to_h do |placeholder, key| [placeholder, yaml_escape(salvaged.fetch(key) { default_book_value(key) })] end pattern = Regexp.union(substitutions.keys) content = File.read(scaffold_path, encoding: 'utf-8').gsub(pattern) { substitutions[it] } # --- Phase: プレースホルダの無いキーの行置換 --- # series / release / contact は scaffold に実値が入っているため、 # 救出値がある場合のみ該当行の値部分を書き換える (salvaged.keys - BOOK_PLACEHOLDERS.values).each do |key| content = replace_scalar_line(content, key, salvaged[key]) end content end |
.replace_scalar_line(content, key, value) ⇒ Object
key: "..." 行の値部分だけを救出値で置き換える(コメントは保持)
193 194 195 196 197 |
# File 'lib/vivlio_starter/cli/doctor/config_salvager.rb', line 193 def replace_scalar_line(content, key, value) content.sub(/^(\s{2}#{Regexp.escape(key.to_s)}:\s*)(?:"(?:[^"\\]|\\.)*"|'[^']*'|[^#\n]*?)(\s*#[^\n]*)?$/) do %(#{Regexp.last_match(1)}"#{yaml_escape(value)}"#{Regexp.last_match(2)}) end end |
.salvage(path, corrupt_content, scaffold_path) ⇒ Result?
破損ファイルから復元内容を生成する。 救出できない・対象外のファイルなら nil(素の scaffold 復元へフォールバック)。
64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/vivlio_starter/cli/doctor/config_salvager.rb', line 64 def salvage(path, corrupt_content, scaffold_path) # 破損ファイルは不正な UTF-8 バイト列を含み得る(ファズテスト FZ-01 で検出)。 # 行スキャンの正規表現が ArgumentError で落ちないよう不正バイトを除去する。 # 拾えない行が増えるだけで best-effort の範囲(§3D.1) content = corrupt_content.to_s.dup.force_encoding(Encoding::UTF_8).scrub('') case File.basename(path) when 'catalog.yml' then salvage_catalog when 'book.yml' then salvage_book(content, scaffold_path) end end |
.salvage_book(corrupt_content, scaffold_path) ⇒ Object
破損 book.yml から単一行スカラーを抽出し、テンプレートへ書き戻す。 1 件も救出できなければ nil(素の scaffold 復元へ)。
143 144 145 146 147 148 149 150 151 152 153 154 |
# File 'lib/vivlio_starter/cli/doctor/config_salvager.rb', line 143 def salvage_book(corrupt_content, scaffold_path) salvaged = extract_book_scalars(corrupt_content) return nil if salvaged.empty? notes = salvaged.map { |key, value| "- #{key}: #{value}" } notes << '🟡 免責・商標などの複数行設定は復元されません。必要なら上記バックアップから書き戻してください' Result.new( content: render_book_yml(scaffold_path, salvaged), summary: 'config/book.yml を復元し、以下の値を救出しました(要確認)', notes: ) end |
.salvage_catalog ⇒ Result?
破損した catalog.yml は解析せず、contents/*.md の命名規約 (NN-slug.md)と章番号レンジから目録を組み立て直す。 部タイトルと意図的な除外(コメントアウト)は catalog.yml にしか 存在しないため構造上復元できない(notes で利用者に明示する)。
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/vivlio_starter/cli/doctor/config_salvager.rb', line 106 def salvage_catalog entries = chapter_entries_from_contents return nil if entries.empty? grouped = entries.group_by { |_basename, kind| kind } body = CATALOG_SECTIONS.map do |kind, section| lines = (grouped[kind] || []).map { |basename, _| " - #{basename}\n" }.join "#{section}:\n#{lines}" end.join("\n") Result.new( content: "# vs doctor --fix が contents/ から再構築した目録です(要確認)\n#{body}", summary: "config/catalog.yml を contents/ から再構築しました(#{entries.size} 章)", notes: ['🟡 部タイトル・除外設定は復元されません。必要なら上記バックアップから書き戻してください'] ) end |
.scalar_value_from(line, key) ⇒ String?
1 行から key: value 形式の値を取り出す。
引用符付き(" / ')と裸の値に対応し、行末コメントは無視する。
閉じ引用符の無い行(破損行)はマッチさせない。
174 175 176 177 178 179 180 181 182 |
# File 'lib/vivlio_starter/cli/doctor/config_salvager.rb', line 174 def scalar_value_from(line, key) pattern = /\A\s{2}#{Regexp.escape(key.to_s)}:\s* (?:"((?:[^"\\]|\\.)*)"|'([^']*)'|([^"'#\s][^#\n]*?)) \s*(?:\#.*)?\z/x match = line.match(pattern) return nil unless match match[1] ? unescape_double_quoted(match[1]) : (match[2] || match[3]&.strip) end |
.unescape_double_quoted(raw) ⇒ Object
double-quoted 値のエスケープを復元する(best-effort: 主要シーケンスのみ)
208 209 210 |
# File 'lib/vivlio_starter/cli/doctor/config_salvager.rb', line 208 def unescape_double_quoted(raw) raw.gsub(/\\(.)/) { { 'n' => "\n", 't' => "\t", 'r' => "\r" }.fetch(Regexp.last_match(1), Regexp.last_match(1)) } end |
.yaml_escape(value) ⇒ Object
YAML double-quoted string 内で安全になるよう値をエスケープする (NewCommands.yaml_escape_double_quoted と同等の最小実装)
201 202 203 204 205 |
# File 'lib/vivlio_starter/cli/doctor/config_salvager.rb', line 201 def yaml_escape(value) value.to_s.gsub(/[\\"\n\r\t]/) do |c| { "\\" => '\\\\', '"' => '\\"', "\n" => '\\n', "\r" => '\\r', "\t" => '\\t' }.fetch(c) end end |