MAS-153: ファイル名ベースの証憑リンク一括再構築機能
概要
| 項目 | 内容 |
|---|---|
| 案件ID | MAS-153 |
| カテゴリ | 自動入力パイプライン(運用ツール) |
| Phase | Phase 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:63HEADERS 配列)
列名が 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.jsのEnv.receiptFolderId()/Env.setReceiptFolderId()が既存(RECEIPT_FOLDER_IDプロパティ)。同パターンでEVIDENCE_FOLDER_ID用の関数を追加する100_config/101_sys_config.jsにメニュー定義が集約されている
修正方針
全体像: 800_ops/809_evidence_link_rebuilder.js を新規作成し、rebuildEvidenceLinks() をメニュー起動のエントリポイントとする。実行フローは以下の 3 段階:
- Step 1: 環境設定とファイル名パーサ —
Env.evidenceFolderId()の追加、MAS-152 ファイル名を解析するヘルパー関数、マッチングキー(YYYYMMDD|略称|金額)の生成 - Step 2: Drive 走査とファイル名マップ構築 —
EVIDENCE_FOLDER_ID配下を再帰走査し、メモリ上にMap<key, FileEntry[]>を一度だけ構築 - 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+$を金額として抽出する両側からのピール方式を採用
- 対策: ファイル名先頭から 8 桁数字+
- 書き込みは Repository 経由: 31 / 32 / 42 は
Repository.findAll()→ DTO 更新 →Repository.save()の往復で行う(Range 直接操作禁止)。35 は専用 Repository が未定義のため、HEADERS.indexOfによる動的列判定で当該セルのみsetValue更新(ハードコード禁止)
マッチングロジックの必須要件
マッチング優先順位(厳守)
- 完全一致: ファイル名の
(YYYYMMDD, 略称, 金額)とレコードの(発生日YYYYMMDD, 正規化後取引先名, 税込金額)が全一致 - 合算完全一致: 同一取引先・同一年月内の複数レコードの税込金額合計がファイルの金額と一致(合算領収書対応)
- 部分一致:
(略称, 金額)のみ一致し、日付が ± 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.js | evidenceFolderId() / setEvidenceFolderId() 追加 | +10行 |
100_config/101_sys_config.js | メニュー項目 addItem 1 行追加 | +1行 |
- 既存動作への影響なし: MAS-152 の領収書取込フロー(
502_receipt_reader.js)は非改修 - DTO / DDL への影響なし: 列構成は不変、既存列(
証憑URL/証跡リンク)の値更新のみ postProcessReceiptData_への影響なし: 取引先名・T番号の突合補正とは独立
注意事項
DriveAppのイテレータは必ず.hasNext()で判定:folder.getFiles()/folder.getFolders()/folder.getFoldersByName()は FolderIterator / FileIterator を返す。truthy チェックで空判定してはならない- ファイル名パースは両側ピール方式: 単純な
split('_')は略称に_を含む場合(OCR 由来の Drive 禁止文字置換)に壊れる。先頭から日付を、末尾から拡張子+元ファイル名を剥がし、残りから金額を抽出する - 列インデックスのハードコード禁止: 35_wrk_receipt の「ファイル名」「証跡リンク」列は
HEADERS.indexOf()で動的取得、-1 なら即エラー(失敗パターン #18 系) - 列名の 2 種混在: 31/32/42 は
証憑URL、35 は証跡リンク。共通マッチング関数には列名を引数で渡す設計にする - GAS 実行時間 6 分制限: ファイル数が数千件規模なら
ScriptApp.newTriggerで分割実行も検討(本仕様では 1 回完結が前提だが、件数閾値を設けて警告) - Drive API クオータ: 1 日のコール数を抑えるため、1 ファイル = 1 回の getId/getName/getUrl 呼び出しのみ。キャッシュ構築時に
{fileId, fileName, driveLink}を一括採取してファイルオブジェクトは以降参照しない getFoldersByNameで同名フォルダ重複時:processed/YYYY-MM/配下に同年月フォルダが重複すると最初の 1 つのみ処理される。DriveApp の仕様どおりの挙動として許容、ログで警告のみ- 権限不足のファイル:
file.getUrl()やfile.getId()で権限エラーが起きた場合は try/catch で捕捉し、unmatchedFilesに記録して処理継続 - 金額ゼロのファイル: MAS-152 でリネームした金額 0 のファイル(
YYYYMMDD_略称_0_...)はマッチングキーに含めるが、レコード側の金額 0 とは Pass 3(部分一致)でのみ紐付けを許容 - 冪等性: 同じフォルダで再実行しても同じ結果になること(ファイル ID が変わらない限り「変更なしスキップ」で終わる)。URL 文字列比較で判定する
- Date 正規化: ソートキー・マッチングキーで
DateやString(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 昇順) | ソート順による決定的割当で再実行の冪等性を担保 |
| 4 | 1 ファイル対 N レコード(合算領収書が正当) | Pass 2(合算完全一致)でのみ許容。合計金額一致時に構成レコード全てへ同一リンクを付与 | 合算領収書の実務運用に対応(失敗パターン #15) |
| 5 | マッチする証憑ファイルが Drive 上に存在しない(孤立レコード) | レコードは未更新で残す、unmatchedRecords[] に記録し最終ダイアログで件数表示 | 手動対応が必要な案件を人間に可視化 |
| 6 | 逆の孤立(ファイルはあるがどのレコードとも一致しない) | orphanFiles[] に記録、ダイアログでファイル名一覧を出力 | MAS-152 以前の古い命名や、手動アップロードの漏れを検出 |
| 7 | 略称が UNKNOWN(MAS-152 で vendor 欠落) | マッチングキーに UNKNOWN を含める。同じく UNKNOWN の孤立レコードがあれば Pass 3 のみ適用 | 低信頼マッチの誤爆防止 |
| 8 | 31_wrk_order の日付が「開始年月」(YYYY-MM 粒度) | YYYY-MM 単位で Pass 2/3 マッチを試行(Pass 1 は適用不可) | 発注は月単位で記録されるため日付粒度が異なる |
| 9 | 42_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 / parseDateToYmd | 000_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/自動処理の結果は必ず人間がレビュー・承認してから確定する」)に準拠し、以下の可視化装備を実装する:
- 更新セルへの背景色付与: リンクを更新した「証憑URL」「証跡リンク」セルに 薄黄色(
#FFF9C4)の背景色 を設定。ユーザーが目視でスポットチェックし、問題なければ手動で背景色をクリア(運用で「確認済」を表現) - 実行後ダイアログで件数サマリ表示: 以下をダイアログ化
- 更新: N 件(黄色ハイライト中)
- スキップ(既に正しい): M 件
- Pass 1 完全一致 / Pass 2 合算 / Pass 3 部分一致 の内訳
- 孤立レコード: O 件(リンクなしのままの行)
- 孤立ファイル: P 件(どのレコードにも紐付かなかったファイル名一覧)
- パース失敗ファイル: Q 件(MAS-152 未適用ファイル)
Utils.logInfoへの完全ログ: 全更新・スキップ・孤立を案件 ID 付きでログ残し。後日の監査や問い合わせ対応に備える- 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.md | Utils.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.md | MAS-153 案件定義・MAS-152 との依存関係 |
人間が検討すべき事項
| # | 項目 | 詳細 |
|---|---|---|
| 1 | 同名ファイル複数時の優先ルール | 本仕様では「作成日時昇順、同着ならファイル名昇順」で古い順に引き当て。TODO_future.md の記載(最新日時 or エラー)と方針が異なる。実運用で検証し必要に応じて調整 |
| 2 | 会計士共有フォルダの権限設定 | 共有先を「閲覧のみ」にすればファイル ID は変わらずコピーも発生しない(この場合は本機能不要)。「編集可」や別 Drive アカウントへコピーする運用なら ID が変わるため本機能が必要 |
| 3 | EVIDENCE_FOLDER_ID 切替 UX | 社内用/会計士共有用を頻繁に切り替える場合、ダイアログから選択式にする方が運用しやすい。初版はプロパティ書き換えのみ(将来 UI 化の余地あり) |
| 4 | ReceiptRepository 新設の是非 | 35_wrk_receipt は現状 Sheet 直接アクセスのみ。他案件でも Receipt への書き込みが増えるなら 202_repository.js に ReceiptRepository を新設する選択肢あり(本案件では既存パターン維持) |
| 5 | 1:N マッチ(合算)の適用範囲 | 32_wrk_invoice の INV 複数行への 1 ファイル紐付けは正当だが、31_wrk_order(発注単位)への合算マッチは業務意味が薄い。タブごとに Pass 2 の有効/無効を切替可能にする設計にするか検討 |
| 6 | Pass 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>