MAS-169: 証憑→20番台マスタ自動起票(仕訳直前インライン自動登録 / MAS-150 派生)
概要
| 項目 | 内容 |
|---|---|
| 案件ID | MAS-169 |
| カテゴリ | 自動入力パイプライン(仕訳行き止まりの解消・マスタ整備の省力化) |
| Phase | Phase 1.5 |
| 優先度 | P1 (★★) |
| 所要時間 | 4-5時間 |
| 対象ファイル | 200_data/202_repository.js(追記: 20番台 Repository 群)、500_import/502_receipt_reader.js(取込ループ内にフック)、400_domain/407_rpa_orchestrator.js(RPA 実行前フック)、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.md(proposer)の 2 系統は、同じ「20 番台マスタ未登録問題」への異なるアプローチ。住み分けは以下:
| 観点 | registration(本仕様) | proposer(姉妹仕様) |
|---|---|---|
| 発動タイミング | 仕訳生成ロジックの直前(インライン) | 取込完了後のバッチ処理(ポスト処理) |
初期 有効フラグ | TRUE(即時仕訳化に乗せる) | FALSE(ユーザーが明示的に有効化) |
初期 確認FLG | TRUE(要レビュー明示) | 未設定(FALSE 行自体が要レビュー状態) |
| 重複防止 | メモリ Set + findAsMap の 2 段階 | findAsMap + ± 10% バケツ |
| 用途 | 行き止まり解消・自動連携重視 | 精度重視・人の確認を強制 |
| 運用推奨シーン | バッチで大量取込する月初・試算段階 | 法定記帳・決算期の厳密運用 |
本仕様は proposer の上位互換ではなく補完関係。どちらをデフォルトとするかは運用ポリシー(確定起票を優先するか、確認前起票を絶対に避けるか)で決定する。
現在のコード
1. 仕訳が「行き止まり」になる経路
500_import/502_receipt_reader.js:119-146 の OCR 結果処理ループで、抽出された extracted.vendor / extracted.accrualDate / extracted.totalAmount は 35_wrk_receipt へそのまま書き込まれる。その後の Action A → Action 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-350 の AccountRepository が findAsMap() + _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_SUBS | 23_bud_subscription | 有効フラグ | 取引先名 × 費用科目 × 税抜金額_計画 |
BUD_ADHOC | 26_bud_adhoc | 有効フラグ | 取引先名 × 科目名 × 税込金額_計画 |
BUD_FIN | 25_bud_finance | 有効フラグ | 取引先名 × 科目名 × 金額 × 収支区分 |
全てに「確認 FLG」列は存在しないが、「備考」列に [I-06 自動登録 YYYY-MM-DD] マーカーを付与することで同等の可視化を実現。追加で条件付き書式で備考列にこのマーカーを含む行の背景色を変更(シート側設定・DDL 範囲外)。
4. Utils.normalizePartnerName(000_infra/004_utils.js:343)
MST_PART の略称で取引先名を正規化する MAS-154 で導入済の関数。本仕様の重複判定の中核で、表記ゆれ吸収に使う。
修正方針
全体像:
- Step 1: 20 番台マスタ用 Repository 群(
BudgetSubscriptionRepository/BudgetAdhocRepository/BudgetFinanceRepository)を202_repository.jsに追加。各 Repo はfindAll / findAsMap / append / resetCacheを公開 - Step 2:
500_import/502_receipt_reader.jsの OCR ループ内・および400_domain/407_rpa_orchestrator.jsの RPA 実行前に、MasterAutoRegistrarモジュール(新規)を呼び出す。未登録取引先をメモリ配列に集約し、バッチ末尾で 一括 append + resetCache - 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 相当・背景色・既存マスタとの衝突防止)
- 備考マーカー
[I-06 自動登録 YYYY-MM-DD]で可視化 - 条件付き書式(シート側の設定)で備考列にマーカーを含む行全体の背景色を
#FFF9C4 有効フラグ=TRUEで起票(仕訳化を止めない)→ 姉妹仕様(proposer)との差分はここ- 手動レビュー手順: ユーザーは月次締めの前にシート上で備考列の
[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.js | BudgetSubscriptionRepository / BudgetAdhocRepository / BudgetFinanceRepository 追加 | +150行 |
500_import/505_master_auto_registrar.js | 新規作成(MasterAutoRegistrar モジュール) | ~200行 |
500_import/502_receipt_reader.js | OCR ループに begin / ensureMaster / commit 3 箇所追加 | +10行 |
400_domain/407_rpa_orchestrator.js | RPA 実行前に同パターン 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 は完全独立
注意事項
- Repository は必ず経由する: 20 番台マスタへの書き込みは新規
BudgetSubscriptionRepository.append等を通す。Range 直接操作禁止(CLAUDE.md 規約 + 失敗パターン #7 系) findAsMapのキーはnormalizePartnerName: 表記ゆれ吸収。MAS-154導入済の関数を再利用。独自の文字列正規化を書かない- バルクインサート必須: 1 バッチ内の未登録は配列に蓄積し、末尾で 1 回だけ
append。1 行ずつappendRowは Sheet API クオータを浪費するため禁止 resetCache必須:append直後に必ずキャッシュリセット。次のループでfindAsMapが古いインデックスを返すと二重追加になる- 二重消費防止 Set: 同一バッチ内で同じ取引先が複数回出現しても pending に 1 回しか追加されない。
key = 'TARGET|normalizedPartner' 仮勘定_要確認科目:11_mst_accountに事前に「仮勘定_要確認」を 1 行用意しておく(運用事前整備)。未整備時はユーザーが 26_bud_adhoc 等のデフォルト科目に書き換える- 列インデックスのハードコード禁止: 追加する DTO → 行配列変換は
Contracts.toRow(headers, dto)経由(003_contracts.js既存 API)。[14]のような固定数値禁止(失敗パターン #18 系) Utils.getSheetByKeyの引数: 第 2 引数にフォールバックシート名を渡す既存パターン厳守。'BUD_SUBS', '23_bud_subscription'のように- LockService での排他: 取込処理本体が既にロックを取得している場合、本モジュールは追加ロック不要。単独で呼ばれる場合のみ
LockService.getDocumentLock().tryLock(10000) _cacheのスコープ: 各 Repository の_cacheはスクリプト実行単位で有効。実行終了で自動破棄されるため手動解放は不要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 の既存マスタと重複する取引先 | findAsMap は FALSE 行を除外するため 未登録扱いで新規追加される(意図的挙動) | FALSE は意味的に削除と同じ。新規追加で人が整理する余地を残す |
| 7 | findAsMap キャッシュが append 後に古い状態で残ると二重追加 | commit 内で resetCache を必ず呼ぶ | 次の読み取りで最新を引かせる |
| 8 | 備考列が既に存在する行で、ユーザーが独自のメモを書いている | マーカー文字列のみ末尾に追記(既存内容は破壊しない) | ユーザー記入情報の保持 |
| 9 | _classifyTarget の判定で源泉不明の取引先 | BUD_ADHOC をデフォルト | 不確実でも単発経費として仕訳可能にする |
| 10 | 大量バッチ(500 件超)で GAS 6 分制限に近づく | バッチ途中で commit → begin を再実行する分割モード | Sheet API 一括呼出し維持しつつ進捗を確実に保存 |
| 11 | Repository の append で Sheet 側の権限不足等の例外 | try/catch で捕捉、pending は保持したまま取込本体を失敗扱いにしない | 後から手動で MasterAutoRegistrar.commit(ctx) を再実行できるよう ctx を ScriptProperties に退避する拡張可 |
| 12 | 同時刻に 2 名のユーザーが取込を実行 | 取込処理本体の LockService が先に効くため、本モジュールは競合しない | 既存ロックの恩恵を受ける |
実データ検証(事前確認項目)
| 確認項目 | 確認方法 | 確認結果 |
|---|---|---|
BUD_SUBS / BUD_ADHOC / BUD_FIN の HEADERS | 100_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()(新規 ScriptPropertyAUTO_REGISTER_MODE = 'registration' | 'proposer' | 'off')で行う
関連ドキュメント
| 仕様書 | 関連箇所 |
|---|---|
| dev_mas-150_budget_master_auto_proposer.md | 姉妹仕様書。同じ MAS-150 問題への別アプローチ(post-import 提案型)。本仕様との住み分けは「概要」参照 |
| dev_mas-154_partner_logical_abbr.md | Utils.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.md | Gemini OCR 結果処理ループへの組込例。科目推論(aiSuggestAccount)との補完関係 |
| CLAUDE.md | Repository 経由原則、列参照ヘッダー名ベース、Human-in-the-Loop |
| failure_patterns.md | #7(ファイル分割時の参照残存)、#16(matched フラグでの二重消費防止)、#18-#20(Read 裏取り) |
| TODO_future.md | MAS-150 案件定義 |
人間が検討すべき事項
| # | 項目 | 詳細 |
|---|---|---|
| 1 | 姉妹仕様(proposer)との切替運用 | Env.autoRegisterMode() で実行時切替可能にするか、片方に統一するか。運用ポリシーの確定が先 |
| 2 | 仮勘定_要確認 科目の事前整備 | 11_mst_account に標準科目として登録する。P/L 大分類・諸表区分のマッピングも設定 |
| 3 | 条件付き書式のシート別設定 | DDL として全シートに自動適用するか、手動で各シートに設定するか。MAS-134(setupAllSchemas 高速化)完了後に統合検討 |
| 4 | 大量バッチの分割 commit 閾値 | 本仕様では 500 件超を目安としたが、実際の API クオータと実行時間から逆算して調整。03_sys_params でパラメータ化候補 |
| 5 | UNKNOWN ダミーマスタの初期化 | 23_bud_subscription / 26_bud_adhoc / 25_bud_finance に UNKNOWN 取引先の行を事前作成し、自動フォールバック先を安定化する案 |
| 6 | 自動登録行の自動クリーンアップ | 90 日以上レビューされなかった [I-06 自動登録] 行をアラート or 自動 FALSE 化する仕組み(将来案件) |
| 7 | _classifyTarget の精緻化 | 姉妹仕様(proposer)の定期性判定ロジックを本仕様にも移植するか、簡易版で十分か。運用開始後に評価 |
| 8 | RPA オーケストレータへの組込位置 | 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.6 | Repository 基盤の拡張 + 二重消費防止ロック + 姉妹仕様との住み分けの複合設計 |
| 実装 | 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>