概要

項目内容
案件IDMAS-153
カテゴリ自動入力パイプライン(運用ツール)
PhasePhase 1.5
優先度P1 (★★★)
所要時間2-3時間
対象ファイル800_ops/809_evidence_link_rebuilder.js(新規作成)、000_infra/001_env.js(追記)、100_config/101_sys_config.js(メニュー追記)
前提案件MAS-152(電帳法準拠リネーム・完了済)
後続案件

目的

会計士へのデータ共有時、証憑 PDF を別の Google Drive フォルダにコピーすると、各タブの「証跡リンク」「証憑URL」列が元フォルダのファイル ID を参照しているため全件リンク切れになる問題を解決する。

MAS-152 で統一された電帳法準拠ファイル名(YYYYMMDD_取引先略称_金額_元ファイル名.ext)を解析し、指定フォルダ内のファイルと 35_wrk_receipt / 32_wrk_invoice / 31_wrk_order / 42_trn_journal の各レコードを照合、リンクを一括再生成する。スクリプトプロパティ EVIDENCE_FOLDER_ID でフォルダを切替可能にし、社内用/会計士共有用を運用で使い分ける。

前提案件と後続案件

本案件は MAS-152(電帳法準拠リネーム)完了を前提とする。MAS-152 によりファイル名規約が統一されていることで、日付・取引先略称・金額の 3 項目による機械的マッチングが可能となる(TODO_future.md: 「MAS-152 で電帳法準拠のファイル名が統一されていれば照合精度 100%」)。

現在のコード

1. 502_receipt_reader.js でのリンク生成(MAS-152 マージ後)

MAS-152 により 35_wrk_receipt の「証跡リンク」列は Drive ファイル ID ベースの URL で書き込まれている(502_receipt_reader.js:107):

var driveLink = 'https://drive.google.com/file/d/' + file.getId() + '/view';

この URL はファイル ID に依存するため、Drive の別フォルダへコピーした時点で別ファイル ID となりリンク切れが発生する。moveTo と異なり makeCopy / 手動コピー操作では元リンクは維持されない。

2. 各タブの証憑リンク列(既存 DTO 定義)

000_infra/003_contracts.js に定義済みの DTO:

  • OrderDTO: 証憑URL(L35)
  • InvoiceDTO: 証憑URL(L65)
  • JournalEntryDTO: 証憑URL(L115)
  • BankTxDTO: 証憑 URL 列なし(本案件の対象外)
  • 35_wrk_receipt: 「証跡リンク」列(502_receipt_reader.js:63 HEADERS 配列)

列名が 2 種類(証憑URL / 証跡リンク)混在している ことに注意。コードは両方に対応する必要がある。

3. 既存の Repository 基盤

200_data/202_repository.js に以下が存在する:

  • OrderRepository / InvoiceRepository / JournalRepository: いずれも findAll() / save() / append() を公開
  • ReceiptRepository未定義。35_wrk_receipt は 502_receipt_reader.js から直接 Sheet API でアクセスされている
  • 本案件では新規 Repository 追加はせず、受領レコード(35)のみ Sheet 直接アクセスとする(既存パターン踏襲)

4. 既存の Env / メニュー基盤

  • 000_infra/001_env.jsEnv.receiptFolderId() / Env.setReceiptFolderId() が既存(RECEIPT_FOLDER_ID プロパティ)。同パターンで EVIDENCE_FOLDER_ID 用の関数を追加する
  • 100_config/101_sys_config.js にメニュー定義が集約されている

修正方針

全体像: 800_ops/809_evidence_link_rebuilder.js を新規作成し、rebuildEvidenceLinks() をメニュー起動のエントリポイントとする。実行フローは以下の 3 段階:

  1. Step 1: 環境設定とファイル名パーサEnv.evidenceFolderId() の追加、MAS-152 ファイル名を解析するヘルパー関数、マッチングキー(YYYYMMDD|略称|金額)の生成
  2. Step 2: Drive 走査とファイル名マップ構築EVIDENCE_FOLDER_ID 配下を再帰走査し、メモリ上に Map<key, FileEntry[]> を一度だけ構築
  3. Step 3: 4 タブのマッチング・リンク再生成 — 35_wrk_receipt / 32_wrk_invoice / 31_wrk_order / 42_trn_journal を順次処理し、優先順位付きでファイルを引き当てリンクを更新

アーキテクチャ決定事項(GAS 実行時間・クオータ対策)

  • Drive API コール最小化: シート行ごとに DriveApp.searchFiles / getFilesByName を呼ぶのは厳禁。GAS の 6 分実行制限と Drive API クオータ(1 日 10,000 回程度)に抵触するため、対象フォルダのファイルは一度だけ全取得し、Map<key, FileEntry[]> としてメモリ上に保持してから各行を照合する
  • 走査方式: folder.getFiles() + folder.getFolders() を再帰呼び出しで巡回。DriveApp が返すイテレータは 必ず .hasNext() で判定(失敗パターン #19 系の罠を継承)
  • ファイル名パースの頑健性: MAS-152 の取引先略称部は sanitizeFileNamePart_ 通過後の値。Drive 禁止文字は _ へ置換される仕様のため 略称部は _ を含み得る。単純な split('_') では壊れる
    • 対策: ファイル名先頭から 8 桁数字+_ を剥がし、末尾から拡張子+_元ファイル名 を剥がし、残りを「略称_金額」として最後の _\d+$ を金額として抽出する両側からのピール方式を採用
  • 書き込みは Repository 経由: 31 / 32 / 42 は Repository.findAll() → DTO 更新 → Repository.save() の往復で行う(Range 直接操作禁止)。35 は専用 Repository が未定義のため、HEADERS.indexOf による動的列判定で当該セルのみ setValue 更新(ハードコード禁止)

マッチングロジックの必須要件

マッチング優先順位(厳守)

  1. 完全一致: ファイル名の (YYYYMMDD, 略称, 金額) とレコードの (発生日YYYYMMDD, 正規化後取引先名, 税込金額) が全一致
  2. 合算完全一致: 同一取引先・同一年月内の複数レコードの税込金額合計がファイルの金額と一致(合算領収書対応)
  3. 部分一致: (略称, 金額) のみ一致し、日付が ± 31 日以内にある場合(月またぎの記帳ずれ対応)

優先順位の逆転(Pass 3 が Pass 2 より先に発火する等)は禁止(失敗パターン #13 の再発防止)。

合算マッチの仕様

  • 取引先略称でグルーピング → 年月でサブグルーピング → 同一グループ内で部分集合探索
  • 部分集合探索は貪欲法(日付昇順に加算)+同一金額 N 件束ねの 2 方式(失敗パターン #15 の対策)
  • 全件合計のみで照合してはならない(失敗パターン #14)

マッチ成功時のロック処理(最重要)

  • ファイル側に matched: boolean フラグを立て、一度引き当てられたファイルは以降のパスで再利用させない(失敗パターン #16 の対策)
  • ただし合算マッチでは 1 ファイル : N レコード が正当なので、合算成功時はファイルを即ロック、構成要素レコードはそれぞれ別々に消費
  • レコード側にもメモリ上フラグを持たせ、同一レコードが複数ファイルに紐付くのを防ぐ

処理順序のソート

  • 対象レコードは処理前に 発生日昇順でソート(失敗パターン #17 継承)
  • ソートキーは必ず Utils.parseDateToYm() または Utils.parseDateToYmd() で正規化した文字列を使用。Date オブジェクトや String(Date) での比較はタイムゾーン起因の順序崩れを招くため禁止

冪等性の担保

  • 更新前に「現在の証憑 URL」と「再生成候補の URL」を文字列比較し、一致する場合は updateSkipped++ としてスキップ
  • ファイル ID が同じなら URL も同じになるため、冪等再実行で無駄な書き込みを発生させない
  • 再マッチング対象は「URL が空」または「URL のファイル ID が現在のファイル ID と異なる」レコードのみ

関数設計(抜粋)

// エントリポイント(メニュー起動)
function rebuildEvidenceLinks() { /* ... */ }

// ファイル名パーサ(両側ピール方式)
function parseEvidenceFileName_(fileName) {
  // 戻り値: { ymd: 'YYYYMMDD', partner: '略称', amount: Number } | null
}

// Drive 再帰走査 + ファイルマップ構築
function buildEvidenceFileMap_(rootFolder) {
  // 戻り値: { byExactKey: Map<'YYYYMMDD|略称|金額', FileEntry[]>, byLoose: Map<'略称|金額', FileEntry[]> }
}

// 3-pass マッチング(共通)
function matchRecordsToFiles_(records, fileMap, opts) {
  // opts: { dateField, partnerField, amountField, urlField }
  // 戻り値: { updated, skipped, unmatched, aggregateMatched }
}

// タブ個別のドライバ
function rebuildReceiptLinks_(fileMap) { /* 35_wrk_receipt - Sheet 直接 */ }
function rebuildInvoiceLinks_(fileMap) { /* InvoiceRepository */ }
function rebuildOrderLinks_(fileMap)   { /* OrderRepository */ }
function rebuildJournalLinks_(fileMap) { /* JournalRepository */ }

メニュー追加(101_sys_config.js

既存の「🔧 開発・設定」または「📄 消込・マッチング」メニュー配下に新項目を追加:

.addItem('📎 証憑リンク一括再構築 (Drive)', 'rebuildEvidenceLinks')

Env 追加(000_infra/001_env.js

receiptFolderId() と同パターンで追加:

evidenceFolderId: function() {
  return PropertiesService.getScriptProperties().getProperty('EVIDENCE_FOLDER_ID');
},
setEvidenceFolderId: function(id) {
  PropertiesService.getScriptProperties().setProperty('EVIDENCE_FOLDER_ID', id);
},

影響範囲

変更対象変更内容変更量
800_ops/809_evidence_link_rebuilder.js新規作成(エントリ関数 + ヘルパー関数 8 個程度)~400行
000_infra/001_env.jsevidenceFolderId() / setEvidenceFolderId() 追加+10行
100_config/101_sys_config.jsメニュー項目 addItem 1 行追加+1行
  • 既存動作への影響なし: MAS-152 の領収書取込フロー(502_receipt_reader.js)は非改修
  • DTO / DDL への影響なし: 列構成は不変、既存列(証憑URL / 証跡リンク)の値更新のみ
  • postProcessReceiptData_ への影響なし: 取引先名・T番号の突合補正とは独立

注意事項

  1. DriveApp のイテレータは必ず .hasNext() で判定: folder.getFiles() / folder.getFolders() / folder.getFoldersByName() は FolderIterator / FileIterator を返す。truthy チェックで空判定してはならない
  2. ファイル名パースは両側ピール方式: 単純な split('_') は略称に _ を含む場合(OCR 由来の Drive 禁止文字置換)に壊れる。先頭から日付を、末尾から拡張子+元ファイル名を剥がし、残りから金額を抽出する
  3. 列インデックスのハードコード禁止: 35_wrk_receipt の「ファイル名」「証跡リンク」列は HEADERS.indexOf() で動的取得、-1 なら即エラー(失敗パターン #18 系)
  4. 列名の 2 種混在: 31/32/42 は 証憑URL、35 は 証跡リンク。共通マッチング関数には列名を引数で渡す設計にする
  5. GAS 実行時間 6 分制限: ファイル数が数千件規模なら ScriptApp.newTrigger で分割実行も検討(本仕様では 1 回完結が前提だが、件数閾値を設けて警告)
  6. Drive API クオータ: 1 日のコール数を抑えるため、1 ファイル = 1 回の getId/getName/getUrl 呼び出しのみ。キャッシュ構築時に {fileId, fileName, driveLink} を一括採取してファイルオブジェクトは以降参照しない
  7. getFoldersByName で同名フォルダ重複時: processed/YYYY-MM/ 配下に同年月フォルダが重複すると最初の 1 つのみ処理される。DriveApp の仕様どおりの挙動として許容、ログで警告のみ
  8. 権限不足のファイル: file.getUrl()file.getId() で権限エラーが起きた場合は try/catch で捕捉し、unmatchedFiles に記録して処理継続
  9. 金額ゼロのファイル: MAS-152 でリネームした金額 0 のファイル(YYYYMMDD_略称_0_...)はマッチングキーに含めるが、レコード側の金額 0 とは Pass 3(部分一致)でのみ紐付けを許容
  10. 冪等性: 同じフォルダで再実行しても同じ結果になること(ファイル ID が変わらない限り「変更なしスキップ」で終わる)。URL 文字列比較で判定する
  11. Date 正規化: ソートキー・マッチングキーで DateString(Date) を比較禁止。必ず Utils.parseDateToYmd() → 文字列 YYYY-MM-DD → ハイフン除去で YYYYMMDD に統一(失敗パターン #17 継承)

エッジケース

#条件処理理由
1既に正しいリンクが設定されている(現 URL = 再生成 URL)更新スキップskipped++冪等性担保、無駄な書き込み回避、実行時間短縮
2レコードの税込金額 = 0 または空欄Pass 1/2 からは除外、Pass 3(部分一致)のみ対象金額 0 は電帳法検索キーとして弱く、誤マッチ防止を優先
3同名条件のファイルが複数存在(_2, _3 付き)古いファイルから順次引き当てfile.getDateCreated() 昇順、ソートキー一致時は fileName 昇順)ソート順による決定的割当で再実行の冪等性を担保
41 ファイル対 N レコード(合算領収書が正当)Pass 2(合算完全一致)でのみ許容。合計金額一致時に構成レコード全てへ同一リンクを付与合算領収書の実務運用に対応(失敗パターン #15)
5マッチする証憑ファイルが Drive 上に存在しない(孤立レコード)レコードは未更新で残すunmatchedRecords[] に記録し最終ダイアログで件数表示手動対応が必要な案件を人間に可視化
6逆の孤立(ファイルはあるがどのレコードとも一致しない)orphanFiles[] に記録、ダイアログでファイル名一覧を出力MAS-152 以前の古い命名や、手動アップロードの漏れを検出
7略称が UNKNOWN(MAS-152 で vendor 欠落)マッチングキーに UNKNOWN を含める。同じく UNKNOWN の孤立レコードがあれば Pass 3 のみ適用低信頼マッチの誤爆防止
831_wrk_order の日付が「開始年月」(YYYY-MM 粒度)YYYY-MM 単位で Pass 2/3 マッチを試行(Pass 1 は適用不可)発注は月単位で記録されるため日付粒度が異なる
942_trn_journal の「仕訳振替」行(証憑なしが正常)仕訳ステータス = "仕訳振替" の行は処理対象から除外仕訳振替は内部仕訳で外部証憑が存在しない
10ファイル名パース失敗(規約違反の命名)unparsedFiles[] に記録、Pass 3 の loose マッチからも除外MAS-152 未適用のレガシーファイルを明示的に識別
11有効フラグ = FALSE の行処理対象から除外CLAUDE.md コーディング規約(有効フラグ判定)に準拠
12実行時間が 5 分を超えた段階残件があっても安全に中断、進捗を Utils.logInfo で記録し次回再実行を促すGAS 6 分制限への保険。冪等性があるため再実行で継続可能

実データ検証(事前確認項目)

確認項目確認方法確認結果(Phase 1 で調査済)
35_wrk_receipt の列名「ファイル名」「証跡リンク」の存在502_receipt_reader.js:63 の HEADERS 配列を Read✅ 確認済
32_wrk_invoice / 31_wrk_order / 42_trn_journal の「証憑URL」列の存在000_infra/003_contracts.js の DTO 定義を Read✅ 確認済(3 DTO すべてに存在)
既存 Repository の findAll() / save() メソッド200_data/202_repository.js を Read✅ 確認済(OrderRepository / InvoiceRepository / JournalRepository)
ReceiptRepository の有無200_data/202_repository.js を Grep❌ 未定義(35 は Sheet 直接アクセス必要)
Utils.normalizePartnerName / parseDateToYmd000_infra/004_utils.js:108, 343 を Read✅ 確認済(MAS-154 で導入済)
「仕訳振替」行の識別方法DTO 定義の 仕訳ステータス 列(L114)=== "仕訳振替" で完全一致判定(CLAUDE.md 会計ロジック規約どおり)
MST_PART 実データの略称列101_sys_config.js:645 の headers 配列✅ 確認済(「略称」列あり)
EVIDENCE_FOLDER_ID プロパティの命名衝突receiptFolderId()RECEIPT_FOLDER_ID、別プロパティなので衝突なし✅ 新規プロパティ追加可

実行前に運用で追加確認すべき項目:

  • EVIDENCE_FOLDER_ID に指定するフォルダ配下のファイル件数(数千件超なら分割実行を検討)
  • 会計士共有フォルダに対して実行する場合、スクリプト実行アカウントに少なくとも閲覧権限があること(ファイル ID 取得に必要)
  • 本番実行前に dev 環境で 1 年分のデータに対して試行し、unmatchedRecords / orphanFiles の件数を把握しておく

プロダクトポリシー

Human-in-the-Loop

CLAUDE.md および PRD のプロダクトポリシー(「AI/自動処理の結果は必ず人間がレビュー・承認してから確定する」)に準拠し、以下の可視化装備を実装する:

  1. 更新セルへの背景色付与: リンクを更新した「証憑URL」「証跡リンク」セルに 薄黄色(#FFF9C4)の背景色 を設定。ユーザーが目視でスポットチェックし、問題なければ手動で背景色をクリア(運用で「確認済」を表現)
  2. 実行後ダイアログで件数サマリ表示: 以下をダイアログ化
    • 更新: N 件(黄色ハイライト中)
    • スキップ(既に正しい): M 件
    • Pass 1 完全一致 / Pass 2 合算 / Pass 3 部分一致 の内訳
    • 孤立レコード: O 件(リンクなしのままの行)
    • 孤立ファイル: P 件(どのレコードにも紐付かなかったファイル名一覧)
    • パース失敗ファイル: Q 件(MAS-152 未適用ファイル)
  3. Utils.logInfo への完全ログ: 全更新・スキップ・孤立を案件 ID 付きでログ残し。後日の監査や問い合わせ対応に備える
  4. dry-run モード: 第 2 引数に { dryRun: true } を渡した場合は実書き込みを行わず、更新候補件数のみダイアログ表示。本番実行前の影響確認手段として提供

安全運用ルール

  • 実行中は他ユーザーによる該当タブの編集を抑止するため、LockService.getDocumentLock().tryLock(30000) で 30 秒タイムアウトの排他ロックを取得
  • Repository.save() は全行置換のため、実行中にデータ同時編集があると損失する。ロック取得失敗時は即エラーダイアログで中断

関連ドキュメント

仕様書関連箇所
dev_mas-152_evidence_filename_rename.mdファイル名規約(YYYYMMDD_取引先略称_金額_元ファイル名.ext)・processed/YYYY-MM/ フォルダ構造
dev_mas-154_partner_logical_abbr.mdUtils.normalizePartnerName() / generateLogicalAbbr() の挙動
dev_mas-162_bank_combo_match.md合算マッチのパターン(部分集合探索・グルーピング・matched フラグ)
CLAUDE.md列参照のヘッダー名ベース規約、有効フラグ判定、Repository 経由アクセス原則
failure_patterns.md#13-#17(マッチング設計)、#18-#20(固有名詞の Read 裏取り)、#19(DriveApp イテレータの .hasNext() 罠)
TODO_future.mdMAS-153 案件定義・MAS-152 との依存関係

人間が検討すべき事項

#項目詳細
1同名ファイル複数時の優先ルール本仕様では「作成日時昇順、同着ならファイル名昇順」で古い順に引き当て。TODO_future.md の記載(最新日時 or エラー)と方針が異なる。実運用で検証し必要に応じて調整
2会計士共有フォルダの権限設定共有先を「閲覧のみ」にすればファイル ID は変わらずコピーも発生しない(この場合は本機能不要)。「編集可」や別 Drive アカウントへコピーする運用なら ID が変わるため本機能が必要
3EVIDENCE_FOLDER_ID 切替 UX社内用/会計士共有用を頻繁に切り替える場合、ダイアログから選択式にする方が運用しやすい。初版はプロパティ書き換えのみ(将来 UI 化の余地あり)
4ReceiptRepository 新設の是非35_wrk_receipt は現状 Sheet 直接アクセスのみ。他案件でも Receipt への書き込みが増えるなら 202_repository.js に ReceiptRepository を新設する選択肢あり(本案件では既存パターン維持)
51:N マッチ(合算)の適用範囲32_wrk_invoice の INV 複数行への 1 ファイル紐付けは正当だが、31_wrk_order(発注単位)への合算マッチは業務意味が薄い。タブごとに Pass 2 の有効/無効を切替可能にする設計にするか検討
6Pass 3(部分一致)の日付許容範囲本仕様では ± 31 日。月またぎのケースを想定だが、業務実態によっては ± 7 日に絞る選択肢あり。パラメータ化しておく
7背景色クリアの自動化「確認済み」を手動背景色クリアで表現する UX は手間。onEdit で確認 FLG 列を立てたら背景色自動クリア、という拡張案あり(将来案件として分離)
8実行時間超過時のレジュームGAS 6 分制限を超える規模になったら、処理済みフォルダ年月のリストを ScriptProperties に保存し、次回再実行時に未処理年月のみ走査する分割実行機構を追加する

実装プロンプト(Claude Code 用)

【タイムアウト回避・実行原則(v1.7)】
1. Phase 1(設計)では拡張思考フル活用・Read で裏取り。Phase 2(清書)の各 Step は最小限思考で書き下し。
2. 「〜作成します」等の text のみで tool_use なしに turn 終了しない。
3. 実装は 骨格 Write → 追記 Edit/Bash を分割実行。1 回あたり ~300 行以内。
4. 各 Step で書く内容を事前に洗い出してから tool_use へ進む。

あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-153「ファイル名ベースの証憑リンク一括再構築機能」を実装してください。

## 実行前タスク
以下のファイルを読み込み、既存パターンを把握してください:
1. `docs/dev/dev_mas-153_evidence_link_rebuilder.md` — 本仕様書
2. `docs/dev/dev_mas-152_evidence_filename_rename.md` — ファイル名規約の前提
3. `500_import/502_receipt_reader.js` — 35_wrk_receipt へのアクセス方法(Sheet 直接、HEADERS 定義)
4. `200_data/202_repository.js` — OrderRepository / InvoiceRepository / JournalRepository の findAll/save パターン
5. `000_infra/003_contracts.js` — 各 DTO の「証憑URL」列定義
6. `000_infra/004_utils.js` — `normalizePartnerName` / `parseDateToYmd` の再利用ポイント
7. `000_infra/001_env.js` — `receiptFolderId()` のパターン(`evidenceFolderId()` 追加の参考)
8. `100_config/101_sys_config.js` — メニュー定義箇所(`addItem` 追記位置)
9. `CLAUDE.md` — 列参照ヘッダー名ベース規約、有効フラグ判定、Repository 経由アクセス原則
10. `docs/_internal/failure_patterns.md` — #13-#17(マッチング)、#18-#20(Read 裏取り)

## 修正対象ファイル
- `800_ops/809_evidence_link_rebuilder.js` — **新規作成**
- `000_infra/001_env.js` — `evidenceFolderId()` / `setEvidenceFolderId()` 追記のみ
- `100_config/101_sys_config.js` — メニュー項目 `addItem` 1 行追加のみ

## 実装内容

### A. `000_infra/001_env.js` への追記

`Env` オブジェクト内の `receiptFolderId` / `setReceiptFolderId` の直後に以下を追加:

    evidenceFolderId: function() {
      return PropertiesService.getScriptProperties().getProperty('EVIDENCE_FOLDER_ID');
    },
    setEvidenceFolderId: function(id) {
      PropertiesService.getScriptProperties().setProperty('EVIDENCE_FOLDER_ID', id);
    },

### B. `800_ops/809_evidence_link_rebuilder.js` の新規作成

以下の関数を定義する:

1. **`rebuildEvidenceLinks(opts)`** — メニュー起動のエントリ。引数 `opts = { dryRun: boolean }`。
   - LockService で 30 秒タイムアウトの排他ロック取得。失敗時は即エラーダイアログ
   - `Env.evidenceFolderId()` 取得、未設定ならダイアログで入力→保存
   - `buildEvidenceFileMap_()` で Drive 走査
   - `rebuildReceiptLinks_()` / `rebuildInvoiceLinks_()` / `rebuildOrderLinks_()` / `rebuildJournalLinks_()` を順次呼出
   - 結果をサマリダイアログ表示(dryRun なら「(試算のみ)」タイトル付き)

2. **`parseEvidenceFileName_(fileName)`** — 両側ピール方式のファイル名パーサ。
   - 戻り値: `{ ymd: 'YYYYMMDD', partner: string, amount: number } | null`
   - 手順: (a) 先頭 8 桁+`_` を剥がす、(b) 末尾から `.ext` を剥がす、(c) 末尾から `_元ファイル名` を剥がす(最後の `_\d+$` の右側)、(d) 残り `略称_金額` から末尾の `_\d+` を金額として分離、(e) 残りが略称
   - 正規表現 `/^(\d{8})_(.+)_(\d+)_([^.]+)\.[^.]+$/` を基本、partial の `.+` は greedy なので最後の `_\d+_` を識別する
   - パース失敗時は `null` 返却(呼び出し元で `unparsedFiles[]` に記録)

3. **`buildEvidenceFileMap_(rootFolder)`** — Drive 再帰走査。
   - `folder.getFiles()` + `folder.getFolders()` のイテレータを **`.hasNext()` 必須** で巡回
   - 1 ファイルにつき `{ fileId, fileName, driveLink, parsed, dateCreated, matched: false }` を保持
   - 戻り値: `{ byExactKey: Map, byLoose: Map }` (キーは後述)
   - byExactKey: `'YYYYMMDD|略称|金額'` → `FileEntry[]`
   - byLoose: `'略称|金額'` → `FileEntry[]`
   - 各 Map の値配列は `dateCreated` 昇順、同着時 `fileName` 昇順でソート

4. **`matchRecordsToFiles_(records, fileMap, opts)`** — 3-pass マッチング共通関数。
   - opts: `{ dateField, partnerField, amountField, urlField, datePrecision: 'ymd'|'ym' }`
   - records を `Utils.parseDateToYmd` 正規化後の日付で昇順ソート
   - Pass 1: 完全一致(opts.datePrecision=ymd のみ)
   - Pass 2: 合算マッチ(同一取引先×年月でグループ化、貪欲法+同一金額 N 件束ね)
   - Pass 3: 部分一致(略称×金額、日付 ±31 日以内)
   - マッチ成功時はファイルに `matched=true`、レコードに `linked=true` を立てる
   - 戻り値: `{ updated, skipped, unmatched, pass1Count, pass2Count, pass3Count }`

5. **`rebuildReceiptLinks_(fileMap)`** — 35_wrk_receipt 専用。
   - `ss.getSheetByName('35_wrk_receipt')` 取得
   - `HEADERS = sheet.getRange(1,1,1,lastCol).getValues()[0]`
   - `iFileName = HEADERS.indexOf('ファイル名')`、`iLink = HEADERS.indexOf('証跡リンク')`、両方 -1 判定で throw
   - matchRecordsToFiles_ の結果に基づき、該当セルのみ `setValue` 更新、背景色 `#FFF9C4` を `setBackground`
   - 日付フィールド: `発生日(P/L計上日)` 優先、空なら `発行日`、金額: `税込金額_決済`、取引先: `取引先名`(既に normalizePartnerName 正規化済)

6. **`rebuildInvoiceLinks_(fileMap)`** — 32_wrk_invoice。
   - `InvoiceRepository.findAll()` → DTO 配列でマッチング → DTO の `証憑URL` 更新 → `InvoiceRepository.save(dtos)`
   - 背景色は save 前後で別途セル単位設定(save は clearDataValidations 後に setValues なので、背景色は save 後に該当行へ setBackground)
   - 日付: `発生日(P/L計上日)`、金額: `税込金額_計画`、取引先: `Utils.normalizePartnerName(dto['取引先名'])`

7. **`rebuildOrderLinks_(fileMap)`** — 31_wrk_order。
   - `OrderRepository.findAll()` / `save()`
   - **datePrecision: 'ym'** を opts で渡す(日付粒度が「開始年月」= YYYY-MM のため)
   - Pass 1 は事実上スキップ、Pass 2/3 のみ

8. **`rebuildJournalLinks_(fileMap)`** — 42_trn_journal。
   - 仕訳ステータス = `"仕訳振替"` の行は処理対象から除外(`=== '仕訳振替'` 完全一致)
   - 日付: `発生日(P/L計上日)`、金額: `税込金額_実績`、取引先: `Utils.normalizePartnerName(dto['取引先名'])`

9. **`showRebuildSummary_(results, dryRun)`** — 結果ダイアログ。
   - タブ別内訳、Pass 別内訳、孤立件数を改行区切りで表示
   - 孤立ファイル・パース失敗ファイルはファイル名一覧を 20 件まで列挙(それ以上は「他 N 件」)

### C. `100_config/101_sys_config.js` のメニュー追記

既存の「🔧 マイグレーション」メニュー内、MAS-154 マイグレーション等の近傍に以下を追加:

    .addItem('📎 証憑リンク一括再構築 (Drive)', 'rebuildEvidenceLinks')

## 制約
- **列インデックスは必ず `HEADERS.indexOf()` で動的取得**。固定数値のハードコード禁止
- **`DriveApp` のイテレータは必ず `.hasNext()` 判定**。truthy チェック禁止
- **`Repository.findAll().save()` で Range 直接操作禁止**(35 のみ Sheet 直接可、ただし HEADERS 経由)
- **`Date` / `String(Date)` でのソート禁止**。必ず `Utils.parseDateToYmd` 正規化文字列で比較
- **ロック未取得時は即中断**(実行中同時編集の破壊防止)
- **冪等性: 同じフォルダで再実行しても同じ結果**。URL 文字列比較で更新判定
- **仕訳振替行は処理対象外**(`仕訳ステータス === '仕訳振替'` 完全一致判定)
- **有効フラグ = FALSE の行は全処理でスキップ**(CLAUDE.md 規約)

## エッジケース
1. 既に正しい URL → 更新スキップ(文字列比較)
2. 金額 0 / 空 → Pass 1/2 除外、Pass 3 のみ対象
3. 同条件ファイル複数 → `dateCreated` 昇順、同着 `fileName` 昇順
4. 1 対 N(合算) → Pass 2 でのみ許容、構成レコード全件に同一 URL 付与
5. 孤立レコード → 未更新、`unmatchedRecords[]` 記録
6. 孤立ファイル → `orphanFiles[]` 記録
7. 略称 `UNKNOWN` → Pass 3 のみ適用
8. 31_wrk_order の YYYY-MM 粒度 → datePrecision=ym、Pass 1 スキップ
9. 仕訳振替行 → 除外
10. パース失敗ファイル → `unparsedFiles[]` 記録

## 実データ検証
- 35_wrk_receipt の HEADERS に「ファイル名」「証跡リンク」列存在(`502_receipt_reader.js:63` で確認済)
- 31/32/42 DTO に「証憑URL」列存在(`000_infra/003_contracts.js` L35, L65, L115 で確認済)
- `Utils.normalizePartnerName` は MST_PART の略称列を返す(`000_infra/004_utils.js:343` で確認済)

## 動作確認
`npm run push:dev` 後:
1. スクリプトプロパティ `EVIDENCE_FOLDER_ID` に test フォルダ ID を設定
2. MAS-152 でリネーム済の PDF をコピーして test フォルダへ配置
3. メニュー「📎 証憑リンク一括再構築 (Drive)」実行
4. **検証**: 35_wrk_receipt / 32 / 31 / 42 の該当行の URL が新ファイル ID に更新されている
5. **検証**: 更新セルが黄色背景 `#FFF9C4` になっている
6. **検証**: ダイアログに Pass 1/2/3 内訳と孤立件数が表示されている
7. **検証**: 同じ状態で再実行すると全件「スキップ」となり書き込みが発生しない(冪等性)
8. **検証**: 規約違反ファイル名(先頭 8 桁数字なし)を投入すると `unparsedFiles[]` に入る
9. **検証**: 仕訳振替行の「証憑URL」列は touch されない
10. **検証**: dryRun モード(第 2 引数指定)で実書き込みなし・件数表示のみ

### 拡張思考の使用状況

| フェーズ | 拡張思考 | 備考 |
|---------|:--------:|------|
| ファイル名パーサ実装 | あり | 両側ピール方式の正確性、略称に `_` 含む場合の境界条件 |
| Drive 再帰走査 | なし | 仕様書で .hasNext() 必須が定義済み |
| 3-pass マッチング | あり | 優先順位・グルーピング・ロックの正確な実装 |
| 各 Repository 呼び出し | なし | findAll/save パターンの定型 |
| 背景色・ダイアログ | なし | 定型作業 |

推奨実行モデル

工程推奨モデル理由
仕様書作成(本ドキュメント)Claude Opus 4.6複数タブ横断のマッチング設計、両側ピール方式、合算マッチ・ロック処理の正確性に高い推論力が必要
実装Claude Sonnet 4.6仕様書で関数シグネチャ・優先順位が確定済みだが、両側ピール方式の境界条件と 3-pass のロック伝播に中程度の判断が必要
動作確認ユーザー手動Drive への PDF 配置・スクリプトプロパティ設定・各タブの目視確認が必要

変更履歴

日付変更内容
2026-04-18初版作成

仕様書作成プロンプト(再現性・監査性のため記録)

仕様書作成プロンプト(再現性・監査性のため記録)

展開して表示
<instruction>
【タイムアウト回避・実行原則(v1.7・必ず遵守すること)】
Claude Code が Phase 2 で API ストリーム idle timeout を起こさないための装備:

1. **拡張思考の使い分け**:
   - Phase 1(設計)では拡張思考をフル活用し、ファイル名形式・エッジケース一覧・Step 分割粒度・固有名詞(関数名/シート名/列名/行番号)を完全に確定させる。
   - Phase 2(清書)の各 Step 内では拡張思考を最小限に抑え、Phase 1 で確定済みの内容の書き下しに徹する。出力途中で再考しない。

2. **テキスト報告の禁止**:
   - 「〜を作成します」等の text のみで tool_use なしに turn を終了しない。
   - 説明は 1 文以内。直ちに tool を呼ぶ。

3. **4-5 分割の Write/Edit 実行**:
   - 仕様書作成は以下の Step に分けて実行する:
     - 2-1 骨格 Write(~20行)
     - 2-2 概要〜注意事項 Edit/Bash(~300行)
     - 2-3a エッジケース〜人間検討事項 Edit/Bash(~200行)
     - 2-3b 実装プロンプト〜変更履歴 Edit/Bash(~250行)
     - 2-4 `<details>` にプロンプト全文記録 Edit/Bash(最重量・必ず独立 Step)
   - 1 回の Write/Edit は約 300 行以内を目安にする。

4. **各 Step で何を書くかを具体指示**:
   - 設計判断を Phase 2 実行時に持ち込まないよう、プロンプト内で指定された各 Step の内容(アーキテクチャ・エッジケース等)を忠実に書き下すこと。

======================================================================

あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者兼仕様書ライターです。
CLIエージェントである「Claude Code」として、上記の原則と以下のフェーズに従い、案件 I-09「証憑リンク一括再構築機能」の開発仕様書を作成してください。

## Phase 1: 実行前タスク(必読・必ずツールを使用して順次実行)
(※テキストでの状況報告は一切行わず、直ちにツールの使用を開始してください)

1. `docs/_internal/TODO_future.md` の I-09 を特定し、「案件名」「概要」「期待される効果」「人間が検討すべき事項」を把握する。
2. 前提案件の仕様書 `docs/dev/dev_mas-152_evidence_filename_rename.md` を読み、対象ファイルの命名規則(`YYYYMMDD_取引先略称_金額_*.pdf`)とフォルダ構造(`processed/YYYY-MM/`)の前提を完全に把握する。
3. `CLAUDE.md` と `docs/_internal/failure_patterns.md` を読む。
4. 影響を受けるデータアクセス層 `200_data/202_repository.js`(対象の Repository)と `000_infra/003_contracts.js`(対象 DTO)を読む。
5. 関連する定数・マスタ `000_infra/002_constants.js`、`100_config/101_sys_config.js` を読む。
6. マッチングに利用する `000_infra/004_utils.js` の日付パース (`parseDateToYm`, `parseDateToYmd`) 関数を確認する。
7. `docs/_internal/dev_spec_prompt_template.md` の Phase 2 構成と実装プロンプトフォーマットを読む。
8. ツール(MCP等)を使って、対象シート(例: `35_wrk_receipt` や仕訳帳など)の「証跡リンク」「ファイル名」列の現状や、DDLコード値と実データの乖離がないかを事前確認する。

## 既存実装の前提知識(車輪の再発明を防ぐ)
- I-08 のファイル名(YYYYMMDD_略称_金額)のパース処理は、汎用的なヘルパー関数として実装し、既存の Utils 関数群を最大限活用すること。
- シートの読み書きは Repository の `findAll()` と `save()` を使用し、直接の Range 操作は行わないこと。

## Phase 2: 仕様書の分割作成
出力先: `docs/dev/dev_mas-153_evidence_link_rebuilder.md`
**【重要】絶対に1回のツール呼び出しで全内容を出力せず、以下の Step 2-1 〜 2-4 に厳密に分割して実行してください。**

### Step 2-1: 骨格の作成 (File Write)
対象ファイルに、仕様書テンプレートに準拠した見出し(`## 概要`, `## 目的`, `## 現在のコード`, `## 修正方針` 等)の骨格のみを Write ツールで作成して保存してください。本文は空で構いません。

### Step 2-2: 前半セクションの追記 (File Edit または Bash)
「概要」「目的」「現在のコード」「修正方針」「影響範囲」「注意事項」を追記してください。以下を必ず含めること:
- **アーキテクチャの決定事項**:
  - GASの実行時間制限(6分)とDrive APIのクオータ消費を回避するため、シートの行ごとに `DriveApp.searchFiles` を呼ぶのは厳禁。必ず「対象年月フォルダのファイルを一度だけ全取得し、メモリ上に Map(キャッシュ)として保持してから照合する」設計とすること。
- **マッチングロジックの必須要件**:
  - **マッチング優先順位の明記**: 1. 完全一致(日付・取引先略称・金額) > 2. 合算完全一致 > 3. 部分一致。
  - **合算マッチの仕様**: 取引先でグルーピングし、部分集合探索方式を用いて、複数行の合計金額がファイル名の金額と一致するか判定する。
  - **マッチ成功時のロック処理(最重要)**: 一度紐付けたファイルやレコードにはメモリ上で `matched=true` フラグを立て、二重消費(1つの証憑を別の複数レコードに誤って紐付けること)を防止する。
  - **処理順序のソート**: 対象レコードを必ず「日付昇順」でソートしてから処理し、整合性を確保する。
  - **日付の比較**: Date オブジェクトの比較は必ず `Utils.parseDateToYm()` または `parseDateToYmd()` で正規化して行う。

### Step 2-3a: エッジケース〜人間検討事項の追記 (File Edit または Bash)
「エッジケース」「実データ検証」「関連ドキュメント」「人間が検討すべき事項」を追記してください。
- **エッジケース(テーブル形式で必須)**:
  1. 既に正しいリンクが設定されている場合(冪等性の確保のため更新スキップ)。
  2. 金額ゼロ、または金額が空欄のレコードの扱い(原則スキップまたは低い優先度)。
  3. 証憑ファイルが重複している(同名・同条件で `_2` などが存在する)場合、ソート順に基づき古い行から順次引き当てる。
  4. 1対多の対応(1つの合算領収書ファイルを複数の明細行に紐付ける)が正当な場合の処理。
  5. マッチする証憑ファイルがDrive上に一つも存在しない(孤立レコード)。
- **プロダクトポリシー**: Human-in-the-Loopの観点から、自動でリンクが設定・更新されたレコードに対して「確認FLG」のセットや背景色変更を行い、後から人間が目視レビュー・修正しやすいよう設計すること。
- **実データ検証**: Step 1で確認したマスタ確認・DDL乖離チェック結果を記載する。

### Step 2-3b: 実装プロンプト〜変更履歴の追記 (File Edit または Bash)
「実装プロンプト(Claude Code用)」「推奨実行モデル」「変更履歴」を追記してください。
- **実装プロンプト**: バッククォート(```)で囲まず、全行を行頭4スペースインデントで出力すること。過去の失敗パターンを踏まえた注意事項(列インデックスのハードコード禁止、GAS特有の DriveApp イテレータ `.hasNext()` の必須化など)を盛り込むこと。
- **変更履歴**: 当日の日付で「初版作成」と記載する。

### Step 2-4: 仕様書作成プロンプトの記録 (File Edit または Bash)
対象ファイルの末尾に `<details><summary>展開して表示</summary>` を設け、**この `<instruction>` タグの最初から最後まで(今あなたが読んでいるプロンプト全文)**を一言一句そのまま追記して `<details>` を閉じてください。
※この処理が最も出力トークンを消費し重いため、必ず独立したステップとして実行してください。

## Phase 3: `_config.json` への追記と構文チェック
1. `docs/_config.json` の該当箇所(例: パイプライン・RPA・外部連携)に今回の仕様書へのリンクと説明を追記して保存。
2. 保存後、ターミナルで `node -e "require('./docs/_config.json')"` 等を実行し、JSONの構文エラー(カンマ抜け、括弧の不整合など)がないか自己チェック・修正する。
</instruction>