概要

項目内容
案件IDMAS-169
カテゴリ自動入力パイプライン(仕訳行き止まりの解消・マスタ整備の省力化)
PhasePhase 1.5
優先度P1 (★★)
所要時間4-5時間
対象ファイル200_data/202_repository.js(追記: 20番台 Repository 群)、500_import/502_receipt_reader.js(取込ループ内にフック)、400_domain/407_rpa_orchestrator.jsRPA 実行前フック)、100_config/101_sys_config.js(メニュー項目)
前提案件MAS-145(銀行CSV取込・完了済)、MAS-154(取引先略称・完了済)
関連案件MAS-150(取込後の提案型)— 本仕様はその代替アプローチ(仕訳直前のインライン自動登録)

目的

証憑取込(銀行 CSV・カード明細・領収書 OCR)から仕訳を生成する過程で、対応する 20 番台予算マスタ(BUD_SUBS / BUD_ADHOC / BUD_FIN 等)が未登録だと 仕訳化処理は当該行をスキップし、「取り込んだのに仕訳に反映されない」行き止まりが発生する。本仕様では、仕訳生成ロジックの直前に 20 番台マスタの**存在チェック+自動登録(inline auto-registration)**を挿入し、行き止まりを完全に解消する。

  • ポリシー: 処理中にエラーで止めるのではなく、証憑から推測可能な最小情報で一時マスタを自動追加し、後工程に進める
  • Human-in-the-Loop: 自動追加された行は情報が不完全なため 確認 FLG=TRUE + 条件付き書式で目立たせ、ユーザーが後日レビュー・修正
  • 性能: findAsMap() の O(1) 判定 + 新規分のバルクインサートで Sheet API コールを最小化
  • 冪等性: 同一バッチ内の重複登録を メモリ Map/Set で阻止(二重消費防止ロック)

姉妹仕様書との住み分け

本仕様(registration)と dev_mas-150_budget_master_auto_proposer.mdproposer)の 2 系統は、同じ「20 番台マスタ未登録問題」への異なるアプローチ。住み分けは以下:

観点registration(本仕様)proposer(姉妹仕様)
発動タイミング仕訳生成ロジックの直前(インライン)取込完了後のバッチ処理(ポスト処理)
初期 有効フラグTRUE(即時仕訳化に乗せる)FALSE(ユーザーが明示的に有効化)
初期 確認FLGTRUE(要レビュー明示)未設定(FALSE 行自体が要レビュー状態)
重複防止メモリ Set + findAsMap の 2 段階findAsMap + ± 10% バケツ
用途行き止まり解消・自動連携重視精度重視・人の確認を強制
運用推奨シーンバッチで大量取込する月初・試算段階法定記帳・決算期の厳密運用

本仕様は proposer の上位互換ではなく補完関係。どちらをデフォルトとするかは運用ポリシー(確定起票を優先するか、確認前起票を絶対に避けるか)で決定する。

現在のコード

1. 仕訳が「行き止まり」になる経路

500_import/502_receipt_reader.js:119-146 の OCR 結果処理ループで、抽出された extracted.vendor / extracted.accrualDate / extracted.totalAmount35_wrk_receipt へそのまま書き込まれる。その後の Action AAction B → 自動仕訳生成では、12_mst_partner や 20 番台予算マスタに該当取引先が無い場合、該当行の仕訳化は行われず未処理として残る

関連コード(400_domain/407_rpa_orchestrator.js 推定・RPA オーケストレータ)では、BudgetSubscriptionRepository.findAsMap() のような O(1) マスタ検索が必要だが、20 番台マスタ向け Repository は未実装200_data/202_repository.js には 11/31/32/33/42 のみ)。

2. 既存 Repository パターン(模倣対象)

200_data/202_repository.js:304-350AccountRepositoryfindAsMap() + _cache + resetCache() の模範パターン:

var AccountRepository = {
  _getSheet: function() { return Utils.getSheetByKey('MST_ACCT', '11_mst_account'); },
  findAll: function() { return readSheetAsDtos_(AccountRepository._getSheet()); },
  findAsMap: function() {
    if (AccountRepository._cache) return AccountRepository._cache;
    // ... 有効フラグ判定後、キー: 科目名 でインデックス
    AccountRepository._cache = map;
    return map;
  },
  _cache: null,
  resetCache: function() { AccountRepository._cache = null; },
};

本仕様ではこのパターンを 20 番台マスタへ横展開する。

3. 20 番台マスタの HEADERS(100_config/101_sys_config.js:658-663

キーシート先頭列(有効フラグ)主キー項目
BUD_SUBS23_bud_subscription有効フラグ取引先名 × 費用科目 × 税抜金額_計画
BUD_ADHOC26_bud_adhoc有効フラグ取引先名 × 科目名 × 税込金額_計画
BUD_FIN25_bud_finance有効フラグ取引先名 × 科目名 × 金額 × 収支区分

全てに「確認 FLG」列は存在しないが、「備考」列に [I-06 自動登録 YYYY-MM-DD] マーカーを付与することで同等の可視化を実現。追加で条件付き書式で備考列にこのマーカーを含む行の背景色を変更(シート側設定・DDL 範囲外)。

4. Utils.normalizePartnerName000_infra/004_utils.js:343

MST_PART の略称で取引先名を正規化する MAS-154 で導入済の関数。本仕様の重複判定の中核で、表記ゆれ吸収に使う。

修正方針

全体像:

  1. Step 1: 20 番台マスタ用 Repository 群(BudgetSubscriptionRepository / BudgetAdhocRepository / BudgetFinanceRepository)を 202_repository.js に追加。各 Repo は findAll / findAsMap / append / resetCache を公開
  2. Step 2: 500_import/502_receipt_reader.js の OCR ループ内・および 400_domain/407_rpa_orchestrator.js の RPA 実行前に、MasterAutoRegistrar モジュール(新規)を呼び出す。未登録取引先をメモリ配列に集約し、バッチ末尾で 一括 append + resetCache
  3. Step 3: 自動追加行に 確認FLG 相当(備考列マーカー)+ 条件付き書式を付与し、Human-in-the-Loop を担保

Step 1: 20番台 Repository 新設(findAsMap / append / resetCache

200_data/202_repository.js 末尾に追加。AccountRepository パターンを踏襲しつつ、キーは Utils.normalizePartnerName(取引先名) に統一:

// =====================================================================
// BudgetSubscriptionRepository — 23_bud_subscription
// =====================================================================
var BudgetSubscriptionRepository = {
  _getSheet: function() { return Utils.getSheetByKey('BUD_SUBS', '23_bud_subscription'); },
  findAll: function() { return readSheetAsDtos_(BudgetSubscriptionRepository._getSheet()); },
  findAsMap: function() {
    if (BudgetSubscriptionRepository._cache) return BudgetSubscriptionRepository._cache;
    var result = BudgetSubscriptionRepository.findAll();
    var map = {};
    for (var i = 0; i < result.dtos.length; i++) {
      var dto = result.dtos[i];
      var flag = dto['有効フラグ'];
      if (flag === false || String(flag).toUpperCase() === 'FALSE') continue;
      var partnerKey = Utils.normalizePartnerName(String(dto['取引先名'] || ''));
      if (partnerKey) {
        if (!map[partnerKey]) map[partnerKey] = [];
        map[partnerKey].push(dto);
      }
    }
    BudgetSubscriptionRepository._cache = map;
    return map;
  },
  append: function(dtos) {
    var sheet = BudgetSubscriptionRepository._getSheet();
    if (!sheet || !dtos || !dtos.length) return 0;
    var headers = sheet.getRange(1, 1, 1, sheet.getMaxColumns()).getValues()[0]
      .map(function(h) { return String(h).trim(); });
    return appendDtosToSheet_(sheet, headers, dtos, 1);
  },
  _cache: null,
  resetCache: function() { BudgetSubscriptionRepository._cache = null; },
};

// BudgetAdhocRepository (26_bud_adhoc) と BudgetFinanceRepository (25_bud_finance) も同型で定義

Step 2: 取込フローへの「存在チェック→バルク未登録集約→一括 append」の組込

新規モジュール 500_import/505_master_auto_registrar.js を作成し、以下を提供:

var MasterAutoRegistrar = {

  /**
   * バッチ開始時に呼ぶ。findAsMap の結果 + 未登録集約用の pendingSet を返す
   */
  begin: function() {
    return {
      subsMap:   BudgetSubscriptionRepository.findAsMap(),
      adhocMap:  BudgetAdhocRepository.findAsMap(),
      finMap:    BudgetFinanceRepository.findAsMap(),
      pending:   { SUBS: [], ADHOC: [], FIN: [] },
      seen:      new Set(),  // キー = 'TARGET|normalizedPartner' で二重消費防止
    };
  },

  /**
   * 1 行の取込データに対して、必要なら未登録マスタを pending 配列に追加する
   * 既に存在 or 同バッチ内で追加済なら no-op
   */
  ensureMaster: function(ctx, source, row) {
    var partnerRaw = row.vendor || row.取引先名 || '';
    var partner = Utils.normalizePartnerName(partnerRaw);
    if (!partner) partner = 'UNKNOWN';  // フォールバック(エッジケース参照)

    var target = MasterAutoRegistrar._classifyTarget(source, row); // 'SUBS'|'ADHOC'|'FIN'
    var key = target + '|' + partner;

    // 二重消費防止: 同バッチ内で既に pending なら即 return
    if (ctx.seen.has(key)) return { target: target, partner: partner, skipped: 'bundled' };

    var targetMap = target === 'SUBS' ? ctx.subsMap : (target === 'FIN' ? ctx.finMap : ctx.adhocMap);
    if (targetMap[partner]) return { target: target, partner: partner, skipped: 'exists' };

    // 未登録: pending に追加
    ctx.pending[target].push(MasterAutoRegistrar._buildDefaultDto(target, partner, row));
    ctx.seen.add(key);
    return { target: target, partner: partner, added: true };
  },

  /**
   * バッチ末尾に呼ぶ。pending 配列を一括 append し、cache を reset する
   */
  commit: function(ctx) {
    var results = { SUBS: 0, ADHOC: 0, FIN: 0 };
    if (ctx.pending.SUBS.length)  { results.SUBS  = BudgetSubscriptionRepository.append(ctx.pending.SUBS); BudgetSubscriptionRepository.resetCache(); }
    if (ctx.pending.ADHOC.length) { results.ADHOC = BudgetAdhocRepository.append(ctx.pending.ADHOC);         BudgetAdhocRepository.resetCache(); }
    if (ctx.pending.FIN.length)   { results.FIN   = BudgetFinanceRepository.append(ctx.pending.FIN);           BudgetFinanceRepository.resetCache(); }
    return results;
  },

  _classifyTarget: function(source, row) {
    // BUD_FIN: 銀行 CSV 由来 + 摘要/取引先名に 借入/返済/利息/配当/増資 を含む
    // BUD_SUBS: クレカ由来 で定期性あり(本仕様では単純版=クレカはすべて SUBS へ)
    // BUD_ADHOC: 領収書 or その他
    // 判定は 姉妹仕様の proposer と同等。ここでは簡易判定のみを示す
    if (source === 'bank') {
      var memo = String(row.memo || row.摘要 || '');
      if (/借入|返済|利息|配当|増資|減資|社債/.test(memo)) return 'FIN';
      return 'ADHOC';
    }
    if (source === 'card') return 'SUBS';
    return 'ADHOC';
  },

  _buildDefaultDto: function(target, partner, row) {
    var dto = {};
    dto['有効フラグ'] = true;
    dto['取引先名'] = partner;
    dto['備考'] = '[I-06 自動登録 ' + Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'yyyy-MM-dd') + ']';
    if (target === 'SUBS') {
      dto['費用科目'] = '仮勘定_要確認';           // 既存マスタの「仮勘定_要確認」科目を既定。未整備なら管理画面から先に追加
      dto['税抜金額_計画'] = Math.round(Number(row.amount || 0) / 1.1);
      dto['消費税額_計画'] = Math.round(Number(row.amount || 0) * 0.1 / 1.1);
      dto['契約形態'] = '継続';
    } else if (target === 'ADHOC') {
      dto['科目名'] = '仮勘定_要確認';
      dto['税込金額_計画'] = Math.round(Number(row.amount || 0));
    } else if (target === 'FIN') {
      dto['科目名'] = '仮勘定_要確認';
      dto['金額']   = Math.round(Number(row.amount || 0));
      dto['収支区分'] = row.amount >= 0 ? '収入' : '支出';
    }
    return dto;
  },
};

Step 3: Human-in-the-Loop(確認FLG=TRUE 相当・背景色・既存マスタとの衝突防止)

  1. 備考マーカー [I-06 自動登録 YYYY-MM-DD] で可視化
  2. 条件付き書式(シート側の設定)で備考列にマーカーを含む行全体の背景色を #FFF9C4
  3. 有効フラグ=TRUE で起票(仕訳化を止めない)→ 姉妹仕様(proposer)との差分はここ
  4. 手動レビュー手順: ユーザーは月次締めの前にシート上で備考列の [I-06 自動登録] 行を一覧し、科目名(仮勘定_要確認)と金額を本来の値に修正。修正後に条件付き書式の色が消えるよう、備考欄のマーカーも削除する運用

取込フロー側の呼び出しパターン

500_import/502_receipt_reader.js の OCR 結果処理ループ直前に:

var registrarCtx = MasterAutoRegistrar.begin();
// ... 既存ループ内で extractedList の各行ごとに:
MasterAutoRegistrar.ensureMaster(registrarCtx, 'receipt', { vendor: extracted.vendor, amount: extracted.totalAmount });
// ... ループ終了後:
var registered = MasterAutoRegistrar.commit(registrarCtx);
Utils.logInfo(FUNC, 'I-06 自動登録: SUBS=' + registered.SUBS + ' ADHOC=' + registered.ADHOC + ' FIN=' + registered.FIN);

同様に 400_domain/407_rpa_orchestrator.js の RPA 実行前にも挿入。1 リクエスト内で複数回呼ばれる場合、begin() は 1 回のみ実行し commit() も最後 1 回に集約する。

影響範囲

変更対象変更内容変更量
200_data/202_repository.jsBudgetSubscriptionRepository / BudgetAdhocRepository / BudgetFinanceRepository 追加+150行
500_import/505_master_auto_registrar.js新規作成(MasterAutoRegistrar モジュール)~200行
500_import/502_receipt_reader.jsOCR ループに begin / ensureMaster / commit 3 箇所追加+10行
400_domain/407_rpa_orchestrator.jsRPA 実行前に同パターン 3 箇所追加+10行
100_config/101_sys_config.js条件付き書式の初期設定に [I-06 自動登録] マーカー対応を追加(optional)+数行
  • 既存動作への影響: MasterAutoRegistrar.ensureMaster は既存マスタがあれば no-op、二重消費防止 Set があるため何度呼んでも安全
  • DTO / DDL 変更なし: 20 番台マスタの列構成は不変
  • 既存 Repository(11/31/32/33/42)への影響なし: 新規 Repo は完全独立

注意事項

  1. Repository は必ず経由する: 20 番台マスタへの書き込みは新規 BudgetSubscriptionRepository.append 等を通す。Range 直接操作禁止(CLAUDE.md 規約 + 失敗パターン #7 系)
  2. findAsMap のキーは normalizePartnerName: 表記ゆれ吸収。MAS-154 導入済の関数を再利用。独自の文字列正規化を書かない
  3. バルクインサート必須: 1 バッチ内の未登録は配列に蓄積し、末尾で 1 回だけ append。1 行ずつ appendRow は Sheet API クオータを浪費するため禁止
  4. resetCache 必須: append 直後に必ずキャッシュリセット。次のループで findAsMap が古いインデックスを返すと二重追加になる
  5. 二重消費防止 Set: 同一バッチ内で同じ取引先が複数回出現しても pending に 1 回しか追加されない。key = 'TARGET|normalizedPartner'
  6. 仮勘定_要確認 科目: 11_mst_account に事前に「仮勘定_要確認」を 1 行用意しておく(運用事前整備)。未整備時はユーザーが 26_bud_adhoc 等のデフォルト科目に書き換える
  7. 列インデックスのハードコード禁止: 追加する DTO → 行配列変換は Contracts.toRow(headers, dto) 経由(003_contracts.js 既存 API)。[14] のような固定数値禁止(失敗パターン #18 系)
  8. Utils.getSheetByKey の引数: 第 2 引数にフォールバックシート名を渡す既存パターン厳守。'BUD_SUBS', '23_bud_subscription' のように
  9. LockService での排他: 取込処理本体が既にロックを取得している場合、本モジュールは追加ロック不要。単独で呼ばれる場合のみ LockService.getDocumentLock().tryLock(10000)
  10. _cache のスコープ: 各 Repository の _cache はスクリプト実行単位で有効。実行終了で自動破棄されるため手動解放は不要
  11. readSheetAsDtos_ / appendDtosToSheet_ は private: 202_repository.js ファイル内からのみ呼べる。新規 Repository を定義する際は同ファイル内で完結させる

エッジケース

#条件処理理由
1証憑から取引先名が一切取得できない(空欄)partner = UNKNOWN にフォールバックし、UNKNOWN 専用の一時マスタ行(BUD_ADHOC)へ紐付け処理の行き止まりを避ける。後で人が実名に修正できるよう備考にマーカー
2取引先名は取れたが金額が 0 / null / NaN金額 = 0 で登録、備考に 要金額確認 を付記金額不明でも取引先は見える化。後で補正可能
3必須項目(科目名など)が推論不可仮勘定_要確認 をデフォルト割り当て仕訳化を止めずに後工程へ進める。ユーザーが月次締め前に修正
4同一バッチ内に完全に同一の未登録取引先が N 件出現メモリ Set で一意化、pending 配列には 1 件のみ追加二重起票防止(最重要ロック)
5既存マスタに表記ゆれがある(「株式会社」「(株)」「全角/半角」等)Utils.normalizePartnerName で正規化後のキーで一致判定 → 重複扱いMAS-154 の略称ベース判定を踏襲。誤検知(二重登録)防止
6有効フラグ=FALSE の既存マスタと重複する取引先findAsMapFALSE 行を除外するため 未登録扱いで新規追加される(意図的挙動)FALSE は意味的に削除と同じ。新規追加で人が整理する余地を残す
7findAsMap キャッシュが append 後に古い状態で残ると二重追加commit 内で resetCache を必ず呼ぶ次の読み取りで最新を引かせる
8備考列が既に存在する行で、ユーザーが独自のメモを書いているマーカー文字列のみ末尾に追記(既存内容は破壊しない)ユーザー記入情報の保持
9_classifyTarget の判定で源泉不明の取引先BUD_ADHOC をデフォルト不確実でも単発経費として仕訳可能にする
10大量バッチ(500 件超)で GAS 6 分制限に近づくバッチ途中で commitbegin を再実行する分割モードSheet API 一括呼出し維持しつつ進捗を確実に保存
11Repository の append で Sheet 側の権限不足等の例外try/catch で捕捉、pending は保持したまま取込本体を失敗扱いにしない後から手動で MasterAutoRegistrar.commit(ctx) を再実行できるよう ctx を ScriptProperties に退避する拡張可
12同時刻に 2 名のユーザーが取込を実行取込処理本体の LockService が先に効くため、本モジュールは競合しない既存ロックの恩恵を受ける

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

確認項目確認方法確認結果
BUD_SUBS / BUD_ADHOC / BUD_FIN の HEADERS100_config/101_sys_config.js:658-663 の DDL✅ Phase 1 で確認済
各マスタの「有効フラグ」列位置全マスタで 1 列目(findAsMap の判定ロジックに一致)✅ 確認済
「仮勘定_要確認」科目の存否11_mst_account の実データ❌ 未確認。運用事前整備が必要(注意事項 6 に明記)
AccountRepository パターンの適用性202_repository.js:304-350✅ 本仕様の Repository 3 件はこのパターンを忠実に複製
Utils.normalizePartnerName の空文字挙動004_utils.js:343 を Read✅ 空入力なら空文字を返す → フォールバック UNKNOWN 必須
条件付き書式の Sheet API 対応GAS 標準機能✅ 利用可(設定は手動 or DDL 拡張)
Contracts.toRow(headers, dto) の使用可否003_contracts.js:193✅ DTO → 行配列変換は既存 API で完結

MCP で事前確認が望ましい項目(実運用前):

  • 11_mst_account に「仮勘定_要確認」科目が存在するか。無ければ先に手動追加
  • 23_bud_subscription / 26_bud_adhoc / 25_bud_finance の「備考」列の現状(独自記入の有無、マーカー追記時に破壊しないか)
  • 条件付き書式が既に設定されていないか(重複・競合の有無)

プロダクトポリシー

Human-in-the-Loop(CLAUDE.md 規約準拠)

  • 自動登録行には 確認FLG=TRUE 相当の可視化: 備考列マーカー [I-06 自動登録 YYYY-MM-DD] + 条件付き書式による黄色背景
  • 仮勘定_要確認 科目をデフォルト割り当て: 決算時に自動で検索可能。月次締め前のレビューを強制する仕組み
  • Utils.logInfo で登録件数を完全記録: 後日監査・手戻り調査の手掛かり
  • dryRun モードの提供(拡張余地): MasterAutoRegistrar.begin({ dryRun: true }) で pending 蓄積のみ・commit は何もしない
  • 結果サマリ: 取込完了ダイアログに「自動登録: SUBS=N ADHOC=M FIN=K」を表示(取込本体の成功メッセージに追記)

姉妹仕様(proposer)との切替運用

  • 本仕様のデフォルト ON: 試算・月初の大量取込時は本仕様を ON にして仕訳化を止めない
  • 決算期は proposer に切替: 精度重視で 有効フラグ=FALSE 起票の proposer を有効化し、ユーザー承認を経た行のみ仕訳化
  • 切替は Env.autoRegisterMode() (新規 ScriptProperty AUTO_REGISTER_MODE = 'registration' | 'proposer' | 'off')で行う

関連ドキュメント

仕様書関連箇所
dev_mas-150_budget_master_auto_proposer.md姉妹仕様書。同じ MAS-150 問題への別アプローチ(post-import 提案型)。本仕様との住み分けは「概要」参照
dev_mas-154_partner_logical_abbr.mdUtils.normalizePartnerName の再利用元
dev_mas-162_bank_combo_match.md合算マッチの matched フラグ継承。本仕様の「seen Set」の設計思想も同系列
dev_mas-145_bank_csv_import.md銀行 CSV 取込フローへの組込先
dev_mas-147_invoice_ocr_auto_posting.mdGemini OCR 結果処理ループへの組込例。科目推論(aiSuggestAccount)との補完関係
CLAUDE.mdRepository 経由原則、列参照ヘッダー名ベース、Human-in-the-Loop
failure_patterns.md#7(ファイル分割時の参照残存)、#16(matched フラグでの二重消費防止)、#18-#20(Read 裏取り)
TODO_future.mdMAS-150 案件定義

人間が検討すべき事項

#項目詳細
1姉妹仕様(proposer)との切替運用Env.autoRegisterMode() で実行時切替可能にするか、片方に統一するか。運用ポリシーの確定が先
2仮勘定_要確認 科目の事前整備11_mst_account に標準科目として登録する。P/L 大分類・諸表区分のマッピングも設定
3条件付き書式のシート別設定DDL として全シートに自動適用するか、手動で各シートに設定するか。MAS-134(setupAllSchemas 高速化)完了後に統合検討
4大量バッチの分割 commit 閾値本仕様では 500 件超を目安としたが、実際の API クオータと実行時間から逆算して調整。03_sys_params でパラメータ化候補
5UNKNOWN ダミーマスタの初期化23_bud_subscription / 26_bud_adhoc / 25_bud_financeUNKNOWN 取引先の行を事前作成し、自動フォールバック先を安定化する案
6自動登録行の自動クリーンアップ90 日以上レビューされなかった [I-06 自動登録] 行をアラート or 自動 FALSE 化する仕組み(将来案件)
7_classifyTarget の精緻化姉妹仕様(proposer)の定期性判定ロジックを本仕様にも移植するか、簡易版で十分か。運用開始後に評価
8RPA オーケストレータへの組込位置400_domain/407_rpa_orchestrator.js のどのフェーズで ensureMaster を呼ぶか(Action A 直前 or 各 RPA サービス内)。影響範囲が広い場合は段階的に組込
9取引先略称が重複した場合の扱いnormalizePartnerName が同じ略称を返す別法人(実名が異なる)をどう扱うか。現状は 1 行に集約されるが、実務上の区別が必要なら拡張

実装プロンプト(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-169_master_auto_registration.md` — 本仕様書
2. `docs/dev/dev_mas-150_budget_master_auto_proposer.md` — 前身の MAS-150 仕様書(住み分けを理解)
3. `200_data/202_repository.js` — 特に `AccountRepository`(L304-350)パターン。`findAsMap / _cache / resetCache` の正確な模倣が必要
4. `000_infra/003_contracts.js` — `Contracts.toRow(headers, dto)` / `toRows` の既存 API
5. `500_import/502_receipt_reader.js` — OCR ループ内(L119-146)が組込対象
6. `400_domain/407_rpa_orchestrator.js` — RPA 実行前フック対象(関数配置を Read で確認)
7. `100_config/101_sys_config.js:658-663` — BUD_SUBS / BUD_ADHOC / BUD_FIN の HEADERS
8. `000_infra/004_utils.js` — `normalizePartnerName`(L343)。独自正規化禁止
9. `CLAUDE.md` — Repository 経由原則、ヘッダー名ベース、Human-in-the-Loop
10. `docs/_internal/failure_patterns.md` — #7、#16、#18-#20

## 修正対象ファイル
- `200_data/202_repository.js` — **Repository 3 件を追加**(既存 AccountRepository の完全模倣)
- `500_import/505_master_auto_registrar.js` — **新規作成**
- `500_import/502_receipt_reader.js` — OCR ループ直前・ループ内・ループ後の 3 箇所にフック追加(try/catch で握り潰し)
- `400_domain/407_rpa_orchestrator.js` — RPA 実行前フック追加
- `100_config/101_sys_config.js` — (optional)条件付き書式の自動適用

## 実装内容

### A. `200_data/202_repository.js` への追記

`AccountRepository` の直後に以下 3 件を追加:

1. `BudgetSubscriptionRepository`:
   - `_getSheet`: `Utils.getSheetByKey('BUD_SUBS', '23_bud_subscription')`
   - `findAll / findAsMap / append / resetCache` を `AccountRepository` パターンで実装
   - `findAsMap` のキーは `Utils.normalizePartnerName(dto['取引先名'])`、値は **DTO 配列**(同一取引先の複数契約を許容)
   - 有効フラグ=FALSE の行は除外

2. `BudgetAdhocRepository`:
   - `_getSheet`: `Utils.getSheetByKey('BUD_ADHOC', '26_bud_adhoc')`
   - その他は Subs と同型

3. `BudgetFinanceRepository`:
   - `_getSheet`: `Utils.getSheetByKey('BUD_FIN', '25_bud_finance')`
   - その他は Subs と同型

### B. `500_import/505_master_auto_registrar.js` の新規作成

以下を公開する `MasterAutoRegistrar` モジュール:

1. `begin()`:
   - 3 Repository の `findAsMap()` を事前取得(キャッシュビルド)
   - ctx = `{ subsMap, adhocMap, finMap, pending: { SUBS: [], ADHOC: [], FIN: [] }, seen: new Set() }` を返す

2. `ensureMaster(ctx, source, row)`:
   - `partner = Utils.normalizePartnerName(row.vendor || row['取引先名'] || '')`
   - partner が空なら `'UNKNOWN'` へフォールバック
   - `target = _classifyTarget(source, row)` で `'SUBS' | 'ADHOC' | 'FIN'` を決定
   - `key = target + '|' + partner`
   - `ctx.seen.has(key)` なら `{ skipped: 'bundled' }` を返し即 return(二重消費防止ロック)
   - 該当 Map に `partner` があれば `{ skipped: 'exists' }` を返し return
   - `_buildDefaultDto` で DTO を構築し、`ctx.pending[target]` に push、`ctx.seen.add(key)`
   - `{ target, partner, added: true }` を返す

3. `commit(ctx)`:
   - 各 target の pending 配列が非空なら、対応 Repository の `append(pending)` を呼び、`resetCache()` を直後に呼ぶ
   - 戻り値 `{ SUBS: n, ADHOC: n, FIN: n }`(各 append 件数)

4. `_classifyTarget(source, row)`:
   - source==='bank' で摘要に 借入/返済/利息/配当/増資/減資/社債 → 'FIN'
   - source==='bank' その他 → 'ADHOC'
   - source==='card' → 'SUBS'
   - source==='receipt' or その他 → 'ADHOC'

5. `_buildDefaultDto(target, partner, row)`:
   - 共通: `有効フラグ=true`、`取引先名=partner`、`備考='[I-06 自動登録 YYYY-MM-DD]'`
   - target='SUBS': `費用科目='仮勘定_要確認'`、`税抜金額_計画 = Math.round(Number(row.amount)/1.1)`、`消費税額_計画 = Math.round(Number(row.amount)*0.1/1.1)`、`契約形態='継続'`
   - target='ADHOC': `科目名='仮勘定_要確認'`、`税込金額_計画 = Math.round(Number(row.amount))`
   - target='FIN': `科目名='仮勘定_要確認'`、`金額=Math.round(Number(row.amount))`、`収支区分 = row.amount>=0 ? '収入' : '支出'`

### C. `500_import/502_receipt_reader.js` への組込

`importReceiptPdfs()` 内、OCR 結果処理ループの直前に:

    var registrarCtx = MasterAutoRegistrar.begin();

ループ内で各 extracted を処理する際に:

    try { MasterAutoRegistrar.ensureMaster(registrarCtx, 'receipt', { vendor: extracted.vendor, amount: extracted.totalAmount }); } catch (e) { Utils.logInfo(FUNC, 'MAS-150 ensureMaster error: ' + e.message); }

ループ終了後、結果ダイアログ表示前に:

    try { var registered = MasterAutoRegistrar.commit(registrarCtx); Utils.logInfo(FUNC, 'MAS-150 自動登録: SUBS=' + registered.SUBS + ' ADHOC=' + registered.ADHOC + ' FIN=' + registered.FIN); } catch (e) { Utils.logInfo(FUNC, 'MAS-150 commit error: ' + e.message); }

### D. `400_domain/407_rpa_orchestrator.js` への組込

RPA 実行前(Action A の前)に同パターンで begin/ensureMaster/commit を呼ぶ。
複数の RPA サービスが順次動く場合、1 実行全体で `begin()` は 1 回、`commit()` も 1 回のみに集約する(各サービス内では `ensureMaster` のみを呼ぶ)。

## 制約
- **Repository 経由必須**: 20 番台マスタへの書き込みは新規 Repository の `append` のみ。Range 直接操作禁止
- **`findAsMap` のキーは `Utils.normalizePartnerName`**: 独自の文字列正規化禁止
- **バルクインサート必須**: 1 行ずつ `appendRow` 禁止。pending 配列で蓄積→末尾で 1 回 append
- **`resetCache` 必須**: `append` 直後に呼ばないと次の `findAsMap` が古いキャッシュを返す
- **`seen Set` で二重消費防止**: 同一バッチ内の重複起票を絶対に許さない
- **Contracts.toRow 経由で行配列化**: `appendDtosToSheet_` の内部で既存 API 使用。列インデックス固定数値ハードコード禁止
- **`Utils.getSheetByKey` の 2 引数パターン厳守**: キー + フォールバックシート名
- **try/catch で取込本体を失敗扱いにしない**: `ensureMaster` / `commit` のエラーは `Utils.logInfo` のみ

## エッジケース
1. partner 取得不可 → `UNKNOWN` フォールバック
2. 金額 0 / null / NaN → 金額 0 で登録、備考に `要金額確認` 付記
3. 必須項目不明 → `仮勘定_要確認` デフォルト
4. バッチ内同一未登録 N 件 → メモリ Set で一意化
5. 表記ゆれ既存マスタ → `normalizePartnerName` で一致判定し重複扱い
6. `有効フラグ=FALSE` の既存マスタ → 未登録扱いで新規追加(意図的)
7. `findAsMap` 古キャッシュ → `commit` 内で `resetCache` 必須
8. 備考列に既存記入あり → マーカー追記(既存破壊しない)
9. `_classifyTarget` 不明 → ADHOC デフォルト
10. 500 件超バッチ → 途中 `commit` → `begin` 再実行の分割モード
11. `append` 権限エラー → try/catch で取込本体成功維持、ctx 退避して後から再 commit 可
12. 同時実行 → 取込本体の LockService で競合回避

## 実データ検証
- `23_bud_subscription` / `26_bud_adhoc` / `25_bud_finance` の HEADERS 確認済(`101_sys_config.js:658-663`)
- `AccountRepository` パターン確認済(`202_repository.js:304-350`)
- `Utils.normalizePartnerName` は MAS-154 で導入済(`004_utils.js:343`)
- **`11_mst_account` に「仮勘定_要確認」科目が無ければ実装前に手動追加**

## 動作確認
`npm run push:dev` 後:
1. 領収書 PDF(未登録取引先を含む)を 2-3 件 Drive に配置
2. メニュー「📄 領収書PDFの読み込み (Drive)」実行
3. **検証**: 26_bud_adhoc に未登録取引先の行が `有効フラグ=TRUE`、`科目名='仮勘定_要確認'`、備考に `[I-06 自動登録 YYYY-MM-DD]` で追加されている
4. **検証**: 同一バッチ内に同じ取引先が複数回出現しても 1 行しか追加されない(Set 動作)
5. **検証**: 既存マスタに表記ゆれがある取引先(「株式会社X」「X株式会社」)は正規化で一致判定され、新規追加されない
6. **検証**: 金額不明の領収書は金額 0 で登録、備考に `要金額確認` 付記
7. **検証**: `Utils.logInfo` に `I-06 自動登録: SUBS=n ADHOC=n FIN=n` が記録されている
8. **検証**: 同取込を再実行しても新規追加行は 0(冪等性、`findAsMap` の `resetCache` 動作確認)
9. **検証**: 銀行 CSV 取込で「借入」「返済」キーワードを含む取引は 25_bud_finance に起票
10. **検証**: 取込本体(502_receipt_reader.js)がエラーなく完走、結果ダイアログに自動登録件数が表示される

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

| フェーズ | 拡張思考 | 備考 |
|---------|:--------:|------|
| Repository 3 件の追加 | なし | AccountRepository の定型複製 |
| `ensureMaster` + `seen Set` 実装 | あり | 二重消費防止ロックの境界条件 |
| `_buildDefaultDto` の税抜計算 | あり | 税抜・消費税の整数丸めと誤差吸収 |
| 取込フローへの 3 箇所フック挿入 | なし | 仕様書で挿入位置が確定済み |

推奨実行モデル

工程推奨モデル理由
仕様書作成(本ドキュメント)Claude Opus 4.6Repository 基盤の拡張 + 二重消費防止ロック + 姉妹仕様との住み分けの複合設計
実装Claude Sonnet 4.6仕様書で関数シグネチャ・判定ルールが確定済みだが、既存 AccountRepository の忠実な複製と seen Set の境界条件に中程度の判断が必要
動作確認ユーザー手動領収書 PDF・銀行 CSV の取込操作と 20 番台マスタへの自動追加結果の目視確認が必要

変更履歴

日付変更内容
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「証憑→20番台マスタ自動起票 予算マスタ未登録による仕訳行き止まりの解消」の開発仕様書を作成してください。

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

1. `docs/_internal/TODO_future.md` を検索し、案件 **I-06** の「案件名」「概要」「期待される効果」「人間が検討すべき事項」を特定・完全に把握する。
2. `CLAUDE.md` と `docs/_internal/failure_patterns.md` を読む。
3. 証憑読み取りや仕訳作成の起点となる既存ロジック(`500_import/502_receipt_reader.js` または該当するインポート処理)を読む。
4. 影響を受けるデータアクセス層(`200_data/202_repository.js` の20番台マスタ・予算関連の Repository、および `000_infra/003_contracts.js` の関連 DTO)を読む。
5. 関連する定数・マスタ定義(`000_infra/002_constants.js`、`100_config/101_sys_config.js`)を読み、20番台マスタの必須項目を確認する。
6. `000_infra/004_utils.js` にある `normalizePartnerName` 等の文字列正規化ロジックを確認する。
7. `docs/_internal/dev_spec_prompt_template.md` の Phase 2 構成と実装プロンプトフォーマットを読む。
8. ツール(MCP等)を使って、対象となる20番台マスタシートの DDLコード値と実データの必須カラム・デフォルト値・フラグ列の状況を事前確認(実データ検証)する。

## 既存実装の前提知識(車輪の再発明を防ぐ)
- マスタの読み書きは直接の Range 操作を行わず、必ず Repository の `findAsMap()`, `findAll()`, `append()` を介して行うこと。
- 新規追加のマスタデータは、都度 `append()` するのではなく配列に蓄積し、一括で書き込むバルクインサートの仕組みを利用すること。
- マスタのマッチング判定(取引先名など)は `Utils.normalizePartnerName()` 等で正規化した値を用いて行うこと。

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

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

### Step 2-2: 前半セクションの追記 (File Edit または Bash)
「概要」「目的」「現在のコード」「修正方針」「影響範囲」「注意事項」を追記してください。以下を必ず含めること:
- **アーキテクチャの決定事項**:
  - 証憑解析後、仕訳データに変換するロジックの直前に「20番台マスタの存在チェック」を挟み込む。
  - 存在チェックには Repository の `findAsMap()` またはインメモリキャッシュを用いて、O(1) で高速に判定を行う。
  - 未登録マスタが検出された場合、エラーとして処理を止める(行き止まり)のではなく、証憑データから推測可能な情報を元に新規DTOを生成し、メモリ上の「新規追加用配列」に保持する。
  - **重複登録防止(二重消費防止ロック)**: 同一バッチ(複数行の証憑データ)内で同じ未登録取引先が複数回出現した場合、2回目以降はメモリ上の「新規追加用配列(または Set/Map)」を参照し、二重起票を完全に防止する。
  - 処理の最後に Repository の `append()` を用いてマスタシートへ一括書き込み(バルクインサート)を行う。
  - マスタ書き込み直後に Repository のキャッシュリセット(`resetCache`等)を行い、後続の仕訳作成処理で最新のマスタを引けるようにする。

### Step 2-3a: エッジケース〜人間検討事項の追記 (File Edit または Bash)
「エッジケース」「実データ検証」「関連ドキュメント」「人間が検討すべき事項」を追記してください。
- **エッジケース(テーブル形式で必須)**:
  1. 証憑から取引先名や品目名が一切取得できない(空欄・UNKNOWN)場合のフォールバック(例:特定の「不明」ダミーマスタへ紐付け、または起票をスキップ)。
  2. マスタの必須項目(勘定科目・税区分など)が証憑から決定できない場合のデフォルト値設定ルール(例:仮払金や未分類勘定の一時割り当て)。
  3. 処理対象の証憑内に、完全に同一の未登録取引先が複数存在した場合の重複回避(インメモリでの一意化)。
  4. 既存マスタに「表記ゆれ」(全角半角、株式会社の有無)が存在する場合、`Utils.normalizePartnerName()` による正規化比較を用いた誤検知(二重登録)の防止。
- **プロダクトポリシー(Human-in-the-Loop)**:
  - 自動起票されたマスタレコードは情報が不完全(デフォルト勘定科目など)であるため、必ず「確認FLG=TRUE」をセットする。
  - また、必要に応じて背景色を変更する条件付き書式の対象にすることで、後から人間が目視レビューし、正しい勘定科目や部門への修正・フラグ解除を行える運用フローを必須とする。
- **実データ検証**:
  - Phase 1(Step 8)で確認したマスタの必須カラムや DDL 乖離チェック結果を記載する。

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

### 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>