概要

項目内容
案件IDMAS-180
カテゴリ外部連携 / AI (OCR 強化)
PhaseP2
優先度★★
所要時間2-3 時間
対象ファイル500_import/502_receipt_reader.js(関数追加)/ 100_config/101_sys_config.jssetupAllSchemas への WRK_OCRQ スキーマ追加・01_sys_config キー登録)/ 000_infra/002_constants.jsConstants.SHEET_DEFAULTS / Constants.ID_PREFIX_MAP / Constants.MENU_DEFINITION への追記)
前提案件MAS-147(請求書 PDF→INV 自動起票・v2 仕様書完了)/ MAS-154(取引先略称自動生成・完了)/ MAS-157(写真 OCR・仕様書完了)/ MAS-216(Vertex AI 移行・仕様書完了)
関連案件MAS-161(OCR 結果の手動補正アシスト)/ MAS-152(電帳法リネーム)/ MAS-203(プロンプトライブラリ版管理)

本仕様書は TODO_future.md §5.1 No.11「MAS-180 OCR 精度強化」(docs/_internal/TODO_future.md L437)の実装仕様である。

目的

証憑画像(PDF/JPEG/PNG)から Gemini API(500_import/502_receipt_reader.js L10-14 の RECEIPT_GEMINI_PROMPT_)で抽出した OCR 結果を、信頼度判定を経て InvoiceRepository.append()200_data/202_repository.js L185-207)経由で 32_wrk_invoice に自動起票するパイプラインを新設する。低信頼度レコードは新規シート 36_wrk_ocr_queue(システムキー WRK_OCRQ)に「要確認」ステータスで蓄積し、人間がレビュー・修正したうえで手動メニューから起票する Human-in-the-Loop(HitL) 設計とする。これにより現行 importReceiptPdfs()(L22-185)が 35_wrk_receipt への転記で止まっている情報フローを、INV 起票まで直結させ、手動 INV 作成工数を削減する。

現在のコード

現行の証憑 OCR 取込(500_import/502_receipt_reader.js)の主要関数と責務範囲は以下のとおり。Phase 1 で ls 500_import/ にて 501_cc_importer.js / 502_bank_importer.js / 502_receipt_reader.js / 504_invoice_importer.js の 4 ファイル実在を確認済み。502 番が重複しているが、本案件の拡張対象は 502_receipt_reader.js(領収書/請求書 PDF)である

関数行番号役割備考
importReceiptPdfs()L22-185メニュー起点。Drive フォルダの PDF を 1 件ずつ Gemini API で抽出し、35_wrk_receipt シートに書込むINV 起票は行わない
callGeminiForReceipt_(apiKey, base64, fileName)L194-269Google AI Studio 直叩きで Gemini 2.5-flash を呼び出し。429/503 Exponential Backoff(最大 4 回)実装済MAS-216 で Vertex AI 並行稼働化
parseGeminiReceiptText_(text)L278-323Gemini レスポンスから JSON 配列を抽出。ブラケット不整合の自動補修機能あり共通ヘルパー
postProcessReceiptData_(rcpSheet)L330-403同一取引先の T 番号・住所を多数決で補正(2 件以上の一致で採用)本案件と独立
callGeminiForReceiptOnVertex_(base64, fileName)L416-521Vertex AI 経由版。asia-northeast1us-east5 フォールバック実装済MAS-216 並行稼働

出力先は 35_wrk_receiptWRK_RCPT スキーマ:100_config/101_sys_config.js L898 で定義)のみで、Gemini の抽出結果は以下のフィールドで保持される:管理IDRCP_NNNN)/処理日時証憑種別取引先名Utils.normalizePartnerName() 適用済)/🏢住所税込金額_決済税抜金額_決済消費税額_決済源泉税額T番号帳票番号発行日発生日(P/L計上日)決済日_実績決済手段摘要ファイル名証跡リンク

本案件の課題: 35_wrk_receipt は「読み取ったデータの置き場」に留まり、INV 起票は人手でトリガされる(transferReceiptToAdhoc 等の別メニュー経由)。OCR 結果の信頼度判定ロジックも未実装で、金額不整合(税抜+消費税 ≠ 税込)や取引先マスタ未ヒットの判別も手動目視に依存している。

修正方針

本案件では 502_receipt_reader.js を拡張する形で、OCR → 信頼度判定 → 自動/確認キュー振り分け → INV 起票 の 3 Step を実装する。既存 importReceiptPdfs() は変更せず、新規関数を追加する方式で後方互換性を保つ。

Step 1: 36_wrk_ocr_queue シートの新設(確認キュー基盤)

対象ファイル: 100_config/101_sys_config.jsDDL)/000_infra/002_constants.js(定数)

  1. 100_config/101_sys_config.jssetupAllSchemas()WRK_OCRQ スキーマを追加

    • schemas オブジェクト(L826-901)に以下のキーを追加:
      'WRK_OCRQ': { headers: [...], color: "#38761d" }
      
    • ヘッダー構成は「エッジケース」セクションの「36_wrk_ocr_queue の列構成」に準拠(18 列)。
    • 01_sys_config への登録:L820-823 の if (!existKeys.includes('WRK_CARD')) confSheet.appendRow(...) パターンに倣い、WRK_OCRQ キーを appendRow(['WRK_OCRQ', '', '36_wrk_ocr_queue', 'AI-OCR確認キュー']) で追加。
  2. 000_infra/002_constants.jsID_PREFIX_MAP(L93-112)にエントリ追加

    • 既存の { pattern: '16_wrk_master', prefix: 'REQ_', digit: 4, isDate: true } パターンに倣い、以下を追加:
      { pattern: '36_wrk_ocr_queue', prefix: 'OCQ_', digit: 4, isDate: true }
      
    • これにより Utils.getIdPrefixConfig('36_wrk_ocr_queue')004_utils.js L482-487)が OCQ_YYYYMMDD_NNNN 形式の ID を発番できる。
  3. Constants.SHEET_DEFAULTS(L73-87)にエントリ追加

    • 既存 L85 { pattern: '32_wrk_invoice', defaults: { '請求ステータス': '未処理', '収支区分': '支出', '通貨': 'JPY', '申請種別': '請求書受領(AP)' } } のオブジェクト型({ pattern, defaults } 形式。prefix は ID 発番用で必須ではない)に倣い、以下を追加:
      { pattern: '36_wrk_ocr_queue', defaults: { 'ステータス': '要確認' } }
      
    • 失敗パターン #18(MAS-003)の教訓: SHEET_DEFAULTS は 3 要素配列ではなくオブジェクト配列である。必ず Read で確認してから追加すること。
  4. Constants.MENU_DEFINITION(L206-324)への起票メニュー追加(Step 3 で詳述)

: 36_wrk_bank_import(既存・WRK_BANK_IMPORT スキーマ、L822)とシート番号が重複するが、36_wrk_bank_import は銀行 CSV 取込専用で既に使用中のため、本案件のシートは物理名を 36_wrk_ocr_queue とする(両方とも 36_wrk_* 系で CSV/OCR 取込系の共通カテゴリ)。Utils の getSheetByKey 経由で参照するため物理名衝突のみが問題となるが、数値の重複自体は既存の 502_ 番号重複(502_bank_importer.js502_receipt_reader.js)と同様に許容されている運用。

Step 2: OCR パイプライン関数の実装

対象ファイル: 500_import/502_receipt_reader.js(既存関数は一切変更せず、末尾に関数追加)

処理フロー

  [Drive フォルダの PDF/画像]
           │
           ▼
  ┌─────────────────────────────┐
  │ (1) LockService.waitLock(30000) │ ← 排他制御。失敗時は Toast で終了
  └─────────────────────────────┘
           │
           ▼
  ┌─────────────────────────────┐
  │ (2) MD5 ハッシュ計算          │
  │     Utilities.computeDigest(   │
  │       Utilities.DigestAlgorithm│
  │         .MD5,                  │
  │       blob.getBytes()          │
  │     )                          │
  │     → 既に 36_wrk_ocr_queue に │
  │       ある → スキップ          │
  └─────────────────────────────┘
           │
           ▼
  ┌─────────────────────────────┐
  │ (3) Gemini API 呼び出し       │
  │     callGeminiForReceipt_() を│
  │     再利用(既存関数)         │
  │     N-40 分岐は自動で効く      │
  │     複数ページは「最終ページの  │
  │     合計金額のみ」指定          │
  └─────────────────────────────┘
           │
           ▼
  ┌─────────────────────────────┐
  │ (4) 正規化                    │
  │     Utils.normalizePartnerName()│
  │     Utils.aiSuggestAccount()   │
  └─────────────────────────────┘
           │
           ▼
  ┌─────────────────────────────┐
  │ (5) 信頼度判定                 │
  │  必須: T 番号・取引先・金額・日│
  │        付・科目名 が全て存在   │
  │  金額整合: 税抜 + 税 = 税込    │
  │  マスタ: 取引先名が MST_PART に│
  │          ヒット                │
  └─────────────────────────────┘
           │
      ┌────┴────┐
      │         │
      ▼         ▼
  [高信頼度]  [低信頼度]
      │         │
      ▼         ▼
  InvoiceRepo   36_wrk_ocr_queue
  .append([dto]) に要確認で登録
      │             │
      ▼             ▼
  36_wrk_ocr_queue  Toast で人間に確認依頼
  に起票済で記録

実装詳細

  • 新規公開関数: importReceiptPdfsAutoPosting()(メニュー起点)。importReceiptPdfs() は温存。
  • LockService:
    • var lock = LockService.getScriptLock(); try { lock.waitLock(30000); } catch(e) { SpreadsheetApp.getActive().toast('他の処理実行中のため中断', 'AI-OCR', 5); return; }
    • finally { lock.releaseLock(); } を必ず付ける。
  • MD5 ハッシュ:
    • var blob = file.getBlob(); var md5 = Utilities.computeDigest(Utilities.DigestAlgorithm.MD5, blob.getBytes()).map(function(b) { return ('0' + (b & 0xFF).toString(16)).slice(-2); }).join('');
    • 重複判定は 36_wrk_ocr_queueファイルMD5 列を全件読み取り、一致時は Utils.logInfo('importReceiptPdfsAutoPosting', '処理済みスキップ: ' + fileName) でログ出力してスキップ。
  • Gemini 呼び出し: 既存 callGeminiForReceipt_(apiKey, base64, fileName)(L194)を再利用。MAS-216 の Vertex 分岐(Env.gcpProjectId() が設定済なら自動委譲・L196-198)もそのまま効く。
  • 正規化:
    • 取引先名: Utils.normalizePartnerName(extracted.vendor)004_utils.js L657-680)。12_mst_partner略称 列または 取引先名_正式 列に一致すれば略称を返し、未ヒットなら generateLogicalAbbr() で論理略称を生成。
    • 科目名: Utils.aiSuggestAccount(extracted.description + ' ' + extracted.vendor)004_utils.js L467-475)で推論。該当なしなら空文字。
  • 信頼度判定:
    • 「高信頼度」の条件(すべて真):
      1. 必須フィールド(totalAmount / accrualDate / 正規化後取引先名 / invoiceNumber が T + 13 桁の形式 / aiSuggestAccount が科目名を返す)がすべて充足
      2. 金額整合性:Math.abs((extracted.subtotal + extracted.tax) - extracted.totalAmount) <= 1(1 円誤差まで許容)
      3. 推論科目が 11_mst_account に登録済み(AccountRepository.findAsMap()202_repository.js L323-341)で照合)
    • 「低信頼度」:上記いずれか不充足
  • InvoiceDTO への設定値(Phase 1 で 000_infra/003_contracts.js L40-67 / 100_config/101_sys_config.js L847 / 000_infra/002_constants.js L85 を裏取り確定):
フィールド設定値根拠
申請種別"請求書受領(AP)"Constants.SHEET_DEFAULTS L85 で 32_wrk_invoice申請種別 デフォルト値として定義(DDL 格納実データと一致)
発生日(P/L計上日)extracted.accrualDate(YYYY-MM-DD)Gemini プロンプト L12「請求期間の終了日(最終日)」
決済日_計画extracted.paymentDate(空なら accrualDate + 30 日Gemini は paymentDate として抽出
請求ステータス"未処理"SHEET_DEFAULTS 既定値
収支区分"支出"SHEET_DEFAULTS 既定値(領収書/請求書受領なので支出固定)
取引先名Utils.normalizePartnerName(vendor) 結果MAS-154 略称マスタ準拠
科目名Utils.aiSuggestAccount(description) 結果InvoiceRepository.append()AccountRepository.findAsMap() で諸表区分・大分類を自動付与(L192-199)
税区分"課税"(T 番号ありかつ金額整合 OK の場合)/"対象外"(その他)インボイス登録事業者判定の暫定ロジック
通貨"JPY"SHEET_DEFAULTS 既定値
税抜金額_計画extracted.subtotal
消費税額_計画extracted.tax
税込金額_計画extracted.totalAmount
摘要extracted.description
証憑URL'https://drive.google.com/file/d/' + file.getId() + '/view'既存 importReceiptPdfs L117 と同形式
PJ名未設定(InvoiceRepository.append()'指定なし_共通費など' をデフォルトで付与・L200-203)
  • 起票後処理: 36_wrk_ocr_queueステータス: 起票済 / 起票済INV_ID: <払い出された INV_ID> を書き込む。INV_ID は appendDtosToSheet_ 戻り値経由では取得できないため、InvoiceRepository.findAll() の末尾行から取り直すか、既存 appendDtosToSheet_ のパターン踏襲で事前に ID を採番して DTO に詰める(推奨: 既存 importReceiptPdfs 方式の「シート最終行から最大連番取得 → padStart で整形」を踏襲)。

Step 3: 確認キューからの手動起票メニュー追加

対象ファイル: 000_infra/002_constants.js(メニュー定義)/500_import/502_receipt_reader.js(ハンドラ関数)

  1. メニュー追加: Constants.MENU_DEFINITION(L206-324)の 📋 サイドバー: 🔍 消込・マッチング カテゴリ(L268-280、既存の領収書 OCR 3 項目と同カテゴリ)に以下 2 項目を追加:

    { label: '🤖 AI-OCR 自動起票 (Drive)', funcName: 'importReceiptPdfsAutoPosting', description: 'Drive の PDF/画像を Gemini OCR で読み取り、32_wrk_invoice に自動起票(低信頼度は 36_wrk_ocr_queue)' },
    { label: '✅ 確認キューから INV 起票', funcName: 'postApprovedOcrQueueRows', description: '36_wrk_ocr_queue の選択行(ステータス=修正済)を 32_wrk_invoice に起票' },
    

    造語禁止(失敗パターン #20):onOpen()101_sys_config.js L323-350)は Constants.MENU_DEFINITION を動的ループするだけなので、メニュー実体は 002_constants.js。既存 { label: '📄 領収書PDF読込 (Drive)', funcName: 'importReceiptPdfs' }(L274)を参考に文字列を命名。

  2. ハンドラ関数 postApprovedOcrQueueRows():

    • 選択範囲の行番号を SpreadsheetApp.getActiveSheet().getActiveRange() で取得。
    • 選択行のシート名が 36_wrk_ocr_queue でなければエラー表示して終了。
    • 選択行を ステータス === '修正済' のみで絞り込み、各行を InvoiceDTO に詰め替えて InvoiceRepository.append(dtos) を呼ぶ。
    • 成功時は当該行の ステータス起票済起票済INV_ID に払い出された INV_ID を書き込み、Toast で件数通知。
    • バリデーション: 税抜 + 税 ≠ 税込 なら起票を拒否してエラー表示(低信頼度判定と同一基準)。

影響範囲

区分対象変更内容
ファイル変更500_import/502_receipt_reader.js既存関数は不変。末尾に importReceiptPdfsAutoPosting / postApprovedOcrQueueRows / 内部ヘルパー 2-3 本を追加(想定 200-300 行)
ファイル変更100_config/101_sys_config.jssetupAllSchemas()schemas オブジェクトに WRK_OCRQ 追加(1 行)/01_sys_config 登録 appendRow 追加(1 行)
ファイル変更000_infra/002_constants.jsID_PREFIX_MAP / SHEET_DEFAULTS / MENU_DEFINITION に各 1 エントリ追加(計 4 行程度)
新規シート36_wrk_ocr_queueWRK_OCRQ18 列。DDL 管理。初期ステータス 要確認
新規システムキー01_sys_configWRK_OCRQsetupAllSchemas が冪等追加
既存動作への影響なし既存 importReceiptPdfs() / 35_wrk_receipt / 501_cc_importer は完全不変

注意事項

  1. 科目マスタ未登録の aiSuggestAccount 結果は自動起票禁止InvoiceRepository.append()(L185-207)は 科目名 をキーに AccountRepository.findAsMap() を引いて 諸表区分大分類 を自動付与する(L192-199)。未登録科目だとマップヒットせず諸表区分・大分類が空のまま書き込まれ、後続の P/LB/S マート集計で抜け落ちる。自動起票ルートでは AccountRepository.findAsMap()[科目名] が存在する場合のみ高信頼度扱いとする。未登録なら低信頼度として確認キュー送り。
  2. 36_wrk_ocr_queue は DDL 管理の新規シート。スキーマ追加後は dev で 📋 サイドバー: 🔧 開発・設定 → DDL 全更新 (Full) を実行して作成する必要がある。既存 Full DDL 経由で作成されるので、手動作成は不要。
  3. Utilities.computeDigest() の引数型: Utilities.computeDigest(Utilities.DigestAlgorithm.MD5, bytes)bytesbyte[]DriveApp.getFileById(id).getBlob().getBytes() で取得する。String を渡すと Invalid argument になる。
  4. LockService.getScriptLock().waitLock(30000) は 30 秒タイムアウト。取得失敗時は Error を throw するので try/catch で捕捉し、Toast で通知して即 return。finallyreleaseLock() を必ず呼ぶ(内部例外でのロックリーク防止)。
  5. 申請種別 の実格納値は "請求書受領(AP)"(日本語表記):CLAUDE.md は「APL_xxx で統一」としているが、実データは InvoiceDTO コメント(003_contracts.js L46)・Constants.SHEET_DEFAULTS L85・WRK_INVC DDL L847 すべてで "請求書受領(AP)" の日本語表記となっている。本案件は実データに合わせて日本語表記で設定する(DDL 定義値と格納実データの乖離はないと判断)。CLAUDE.md の APL_xxx 記述は将来の正規化方針(別案件)と解釈。
  6. 複数ページ PDF の合算処理:現行 RECEIPT_GEMINI_PROMPT_(L10-14)は「ページごとに 1 つの JSON オブジェクトを返す」指示だが、本案件の自動起票ルートでは最終ページ(=合計金額が記載された請求書の末尾)のみを 1 件として扱うプロンプトを別途用意する(既存プロンプト定数は一字一句変更しないので、新規パイプライン用は別定数として定義)。中間ページの小計を誤採用するリスクを排除する。
  7. 冪等性キー36_wrk_ocr_queue.ファイルMD5 列を一意制約として運用。同一ファイルの再実行は MD5 一致で即スキップ(Utils.logInfo に「処理済みスキップ」出力)。35_wrk_receipt 側の ファイル名 による重複判定とは独立。
  8. 35_wrk_receipt への書き込み連携は行わない:本案件のパイプラインは 36_wrk_ocr_queue32_wrk_invoice のみを操作する。既存 35_wrk_receiptimportReceiptPdfs 専用のまま維持し、二重書き込みは避ける。35_wrk_receipt 経由の消込ルート(importReceiptStatementapplyReceiptSettlement)とは責務を分ける。

エッジケース

信頼度判定と処理振分けテーブル

条件挙動理由
インボイス番号(T 番号)が読み取れない/形式不正(T + 13 桁 以外)確認キューに ステータス: 要確認 で登録。T 番号_OCR 生値 列を空または OCR 生値で保持し、人間に入力を促す形式不正のまま起票すると税区分判定(課税/非課税)が狂う。インボイス登録事業者以外との取引は別仕訳扱いになるため自動起票禁止
取引先名が読み取れない/Utils.normalizePartnerName()12_mst_partner に未ヒット(generateLogicalAbbr フォールバック)確認キューに登録。取引先名_OCR 生値 列に OCR 生値を保持し、マスタ登録(16_wrk_master 申請)を人間に促すInvoiceRepository.append() は取引先名の存在を検証しないが、後続の消込(MAS-145 銀行 CSV マッチ・MAS-146 CC マッチ)で名寄せが崩れる。MAS-154 略称マスタとの整合性を必須化
金額不整合(subtotal + tax ≠ totalAmount、誤差 > 1 円)確認キューに ステータス: 要確認(信頼度「低」)。エラー内容 列に 差額: ¥NNN を記録未決済残高(自動計算) の計算が狂う。自動起票禁止
税抜・消費税・税込のいずれかがゼロまたはマイナス確認キューに ステータス: エラー で登録。エラー内容 列に値を記録会計上の異常値。ゼロ円・マイナス起票は監査観点で手動承認を必須化
複数ページ PDF自動起票ルートでは新設プロンプト(「最終ページの合計金額のみを 1 件で返す」指示)を使用。既存 RECEIPT_GEMINI_PROMPT_ は改変しない中間ページの小計を誤採用するリスクを排除(請求書末尾に合計記載のパターンが多い)
OCR 精度が著しく低い(必須フィールド 3 つ以上空、または Gemini が JSON を返せない)処理中断し確認キューに ステータス: エラーエラー内容: OCR 精度不足 で登録。Toast で再撮影を促す低精度データの自動起票は消込・マートに連鎖影響
同一ファイルの再実行(MD5 ハッシュ一致)即スキップ。Utils.logInfo('importReceiptPdfsAutoPosting', '処理済みスキップ: ' + fileName) でログ出力二重起票防止。36_wrk_ocr_queue.ファイルMD5 を一意制約として運用
LockService タイムアウト(別プロセスが実行中)try/catchError を捕捉し、SpreadsheetApp.getActive().toast('他の処理実行中のため中断', 'AI-OCR', 5) で通知して終了。起票・キュー登録は行わない排他制御の破綻を防ぐ
aiSuggestAccount() が空文字を返す(キーワード辞書未ヒット)確認キューに ステータス: 要確認科目名_推論 列は空・エラー内容: 科目推論不可科目未指定だと InvoiceRepository.append() が諸表区分・大分類を付与できず、マートで分類不能
aiSuggestAccount() が返した科目名が 11_mst_account に未登録確認キューに ステータス: 要確認エラー内容: 推論科目がマスタ未登録AccountRepository.findAsMap() ヒットしないまま起票すると諸表区分・大分類が空欄でマートから抜け落ちる
Gemini API 4 回リトライ後も 429/503既存 callGeminiForReceipt_ が throw → try/catch で捕捉し、確認キューに ステータス: エラーエラー内容: Gemini API retry 上限リトライは既存ロジックに委譲、本案件は例外を握りつぶさない
Drive フォルダに処理対象ファイルなしToast で 対象ファイルなし を通知して終了既存 importReceiptPdfs L55 と同挙動
確認キュー起票メニュー実行時、ステータス !== '修正済' の行が混在修正済 行のみ処理。他ステータスはスキップし、結果ダイアログに「スキップ件数」を表示誤操作防止(人間が確認済フラグを立てていない行は起票しない)
確認キュー起票メニュー実行時、選択範囲に複数行/単一行/範囲未選択範囲未選択は Toast で 行を選択してください → 終了。単一・複数は同じロジックで処理誤操作防止
36_wrk_ocr_queue シートが未作成(DDL 未実行)Utils.getSheetByKey('WRK_OCRQ', '36_wrk_ocr_queue') が null → setupAllSchemas 実行を促す Toast/アラート表示事前の DDL 実行漏れ対策

36_wrk_ocr_queue の列構成(DDL スキーマと完全一致)

#列名備考
1OCQ_ID文字列OCQ_YYYYMMDD_NNNNID_PREFIX_MAP 経由で自動発番
2ステータス文字列要確認 / 修正済 / 起票済 / エラー の 4 値。デフォルト 要確認SHEET_DEFAULTS
3元ファイル名文字列Drive 上のファイル名
4ファイルMD5文字列冪等性キー。32 桁 16 進
5信頼度スコア文字列 / の 2 値(将来数値化の余地あり)
6OCR生データ_JSON文字列Gemini 応答原文(デバッグ用)
7取引先名_OCR生値文字列正規化前の vendor
8取引先名_正規化後文字列normalizePartnerName 結果
9発生日_OCR文字列YYYY-MM-DD
10税抜金額_OCR数値
11消費税額_OCR数値
12税込金額_OCR数値
13インボイス番号_OCR文字列T + 13 桁形式を検証
14科目名_推論文字列aiSuggestAccount 結果
15起票済INV_ID文字列起票成功時に INV_YYYYMMDD_NNNN を書き込み
16エラー内容文字列信頼度低・エラー時の理由
17起票日時日時yyyy-MM-dd HH:mm 形式
18起票者文字列Session.getActiveUser().getEmail()

Human-in-the-Loop フロー

  1. 高信頼度(条件:必須フィールド全充足 + 金額誤差 ≤ 1 円 + 科目名が 11_mst_account 登録済)InvoiceRepository.append() で自動起票 → 同時に 36_wrk_ocr_queueステータス: 起票済起票済INV_ID を書き込み(監査ログ兼用)。
  2. 低信頼度(上記以外)36_wrk_ocr_queueステータス: 要確認エラー内容: <具体的な理由> で登録 → Toast で件数通知。
  3. 人間が確認キューを修正 → 誤読箇所をシート上で直接編集 → ステータス修正済 に手動変更。
  4. 確認キュー起票メニュー実行 → Step 3 の postApprovedOcrQueueRows() が選択範囲のうち 修正済 行のみを InvoiceRepository.append() 経由で起票 → ステータス: 起票済起票済INV_ID を更新。
  5. エラー行の運用ステータス: エラー の行はデータ修正後に 修正済 に変更 → 4 へ。修正不能なら手動で 有効フラグ=FALSE 相当(本シートでは ステータス: エラー 固定)で運用。

実データ検証

実装前に以下を MCP または GAS ログで確認すること(失敗パターン #18-#20・DDL 定義値 vs 実データ乖離対策):

  1. 12_mst_partner シート
    • 略称 / 取引先名_正式 / 有効フラグ 列の列名が Utils.normalizePartnerName()004_utils.js L665-676)で参照する '略称' / '取引先名_正式' / '有効フラグ' と完全一致するか確認。DDL L828 には 略称_4文字 / 取引先名_正式 / 略称 / 銀行摘要名 / UI用取引先名 の複数略称列が存在するため、normalizePartnerName が参照するのは 略称略称_4文字 ではない)点を確認する。
  2. 11_mst_account シート
    • Utils.aiSuggestAccount() が返す全 account 値(Constants.ACCOUNT_RULES002_constants.js L117-136)が 11_mst_account.科目名 に登録されているか確認。対象:新聞図書費 / 旅費交通費 / 交際費 / 通信費 / 支払手数料 / 消耗品費 / 地代家賃 / 水道光熱費 / 売上高 / 売上原価 / 租税公課 / 資本金 / 長期借入金 / 現金及び預金 / 預り金 / 法定福利費 / 給料手当 / 役員報酬。未登録があれば事前に D-01〜D-03 科目マスタ追加 マイグレーションメニュー(migrationD01D03)で追加する。
    • 各科目の 諸表区分 / 大分類 / 有効フラグ を確認(AccountRepository.findAsMap()有効フラグ=FALSE をスキップ・202_repository.js L329-330)。
  3. 32_wrk_invoice シート
    • 既存データで 申請種別 列に格納されている実値を確認。"請求書受領(AP)" が実データと DDL 定義(SHEET_DEFAULTS L85)の両方で一致していることを目視確認。他の申請種別値(経費申請(EX) 等)が混在している場合、本案件の AI-OCR は「請求書受領(AP)」固定で起票する方針を確定させる。
  4. 01_sys_config シート
    • 既存の WRK_ORDR / WRK_INVC / WRK_BANK / WRK_RCPT / WRK_BANK_IMPORT 等の登録行を確認し、WRK_OCRQ 追加時の列構成(論理キー / シート ID / シート名 / 説明)を他エントリと揃える。
  5. Gemini API キー / GCP プロジェクト ID
    • dev 環境で Env.geminiApiKey()Env.gcpProjectId() が設定済みかを確認。未設定時の挙動(MAS-216 並行稼働で gcpProjectId() 空なら AI Studio 直叩きにフォールバック)が自動起票でも期待どおりか動作検証する。

関連ドキュメント

仕様書・定義関連箇所
docs/dev/dev_mas-147_invoice_ocr_auto_posting.md請求書 PDF → INV 自動起票 v2(Document AI ハイブリッド)。本案件は領収書/請求書共通の簡易版。重複する設計観点(T 番号正規化・取引先照合・HitL)を参照
docs/dev/dev_mas-157_photo_ocr.md写真(JPEG/PNG)対応 OCR。本案件は画像対応後も同パイプラインで動作する想定
docs/dev/dev_mas-161_ocr_manual_correction_assist.mdOCR 結果の手動補正アシスト。本案件の確認キュー UI 設計と親和
docs/dev/dev_mas-216_vertex_ai_migration.mdVertex AI 並行稼働。本案件の callGeminiForReceipt_ 呼び出しが MAS-216 で自動切替される前提
docs/dev/dev_mas-154_partner_logical_abbr.md取引先略称自動生成。Utils.normalizePartnerName の実装根拠
000_infra/003_contracts.js L40-67InvoiceDTO フィールド定義
000_infra/004_utils.js L467-475 / L657-680aiSuggestAccount / normalizePartnerName
200_data/202_repository.js L149-207 / L304-341InvoiceRepository.append / AccountRepository.findAsMap の科目マスタ自動付与ロジック
100_config/101_sys_config.js L820-823 / L826-90101_sys_config 登録パターン / schemas 定義パターン
000_infra/002_constants.js L73-87 / L93-112 / L206-324SHEET_DEFAULTS / ID_PREFIX_MAP / MENU_DEFINITION
docs/_internal/failure_patterns.md #18-#20コード未読による固有名詞誤記の防止(MAS-003 起因)

人間が検討すべき事項

TODO_future.md MAS-180 行(L437)から転記:

  • Gemini API の利用料・レート制限。自動起票では OCR 呼出回数が増えるため上限超過リスクあり。MAS-216 で Vertex 移行時は別コスト体系。
  • プロンプトチューニングの工数。本案件の「最終ページの合計金額のみ返す」プロンプトと既存「ページごと 1 JSON」プロンプトの 2 系統併存による保守負荷。

本調査で追加判明した事項:

  1. 確認キュー 36_wrk_ocr_queue のレビュー UI 設計:シート直接編集(本案件のデフォルト)/サイドバー UI(MAS-161 連携)/Google Form の 3 案。シート直接編集は最小工数だが、T 番号・金額のバリデーションを onEdit で組み込むか検討要(MAS-128 承認ワークフロー参照)。
  2. 証憑ファイルの保存先フォルダ管理Env.receiptFolderId()001_env.js)の運用方針。自動起票は既存フォルダと統合か、専用フォルダを分離するか。MAS-152(電帳法リネーム)と統合時のフォルダ構造(processed/YYYY-MM/ 型)との整合。
  3. インボイス番号(T 番号)の連携32_wrk_invoice.税区分 は現状 "課税" / "非課税" / "対象外" の 3 値だが、T 番号の有無で「適格請求書」判定を 42_trn_journal.税区分コード に連携するか。MAS-114(インボイス検証)との連携スコープ。
  4. 処理済みハッシュの永続化先:本案件は 36_wrk_ocr_queue.ファイルMD5 列を唯一ソースとするが、シートが肥大化するため 03_sys_params or 99_cache_* シートへのキャッシュ化も将来検討。MAS-133(年度跨ぎアーカイブ)との整合。
  5. 自動起票ルートと既存 35_wrk_receipt ルートの責務分離importReceiptPdfs35_wrk_receipt 経由の消込ルート、本案件は 32_wrk_invoice 経由の INV 起票ルート。両方が同一 Drive フォルダを処理する場合の重複実行制御(MD5 の相互参照要否)は、将来パイプライン統合時に判断。
  6. 申請種別APL_xxx 正規化案件との切り分け:CLAUDE.md は APL_xxx コード統一を将来方針として記述しているが、本案件は現行実データ "請求書受領(AP)" に合わせる。別案件として APL_xxx マイグレーションが立ち上がる場合、本案件の固定値もその時点で移行する。
  7. MAS-203(AI プロンプトライブラリの版管理)との連携:本案件で追加する「最終ページ合計のみ返せ」プロンプトを RECEIPT_GEMINI_PROMPT_ とは別定数とするが、MAS-203 実装時には外部管理(シート or docs/prompts/)に移行する前提で、ファイル内定数として命名規則 RECEIPT_AUTO_POSTING_GEMINI_PROMPT_V1_ 等で版管理の余地を残す。
  8. 低信頼度キューの自動レビュー強化:現状は単純な必須フィールド充足+金額整合+マスタ照合の 3 条件だが、将来的には Gemini の confidence スコア取得(プロンプト側で要求)や Document AI 併用(MAS-147 v2 参照)への拡張余地。

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

あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-180「AI-OCR連携による証憑自動起票」を以下の Step に分けて実装してください。

## 実行前タスク
- `500_import/502_receipt_reader.js` L22-185 の `importReceiptPdfs()`、L194-269 の `callGeminiForReceipt_()`、L278-323 の `parseGeminiReceiptText_()`、L416-521 の `callGeminiForReceiptOnVertex_()` を Read で確認(既存関数は変更しない)
- `100_config/101_sys_config.js` L323-350 の `onOpen()`(`Constants.MENU_DEFINITION` を動的ループする構造)、L820-823 の `01_sys_config` `appendRow` 登録パターン、L826-901 の `schemas` オブジェクト定義パターンを Read で確認(メニュー実体は `000_infra/002_constants.js` 側)
- `000_infra/002_constants.js` L73-87 の `SHEET_DEFAULTS`(オブジェクト配列 `{ pattern, prefix?, defaults, _dynamic? }`)、L93-112 の `ID_PREFIX_MAP`(`{ pattern, prefix, digit, isDate }`)、L206-324 の `MENU_DEFINITION`(特に L268-280 の「📋 サイドバー: 🔍 消込・マッチング」カテゴリ)を Read で確認してからエントリを追加する(失敗パターン #18・#20 参照)
- `000_infra/003_contracts.js` L40-67 の `InvoiceDTO` 全フィールド定義を Read で確認
- `200_data/202_repository.js` L149-207 の `InvoiceRepository.append()` 内部処理(`AccountRepository.findAsMap()` で科目マスタから `諸表区分`・`大分類` を自動付与、`PJ名` 未指定時に `'指定なし_共通費など'` をデフォルト)、L304-341 の `AccountRepository.findAsMap()` を Read で確認
- `000_infra/004_utils.js` L467-475 の `aiSuggestAccount(text)`、L657-680 の `normalizePartnerName(rawName)`、L482-487 の `getIdPrefixConfig(sheetName)` を Read で確認
- `32_wrk_invoice` の `申請種別` 列の実データ値を GAS ログ(または MCP で実シート値)で確認し、`"請求書受領(AP)"` が実データでも DDL 定義でも一致していることを確定させる

## 修正対象ファイル
- `500_import/502_receipt_reader.js`(末尾に関数追加のみ。既存関数は一切変更しない)
- `100_config/101_sys_config.js`(`setupAllSchemas()` 内の `schemas` オブジェクトに `WRK_OCRQ` キーを追加、同関数内の `01_sys_config` 登録ブロックに `appendRow` を追加。それ以外は変更しない)
- `000_infra/002_constants.js`(`SHEET_DEFAULTS` / `ID_PREFIX_MAP` / `MENU_DEFINITION` の 3 配列への追記のみ)

## Step 1: 36_wrk_ocr_queue シートの新設
- `101_sys_config.js` L820-823 パターンに倣い `01_sys_config` へ `WRK_OCRQ` システムキーを登録: `if (!existKeys.includes('WRK_OCRQ')) confSheet.appendRow(['WRK_OCRQ', '', '36_wrk_ocr_queue', 'AI-OCR確認キュー']);`
- `101_sys_config.js` L826-901 の `schemas` オブジェクトに `'WRK_OCRQ': { headers: [...], color: "#38761d" }` を追加。列構成は仕様書「`36_wrk_ocr_queue` の列構成」に従う(18 列:OCQ_ID / ステータス / 元ファイル名 / ファイルMD5 / 信頼度スコア / OCR生データ_JSON / 取引先名_OCR生値 / 取引先名_正規化後 / 発生日_OCR / 税抜金額_OCR / 消費税額_OCR / 税込金額_OCR / インボイス番号_OCR / 科目名_推論 / 起票済INV_ID / エラー内容 / 起票日時 / 起票者)
- `002_constants.js` L93-112 の `ID_PREFIX_MAP` に `{ pattern: '36_wrk_ocr_queue', prefix: 'OCQ_', digit: 4, isDate: true }` を追加
- `002_constants.js` L73-87 の `SHEET_DEFAULTS` に `{ pattern: '36_wrk_ocr_queue', defaults: { 'ステータス': '要確認' } }` を追加(L85 `32_wrk_invoice` エントリと同じ `{ pattern, defaults }` オブジェクト型)
- DDL を dev で実行して `36_wrk_ocr_queue` シートが 18 列で作成されることを確認

## Step 2: OCR パイプライン関数の実装
- `502_receipt_reader.js` の末尾に以下の関数を追加(既存関数は一切変更しない):
  - `importReceiptPdfsAutoPosting()` — メニュー起点
  - 内部ヘルパー: `computeFileMd5_(blob)` / `isOcrQueueDuplicate_(md5)` / `evaluateConfidence_(extracted, normalizedVendor, suggestedAcct, accountMap)` / `buildInvoiceDtoFromOcr_(extracted, normalizedVendor, suggestedAcct, driveLink)` / `appendToOcrQueue_(sheet, ocqId, row)`
- `LockService.getScriptLock().waitLock(30000)` で排他制御。`try/catch` で捕捉し `SpreadsheetApp.getActive().toast('他の処理実行中のため中断', 'AI-OCR', 5)` で終了。`finally { lock.releaseLock(); }` を必ず付ける
- MD5 計算: `Utilities.computeDigest(Utilities.DigestAlgorithm.MD5, blob.getBytes())` の戻り値 `byte[]` を 16 進文字列化(既存 `35_wrk_receipt` 処理にはない新規ヘルパー)
- 重複判定: `36_wrk_ocr_queue.ファイルMD5` 列の全件読み取り → 一致時は `Utils.logInfo('importReceiptPdfsAutoPosting', '処理済みスキップ: ' + fileName)` でログしてスキップ
- Gemini 呼び出し: 既存 `callGeminiForReceipt_(apiKey, base64, fileName)` を再利用(MAS-216 の Vertex 分岐が自動で効く)。ただし本自動起票ルートでは「最終ページの合計金額のみを 1 件で返す」新規プロンプト(例: `RECEIPT_AUTO_POSTING_GEMINI_PROMPT_V1_`)を別定数として定義し、既存 `callGeminiForReceipt_` を呼び出す前に引数で差し替える形にはせず、**新規内部関数 `callGeminiForAutoPosting_(apiKey, base64, fileName)` を別途実装**する(既存関数の一字一句不変を担保)。実装は `callGeminiForReceipt_` を参考に、プロンプト部分のみ差し替え・Backoff ロジックは同一ロジックで複製
- 正規化: `Utils.normalizePartnerName(extracted.vendor)` / `Utils.aiSuggestAccount((extracted.description || '') + ' ' + (extracted.vendor || ''))`
- 信頼度判定「高」: (1) `totalAmount > 0` かつ `subtotal > 0` かつ `tax >= 0`、(2) `accrualDate` が YYYY-MM-DD 形式、(3) `invoiceNumber` が `/^T\d{13}$/` に一致、(4) `normalizedVendor` が空でない、(5) `suggestedAcct` が空でなくかつ `AccountRepository.findAsMap()[suggestedAcct]` が存在、(6) `Math.abs((subtotal + tax) - totalAmount) <= 1`
- 高信頼度 → `buildInvoiceDtoFromOcr_` で `InvoiceDTO` を構築し `InvoiceRepository.append([dto])` で起票。同時に `36_wrk_ocr_queue` へ `ステータス: 起票済`・`起票済INV_ID: <新 INV_ID>` で記録
- 低信頼度 → `36_wrk_ocr_queue` へ `ステータス: 要確認`・`エラー内容: <具体理由>` で登録
- `InvoiceDTO` の `申請種別` は文字列リテラル `"請求書受領(AP)"` を直接指定する(`SHEET_DEFAULTS` のデフォルトに依存しない。明示指定で将来のデフォルト変更にも堅牢)

## Step 3: 確認キューからの手動起票メニュー追加
- `002_constants.js` L268-280 の「📋 サイドバー: 🔍 消込・マッチング」カテゴリの `items` 配列末尾(既存 `importReceiptPdfs` / `importReceiptStatement` / `applyReceiptSettlement` の後)に以下 2 項目を追加:
  - `{ label: '🤖 AI-OCR 自動起票 (Drive)', funcName: 'importReceiptPdfsAutoPosting', description: 'Drive の PDF/画像を Gemini OCR で読み取り、32_wrk_invoice に自動起票(低信頼度は 36_wrk_ocr_queue)' }`
  - `{ label: '✅ 確認キューから INV 起票', funcName: 'postApprovedOcrQueueRows', description: '36_wrk_ocr_queue の選択行(ステータス=修正済)を 32_wrk_invoice に起票' }`
- `502_receipt_reader.js` の末尾に `postApprovedOcrQueueRows()` を追加:
  - `SpreadsheetApp.getActiveSheet().getName()` が `36_wrk_ocr_queue` でなければ `ui.alert('シート違い', '36_wrk_ocr_queue シートで実行してください', ...)` で終了
  - `SpreadsheetApp.getActiveRange()` の行範囲を取得し、`ステータス === '修正済'` のみを対象化
  - 各行から `InvoiceDTO` を構築し、金額整合性(`税抜 + 税 = 税込`)を再検証。不整合は起票拒否しエラー表示
  - `InvoiceRepository.append(dtos)` で起票 → 当該行の `ステータス` を `起票済`・`起票済INV_ID` に新 INV_ID を書き込み
  - Toast で件数(起票成功・スキップ・エラーの内訳)通知

## 制約
- 既存の `importReceiptPdfs` / `callGeminiForReceipt_` / `parseGeminiReceiptText_` / `postProcessReceiptData_` / `callGeminiForReceiptOnVertex_` は**一字一句変更しない**(MAS-216 並行稼働の既存挙動を守るため)
- `RECEIPT_GEMINI_PROMPT_` 定数(L10-14)は変更禁止。本案件用のプロンプトは別定数 `RECEIPT_AUTO_POSTING_GEMINI_PROMPT_V1_` として追加
- `501_cc_importer.js` / `502_bank_importer.js` / `504_invoice_importer.js` は変更対象外
- 科目名には必ず `AccountRepository.findAsMap()[name]` が存在するものだけを渡す。未登録科目の自動起票は禁止(低信頼度で確認キュー送り)
- `SHEET_DEFAULTS` と `ID_PREFIX_MAP` の型は必ず Read で確認してから追加する(#18 失敗パターン参照)
- `MENU_DEFINITION` に追加するメニュー文字列・カテゴリ名は既存エントリ(L274 `'📄 領収書PDF読込 (Drive)'` 等)の表記を参照し造語しない(#20 失敗パターン参照)
- `onOpen()` 本体(`101_sys_config.js` L323-350)は変更しない。メニュー実体の更新は `MENU_DEFINITION` 側のみ

## エッジケース
仕様書「エッジケース」セクションのテーブルに従って実装すること。特に以下を必ず実装する:
- 金額不整合(`|税抜 + 税 - 税込| > 1`)→ 自動起票せず `36_wrk_ocr_queue` に `要確認` で登録
- 同一ファイル MD5 一致 → 即スキップ + ログ出力
- `LockService.waitLock` タイムアウト → Toast 通知して終了・キュー登録なし
- T 番号未検出(`/^T\d{13}$/` 不一致)→ 低信頼度扱い
- 科目推論結果が `11_mst_account` 未登録 → 低信頼度扱い
- `36_wrk_ocr_queue` シート未作成 → `getSheetByKey` が null → `setupAllSchemas` 実行を促すアラート

## 実データ検証
実装前に仕様書「実データ検証」セクションの 5 項目を MCP または GAS ログで全て確認すること。
特に以下 2 点は自動起票の失敗を未然に防ぐため必須:
- `Utils.aiSuggestAccount` が返す全 18 科目(`Constants.ACCOUNT_RULES` L117-136)が `11_mst_account` に登録済みか(未登録があれば `migrationD01D03` で追加)
- `32_wrk_invoice.申請種別` 列に `"請求書受領(AP)"` の実データが存在し、DDL 定義と一致していること

## 動作確認
1. `npm run push:dev` でデプロイ
2. `📋 サイドバー: 🔧 開発・設定 → DDL 全更新 (Full)` を実行し `36_wrk_ocr_queue` が 18 列で作成されることを確認
3. `01_sys_config` に `WRK_OCRQ` 行が追加されていることを確認
4. dev の `Env.receiptFolderId()` 配下にテスト証憑(以下 4 パターン)を投入:
   - A: 鮮明な請求書(T 番号あり・金額整合・科目推論可能)
   - B: 不鮮明な請求書(T 番号なし)
   - C: 金額不整合(`subtotal + tax ≠ totalAmount`)
   - D: A と同一ファイル(再実行による MD5 一致テスト)
5. `🤖 AI-OCR 自動起票 (Drive)` メニューを実行
6. 期待結果:
   - A: `32_wrk_invoice` に 1 行追加(`諸表区分`・`大分類` が科目マスタから自動付与、`申請種別=請求書受領(AP)`)、`36_wrk_ocr_queue` に `ステータス: 起票済`・`起票済INV_ID` が記録
   - B: `36_wrk_ocr_queue` に `ステータス: 要確認`・`エラー内容: T 番号形式不正`、`32_wrk_invoice` 無変化
   - C: `36_wrk_ocr_queue` に `ステータス: 要確認`・`エラー内容: 金額不整合`、`32_wrk_invoice` 無変化
   - D: ログに `処理済みスキップ: <fileName>`、`36_wrk_ocr_queue` 行追加なし
7. B の行の誤読項目を手動修正 → `ステータス` を `修正済` に変更 → 該当行を選択
8. `✅ 確認キューから INV 起票` メニューを実行 → `32_wrk_invoice` に 1 行追加、`36_wrk_ocr_queue` の `ステータス: 起票済`・`起票済INV_ID` が更新されることを確認
9. `LockService` 動作確認: 2 タブで同時にメニュー実行 → 後発側が `他の処理実行中のため中断` の Toast で終了することを確認
10. `901_test_runner.js` に MAS-180 の結合テストケース(高信頼度自動起票・低信頼度キュー登録・MD5 重複スキップ・確認キュー起票)を追加して実行

### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|---------|------|
| Phase 1(調査・設計) | あり | ファイル名(`502_receipt_reader.js` と `502_bank_importer.js` の重複解消)・`申請種別` 実データ値確認・`36_wrk_ocr_queue` 列構成確定に使用 |
| Phase 2(実装) | 最小限 | Phase 1 で確定した内容の実装に徹する |

推奨実行モデル

工程推奨モデル理由
Step 1: WRK_OCRQ シート新設 + 定数追加Claude Haiku 4.5SHEET_DEFAULTS / ID_PREFIX_MAP / DDL schemas のパターン追加のみ。既存 WRK_CARD / WRK_RCPT エントリを参考に機械的に挿入できる
Step 2: OCR パイプライン関数実装Claude Opus 4.6Gemini API 呼び出し(新規プロンプト定数定義+Backoff ロジック複製)・信頼度判定ロジック(5 条件の AND)・InvoiceDTO 構築・LockService 排他制御・Utilities.computeDigest の byte→hex 変換など、複数ファイル横断の設計判断と HitL フローの整合性確認が必要
Step 3: メニュー追加 + 確認キュー起票ハンドラClaude Sonnet 4.6MENU_DEFINITION への追記は機械的だが、postApprovedOcrQueueRows は選択範囲処理・ステータスフィルタ・金額再検証・ステータス更新と複数の判断要素あり

変更履歴

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

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

展開して表示
<instruction>
【タイムアウト回避・実行原則(v1.7・必ず遵守すること)】
1. **拡張思考の使い分け**: Phase 1(設計・調査)では拡張思考をフル活用し、ファイル名・行番号・固有名詞(関数名/シート名/列名)を完全に確定させる。Phase 2(清書)では各 Step 内の拡張思考を最小限に抑え、Phase 1 で確定した内容の書き下しに徹する。出力途中で再考しない。
2. **テキスト報告の禁止**: 「〜を作成します」等の text のみで tool_use なしに turn を終了しない。説明は 1 文以内。直ちに tool を呼ぶ。
3. **4-5 分割の Write/Edit 実行**: Step 2-1(骨格 ~20行) / 2-2(概要〜注意事項 ~300行) / 2-3a(エッジケース〜人間検討事項 ~200行) / 2-3b(実装プロンプト〜変更履歴 ~250行) / 2-4(<details>プロンプト全文記録) に分割。1 回の Write/Edit は 300 行以内を目安にする。
4. **各 Step で何を書くかを具体指示**: 設計判断は Phase 1 で完全に確定させ、Phase 2 では判断を持ち込まない。

======================================================================
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者兼仕様書ライターです。
案件 N-04「AI-OCR連携による証憑自動起票」の開発仕様書を作成してください。
作成完了後は `docs/_config.json` の `nav` 配列の適切なセクションに必ず追記してください。

---

## Phase 1: 実行前タスク(テキスト報告禁止。即座にツール実行)

**Grep は「どこにあるか」の発見まで。「どう書くか」の判断は必ず Read で裏取りすること(失敗パターン #18-#20 の直接対策)。推測した瞬間に手を止めて Read する。**

### 1-A: 案件定義の読み込み

- `docs/_internal/TODO_future.md` を検索し、`MAS-180` の「案件名」「概要」「期待される効果」「人間が検討すべき事項」を取得する。

### 1-B: プロジェクト規約の読み込み

- `CLAUDE.md` を Read し、コーディング規約・ファイル番号体系・マイグレーション運用ガイドライン(800_ops/ への配置ルール、冪等性要件)・`申請種別コード(APL_xxx)`の仕様を把握する。

### 1-C: 既存仕様書テンプレートの読み込み

- `docs/_internal/dev_spec_prompt_template.md` の「Phase 2: 仕様書の作成」セクション構成と「実装プロンプトのフォーマット」を Read し、出力する仕様書がこの構成に完全準拠するようにする。
- `docs/dev/` 配下から外部連携・インポート系の既存仕様書を 1 件 Grep で探して Read し、フォーマットを把握する。

### 1-D: 関連コードの調査(以下を全て Read/Bash で裏取りすること)

**① 既存 OCR ファイルの特定(ファイル名確定が最優先)**

```bash
ls 500_import/

を実行し、実在するファイル名を確認する。CLAUDE.md のファイル番号体系には 501_cc_importer502_bank_importer502_receipt_reader と記載されているが、502 番が重複しており実際のファイル名(502_receipt_reader.js503_receipt_reader.js か)は不明。Bash の出力結果を以降の記述の唯一の根拠とする。

確認後、当該ファイルを Read して以下を把握する:

  • 主要な公開関数名と処理範囲
  • Gemini API の呼び出しパターン(モデル名・プロンプト構造)
  • Google Drive との連携方法(Env.receiptFolderId() の使用有無)

② 再利用ユーティリティの確認

000_infra/004_utils.js を Read し以下を確認する:

  • Utils.normalizePartnerName(rawName) の引数・戻り値・内部で参照するシート名('12_mst_partner' または Utils.getSheetNameByKey('MST_PART')
  • Utils.aiSuggestAccount(text) の引数・戻り値・Constants.ACCOUNT_RULES との関係

※ GAS ビルトイン Utilities.computeDigest(Utilities.DigestAlgorithm.MD5, bytes) を重複チェックのファイルハッシュ計算に使用する(blob.getBytes() を渡す)。

③ InvoiceDTO とリポジトリの確認

  • 000_infra/003_contracts.js を Read し、InvoiceDTO の全フィールドを把握する。OCR から設定が必要な必須フィールド(申請種別発生日(P/L計上日)収支区分取引先名科目名税区分通貨税抜金額_計画消費税額_計画税込金額_計画請求ステータス)を記録する。
  • 200_data/202_repository.js を Read し、InvoiceRepository.append() の内部処理(科目マスタからの諸表区分・大分類自動付与ロジック、AccountRepository.findAsMap() の呼び出し)を確認する。
  • 申請種別 に設定すべき値を確認する。CLAUDE.md には「APL_xxx で統一」とあるが、InvoiceDTO の定義では "請求書受領(AP)" 等の日本語表記が示されている。32_wrk_invoice の実データまたは DDL 定義を MCP/GAS で確認し、実際に格納されている値を特定すること(DDL 定義値 vs 実データの乖離チェック)。

④ 新規シート設計の前提確認

  • 100_config/101_sys_config.js を Read し以下を確認する:
    • setupAllSchemas(DDL)への新規シートスキーマ追加パターン(引数の型・フィールド構造)
    • onOpen() の実在するメニュー名・メニュー構造の文字列(造語禁止)
  • 000_infra/002_constants.js を Read し以下を確認する:
    • Constants.SHEET_DEFAULTS の各エントリの型({ pattern, prefix, defaults, _dynamic } のうち何が必須か)
    • Constants.ID_PREFIX_MAP の各エントリの型({ pattern, prefix, digit, isDate } で確定)
    • 新規 36_wrk_ocr_queue に追加するエントリの形式を確定させる
  • Constants.CONFIG_SHEET(= '01_sys_config')へのシステムキー登録パターンを 101_sys_config.jssetupAllSchemas 内で確認し、36_wrk_ocr_queue に割り当てるキー名(例: WRK_OCRQ)を決定する。

⑤ 既存テストの確認

900_test/901_test_runner.js を Grep し、receipt または ocr に関連するテストケースの有無を確認する。


Phase 2: 仕様書の分割作成

出力先: docs/dev/dev_mas-180_ai_ocr_enhancement.md (※ ISSUE_ID は大文字 MAS-180 を使用)

1 回のツール呼び出しで全内容を出力しない。以下の Step に厳密に分割して実行すること。


Step 2-1: 骨格の作成(Write, ~20行)

dev_spec_prompt_template.md に定義された全セクション見出しのみを持つファイルを Write で作成する(本文空でよい)。セクション順序は以下を厳守:

# MAS-180: AI-OCR連携による証憑自動起票
## 概要
## 目的
## 現在のコード
## 修正方針
## 影響範囲
## 注意事項
## エッジケース
## 実データ検証
## 関連ドキュメント
## 人間が検討すべき事項
## 実装プロンプト(Claude Code 用)
## 推奨実行モデル
## 変更履歴
## 仕様書作成プロンプト(再現性・監査性のため必ず記録)

Step 2-2: 前半セクションの追記(Edit または Bash heredoc, ~300行)

「概要」〜「注意事項」を記述する。Phase 1 で確定した実ファイル名・関数名・行番号のみを使用すること(推測禁止)。

含める内容:

  • 概要テーブル: 案件ID=N-04, カテゴリ=外部連携/AI, Phase・優先度・所要時間は TODO_future.md から転記, 対象ファイル=500_import/[Phase 1 で確定した実ファイル名]100_config/101_sys_config.js000_infra/002_constants.js

  • 目的: 証憑画像から Gemini API で請求情報を自動抽出し、InvoiceRepository.append() 経由で 32_wrk_invoice へ起票するパイプラインを構築する。低信頼度の OCR 結果は新設する 36_wrk_ocr_queue シートで人間が確認・修正してから起票する Human-in-the-Loop 設計とする。

  • 現在のコード: 500_import/[実ファイル名] の主要関数・行番号・処理範囲を Read 結果から正確に転記する

  • 修正方針: 以下の 3 Step に分割して記述する:

    Step 1: 36_wrk_ocr_queue シートの新設

    • 100_config/101_sys_config.jssetupAllSchemas にスキーマを追加(列構成は Step 2-3a で定義した列を使用)
    • 000_infra/002_constants.jsConstants.ID_PREFIX_MAP{ pattern: '36_wrk_ocr_queue', prefix: 'OCQ_', digit: 4, isDate: true } を追加
    • Constants.SHEET_DEFAULTS{ pattern: '36_wrk_ocr_queue', defaults: { 'ステータス': '要確認' } } を追加(Phase 1 で確認した実際の型に従う)
    • 01_sys_config シートへシステムキー WRK_OCRQ(または Phase 1 で決定したキー名)を登録し、Utils.getSheetByKey('WRK_OCRQ', '36_wrk_ocr_queue') で参照できるようにする

    Step 2: OCR パイプライン関数の実装

    • 500_import/[実ファイル名] に関数を追加(既存関数は変更しない)
    • 処理フロー(仕様書に図示すること):
      1. Drive からファイル取得 → Utilities.computeDigest(Utilities.DigestAlgorithm.MD5, blob.getBytes()) でハッシュ計算 → 処理済みなら即リターン
      2. LockService.getScriptLock() で排他ロック取得(取得失敗時は Toast で通知して終了)
      3. Gemini API 呼び出し(複数ページ PDF は「合計金額のみを返せ」とプロンプト指定)
      4. 結果パース → Utils.normalizePartnerName(rawVendorName) で取引先正規化 → Utils.aiSuggestAccount(description) で科目推論
      5. 信頼度判定(必須フィールド充足度 + 金額整合性チェック)
      6. 高信頼度 → InvoiceRepository.append([invoiceDto]) で自動起票 → 36_wrk_ocr_queueステータス: 起票済 で記録
      7. 低信頼度 → 36_wrk_ocr_queueステータス: 要確認 で記録 → Toast で確認を促す
    • InvoiceDTO の各フィールドへの設定値を仕様書に明記する。特に 申請種別 は Phase 1 で確認した実際の格納値を使用する(DDL 定義値 vs 実データの乖離に注意)

    Step 3: 確認キューからの手動起票メニュー追加

    • 100_config/101_sys_config.jsonOpen() に確認キュー起票メニューを追加(Phase 1 で Read した実在するメニュー構造・文字列に倣う。メニュー名を造語しない)
    • 確認キューの選択行を InvoiceRepository.append() で起票し、ステータス起票済起票済INV_ID に INV_ID を書き込む関数を追加
  • 影響範囲: 変更ファイルリスト(500_import/[実ファイル名]100_config/101_sys_config.js000_infra/002_constants.js)、新規シート 36_wrk_ocr_queue01_sys_config へのシステムキー追加

  • 注意事項:

    1. InvoiceRepository.append()AccountRepository.findAsMap() で科目マスタを参照するため、科目名11_mst_account 未登録の値を渡すと諸表区分が空になる。aiSuggestAccount() の返す科目名がマスタに登録されているかを実装前に検証すること(→ 実データ検証セクション)
    2. 36_wrk_ocr_queue は DDL(setupAllSchemas)管理の新規シート。スキーマ変更時は dev で 全シートのスキーマとUIを最新化(DDL) を実行してから動作確認すること
    3. Utilities.computeDigest()byte[] を受け取る。DriveApp.getFileById(id).getBlob().getBytes() で渡すこと
    4. LockService.getScriptLock() は取得タイムアウト(waitLock(30000) 等)を設定し、タイムアウト時は例外を catch して Toast 通知してから終了すること
    5. 申請種別 の値は Phase 1 で確認した実際の格納値を使用すること。CLAUDE.md 記載の APL_xxx コードと InvoiceDTO 定義の日本語表記が乖離している可能性があるため、実データを最優先とする

Step 2-3a: エッジケース〜人間が検討すべき事項の追記(Edit または Bash, ~200行)

エッジケーステーブル(必須。以下の観点を全て網羅すること):

条件挙動理由
インボイス番号(T番号)が読み取れない / 形式不正(T + 13桁以外)確認キューに登録(ステータス: 要確認)。T番号フィールドを空で保持し、人間に入力を促す形式不正のまま起票すると税区分判定処理に影響する
取引先名が読み取れない / normalizePartnerName()12_mst_partner に未ヒット確認キューに登録。取引先名_OCR生値 列に OCR 生値を保持し、マスタ登録を人間に促すInvoiceRepository.append() は取引先名を検証しないが、後続の消込・マート処理で名寄せが崩れる
金額不整合(税抜金額 + 消費税額 ≠ 税込金額)確認キューに登録(信頼度「低」)。エラー内容列に差額を記録未決済残高 の計算が狂う。自動起票禁止
金額がゼロ / マイナス値確認キューに登録(ステータス: エラー)。エラー内容列に値を記録会計上の異常値。ゼロ円・マイナス起票は手動承認を必須とする
複数ページ PDFGemini API プロンプトで「最終ページの合計金額のみを返すこと」と明示指定する中間ページの小計を誤採用するリスクを排除
OCR 精度が著しく低い(必須フィールド 3 つ以上が空 / Gemini が値を返せない)処理中断し確認キューに ステータス: エラー で登録。Toast で再撮影を促す低精度データの自動起票は消込・マート処理全体に悪影響
同一ファイルの再実行(MD5 ハッシュ一致)即スキップ。Utils.logInfo() に「処理済みスキップ: {ファイル名}」を出力二重起票防止
LockService タイムアウト(別プロセスが実行中)例外を catch し Toast で通知して終了。起票・キュー登録は行わない排他制御の破綻を防ぐ

36_wrk_ocr_queue シートの列構成(DDL スキーマと完全一致させること): OCQ_ID / ステータス(要確認/修正済/起票済/エラー)/ 元ファイル名 / ファイルMD5 / 信頼度スコア / OCR生データ_JSON / 取引先名_OCR生値 / 取引先名_正規化後 / 発生日_OCR / 税抜金額_OCR / 消費税額_OCR / 税込金額_OCR / インボイス番号_OCR / 科目名_推論 / 起票済INV_ID / エラー内容 / 起票日時 / 起票者

Human-in-the-Loop フロー(信頼度判定基準を定義すること):

  • 高信頼度の条件(例: 必須フィールド全充足 + 金額整合性 OK)→ InvoiceRepository.append() で自動起票
  • 低信頼度の条件(上記以外)→ 36_wrk_ocr_queue要確認 で登録
  • 人間が確認キューを修正 → Step 3 の「確認キューから起票」メニューを実行 → InvoiceRepository.append() で起票 → ステータス: 起票済 に更新

実データ検証(実装前に MCP または GAS ログで確認すべき項目):

  • 12_mst_partner シートの「略称」「取引先名_正式」「有効フラグ」列が normalizePartnerName() の内部参照列名と一致するか確認
  • 11_mst_account シートに Utils.aiSuggestAccount() が返す全科目名(新聞図書費旅費交通費交際費Constants.ACCOUNT_RULES の全 account 値)が登録されているか確認
  • 32_wrk_invoice申請種別 列に既存データで使われている実際の値("請求書受領(AP)" 等)を確認し、DDL 定義値との乖離チェック

関連ドキュメント:

仕様書・定義関連箇所
000_infra/003_contracts.jsInvoiceDTO フィールド定義
000_infra/004_utils.jsnormalizePartnerName() / aiSuggestAccount()
200_data/202_repository.jsInvoiceRepository.append() の科目マスタ自動付与ロジック
docs/_internal/failure_patterns.md #18-#20コード未読による固有名詞誤記の防止

人間が検討すべき事項TODO_future.md の N-04 から転記 + 以下を追記):

  • 確認キュー 36_wrk_ocr_queue のレビュー UI 設計(サイドバー vs シート直接編集 vs Google Form)
  • Gemini API の利用料・レート制限(UrlFetchApp の呼び出し上限)への対処方針
  • 証憑ファイルの保存先フォルダ管理(Env.receiptFolderId() の活用可否・フォルダ権限設計)
  • インボイス番号(T番号)の 42_trn_journal 課税区分判定への連携可否
  • 処理済みハッシュの永続化先(03_sys_params シートへの書き込みか、36_wrk_ocr_queue 自体を参照するか)

Step 2-3b: 実装プロンプト〜変更履歴の追記(Edit または Bash, ~250行)

実装プロンプトはバッククォートで囲まず、行頭4スペースインデントで記述すること(本文省略・仕様書本体参照)。

推奨実行モデル:

工程推奨モデル理由
Step 1: シート新設 + 定数追加Claude Sonnet 4.6Constants の型確認・DDL パターン適用の判断が必要
Step 2: OCR パイプライン実装Claude Opus 4.6Gemini API 統合・複数ファイル横断・Human-in-the-Loop 設計判断が必要
Step 3: メニュー追加Claude Haiku 4.5101_sys_config.js の既存パターンへの追記のみ

変更履歴:

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

Step 2-4: 仕様書作成プロンプト全文の記録(Edit または Bash)

仕様書末尾の「## 仕様書作成プロンプト(再現性・監査性のため必ず記録)」セクションに以下の形式で追記する:

<details><summary>展開して表示</summary>

(この <instruction> プロンプトの全文をそのまま貼り付ける)

</details>

Phase 3: 保存・登録・コミット

3-A: _config.json への登録(必須)

docs/_config.jsonnav 配列「§E.6 パイプライン・RPA・外部連携」セクションに追加:

{ "file": "dev/dev_mas-180_ai_ocr_enhancement.md", "title": "E.6.X MAS-180 AI-OCR連携による証憑自動起票" }

追記前に必ず git pull origin main で最新を取得し、JSON 構文エラーがないことを確認すること。

3-B: changelog への追記

docs/_internal/changelog.md のヘッダー直後に追記。

3-C: コミット&プッシュ

docs: N-04 AI-OCR連携による証憑自動起票の開発仕様書を作成 のメッセージで commit & push。


</details>