概要

項目内容
案件IDMAS-150
カテゴリ自動入力パイプライン(予算マスタ整備の省力化)
PhasePhase 1.5
優先度P1 (★★)
所要時間3-4時間
対象ファイル500_import/504_budget_auto_proposer.js(新規作成)、500_import/501_cc_importer.js / 502_bank_importer.js / 502_receipt_reader.js(フック呼出追加)、100_config/101_sys_config.js(メニュー追記)
前提案件MAS-145(銀行CSV取込・完了済)、MAS-154(取引先略称・完了済)、MAS-146(カード自動消込
後続案件MAS-151(実績ベース中期予算自動生成)

目的

銀行CSV・カード明細・領収書を取り込んだ際、対応する 20 番台予算マスタ(23_bud_subscription / 26_bud_adhoc / 27_bud_finance 等)が存在しない明細は 「行き先なし」のまま RPAINVSTL パイプラインに乗らない。本案件ではこの隙間を塞ぐため、取込時に不足している予算マスタ行を自動で提案・起票する。

  • 取引先名 × 摘要 × 金額 を入力として、既存マスタと重複しないものだけを候補化
  • 有効フラグ=FALSE の候補行として起票し、ユーザーが金額・科目を確認のうえ TRUE に切替えて確定(Human-in-the-Loop)
  • 取引の性質(SaaS定期 / 単発経費 / 財務取引)を判定し、適切な 20 番台タブへ振り分ける

これにより、取り込んだ明細が予算マスタ未登録のまま仕訳化されない問題を解消し、経理担当者の「マスタ登録忘れ」による月次締めミスを防止する。

現在のコード

1. 既存の取込フロー(3 系統)

取込元エントリ関数出力シート
クレカ明細(JCB CSV)501_cc_importer.js:133 importCcStatement()34_wrk_card(WRK_CARD)
銀行口座 CSV(福井銀行)502_bank_importer.js:86 importBankStatement()36_wrk_bank_import → 33_wrk_bank
領収書 PDF(Gemini OCR)502_receipt_reader.js:12 importReceiptPdfs()35_wrk_receipt(WRK_RCPT)

いずれも取込後にマッチング処理を行うが、マッチ相手(予算マスタ候補)が存在しない場合は単に未マッチのまま残る。現状は経理担当者が目視で検出し、対応する予算マスタ(22_bud_headcount / 23_bud_subscription / 24_bud_capex / 25_bud_finance / 26_bud_adhoc 等)に手入力する運用。

2. 対象予算マスタ(100_config/101_sys_config.js:658-663 DDL)

キーシート主要列(マッチングに使用)
BUD_HC22_bud_headcount氏名・ポジション / 科目名 / 取引先名 / 月額給与・報酬
BUD_SUBS23_bud_subscriptionサービス・ツール名 / 費用科目 / 取引先名 / 税込金額_計画 / 契約形態
BUD_CAPEX24_bud_capex資産・契約名 / 取引先名 / 取得価額 / 償却月数
BUD_FIN25_bud_finance取引名 / 取引先名 / 科目名 / 金額 / 収支区分
BUD_ADHOC26_bud_adhoc取引名 / 取引先名 / 科目名 / 税込金額_計画 / 決済手段

本案件での自動起票対象は BUD_SUBSBUD_ADHOCBUD_FIN の 3 マスタに限定する。理由:

  • BUD_HC(人件費)は給与計算 SaaS(MAS-158 経路)で取込済、CC/銀行/領収書から新規起票するケースは稀
  • BUD_CAPEX(設備投資・借入)は金額が大きく業務的に慎重な起票が必要なため、自動提案対象外

3. 既存の科目推論ヘルパー

000_infra/004_utils.js:205Utils.aiSuggestAccount(text) が存在(Constants.ACCOUNT_RULES ベースのキーワード推論)。摘要テキストから科目名を返すか、該当なしなら空文字。本案件で再利用する。

000_infra/004_utils.js:343Utils.normalizePartnerName(rawName) は MST_PART の略称を返す(MAS-154 で導入済)。取引先名の正規化に再利用する。

4. 既存 Repository 基盤(書き込み経路の統一)

200_data/202_repository.js に登録済の Repository は 31 / 32 / 33 / 42 / 11 のみ。20 番台(予算マスタ)は未登録で、現状は Utils.getSheetByKey 経由の Sheet 直接アクセスが支配的。本案件では既存パターンを踏襲し、20 番台については Sheet 直接+HEADERS 動的取得で書き込む(ReceiptRepository 未定義と同じ判断)。

修正方針

全体像: 500_import/504_budget_auto_proposer.js を新規作成。取込エントリ関数群の末尾から proposeBudgetsFromImports_({source}) を呼び出し、ソース別に候補生成→重複除外→該当 20 番台タブへ 有効フラグ=FALSE で append する。

Step 1: 新規モジュール 504_budget_auto_proposer.js

公開 API

// 手動メニュー起動用エントリ
function proposeBudgetsFromImports(opts) { /* opts.sources = ['card', 'bank', 'receipt'] */ }

// 取込処理から呼ばれるプライベートエントリ
function proposeBudgetsFromSource_(source) { /* source: 'card' | 'bank' | 'receipt' */ }

内部ヘルパー関数群

function collectSourceRows_(source)          // WRK_CARD / WRK_BANK / WRK_RCPT から未処理行を取得
function classifyBudgetTarget_(row, source)  // { target: 'BUD_SUBS'|'BUD_ADHOC'|'BUD_FIN', confidence }
function buildExistingMasterIndex_()         // 20 番台タブ全件を Map<'取引先|科目|金額レンジ', true> で構築
function isDuplicateCandidate_(candidate, index) // 既存マスタとの重複判定
function buildBudgetCandidate_(row, target)  // 対象マスタの HEADERS に沿った DTO を構築
function appendCandidates_(target, candidates) // 有効フラグ=FALSE で append、背景色 #FFF9C4
function showProposalSummary_(results)       // 結果ダイアログ

メニュー追加(101_sys_config.js

.addItem('🔮 予算マスタ候補を提案 (I-06)', 'proposeBudgetsFromImports')

Step 2: 判定ロジック(起票先マスタ・重複除外)

起票先の判定ロジック(classifyBudgetTarget_

優先順位(排他的に 1 カテゴリを返す):

  1. BUD_FIN(財務取引):
    • 銀行 CSV の場合、摘要 or 取引先名に以下のキーワードを含む: 借入 / 返済 / 利息 / 配当 / 増資 / 減資 / 社債
    • 金額が 100 万円以上の銀行出金 → 財務候補
  2. BUD_SUBS(SaaS/定期取引):
    • 同一取引先の取引が過去 3 ヶ月で 2 回以上発生(WRK_CARD / WRK_BANK / WRK_RCPT の横断検索)
    • かつ、金額が過去 3 ヶ月で ± 10 % 以内のばらつき
  3. BUD_ADHOC(単発経費・それ以外):
    • 上記のいずれにも該当しない場合のデフォルト

ソースごとの初期バイアス:

  • 領収書 OCR: 基本 BUD_ADHOC(領収書は単発経費が大多数)。ただし定期性判定をパスしたら BUD_SUBS へ昇格
  • クレカ明細: BUD_SUBS を最優先、定期性なしなら BUD_ADHOC
  • 銀行 CSV: BUD_FIN キーワード最優先、続いて BUD_SUBS / BUD_ADHOC

重複除外ロジック(isDuplicateCandidate_

複合キー: normalizePartnerName(取引先名) + aiSuggestAccount(摘要) + 金額レンジ

  • 金額レンジは ± 10%(例: 月額 1,000 円の SaaS は 900〜1,100 円を同一候補とみなす)
  • 有効フラグ=TRUE/FALSE 問わず既存行にヒットすればスキップ(FALSE でも既に提案済みと判定)
  • 取引先名の比較は MAS-154 の略称で行う(表記ゆれ吸収)

Step 3: 既存 importer との統合と Human-in-the-Loop

統合方式

各 importer の末尾(エラーダイアログ前)に以下を挿入:

// I-06: 取込済データから予算マスタ候補を提案
try { proposeBudgetsFromSource_('card'); } catch (e) { Utils.logInfo(FUNC, '予算候補生成エラー: ' + e.message); }

エラーが発生しても取込本体の成功判定には影響させない(try/catch で握り潰し、ログのみ)。

Human-in-the-Loop の装備

  1. 有効フラグ=FALSE で起票: 自動起票された候補行は 必ず 有効フラグ=FALSE とする。ユーザーが確認して TRUE に切替えるまで RPA パイプラインには乗らない
  2. 背景色 #FFF9C4(黄色): 起票行の全列に設定。ユーザーが確認後、手動で背景色をクリアする運用(「確認済」の視覚的表現)
  3. 備考列に自動起票マーク: 備考 列に [I-06 自動起票 YYYY-MM-DD from card/bank/receipt] を付与
  4. 結果ダイアログ: 提案件数を {target}{source} 別に内訳表示。重複スキップ件数も表示
  5. dryRun モード: proposeBudgetsFromImports({ dryRun: true }) で実書き込みなし・件数のみ算出

マッチングロジックの必須要件(本案件への適用)

本案件は「取込データ → 既存予算マスタのマッチング/重複除外」を行うため、以下を踏襲する:

  • 処理順序のソート: 取込行を 発生日(or 決済日_実績)昇順 でソートしてから判定を開始(Date 比較は Utils.parseDateToYmd() で正規化・失敗パターン #17 継承)
  • マッチング優先順位: 重複判定は「1. 完全一致(取引先+科目+金額ぴったり) > 2. 合算完全一致(SaaS 分割払い等・合計が一致) > 3. 部分一致(取引先+科目のみ、金額 ± 10%)」
  • 合算マッチの仕様: 同一取引先内で部分集合探索(同月内の複数回決済を束ねて月額合計とみなす)
  • マッチ成功時のロック処理: 重複判定でヒットしたら候補側に matched=true を立て、後続パスで再評価させない(二重提案防止・失敗パターン #16 継承)
  • Date 正規化: 過去 3 ヶ月判定の日付比較は必ず Utils.parseDateToYm() で YYYY-MM 化して行う

影響範囲

変更対象変更内容変更量
500_import/504_budget_auto_proposer.js新規作成(エントリ + ヘルパー関数 7 個)~400行
500_import/501_cc_importer.jsimportCcStatement() 末尾にフック呼出 1 行+3行(try/catch 込み)
500_import/502_bank_importer.jsimportBankStatement() 末尾にフック呼出 1 行+3行
500_import/502_receipt_reader.jsimportReceiptPdfs() 末尾にフック呼出 1 行+3行
100_config/101_sys_config.jsメニュー項目 addItem 1 行追加+1行
  • 既存動作への影響: 取込処理は成功時のみフックを呼び、失敗時の分岐には影響させない(try/catch で吸収)
  • DTO / DDL への影響なし: 20 番台タブの列構成は不変
  • postProcessReceiptData_ への影響なし: 取引先名・T番号の突合補正とは独立

注意事項

  1. 20 番台タブは Repository 未定義: Sheet 直接アクセスとなるが、列インデックスは必ず HEADERS.indexOf() で動的取得。固定数値のハードコード禁止(失敗パターン #18 系)
  2. 有効フラグ=FALSE 起票の徹底: ユーザー確認前に RPA パイプラインに混入しないよう、append 後に即 setValue(FALSE) で A 列を上書き確認
  3. 重複判定のキー正規化: 取引先名は Utils.normalizePartnerName、科目は Utils.aiSuggestAccount、金額は Math.round 後 ± 10% レンジ。キー形式のズレは重複見逃しに直結する
  4. aiSuggestAccount が空文字を返す場合: 科目推論不可 → 候補から除外(科目なし候補は起票しない。科目なしでは RPA が仕訳化できないため)
  5. Constants.ACCOUNT_RULES の網羅: 新業種キーワードが追加された際は定数側を拡張。本案件では既存ルール範囲で十分(拡張は別案件)
  6. 金額ゼロ・負値: 候補生成から除外(電帳法的にも金額は正の値が原則)
  7. 取引先が MST_PART 未登録: normalizePartnerNamegenerateLogicalAbbr の fallback 結果を返す。略称が空文字にならない限り候補化を許容
  8. 過去 3 ヶ月判定の境界: 今日 - 90 日 の粗い実装ではなく、今月 - 3 ヶ月 の年月粒度(Utils.parseDateToYm + addMonths(-3))で判定
  9. GAS 実行時間: 取込のたびに実行されるため、判定ロジックは O(N × M) を避ける。既存マスタインデックスは 1 回だけ構築して再利用(最小クオータ原則)
  10. 排他制御: 複数ユーザーが同時に取込を実行すると候補が重複生成されうる。LockService.getDocumentLock().tryLock(10000) で 10 秒タイムアウトのロック取得。取得失敗時はスキップ(エラーにせず、ユーザーの取込自体は成功扱い)

エッジケース

#条件処理理由
1既存マスタに同一取引先×科目×金額の行(有効フラグ=TRUE/FALSE 問わず)が存在候補化スキップduplicates++重複提案の防止。FALSE 行も既に提案済みと判定
2取込行の金額 = 0 / 負値 / NaN候補化スキップ予算マスタは正の金額が原則
3aiSuggestAccount が空文字を返す(推論不可)候補化スキップ科目なしでは RPA 起票できないため
4normalizePartnerName が空文字を返す候補化スキップ取引先不明では消込不可能
5同一取込バッチ内に同一候補が複数回出現(例: 領収書 OCR で複数ページが同取引先同額)1 件に集約、回数を備考に付記複数ページの同一取引を重複起票しない
6取引先が定期性判定を満たす(過去 3 ヶ月で 2 回以上+金額 ±10%)BUD_SUBS へ起票SaaS / サブスクと判定
7銀行 CSV で「借入」「返済」「利息」「配当」等のキーワードを含むBUD_FIN へ起票財務取引は最優先カテゴリ
81 ファイル(領収書)に対する複数 OCR 候補の合算が既存マスタと一致Pass 2(合算マッチ)でスキップ、構成要素に matched=true合算済の案件を二重起票しない
9取込件数が 500 件超(大規模バッチ)ダイアログ警告のうえ継続、ただし GAS 実行時間監視を追加6 分制限到達時の安全なフェイルセーフ
10ユーザーが候補行を修正後に 有効フラグ=TRUE に切替背景色は手動クリア、RPA パイプラインが通常起動Human-in-the-Loop の確定アクション
11候補行の備考マーク([I-06 自動起票])付き行が既存候補化スキップ(再提案防止)冪等性の担保(何度取込を繰り返しても同じ行が生えない)
12Utils.aiSuggestAccount のルールが将来拡張され、既存 FALSE 行と異なる科目が推論された候補化スキップ(重複判定は「取引先×金額レンジ」で行い、科目は後付けの補助情報)科目推論の揺らぎで二重提案しない

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

確認項目確認方法確認結果(Phase 1 で調査済)
20 番台予算マスタの HEADERS100_config/101_sys_config.js:658-663 の DDL 定義✅ BUD_SUBS / BUD_ADHOC / BUD_FIN の列構成確認済
Utils.aiSuggestAccount の挙動000_infra/004_utils.js:205 を Read✅ Constants.ACCOUNT_RULES ベースで空文字も返り得る
Utils.normalizePartnerName の挙動000_infra/004_utils.js:343 を Read✅ MAS-154 で導入済、MST_PART 略称を返す
取込ソースの HEADERS101_sys_config.js:664-666 WRK_BANK_IMPORT / WRK_CARD / WRK_RCPT✅ 確認済(取引先名・税込金額_決済・発生日等)
既存 importer のエントリ関数500_import/501, 502, 502_receipt_reader.js を grepimportCcStatement / importBankStatement / importReceiptPdfs 特定済
LockService の利用可否GAS 標準 API✅ 利用可能(既存の他案件でも使用されている)

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

  • Constants.ACCOUNT_RULES のキーワード網羅範囲(自社で頻出する SaaS 名・取引先名がカバーされているか)
  • 既存 20 番台マスタの 有効フラグ=FALSE 行の件数(過去の提案残渣と本案件による新規提案の識別ができるか)
  • 備考 列が全 20 番台マスタに存在するか(DDL で確認:BUD_ADHOC は「備考」あり、BUD_SUBS は「備考」あり、BUD_FIN は「備考」あり・確認済)

プロダクトポリシー

Human-in-the-Loop(CLAUDE.md プロダクトポリシー準拠)

CLAUDE.md の「AI/自動処理の結果は必ず人間がレビュー・承認してから確定する」に厳密準拠する:

  1. 有効フラグ=FALSE で起票: 自動起票された候補は 必ず FALSE 固定。ユーザーが金額・科目を目視確認のうえ手動で TRUE に切替えた時点で初めて RPA パイプライン(起票ターゲット月の RPA → INV → STL)が起動
  2. 背景色 #FFF9C4(黄色): 起票行の全列に設定。ユーザーが確認後、手動でクリアするか、有効フラグ=TRUE 切替時に onEdit で自動クリアする拡張案あり(本案件では手動クリア)
  3. 備考列の自動起票マーク: [I-06 自動起票 YYYY-MM-DD from {source}] を記載。後日の監査・手戻り調査の手掛かり
  4. 結果ダイアログの詳細内訳: 提案件数・重複スキップ件数・ソース別・target 別の内訳を明示
  5. dryRun モード: proposeBudgetsFromImports({ dryRun: true }) で実書き込みなし・件数サマリのみ。ユーザーが影響範囲を事前確認できる
  6. 完全ログ: Utils.logInfo で「候補化」「重複スキップ」「起票完了」を案件 ID 付きで全件ログに記録

安全運用ルール

  • LockService.getDocumentLock().tryLock(10000) で 10 秒タイムアウトの排他ロック取得
  • ロック取得失敗時は候補化処理をスキップ(取込本体の成功判定には影響させない)
  • 既存マスタインデックスは処理開始時に 1 度だけ構築 し、候補判定フェーズでは参照のみ(書き込みは最後にまとめて append)
  • 書き込みは 20 番台タブの appendRow ではなく バッチで setValues + 背景色 setBackgrounds を使用(Drive/Sheet API コール回数を最小化)

関連ドキュメント

仕様書関連箇所
dev_mas-145_bank_csv_import.md銀行 CSV 取込の既存フロー(本案件のフック先の 1 つ)
dev_mas-146_cc_auto_settlement.mdカード自動消込(本案件と同じタイミングで動作)
dev_mas-154_partner_logical_abbr.mdUtils.normalizePartnerName の再利用
dev_mas-147_invoice_ocr_auto_posting.mdUtils.aiSuggestAccount の既存利用例
dev_mas-162_bank_combo_match.md合算マッチのパターン(部分集合探索・matched フラグ)
CLAUDE.mdHuman-in-the-Loop プロダクトポリシー、列参照ヘッダー名ベース規約、有効フラグ判定
failure_patterns.md#13-#17(マッチング設計)、#18-#20(Read 裏取り)
TODO_future.mdMAS-150 案件定義

人間が検討すべき事項

#項目詳細
1自動起票対象マスタの範囲本仕様では BUD_SUBS / BUD_ADHOC / BUD_FIN の 3 種。BUD_HC(人件費)は MAS-158(給与SaaS取込)経路のため対象外、BUD_CAPEX(設備投資)は業務的な慎重性から対象外。運用開始後に BUD_CAPEX を追加する余地あり
2定期性判定の閾値(過去 3 ヶ月・2 回以上・± 10 %)初期値。実運用で SaaS が 1 ヶ月だけ試用→解約のケースの誤検出が多ければ 4 ヶ月・3 回以上等に調整。03_sys_params でパラメータ化する選択肢あり
3金額レンジ ± 10 %クレカ決済の為替変動や月次微増 SaaS にどこまで寛容にするか。± 5% にすると重複見逃しが減るが、実質同一 SaaS の重複起票リスクが増す
4背景色クリアの自動化本仕様では手動クリア。有効フラグ=TRUE 切替時に onEdit で自動クリアする拡張案あり(別案件として分離)
5aiSuggestAccount 拡張現状ルールベースで精度限定。将来的に Gemini API で摘要から科目推論する MAS-180(OCR 精度強化)の枠組みへ置換する可能性あり
6重複判定の科目要素本仕様では「取引先×金額レンジ」のみ(科目は後付け)。より厳格に「取引先×科目×金額」で判定する運用もあり得るが、科目推論の揺らぎで二重提案を生むため非推奨
7BUD_FIN 判定キーワードの保守「借入 / 返済 / 利息 / 配当」等を Constants.FINANCE_KEYWORDS として定数化するか、Constants.ACCOUNT_RULES の該当ルールを流用するか。将来の法改正・業務変更で追加が必要な場合の保守箇所を一元化
8取込ログからの学習(将来)ユーザーが FALSE→TRUE 切替え時に修正した科目/金額を蓄積し、aiSuggestAccount 改善のフィードバックとする機構は本案件の対象外。MAS-156(運用ナビゲーター)や MAS-180 で対応予定
9メニュー手動起動 vs 自動フック本仕様では両対応。運用開始当初は「手動起動」に限定し、精度検証後に自動フックを有効化する段階導入も検討

実装プロンプト(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-150「証憑取込→20番台予算マスタ自動起票」を実装してください。

## 実行前タスク
以下のファイルを読み込み、既存パターンを把握してください:
1. `docs/dev/dev_mas-150_budget_master_auto_proposer.md` — 本仕様書
2. `500_import/501_cc_importer.js` — `importCcStatement()`(L133)の末尾フック挿入位置
3. `500_import/502_bank_importer.js` — `importBankStatement()`(L86)の末尾フック挿入位置
4. `500_import/502_receipt_reader.js` — `importReceiptPdfs()`(L12)の末尾フック挿入位置
5. `100_config/101_sys_config.js:658-663` — BUD_SUBS / BUD_ADHOC / BUD_FIN の HEADERS 定義
6. `000_infra/004_utils.js` — `aiSuggestAccount`(L205)/ `normalizePartnerName`(L343)/ `parseDateToYmd`(L108)/ `parseDateToYm`・`addMonths`
7. `000_infra/002_constants.js` — `ACCOUNT_RULES` の構造
8. `200_data/202_repository.js` — Repository パターン(20 番台は未登録のため Sheet 直接で記述)
9. `CLAUDE.md` — 列参照ヘッダー名ベース規約、Human-in-the-Loop、有効フラグ判定
10. `docs/_internal/failure_patterns.md` — #13-#17(マッチング)、#18-#20(Read 裏取り)

## 修正対象ファイル
- `500_import/504_budget_auto_proposer.js` — **新規作成**
- `500_import/501_cc_importer.js` — 末尾に try/catch + `proposeBudgetsFromSource_('card')` 3 行追加のみ
- `500_import/502_bank_importer.js` — 同様に `proposeBudgetsFromSource_('bank')` 追加のみ
- `500_import/502_receipt_reader.js` — 同様に `proposeBudgetsFromSource_('receipt')` 追加のみ
- `100_config/101_sys_config.js` — メニュー項目 `addItem` 1 行追加のみ

## 実装内容

### A. `500_import/504_budget_auto_proposer.js` の新規作成

以下の関数を定義する:

1. **`proposeBudgetsFromImports(opts)`** — メニュー起動のエントリ。
   - `opts = { sources: ['card', 'bank', 'receipt'], dryRun: boolean }`
   - LockService で 10 秒タイムアウトの排他ロック取得
   - `buildExistingMasterIndex_()` で既存マスタ 1 回だけ構築
   - sources 配列を順次処理し `proposeBudgetsFromSource_(source)` を呼出
   - `showProposalSummary_(results, dryRun)` で結果ダイアログ

2. **`proposeBudgetsFromSource_(source)`** — 取込処理から呼ばれるエントリ。
   - `collectSourceRows_(source)` で未処理行取得
   - 行ごとに `classifyBudgetTarget_` → `isDuplicateCandidate_` → `buildBudgetCandidate_` → 配列に集約
   - target 別に `appendCandidates_(target, candidates)` で一括書き込み

3. **`collectSourceRows_(source)`** — 取込元シートから候補抽出。
   - source='card' → WRK_CARD の行(確認FLG が空 or 未マッチ)
   - source='bank' → WRK_BANK_IMPORT の行(マッチ決済ID が空)
   - source='receipt' → WRK_RCPT の行(マッチ決済ID が空)
   - **必ず `Utils.parseDateToYmd` 正規化後の日付昇順でソート**

4. **`classifyBudgetTarget_(row, source)`** — 起票先判定。
   - 戻り値: `{ target: 'BUD_SUBS'|'BUD_ADHOC'|'BUD_FIN' }`
   - 優先順位: BUD_FIN キーワード判定 → BUD_SUBS 定期性判定 → BUD_ADHOC デフォルト
   - 定期性判定は WRK_CARD / WRK_BANK / WRK_RCPT を `Utils.parseDateToYm()` で月単位集計し、過去 3 ヶ月で 2 回以上かつ金額 ± 10%

5. **`buildExistingMasterIndex_()`** — 既存 20 番台タブ全件から `Map<'略称|科目|金額レンジ', true>` を構築。
   - BUD_SUBS / BUD_ADHOC / BUD_FIN / BUD_HC / BUD_CAPEX の全件(有効フラグ=TRUE/FALSE 問わず)
   - 金額レンジキーは `Math.round(amount / (amount*0.1 || 1))` でバケツ化
   - 備考列に `[I-06 自動起票]` を含む行もインデックスに含める

6. **`isDuplicateCandidate_(candidate, index)`** — 重複判定。
   - 候補の `(略称, 科目, 金額 ± 10% バケツ)` が index にヒットしたら true
   - 取引先略称は `Utils.normalizePartnerName`、科目は `Utils.aiSuggestAccount(摘要)`

7. **`buildBudgetCandidate_(row, target)`** — 対象マスタ HEADERS に沿った行配列を構築。
   - **HEADERS.indexOf で動的に列位置取得**、ハードコード禁止
   - `有効フラグ = FALSE` 固定
   - `備考 = '[I-06 自動起票 YYYY-MM-DD from ' + source + ']'`
   - 管理ID は対象マスタの既存最大連番 + 1(`BudgetRepository` 不在のため自前で走査)

8. **`appendCandidates_(target, candidates)`** — 対象マスタシートに一括 append。
   - `sheet.getRange(lastRow+1, 1, candidates.length, HEADERS.length).setValues(rows)`
   - 同 range に `setBackgrounds` で `#FFF9C4` を付与
   - 書き込みは 1 回で完結(API コール最小化)

9. **`showProposalSummary_(results, dryRun)`** — 結果ダイアログ。
   - source 別・target 別の件数、重複スキップ件数、エラー件数を表示

### B. 既存 importer へのフック挿入

各 importer のエントリ関数の **成功パス末尾**(ダイアログ表示直前)に以下を挿入:

    // MAS-150: 予算マスタ候補を提案
    try {
      proposeBudgetsFromSource_('card');  // source は 'card' / 'bank' / 'receipt' を適切に設定
    } catch (e) {
      Utils.logInfo(FUNC, 'MAS-150 予算候補生成エラー: ' + e.message);
    }

### C. メニュー追加

`100_config/101_sys_config.js` の「🔧 マイグレーション」または「🔍 消込・マッチング」メニュー内に以下を追加:

    .addItem('🔮 予算マスタ候補を提案 (MAS-150)', 'proposeBudgetsFromImports')

## 制約
- **20 番台タブは Repository 未定義**: Sheet 直接アクセス、ただし **列インデックスは必ず `HEADERS.indexOf()` で動的取得**。固定数値ハードコード禁止
- **`有効フラグ=FALSE` 固定**: append 後に A 列を `setValue(false)` で明示的に上書き確認
- **重複キー正規化**: 取引先は `normalizePartnerName`、科目は `aiSuggestAccount`、金額は ± 10% バケツ
- **Date 比較禁止**: `Utils.parseDateToYmd` / `parseDateToYm` で正規化文字列比較
- **LockService で排他制御**、失敗時は **取込本体を失敗扱いにしない**(try/catch で握り潰し、ログのみ)
- **冪等性**: 同じ取込を複数回実行しても候補は重複起票されない(重複判定で既存 FALSE 行もヒットさせる)
- **`aiSuggestAccount` が空**: 候補化スキップ(科目なし候補は作らない)
- **金額 0 / 負値**: 候補化スキップ

## エッジケース
1. 既存マスタに一致 → スキップ
2. 金額 0 / 負値 → スキップ
3. `aiSuggestAccount` 空 → スキップ
4. 取引先空 / normalizePartnerName 空 → スキップ
5. 同一バッチ内重複 → 1 件集約
6. 定期性(3 ヶ月・2 回・± 10%)→ BUD_SUBS
7. BUD_FIN キーワード → BUD_FIN
8. 合算マッチ → スキップ(二重起票防止)
9. 500 件超 → 警告のうえ継続
10. `[I-06 自動起票]` マーク付き行が既存 → スキップ(冪等性)

## 実データ検証
- BUD_SUBS / BUD_ADHOC / BUD_FIN の HEADERS(`101_sys_config.js:658-663` で確認済)
- `aiSuggestAccount`(`004_utils.js:205`)/ `normalizePartnerName`(`004_utils.js:343`)の既存関数利用
- 取込ソース HEADERS(WRK_BANK_IMPORT / WRK_CARD / WRK_RCPT、`101_sys_config.js:664-666`)

## 動作確認
`npm run push:dev` 後:
1. テスト用のクレカ明細 CSV を取込
2. メニュー「🔮 予算マスタ候補を提案 (MAS-150)」を実行
3. **検証**: 既存マスタに存在しない取引先×金額の行が 26_bud_adhoc に `有効フラグ=FALSE` + 黄色背景で起票されている
4. **検証**: 定期性がある取引(過去 3 ヶ月で 2 回以上)は 23_bud_subscription に起票されている
5. **検証**: 銀行 CSV に「借入」「返済」キーワードがある取引は 25_bud_finance に起票されている
6. **検証**: 同じ CSV を再取込しても候補は増えない(冪等性)
7. **検証**: `備考` 列に `[I-06 自動起票 YYYY-MM-DD from {source}]` が記載されている
8. **検証**: dryRun モードで実書き込みなし・件数表示のみ
9. **検証**: `有効フラグ` を TRUE に切替えると既存 RPA パイプラインで仕訳化される(手動ユーザー確認後の挙動)
10. **検証**: 取込処理本体のエラーは発生しない(MAS-150 フックで例外が出ても握り潰される)

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

| フェーズ | 拡張思考 | 備考 |
|---------|:--------:|------|
| 判定ロジック実装 | あり | BUD_FIN キーワード判定・定期性判定の境界条件の正確性 |
| 重複判定 | あり | 金額レンジバケツ化の境界条件 |
| Sheet 直接書き込み | なし | 仕様書で HEADERS.indexOf 動的取得が定義済み |
| フック挿入 | なし | 定型的な try/catch パターン |

推奨実行モデル

工程推奨モデル理由
仕様書作成(本ドキュメント)Claude Opus 4.63 系統の取込フックと 3 種類の起票先マスタ、定期性判定と重複除外の複合設計で高い推論力が必要
実装Claude Sonnet 4.6仕様書で関数シグネチャ・判定ルールが確定済みだが、金額レンジ±10%バケツ化と定期性判定の境界条件に中程度の判断が必要
動作確認ユーザー手動クレカ CSV・銀行 CSV・領収書 PDF の 3 系統に対する端末からの取込操作と、起票結果の目視確認が必要

変更履歴

日付変更内容
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-06 の開発仕様書を作成してください。

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

1. `docs/_internal/TODO_future.md` を検索し、案件 **I-06** の「案件名」「概要」「期待される効果」「人間が検討すべき事項」を特定・完全に把握する。
2. `CLAUDE.md` と `docs/_internal/failure_patterns.md` を読む。
3. 把握した要件に基づき、類似機能の仕様書(完成度の参考になるもの)を特定して読む。
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等)を使って、対象シートの DDLコード値と実データの間に乖離がないか事前確認(実データ検証)する。

## 既存実装の前提知識(車輪の再発明を防ぐ)
- シートの読み書きは直接の Range 操作を行わず、必ず Repository の `findAll()`, `save()`, `append()` を介して行うこと。
- 日付のパースや比較、取引先名などの文字列処理は、独自実装せず既存の Utils 関数群を最大限活用すること。

## Phase 2: 仕様書の分割作成
出力先: `docs/dev/dev_mas-150_*.md` (※ `*` の部分は Phase 1 で把握した案件名から英語のケバブケースで適切に命名すること)
**【重要】絶対に1回のツール呼び出しで全内容を出力せず、以下の Step 2-1 〜 2-4 に厳密に分割して実行してください。**

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

### Step 2-2: 前半セクションの追記 (File Edit または Bash)
「概要」「目的」「現在のコード」「修正方針」「影響範囲」「注意事項」を追記してください。以下を必ず含めること:
- **アーキテクチャの決定事項**:
  - 専用シートを使うか、直接パースするか等、要件に合わせたアーキテクチャ方針を明記する。
  - 既存コードとの統合方針(リファクタリングのスコープ境界)を明確にする。
- **固有の設計要件(案件の要件に合致するものを必ず適用すること)**:
  - **マッチングロジックが含まれる場合**:
    - 処理順序のソート: 対象レコードを必ず「日付昇順」でソートして整合性を確保する。
    - マッチング優先順位の明記: 1. 完全一致 > 2. 合算完全一致 > 3. 部分一致。
    - 合算マッチの仕様: 取引先でグルーピングし、部分集合探索方式を採用する。
    - マッチ成功時のロック処理(最重要): 一度紐付けたレコードにはメモリ上で `matched=true` をセットし二重消費を絶対に防止する。
    - Date オブジェクトの比較は `Utils.parseDateToYm()` 等で正規化して行う。
  - **isActualOnly 等のフィルタが関連する場合**:
    - 単純な `filterValues` ではなく、フィルタ後に小計・合計行を再計算する `filterWithRecalcTotal` を選択基準とする。

### Step 2-3a: エッジケース〜人間検討事項の追記 (File Edit または Bash)
「エッジケース」「実データ検証」「関連ドキュメント」「人間が検討すべき事項」を追記してください。
- **エッジケース(テーブル形式で必須)**:
  - 金額ゼロや空欄、重複データの対応、1対多の対応ロジックなど、要件に応じたエッジケースを網羅する。
  - **計算式がある場合**: ゼロ除算の防止、比率異常値のハンドリング、単純加算可能項目と非加算項目の区別(非加算項目は再計算)を必ず記載する。
- **プロダクトポリシー**:
  - **Human-in-the-Loop**: 自動処理されたレコードに対しては、確認FLGのセットや背景色の変更を必須とし、後から人間が目視レビュー・修正可能にする。
- **実データ検証**:
  - Phase 1(Step 8)で確認した MCP でのマスタ確認・DDL 乖離チェック結果を記載する。

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

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

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