概要

項目内容
案件IDMAS-009
案件名資金ショート警告アラート
カテゴリFP&A・レポーティング / 資金繰り監視
PhasePhase 3
優先度P3(★)
所要時間2-3 時間
対象ファイル新規 600_report/610_cash_alert.js / 追記 000_infra/004_utils.jsUtils.postToSlack 追加) / 追記 100_config/101_sys_config.js(日次トリガー登録関数 + メニュー項目)
前提案件F-08 仕様書あり(コード未実装)。本案件は F-08 を前提にせず 84_cf_daily_plan600_report/606_datamart_daily_cf.js が生成)を直接参照する
後続案件なし(F-17 資金調達シミュレーションと将来連携余地あり)

目的

日次 CF 計画(84_cf_daily_plan)の将来日残高を監視し、以下のいずれかを検出した場合に Slack または メール で即時通知する日次自動監視機能を新設する:

  1. 予測現預金残高が閾値(円)を下回る最初の日付を検出
  2. ランウェイ(現預金 ÷ 固定費合計)が閾値(月数)を下回る状態を検出(93_kpi_dashboard の K6 ランウェイ行を参照)

これにより、経営陣が資金ショート危機を人間が毎朝ダッシュボードを確認しなくても受動的に受け取れるプッシュ通知系として機能させる。通知はあくまで情報提供であり、対策の判断・実行は人間が行う(Human-in-the-Loop)。

現在のコード

1. データソースとなる 84_cf_daily_plan600_report/606_datamart_daily_cf.js

dmBuildDailyCfPlan_(ctx)84_cf_daily_plan タブを動的に生成する。 ヘッダー行は 606_datamart_daily_cf.js:338 で定義:

var headers = ['決済日_計画', 'ソース', '管理ID', '決済口座', '摘要', '入金', '出金', '現預金残高'];
  • 決済日_計画: YYYY-MM-DD 文字列(過去月の YYYY-07-31 期首残高行を除き、原則未来日。日付文字列ソート済)
  • 現預金残高: 各行時点の予測残高(number)。初期行は期首残高、以降はイベント毎に balance += inAmt - outAmt で更新される
  • 2 行目は 期首残高(B/S現預金) の固定行
  • 以降の行はパイプライン予算・実績 STL 等を日付ソートして列挙

本案件ではこのタブを読み取り専用で利用し、列はヘッダー名ベース(indexOf)で参照する(列番号ハードコード禁止 / CLAUDE.md)。

2. ランウェイ参照元 93_kpi_dashboard600_report/609_datamart_kpi.js

buildKpiDashboard()93_kpi_dashboard を生成する。K6 ランウェイ行(609_datamart_kpi.js:341-345)に以下が入る:

{ type: 'main', name: 'K6 ランウェイ', format: mon, sparklineCell: 'B$34', color: '#CC0000',
  curr: kpiRunwayByIdx_(ctx, curr),
  prev: kpiRunwayByIdx_(ctx, prev),
  ytd:  '="—"' },

計算式は =IFERROR(IF(固定費<=0,"∞",現預金/固定費),"∞")(同ファイル L227)。 戻り値は number(ヶ月)または文字列 "∞" / "—" の 3 パターン。本案件では typeof val === 'number' && isFinite(val) でのみ閾値判定を行い、文字列値はアラート対象外(安全側)として扱う。

3. パラメータ読込基盤 Constants.getParam000_infra/002_constants.js:147-167

_paramsCache: null,
getParam: function(key, defaultVal) {
  if (!this._paramsCache) { /* 03_sys_params を一括読み込み */ }
  var val = this._paramsCache[key];
  if (val === undefined || val === null || val === '') return defaultVal;
  return (typeof defaultVal === 'number') ? Number(val) : String(val);
},
  • 03_sys_params の A 列 = キー、B 列 = 値 の 2 列構造(DDL 管理外の動的シート)
  • defaultVal の型で返却型が決まる(number 期待なら Number(val)、それ以外は String(val)
  • キャッシュは関数オブジェクト単位。トリガー実行のたびに初回呼び出しで読み直される

4. Env モジュール(000_infra/001_env.js

  • 環境依存値(SPREADSHEET_ID / GEMINI_API_KEY / RECEIPT_FOLDER_ID / BACKUP_FOLDER_ID)に限定して専用メソッドが定義されている
  • 汎用 set / get メソッドは存在しない。本案件の「日次送信済みフラグ」は PropertiesService.getScriptProperties() を直接使用する(CLAUDE.md の禁止対象は環境依存値の参照に限定されると解釈。アプリ状態管理用途での直接利用は許容)

5. 既存の時間ベーストリガー登録パターン(100_config/101_sys_config.js:378-389

installAutoOpenSidebarTrigger() が既存パターンの代表例:

var existing = ScriptApp.getProjectTriggers().filter(function(t) {
  return t.getHandlerFunction() === 'onOpenAutoShowSidebar_';
});
existing.forEach(function(t) { ScriptApp.deleteTrigger(t); });
ScriptApp.newTrigger('onOpenAutoShowSidebar_')
  .forSpreadsheet(SpreadsheetApp.getActive())
  .onOpen()
  .create();

既存をすべて削除してから再作成する冪等パターンを採用する。本案件ではこのパターンを踏襲し forSpreadsheettimeBased().everyDays(1).atHour(H).create() に置き換える。

修正方針

全体像

  1. 新規ファイル 600_report/610_cash_alert.js にアラート本体 checkCashShortageAlert() を実装
  2. 000_infra/004_utils.js の末尾に Slack Webhook 送信ヘルパー Utils.postToSlack(webhookUrl, payload) を追加
  3. 100_config/101_sys_config.js に日次トリガー登録関数 installDailyCashAlertTrigger() / 解除関数 uninstallDailyCashAlertTrigger() を追加し、Constants.MENU_DEFINITION の「⚙️ メンテナンス」または「🔧 マイグレーション」カテゴリにメニュー項目を追加
  4. 03_sys_params に 5 パラメータを開発者が手動追加(DDL 管理外のため自動登録しない)

新規ファイル配置番号

600_report/ の既存ファイル:

  • 601_datamart_ingest.js609_datamart_kpi.js まで占有済み
  • 次の空き番号 = 610_cash_alert.js(タスクプロンプトでは 609 を想定していたが、実在ファイルと衝突するため 610 を採用)

03_sys_params に追加する 5 パラメータ

パラメータキー説明デフォルト
F09_ALERT_ENABLEDstring "true"/"false"機能有効フラグ。"true" 以外は処理中断(デフォルト値なし。未設定時は処理中断してログ出力)
F09_ALERT_THRESHOLD_JPYnumber予測現預金残高の警告閾値(円)。84_cf_daily_plan の将来日残高がこの値を下回ればアラート(デフォルト値なし)
F09_ALERT_THRESHOLD_MONTHSnumberランウェイ警告閾値(月数)。93_kpi_dashboard K6 がこの値を下回ればアラート(デフォルト値なし)
F09_ALERT_TARGET_EMAILstringフォールバック通知先メール(カンマ区切り可)(デフォルト値なし)
F09_ALERT_TARGET_SLACK_WEBHOOKstringSlack Incoming Webhook URL。未設定時はメールにフォールバック(未設定可)

デフォルト値でのアラート実行は禁止。未設定パラメータがあれば Utils.logError で記録して return

checkCashShortageAlert() の処理フロー

1. LockService.getScriptLock().tryLock(3000) で排他制御。失敗 → return
2. Constants.getParam で 5 パラメータを読み込み
   - F09_ALERT_ENABLED が "true" 以外 → ログ出力して return
   - THRESHOLD_JPY / THRESHOLD_MONTHS のいずれかが未設定 → logError して return
   - TARGET_EMAIL / TARGET_SLACK_WEBHOOK が両方未設定 → logError して return
3. 日次送信済みフラグを確認
   - PropertiesService.getScriptProperties().getProperty('F09_ALERT_SENT_' + YYYY-MM-DD)
   - 既に "1" ならログ出力して return
4. 84_cf_daily_plan を getSheetByName で取得
   - シート不在 → logError して return
5. ヘッダー行を indexOf で解析 → 決済日_計画 / 現預金残高 の列インデックスを取得
6. データ行をループ(2 行目以降)
   - 決済日_計画 を Date に解釈。今日以降の行のみ評価対象
   - 期首残高行(摘要 == "期首残高(B/S現預金)")はスキップ
   - 現預金残高が THRESHOLD_JPY を下回る最初の行を記録(firstShortageRow)
7. 93_kpi_dashboard を getSheetByName で取得(不在時はランウェイ判定をスキップ)
   - K6 ランウェイ行(A 列が "K6 ランウェイ" と一致する行)を動的検索
   - B 列(curr)の値を取得。typeof === 'number' && isFinite(val) のみ閾値判定
   - THRESHOLD_MONTHS を下回ればランウェイ警告を記録(runwayWarning)
8. firstShortageRow も runwayWarning もなければアラート不要としてログ出力して return
9. アラートメッセージを構成:
   - 閾値を下回る最初の日付(YYYY-MM-DD)
   - 予測残高と設定閾値の具体値(円)
   - 現在のランウェイ(ヶ月)と設定閾値
   - スプレッドシートへの直リンク(SpreadsheetApp.getActiveSpreadsheet().getUrl())
   - 「詳細を確認し、対策を検討してください」文言
10. 通知送信:
    - F09_ALERT_TARGET_SLACK_WEBHOOK があれば Utils.postToSlack を呼ぶ
    - なければ MailApp.sendEmail でフォールバック
    - 送信例外は catch して logError(アラート送信失敗自体はクリティカルだが処理は続行)
11. 日次送信済みフラグを setProperty('F09_ALERT_SENT_' + YYYY-MM-DD, '1') で設定
12. Utils.logInfo で実行結果サマリーを記録
13. lock.releaseLock() (finally 節)

Utils.postToSlack(webhookUrl, payload) の新規実装

000_infra/004_utils.js の末尾(L411 の }; の直前、normalizePartnerName の後ろ)に追記:

/**
 * Slack Incoming Webhook に JSON ペイロードを POST する (F-09)
 * @param {string} webhookUrl - Slack Incoming Webhook URL
 * @param {Object} payload - Slack API に渡す JSON ペイロード(text / blocks 等)
 * @returns {GoogleAppsScript.URL_Fetch.HTTPResponse}
 * @throws 送信失敗時に例外(呼び出し元で catch し logError すること)
 */
postToSlack: function(webhookUrl, payload) {
  var FUNC = 'Utils.postToSlack';
  if (!webhookUrl) throw new Error('webhookUrl が未指定');
  try {
    var res = UrlFetchApp.fetch(webhookUrl, {
      method: 'post',
      contentType: 'application/json',
      payload: JSON.stringify(payload),
      muteHttpExceptions: true
    });
    var code = res.getResponseCode();
    if (code < 200 || code >= 300) {
      throw new Error('Slack API returned HTTP ' + code + ': ' + res.getContentText());
    }
    return res;
  } catch (e) {
    Utils.logError(FUNC, e, 'webhookUrl=' + String(webhookUrl).substring(0, 40) + '...');
    throw e;
  }
},

トリガー登録関数(100_config/101_sys_config.js への追記)

既存 installAutoOpenSidebarTrigger の冪等パターンを踏襲:

/**
 * F-09: 日次アラートトリガーを登録する。既存があれば削除してから再登録する冪等実装。
 * 実行時刻は 03_sys_params の F09_ALERT_HOUR(0-23、未設定時 9)を参照。
 */
function installDailyCashAlertTrigger() {
  var FUNC = 'installDailyCashAlertTrigger';
  var ui = SpreadsheetApp.getUi();
  var existing = ScriptApp.getProjectTriggers().filter(function(t) {
    return t.getHandlerFunction() === 'checkCashShortageAlert';
  });
  existing.forEach(function(t) { ScriptApp.deleteTrigger(t); });
  var hour = Number(Constants.getParam('F09_ALERT_HOUR', 9)) || 9;
  ScriptApp.newTrigger('checkCashShortageAlert')
    .timeBased().everyDays(1).atHour(hour).create();
  try { Utils.auditLog('RUN', '', '', '', FUNC, '', { removed: existing.length, added: 1, hour: hour }, 'F-09 日次トリガー'); } catch (_) {}
  ui.alert('✅ F-09 日次アラート登録完了', '毎日 ' + hour + ' 時に `checkCashShortageAlert` が実行されます。', ui.ButtonSet.OK);
}

function uninstallDailyCashAlertTrigger() {
  var FUNC = 'uninstallDailyCashAlertTrigger';
  var ui = SpreadsheetApp.getUi();
  var triggers = ScriptApp.getProjectTriggers();
  var removed = 0;
  triggers.forEach(function(t) {
    if (t.getHandlerFunction() === 'checkCashShortageAlert') { ScriptApp.deleteTrigger(t); removed++; }
  });
  try { Utils.auditLog('RUN', '', '', '', FUNC, '', { removed: removed }, 'F-09 日次トリガー解除'); } catch (_) {}
  ui.alert('✅ F-09 日次アラート解除完了', removed + ' 件のトリガーを削除しました。', ui.ButtonSet.OK);
}

Constants.MENU_DEFINITION の「⚙️ メンテナンス」カテゴリにメニュー項目を追加:

{ label: '⏰ F-09 日次アラートを登録', funcName: 'installDailyCashAlertTrigger', description: '資金ショート警告の日次トリガー登録' },
{ label: '⏰ F-09 日次アラートを解除', funcName: 'uninstallDailyCashAlertTrigger', description: '資金ショート警告の日次トリガー解除' },

影響範囲

変更対象変更内容変更量
600_report/610_cash_alert.js新規作成checkCashShortageAlert() 本体 + 補助関数+200〜250 行
000_infra/004_utils.js末尾に Utils.postToSlack 追記のみ(既存関数は一切変更しない)+25 行
100_config/101_sys_config.jsinstallDailyCashAlertTrigger / uninstallDailyCashAlertTrigger 新規追加+40 行
000_infra/002_constants.jsMENU_DEFINITION の「⚙️ メンテナンス」カテゴリに 2 項目追加+2 行
03_sys_params(シート)開発者が手動で 5 行追加(DDL 管理外)+5 行
  • 既存動作への影響なし: 新規ファイル / 末尾追記 / 新規トリガー登録関数のみ。既存関数の変更は一切しない
  • DDL への影響なし: 03_sys_params は動的シートで DDL 管理外

注意事項

  1. Utils.postToSlack は新規実装関数004_utils.js の末尾(L411 の }; の直前)に追記し、既存メソッド(normalizePartnerName 等)を変更しない
  2. Constants.getParam03_sys_params シートを起動時 1 回キャッシュする_paramsCache)。トリガー実行ではキャッシュが引き継がれないため、初回呼び出しで確実に読み込まれる。同一実行内で複数回呼んでも I/O は 1 回
  3. Constants.getParam の返却型は defaultVal の型に従う。number 期待なら第 2 引数を数値(0 等)、文字列期待なら空文字 '' を渡す。F09_ALERT_ENABLED は string として扱い "true" との厳密比較で判定
  4. デフォルト値でアラート実行しない: F09_ALERT_THRESHOLD_JPY / F09_ALERT_THRESHOLD_MONTHS / 通知先のいずれかが未設定なら Utils.logError で記録して return。誤検知を防ぐため「運用者が明示的に設定した場合のみ動作」を原則とする
  5. PropertiesService 直接使用(日次フラグ管理)の是非: Env モジュールに汎用 setter/getter が存在しないため、PropertiesService.getScriptProperties() を直接利用する。CLAUDE.md の禁止対象は環境依存値の参照に限定され、アプリ状態管理(日次送信済みフラグ)は許容範囲と判断。F09_ALERT_SENT_YYYY-MM-DD のようなタイムスタンプ付きキーを使用し、過去キーは運用で蓄積するが実害は軽微
  6. 84_cf_daily_plan はユーザー操作で削除・再生成されうる: シート不在時は Utils.logError でログ出力して return。例外をスローしない(日次トリガーでユーザー画面にエラーダイアログが出ないため、ログのみで済ませる)
  7. 93_kpi_dashboard の K6 ランウェイ値は "∞" / "—" / number の 3 パターン: typeof val === 'number' && isFinite(val) の厳密判定のみ閾値判定に使い、それ以外は**安全側(アラートなし)**で扱う
  8. 列インデックスのハードコード禁止: 決済日_計画 / 現預金残高 は必ずヘッダー行の indexOf で位置特定する(CLAUDE.md 規約)
  9. 排他ロック: 万が一重複トリガー発火(過去トリガーの残留等)で checkCashShortageAlert が同時起動した場合も、LockService.getScriptLock().tryLock(3000) で後発が即時 return する。これにより二重通知を防ぐ
  10. アラートメッセージの PII・機密: Slack Webhook URL / メールアドレスは 03_sys_params にしか保存しない。本体のログには URL 先頭 40 文字のみ(substring(0, 40))で記録し、完全 URL のログ出力は避ける
  11. 日付比較は文字列比較で可: 84_cf_daily_plan決済日_計画YYYY-MM-DD 形式なので Utilities.formatDate(new Date(), tz, 'yyyy-MM-dd') で作った今日日付と >= で文字列比較できる
  12. 84_cf_daily_plan の期首残高行(2 行目)はスキップ: 期首残高(B/S現預金) を含む摘要の行は判定対象外(過去の実績値で将来予測ではない)

エッジケース

#条件動作理由
1F09_ALERT_ENABLED"true" 以外または未設定処理中断・Utils.logInfo設定不備を検出。デフォルト動作させない
2F09_ALERT_THRESHOLD_JPY / THRESHOLD_MONTHS のいずれか未設定処理中断・Utils.logErrorデフォルト値でアラート実行は禁止
3F09_ALERT_TARGET_EMAIL / TARGET_SLACK_WEBHOOK 両方未設定処理中断・Utils.logError通知不能状態を記録。運用者に要対処
4F09_ALERT_TARGET_SLACK_WEBHOOK 未設定、EMAIL 設定ありメールにフォールバック通知経路の冗長化
5F09_ALERT_TARGET_SLACK_WEBHOOK 設定あり、送信失敗(HTTP 非 2xx)logError してメールにフォールバックSlack 側の一時障害を吸収
684_cf_daily_plan シートが存在しない処理中断・Utils.logErrorシート再生成待ち。例外スローはしない(日次トリガーはサイレント失敗)
784_cf_daily_plan にデータ行がない(ヘッダーのみ or 期首残高行のみ)アラートなし・ログ出力判定対象なし
893_kpi_dashboard シートが存在しない、または K6 ランウェイ行が見つからないランウェイ判定をスキップし残高判定のみ継続部分的に機能する方が全停止より価値が高い
9K6 ランウェイ値が "∞" / "—" / 非 numberランウェイ警告は発出しない(安全側スキップ)typeof val === 'number' && isFinite(val) で厳密判定
10残高閾値を下回る日付がない、かつランウェイ警告もないアラートなし・ログ出力正常状態
11同日に複数トリガーが発火(重複登録等)LockService.tryLock(3000) で後発が即時 return排他制御で二重通知を防ぐ
12同日に既にアラート送信済み日次フラグで検出し return毎日 1 回のみ。二重送信を防ぐ
13今日の日付より過去の行のみ判定対象外(未来日のみ評価)過去実績で警告しても意味がない
14決済日_計画 が未パース(空文字・不正形式)行スキップデータ欠損行を通す
15現預金残高列が非数値("" 等)行スキップ部分欠損を許容
16Slack / メール両方送信失敗logError のみ。処理は完了扱いで日次フラグは立てない次回再トライの余地を残す
17F09_ALERT_HOUR が範囲外(0-23 以外)Number(...) || 9 で 9 にフォールバック不正値でもトリガー登録は失敗させない

実データ検証

実装着手前に以下を MCP で確認する:

#確認項目確認方法期待結果
184_cf_daily_plan のヘッダー行(1 行目)MCP read_rows で 1-2 行目取得決済日_計画 / ソース / 管理ID / 決済口座 / 摘要 / 入金 / 出金 / 現預金残高 の 8 列
284_cf_daily_plan の 2 行目(期首残高行)の摘要列MCP read_rows期首残高(B/S現預金) を含む
384_cf_daily_plan決済日_計画 列の書式MCP read_rows でサンプル取得YYYY-MM-DD 文字列
493_kpi_dashboard の K6 ランウェイ行位置MCP read_rows で A 列全体A 列値 K6 ランウェイ の行(概ね r=26 付近)
593_kpi_dashboard の B 列(K6 行)の値MCP read_rows数値 or "∞" or "—"
603_sys_params の列構造MCP read_rows で 1 行目取得A=キー, B=値, C 以降は説明等(任意)
703_sys_paramsF09_* キーが未登録MCP read_rows でキー列確認既存キーとの衝突なし

関連ドキュメント

仕様書 / ファイル関連箇所
dev_F-08_cash_runway.md月次ランウェイ算出の前提仕様(F-08 未実装時も F-09 は 84_cf_daily_plan + 93_kpi_dashboard で単独動作可)
dev_F-03_kpi_dashboard.mdK6 ランウェイ行の配置(r=26 付近、A 列 = K6 ランウェイ
600_report/606_datamart_daily_cf.js84_cf_daily_plan の生成ロジック。ヘッダー定義は L338
600_report/609_datamart_kpi.js93_kpi_dashboard の生成ロジック。K6 ランウェイは L341-345 / kpiRunwayByIdx_ は L220-228
000_infra/002_constants.jsConstants.getParam の実装(L143-167)
000_infra/001_env.jsEnv モジュール。汎用 set/get なし(日次フラグは PropertiesService 直利用)
100_config/101_sys_config.js既存トリガー登録パターン(installAutoOpenSidebarTrigger L378-389)
CLAUDE.md列参照ヘッダー名ベース、03_sys_params の位置、Env 経由の環境値アクセス原則
TODO_future.md §3.2 F-09 行案件定義(シミュレーション / P3 / ★ / 人間検討: 通知先と閾値設定)

人間が検討すべき事項

#項目詳細
1通知先の選択Slack Webhook URL(推奨)/メール/両方。小規模組織ではメール、Slack 導入済みなら Webhook 推奨。TODO_future.md 原文の「通知先(Slack/メール)と閾値の設定」に相当
2残高閾値 F09_ALERT_THRESHOLD_JPY月次固定費の 2-3 ヶ月分が目安。例: 月次固定費 300 万円なら閾値 900 万円。業種・成長ステージに応じて運用で調整
3ランウェイ閾値 F09_ALERT_THRESHOLD_MONTHSF-08 仕様書は CASH_RUNWAY_MIN_THRESHOLD を 6 ヶ月にしているため、F-09 でも初期値 6 推奨。シード VC 標準は 12 ヶ月
4アラート送信時刻 F09_ALERT_HOUR毎日何時に実行するか(0-23)。業務開始前の 7-9 時推奨。未設定時は 9 時
5アラートメッセージの文言・形式Slack なら blocks 構造化、メールなら HTML vs plain。最低限含めるべき要素は仕様書内で定義済(閾値を下回る最初の日付 / 予測残高と閾値 / スプレッドシートリンク / 対策検討の促し)
6複数宛先の扱いF09_ALERT_TARGET_EMAIL をカンマ区切りで複数指定可能にするか。本仕様では MailApp.sendEmailto にそのまま渡すのみ(MailApp 側が CSV 対応)
7日次フラグのクリーンアップF09_ALERT_SENT_YYYY-MM-DD キーは毎日増え続ける。PropertiesService は 500KB 上限があるため年 1 回程度のクリーンアップ運用を推奨(または自動 GC 関数を後続案件で追加)
8Slack メッセージのメンション<@U01234567><!channel> 等のメンション可否。初期値はメンションなし、運用で追加
9Human-in-the-Loop 原則通知はあくまで情報提供。対策の判断・実行は人間が行う(CLAUDE.md のプロダクトポリシー準拠)。自動で借入発生等の副作用を伴う動作は本案件では絶対に行わない
1003_sys_params パラメータ追加は開発者が手動で実施DDL 管理外の動的シート。setupAllSchemas では追加されないため、運用ドキュメントに手順を記載する

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

あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 F-09「資金ショート警告アラート」を実装してください。

## 実行前タスク(必ずツールを使用して順次確認)
1. `docs/dev/dev_F-09_cash_shortage_alert.md` を Read し、修正方針・エッジケース・実データ検証を把握する
2. `000_infra/001_env.js` を Read し、`PropertiesService` 直接利用の可否(汎用 set/get が存在しないことの最終確認)を行う
3. `000_infra/002_constants.js` を Read し、`Constants.getParam(key, defaultVal)` の返却型ルールを確認する
4. `000_infra/004_utils.js` を Read し、`Utils` オブジェクトの末尾行番号(`normalizePartnerName` の閉じ `},` と `};` の位置)を確認する
5. `100_config/101_sys_config.js` の `installAutoOpenSidebarTrigger` / `uninstallAutoOpenSidebarTrigger`(L378-399 付近)を Read し、冪等パターン(既存削除→再作成)を把握する
6. `600_report/606_datamart_daily_cf.js:338` を Read し、`84_cf_daily_plan` のヘッダー定義を確認する
7. `600_report/609_datamart_kpi.js` を Read し、K6 ランウェイ行(L341-345)と `kpiRunwayByIdx_`(L220-228)の戻り値パターン(`"∞"` / `"—"` / number)を把握する
8. MCP ツールで `84_cf_daily_plan` / `93_kpi_dashboard` / `03_sys_params` の実データ(ヘッダー行・サンプル行)を確認する

## 修正対象ファイル
- `600_report/610_cash_alert.js` — **新規作成**。メイン実装
- `000_infra/004_utils.js` — 末尾への追記のみ(`Utils.postToSlack` を追加)。既存メソッドは変更しない
- `100_config/101_sys_config.js` — 末尾に `installDailyCashAlertTrigger` / `uninstallDailyCashAlertTrigger` を追加
- `000_infra/002_constants.js` — `MENU_DEFINITION` の「⚙️ メンテナンス」カテゴリに 2 項目追加

## 実装内容

### A. `600_report/610_cash_alert.js`(新規)

`checkCashShortageAlert()` を公開関数として定義し、以下を満たす:

1. `LockService.getScriptLock().tryLock(3000)` で排他制御(失敗時 return)。`finally` で `releaseLock`
2. 5 パラメータを `Constants.getParam` で読み込み:
   - `F09_ALERT_ENABLED`(string, default `''`)。`"true"` 以外なら `Utils.logInfo` して return
   - `F09_ALERT_THRESHOLD_JPY`(number, default `0`)。`0` / `NaN` なら `Utils.logError` して return
   - `F09_ALERT_THRESHOLD_MONTHS`(number, default `0`)。同上
   - `F09_ALERT_TARGET_EMAIL`(string, default `''`)
   - `F09_ALERT_TARGET_SLACK_WEBHOOK`(string, default `''`)
   - email / webhook 両方空なら `Utils.logError` して return
3. 日次送信済みフラグの確認:
   - 今日の日付を `Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'yyyy-MM-dd')` で取得
   - `PropertiesService.getScriptProperties().getProperty('F09_ALERT_SENT_' + today)` が `"1"` なら return
4. `84_cf_daily_plan` を `getWebSpreadsheet_().getSheetByName('84_cf_daily_plan')` で取得。不在なら `Utils.logError` して return
5. ヘッダー行を `indexOf` で解析:
   - `iDate = headers.indexOf('決済日_計画')`
   - `iBalance = headers.indexOf('現預金残高')`
   - `iMemo = headers.indexOf('摘要')`
   - いずれかが -1 なら `Utils.logError` して return
6. データ行をループ(2 行目以降 = `r=1` from `getDataRange().getValues()`):
   - 摘要が `期首残高(B/S現預金)` を含む行はスキップ
   - `決済日_計画` が今日の日付文字列未満(過去)ならスキップ
   - `現預金残高` が非 number ならスキップ
   - `現預金残高 < F09_ALERT_THRESHOLD_JPY` となる**最初の行**を `firstShortageRow` に保存してループ終了
7. `93_kpi_dashboard` を取得(不在時はランウェイ判定スキップ):
   - A 列を `getDataRange().getValues()` して `row[0] === 'K6 ランウェイ'` を線形検索
   - 一致行の B 列値を取得(`typeof val === 'number' && isFinite(val)` の場合のみ)
   - `val < F09_ALERT_THRESHOLD_MONTHS` なら `runwayWarning = { value: val, threshold: THRESHOLD_MONTHS }`
8. `firstShortageRow` も `runwayWarning` もなければ `Utils.logInfo('checkCashShortageAlert', '資金ショート兆候なし')` して return
9. アラートメッセージ構成(Slack と メール で本文を別生成してよい):
   - タイトル: `🚨 資金ショート警告 (F-09)`
   - 本文に含める要素:
     (1) 閾値を下回る最初の日付・残高・閾値(`firstShortageRow` がある場合)
     (2) 現在のランウェイ月数・閾値(`runwayWarning` がある場合)
     (3) スプレッドシートへの URL(`SpreadsheetApp.getActiveSpreadsheet().getUrl()`)
     (4) 「詳細を確認し、対策を検討してください」文言
10. 通知送信:
    - Slack Webhook が設定されていれば `Utils.postToSlack(webhookUrl, { text: message })` を try-catch で呼ぶ
    - Slack 失敗 or 未設定かつ email があれば `MailApp.sendEmail({ to: email, subject: title, htmlBody: body })`
    - 両方失敗 → `Utils.logError` のみ。**日次フラグは立てない**(次回再トライ可)
11. 通知成功時のみ `PropertiesService.getScriptProperties().setProperty('F09_ALERT_SENT_' + today, '1')`
12. `Utils.logInfo('checkCashShortageAlert', 実行結果サマリー)`

### B. `000_infra/004_utils.js` への追記(末尾)

`normalizePartnerName` の閉じ `},` の**直後**、オブジェクト終端 `};` の**直前**に追記:

    /**
     * Slack Incoming Webhook に JSON ペイロードを POST する (F-09)
     * @param {string} webhookUrl - Slack Incoming Webhook URL
     * @param {Object} payload - Slack API に渡す JSON ペイロード(text / blocks 等)
     * @returns {GoogleAppsScript.URL_Fetch.HTTPResponse}
     * @throws 送信失敗時に例外(呼び出し元で catch し logError すること)
     */
    postToSlack: function(webhookUrl, payload) {
      var FUNC = 'Utils.postToSlack';
      if (!webhookUrl) throw new Error('webhookUrl が未指定');
      try {
        var res = UrlFetchApp.fetch(webhookUrl, {
          method: 'post',
          contentType: 'application/json',
          payload: JSON.stringify(payload),
          muteHttpExceptions: true
        });
        var code = res.getResponseCode();
        if (code < 200 || code >= 300) {
          throw new Error('Slack API returned HTTP ' + code + ': ' + res.getContentText());
        }
        return res;
      } catch (e) {
        Utils.logError(FUNC, e, 'webhookUrl=' + String(webhookUrl).substring(0, 40) + '...');
        throw e;
      }
    },

**既存メソッドは変更しない**。追加はこの 1 メソッドのみ。

### C. `100_config/101_sys_config.js` への追記

ファイル末尾に以下 2 関数を追加(既存 `installAutoOpenSidebarTrigger` と同じ冪等パターン):

    /**
     * F-09: 資金ショート警告アラートの日次トリガーを登録する(冪等)
     */
    function installDailyCashAlertTrigger() {
      var FUNC = 'installDailyCashAlertTrigger';
      var ui = SpreadsheetApp.getUi();
      var existing = ScriptApp.getProjectTriggers().filter(function(t) {
        return t.getHandlerFunction() === 'checkCashShortageAlert';
      });
      existing.forEach(function(t) { ScriptApp.deleteTrigger(t); });
      var hour = Number(Constants.getParam('F09_ALERT_HOUR', 9)) || 9;
      if (hour < 0 || hour > 23) hour = 9;
      ScriptApp.newTrigger('checkCashShortageAlert')
        .timeBased().everyDays(1).atHour(hour).create();
      try { Utils.auditLog('RUN', '', '', '', FUNC, '', { removed: existing.length, added: 1, hour: hour }, 'F-09 日次トリガー'); } catch (_) {}
      ui.alert('✅ F-09 日次アラート登録完了', '毎日 ' + hour + ' 時に checkCashShortageAlert が実行されます。', ui.ButtonSet.OK);
    }

    /** F-09: 日次アラートトリガーを解除する */
    function uninstallDailyCashAlertTrigger() {
      var FUNC = 'uninstallDailyCashAlertTrigger';
      var ui = SpreadsheetApp.getUi();
      var triggers = ScriptApp.getProjectTriggers();
      var removed = 0;
      triggers.forEach(function(t) {
        if (t.getHandlerFunction() === 'checkCashShortageAlert') {
          ScriptApp.deleteTrigger(t); removed++;
        }
      });
      try { Utils.auditLog('RUN', '', '', '', FUNC, '', { removed: removed }, 'F-09 日次トリガー解除'); } catch (_) {}
      ui.alert('✅ F-09 日次アラート解除完了', removed + ' 件のトリガーを削除しました。', ui.ButtonSet.OK);
    }

### D. `000_infra/002_constants.js` への追記

`MENU_DEFINITION` の「⚙️ メンテナンス」カテゴリの `items` 配列末尾に以下 2 項目を追加:

    { label: '⏰ F-09 日次アラートを登録', funcName: 'installDailyCashAlertTrigger', description: '資金ショート警告の日次トリガー登録' },
    { label: '⏰ F-09 日次アラートを解除', funcName: 'uninstallDailyCashAlertTrigger', description: '資金ショート警告の日次トリガー解除' },

## 制約

- **`000_infra/004_utils.js` は末尾への追記のみ**。既存メソッドを変更しない
- **列参照はヘッダー名ベース**(`indexOf`)。列番号ハードコード禁止(CLAUDE.md 規約)
- **`Constants.getParam` は `Utils.getSystemParam` ではない**(後者は存在しない)
- **デフォルト値でアラート実行禁止**。閾値・通知先のいずれかが未設定なら必ず処理中断
- **`93_kpi_dashboard` K6 値は `"∞"` / `"—"` / number の 3 パターン**。`typeof === 'number' && isFinite(val)` で厳密判定
- **`Env` モジュールを経由する値は環境依存値のみ**。日次フラグは `PropertiesService` 直接利用で OK
- **Slack Webhook URL / メールアドレスの完全値をログに残さない**(先頭 40 文字のみ)
- **例外は原則呼び出し元(`checkCashShortageAlert`)で catch**。日次トリガーでユーザー画面にエラーダイアログを出さない

## エッジケース(実装上の必須考慮)

1. `F09_ALERT_ENABLED` が `"true"` 以外 → 即 return
2. 閾値 / 通知先の未設定 → `logError` して return
3. `84_cf_daily_plan` シート不在 → `logError` して return
4. `93_kpi_dashboard` 不在 → ランウェイ判定のみスキップ、残高判定は継続
5. K6 値が `"∞"` / `"—"` → ランウェイ警告なし(安全側スキップ)
6. 残高閾値を下回る行なし、かつ ランウェイ警告なし → アラートなしで正常 return
7. 同日 2 回目以降 → 日次フラグで return
8. 並行実行 → `LockService` で後発 return
9. Slack / メール両方失敗 → `logError` のみ、日次フラグ立てない(再トライ余地)

## 実データ検証

実装前に MCP で確認:
- `84_cf_daily_plan` のヘッダー行: `決済日_計画 / ソース / 管理ID / 決済口座 / 摘要 / 入金 / 出金 / 現預金残高`
- `84_cf_daily_plan` の 2 行目摘要: `期首残高(B/S現預金)` を含む
- `93_kpi_dashboard` の K6 ランウェイ行位置と B 列値の型
- `03_sys_params` の A/B 列構造と `F09_*` キーの未存在

## 動作確認

1. `03_sys_params` に 5 パラメータを手動追加(`F09_ALERT_ENABLED=true` / `F09_ALERT_THRESHOLD_JPY=5000000` / `F09_ALERT_THRESHOLD_MONTHS=3` / `[email protected]` / `F09_ALERT_TARGET_SLACK_WEBHOOK=` 空)
2. `npm run push:dev` で開発環境にデプロイ
3. GAS エディタで `checkCashShortageAlert` を直接実行し、ログに正常終了(またはアラート送信済み)を確認
4. `F09_ALERT_TARGET_SLACK_WEBHOOK` に有効な Webhook URL を設定し、Slack メッセージを受信できることを確認
5. `F09_ALERT_TARGET_SLACK_WEBHOOK` を空に戻し、メール通知のフォールバックが動作することを確認
6. 日次フラグ(`F09_ALERT_SENT_YYYY-MM-DD`)が設定された状態で再実行し、二重通知が発生しないことを確認
7. `installDailyCashAlertTrigger` を実行し、Apps Script の「トリガー」画面で `checkCashShortageAlert` が毎日 9 時で登録されていることを確認
8. `uninstallDailyCashAlertTrigger` を実行し、トリガーが削除されていることを確認
9. `F09_ALERT_ENABLED=false` で再実行 → 処理中断されログのみ出力されることを確認
10. `84_cf_daily_plan` を一時削除し再実行 → `logError` されダイアログが出ないことを確認

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

| フェーズ | 拡張思考 | 備考 |
|---|---|---|
| 実行前タスク | あり | 追記位置・ヘッダー名・K6 値の型確定 |
| 実装 | なし | 仕様書の書き下しに徹する |

推奨実行モデル

工程推奨モデル理由
仕様書作成(本ドキュメント)Claude Opus 4.7複数ファイル横断(新規 + Utils 追記 + トリガー + メニュー)/ 通知失敗フォールバック順序 / 日次フラグ設計の複合判断
実装Claude Sonnet 4.6関数シグネチャ・処理フロー・エッジケースが仕様書で確定済み。Utils 末尾挿入位置の特定と既存冪等パターン踏襲に中程度の判断が必要
動作確認ユーザー手動Slack 受信・メール受信・トリガー登録画面の目視確認

変更履歴

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

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

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

======================================================================
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者兼仕様書ライターです。
案件 F-09「資金ショート警告アラート」の開発仕様書を作成してください。
作成後は `docs/_config.json` の `nav` 配列の適切なセクション(§E.5 FP&A・レポーティング)に必ず追記すること。

---

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

以下を **この順序で** Read し、Phase 2 の設計を確定させること。推測で仕様書を書かない。

### 1-A: 案件定義の把握
- `docs/_internal/TODO_future.md` — F-09 の案件名・概要・人間が検討すべき事項を取得

### 1-B: 前提案件の仕様把握
- `docs/dev/dev_F-08_cash_runway.md` — F-08「資金繰り予測の高度化」の仕様書を読み込み、以下を確定させる:
  - アラートのデータソースとなるシート名(`84_cf_daily_plan` が候補だが実在を確認)
  - 当該シートの列構造(残高列・ランウェイ列・日付列の正確なヘッダー名)
  - ランウェイが非数値になるケース(「黒字」「Infinity」等)の表現形式
  - ※ `dev_F-08_cash_runway.md` が存在しない場合は CLAUDE.md の `84_cf_daily_plan` 記述を手掛かりに `600_report/` 配下の CF 関連ファイル(`604_datamart_cf.js` 等)を Grep で特定し Read する

### 1-C: インフラ層・設定層の調査
- `000_infra/001_env.js` — `Env` モジュールの全メソッドを確認。`PropertiesService.getScriptProperties()` の直接呼び出しがどの範囲で禁止されているかを確認する(CLAUDE.md では環境依存値の参照に限定して禁止と読めるが、日次送信済みフラグのようなアプリ状態管理でも禁止対象かを判断する。Env モジュールに `set`/`get` の汎用メソッドがある場合はそれを使う。ない場合は `PropertiesService.getScriptProperties()` 直接利用を採用し注意事項に記載する)
- `000_infra/002_constants.js` — `Constants.getParam(key, defaultVal)` の実装を確認(`03_sys_params` シートから読み込む実在メソッド)
- `000_infra/004_utils.js` — `Utils` オブジェクトの末尾行番号を確認し、`postToSlack` の追加挿入位置を特定する。既存の HTTP リクエスト系メソッドがある場合はパターンを踏襲する
- `100_config/101_sys_config.js` — 以下の 3 点を確認する:
  1. 時間ベーストリガーの登録パターン(`ScriptApp.newTrigger` の既存呼び出し箇所と関数名)
  2. メニュー登録パターン(`onOpen()` 内の `ui.createMenu` の構造と実在するメニュー名)
  3. `03_sys_params` シートへのパラメータ追加パターン(`setupAllSchemas` 等でパラメータ行を追加している箇所があれば形式を確認)

### 1-D: 新規ファイルの配置番号確定
- `600_report/` ディレクトリを Bash `ls` で確認し、現在の最大番号を特定する。新規実装ファイルは `600_report/609_cash_alert.js`(または次の空き番号)とする。CLAUDE.md のファイル番号体系(百の位=レイヤー、十・一の位=順序)に従うこと

### 1-E: 既存テストの確認
- `900_test/901_test_runner.js` — F-09 または F-08 に関連するテストケースの有無を確認

---

## Phase 2: 仕様書の分割作成

**出力先**: `docs/dev/dev_F-09_cash_shortage_alert.md`

### Step 2-1: 骨格の作成(File Write、〜20行)
見出しのみ。本文は空で可。以下のセクション構成:
`# F-09: 資金ショート警告アラート` / `## 概要` / `## 目的` / `## 現在のコード` / `## 修正方針` / `## 影響範囲` / `## 注意事項` / `## エッジケース` / `## 実データ検証` / `## 関連ドキュメント` / `## 人間が検討すべき事項` / `## 実装プロンプト(Claude Code 用)` / `## 推奨実行モデル` / `## 変更履歴` / `## 仕様書作成プロンプト`

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

以下の内容を書く(Phase 1 で確定した固有名詞を使うこと。推測で書かない):

**## 概要**(テーブル: 案件ID=F-09, カテゴリ=FP&A・レポーティング, Phase, 優先度, 対象ファイル=新規 `600_report/609_cash_alert.js` + `000_infra/004_utils.js`, 前提案件=F-08)

**## 目的**(資金ショートリスクを事前検知し、Slack/メールで担当者へ自動通知する日次監視機能の新設)

**## 現在のコード**(新規実装のため「なし」。前提となる F-08 のデータソースシートとその構造を記載)

**## 修正方針**(以下の設計方針を具体的に記述):
- **アーキテクチャ**: GAS 時間ベーストリガー(日次)で `checkCashShortageAlert()` 関数を起動。新規ファイル `600_report/609_cash_alert.js` に実装
- **データソース**: Phase 1 で確定したシート名(`84_cf_daily_plan` 等)から残高・ランウェイ列を読み取る。`Utils.getSheetByKey()` または `getWebSpreadsheet_().getSheetByName()` を使用
- **設定値管理**: `Constants.getParam(key, defaultVal)` で `03_sys_params` から読み込む。以下の 5 パラメータを追加(仕様書内に一覧表として記載):

  | パラメータキー | 型 | 説明 | デフォルト |
  |---|---|---|---|
  | `F09_ALERT_ENABLED` | string `"true"/"false"` | 機能有効フラグ | (デフォルト値なし。未設定時は処理中断) |
  | `F09_ALERT_THRESHOLD_JPY` | number | 残高警告閾値(円) | (デフォルト値なし) |
  | `F09_ALERT_THRESHOLD_MONTHS` | number | ランウェイ警告閾値(月数) | (デフォルト値なし) |
  | `F09_ALERT_TARGET_EMAIL` | string | フォールバック通知先メール | (デフォルト値なし) |
  | `F09_ALERT_TARGET_SLACK_WEBHOOK` | string | Slack Webhook URL | (未設定時はメールにフォールバック) |

- **通知手段**: `F09_ALERT_TARGET_SLACK_WEBHOOK` が設定されていれば `Utils.postToSlack(webhookUrl, payload)` を呼び出す(本関数は `000_infra/004_utils.js` に **新規追加** する。Phase 1 で確認した末尾行の後に追記)。未設定の場合は `MailApp.sendEmail()` でフォールバック通知
- **排他ロック**: `LockService.getScriptLock()` で排他制御。`lock.tryLock(3000)` が失敗した場合は即時 `return`
- **日次重複通知防止**: Phase 1 で確認した `Env` モジュールの対応状況に基づき、以下のいずれかで実装:
  - `Env` に汎用 set/get がある場合: `Env` 経由で `F09_ALERT_SENT_YYYY-MM-DD` キーを管理
  - ない場合: `PropertiesService.getScriptProperties().getProperty()` / `setProperty()` を直接使用(CLAUDE.md の禁止対象は環境依存値の参照に限定されるため、アプリ状態管理での直接利用は許容と判断。注意事項に記載)
- **トリガー登録**: Phase 1 で確認した `101_sys_config.js` の既存パターンに従い、`checkCashShortageAlert` を日次トリガーに登録する手順を動作確認セクションに記載

**## 影響範囲**(新規ファイル `609_cash_alert.js` / `004_utils.js` への `postToSlack` 追記 / `101_sys_config.js` へのトリガー登録追加 / `03_sys_params` への 5 パラメータ行追加)

**## 注意事項**(番号付きリストで以下を記載):
1. `Utils.postToSlack` は新規実装関数。`004_utils.js` の末尾に追記し、既存関数を変更しない
2. `Constants.getParam` は `03_sys_params` シートを毎回読み込む(`_paramsCache` あり)。トリガー実行ではキャッシュが引き継がれないため初回呼び出しで確実に読み込まれる
3. パラメータ未設定時はデフォルト値で動かさず処理中断し `Utils.logError` でログ出力する
4. `PropertiesService` 直接使用(日次フラグ管理)は `Env` モジュールの汎用 setter/getter が存在しない場合の代替措置(Phase 1 確認結果を反映)
5. `84_cf_daily_plan` シートはユーザー操作で削除・再生成されうる。シート不在時は `Utils.logError` でログ出力して処理中断

### Step 2-3a: エッジケース〜人間検討事項の追記(File Edit または Bash、〜200行)

**## エッジケース**(テーブル形式):

| 条件 | 動作 | 理由 |
|---|---|---|
| `F09_ALERT_ENABLED` が `"true"` 以外または未設定 | 処理中断・ログ出力 | 設定不備を検出。デフォルト動作させない |
| Slack Webhook URL 未設定 | メールにフォールバック | 通知経路の冗長化 |
| メールアドレス・Webhook URL 両方未設定 | 処理中断・`Utils.logError` | 通知不能状態を記録 |
| データソースシートが存在しない | 処理中断・`Utils.logError` | シート再生成待ち |
| データソースの最終更新日が 2 日以上前 | アラートスキップ・ログ出力 | 古いデータで誤検知を防ぐ。更新日列の特定は Phase 1 で確認した列構造に依存 |
| ランウェイ値が `"黒字"` / `Infinity` / 非数値 | アラート対象外(正常スキップ) | 資金ショートリスクなし。`typeof val === 'number' && isFinite(val)` でチェック |
| 予測残高が既にマイナス(資金ショート中) | 閾値判定を通常通り実施。日次フラグにより 1 日 1 回に抑制 | 毎時通知は避ける |
| 同日に複数トリガーが発火(GAS 重複実行) | `LockService` で後発が即時終了 | 排他制御で二重通知を防ぐ |
| 対象月のデータが空行 | 行スキップ | ヘッダー名ベースで値を取得し空値チェック |

**## 実データ検証**(実装前に MCP で確認):
- `84_cf_daily_plan`(または Phase 1 で確定したシート名)の実際のヘッダー行:残高列・ランウェイ列・対象年月列の正確な列名
- `03_sys_params` シートの構造(A列=キー、B列=値 の形式を確認)
- `F09_ALERT_ENABLED` 等 5 パラメータが未登録であることを確認(既存キーとの衝突チェック)

**## 関連ドキュメント**(テーブル: F-08 仕様書へのリンク・`84_cf_daily_plan` 関連レポートファイル)

**## 人間が検討すべき事項**:
- TODO_future.md から転記した内容
- アラートメッセージに必ず含める要素: (1) 閾値を下回る最初の将来月、(2) 予測残高と設定閾値の具体値、(3) 資金繰り予測シートへの直接リンク(`SpreadsheetApp.getActiveSpreadsheet().getUrl()`)、(4) 詳細確認を促す文言
- 通知はあくまで情報提供。対策の判断と実行は人間が行う(Human-in-the-Loop)
- `03_sys_params` へのパラメータ追加は開発者が手動で行う(DDL 管理対象外の動的シート)

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

(省略。仕様書本体 ## 実装プロンプト(Claude Code 用) セクションを参照)

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

仕様書末尾に以下を追記:

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

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

    (この <instruction> 全文をここに記録)

    </details>

---

## Phase 3: 後処理(3 ステップ)

### 3-A: `docs/_config.json` への登録(必須)
`nav` 配列の §E.5(FP&A・レポーティング)セクションに追記:
```json
{ "file": "dev/dev_F-09_cash_shortage_alert.md", "title": "E.5.X F-09 資金ショート警告アラート" }
```
追記後に JSON 構文を確認する(末尾カンマ等のエラーチェック)。

### 3-B: changelog 追記
`docs/_internal/changelog.md` の先頭行(ヘッダー直後)に追記:
```
| 2026-04-20 | [dev_F-09_cash_shortage_alert.md](dev_mas-009_cash_shortage_alert.md) | 初版作成。日次トリガーによる資金ショート警告アラート仕様 |
```

### 3-C: コミット&プッシュ
```
git add docs/dev/dev_F-09_cash_shortage_alert.md docs/_internal/changelog.md docs/_config.json
git commit -m "docs: F-09 資金ショート警告アラートの開発仕様書を作成

GAS日次トリガーによる自動監視・Slack/メール通知機能の仕様。
前提案件F-08のデータソース構造を踏まえたエッジケース設計含む。

https://claude.ai/code/session_XXXXX"
git push -u origin docs/dev-F-09
```
</instruction>

📌 取り込み時の注記 (2026-06-02 sub 復元)

本仕様書は旧 F-番号体系で作成され PR 未マージのまま孤立していたドラフトを、origin/docs/dev-* ブランチから内容無改変で復元し、案件ID のみ MAS 体系へ正規化したもの。status: Open(未実装)

⚠️ ファイル番号ドリフト: 本文「対象ファイル」が指す 600_report/610〜612_*.js は現行 main で 別機能に使用済み(610=投資分析/MAS-013・611=財務モデリング/MAS-010・612=採用sim/MAS-012)。 実装時にファイル番号の再割当が必要。