概要

項目内容
案件IDMAS-030
カテゴリFP&A・レポーティング(プロダクト事業メトリクス / BizDev)
PhaseP3
優先度★★
所要時間3-4時間
対象ファイル600_report/611_cohort_analysis.js(新規作成)
100_config/101_sys_config.jssetupAllSchemas にスキーマ追加、confSheet.appendRow でシートキー登録)
000_infra/002_constants.jsMENU_DEFINITION の「📋 サイドバー: 📊 マート更新」カテゴリにメニュー項目追加)

目的

顧客獲得月を縦軸(コホート)・経過月(M0, M1, M2...)を横軸とした コホート分析表 を自動生成し、顧客リテンション・ARPU(Average Revenue Per User)・収益成長率を可視化する。TODO_future.md §3.2.1 プロダクト事業メトリクスの一環として、「いつ獲得した顧客がどう育っているか(Land & Expand)」を定量的に追跡できるようにする。

出力は新規シート 69_biz_cohort_analysis の 1 枚に 4 表(顧客数リテンション表 / リテンション率表 / ARPU 表 / 収益成長率表)を順次配置する単純レポーティング機能で、既存仕訳42_trn_journal)への書き込みは行わない。

現在のコード

当該分析シート・ビルダー関数ともに未存在。新規作成案件。依存する既存 API は以下:

  • JournalRepository.findAll() → 戻り値型 { headers: string[], dtos: JournalEntryDTO[] }200_data/202_repository.js:270)。内部の readSheetAsDtos_() で DTO 変換済みのため、追加で Contracts.toDtoList() を呼ぶ必要はない。
  • Utils.parseDateToYm(val)Date|string|number を受け取り "YYYY-MM" 文字列 or ""(パース不可)を返す(000_infra/004_utils.js:92-99)。
  • Utils.parseAmt(val) → カンマ区切り・全角数字に対応した数値パース(000_infra/004_utils.js:191-198)。
  • Utils.logInfo(funcName, message) / Utils.toastResult(funcName, message, duration) → ログ・完了通知(000_infra/004_utils.js:232, 258)。
  • JournalEntryDTO000_infra/003_contracts.js:96-129)の主要フィールド:
    • 発生日(P/L計上日) : Date|string
    • 収支区分 : "収入" | "支出"
    • 取引先名 : string
    • 税抜金額_実績 : number
    • 有効フラグ は未定義42_trn_journal スキーマ定義にも存在しない。101_sys_config.js:860 参照)

修正方針

アーキテクチャ

JournalRepository.findAll()
  → dtos を 収支区分 === "収入" でフィルタ
  → インメモリで { 取引先名 × 年月 → 収益合計 } にクロス集計
  → { 取引先名 → 獲得月(最小年月) } を算出
  → コホート(獲得月) × 経過月(M0, M1, ...) の 4 表を構築
  → 69_biz_cohort_analysis シートをクリアして出力

新規ファイル構成

  • 600_report/611_cohort_analysis.js
    • 既存 609_datamart_kpi.js(F-03 KPIダッシュボード)・610_datamart_saas_kpi.js(F-28 ARR/MRR トラッキング 仕様書予約)に続く番号として 611 を採用。
    • トップレベル関数: runCohortAnalysis()
    • 内部ヘルパー(いずれも ファイル内 local、namespace 不要):
      • buildAcquisitionMap_(dtos) — 取引先名ごとの獲得月(最小年月)マップ
      • buildRevenueMatrix_(dtos){ [取引先名]: { [年月]: 収益合計 } } のクロス集計
      • buildCohortTables_(acqMap, revMatrix, allMonths) — 4 表 2D 配列を構築
      • writeCohortSheet_(sheet, tables) — クリア + setValues + 条件付き書式

多重実行防止

関数冒頭で以下を取得し、try { … } finally { lock.releaseLock(); } で確実に解放する:

var lock = LockService.getScriptLock();
if (!lock.tryLock(5000)) {
  SpreadsheetApp.getUi().alert('🚫 他のコホート分析処理が実行中です。しばらくしてから再実行してください。');
  return;
}

主要ロジック(3 ステップ)

  1. 獲得月マップ生成: dtos.filter(d => d.収支区分 === '収入') で抽出した各 DTO について、Utils.parseDateToYm(d['発生日(P/L計上日)']) で年月を取得。取引先名ごとに「最小年月」を保持し、{ [取引先名]: 獲得月YYYY-MM } のマップを生成。Utils.parseDateToYm()"" を返した行は Utils.logInfo() でログ出力してスキップ。

  2. クロス集計: 同じフィルタ結果について、{ [取引先名]: { [年月]: 税抜金額_実績合計 } } を生成。金額は Utils.parseAmt(d['税抜金額_実績']) で数値化(万一文字列が混入していても防御)。同一月・同一取引先の複数仕訳は加算する。

  3. 4 表構築: 全ての獲得月の一覧(昇順ソート)× 全ての観測月(昇順ソート)を軸として、monthsDiff(cohortYm, obsYm) が 0 以上となるセルを埋める。経過月インデックス(M0, M1, M2, ...)は (obsY - cohY) * 12 + (obsM - cohM) で算出する。

出力 4 表の定義(同一シート内に上から順に配置)

内容
1. 顧客数リテンション表整数(アクティブ顧客数)各コホートで対象経過月に売上が 1 円以上あった顧客数
2. リテンション率(%)表小数(M0=100%基準)表1[cohort][Mi] / 表1[cohort][M0]。条件付き書式でヒートマップ化(緑系グラデーション・0%=白・100%=濃緑)
3. ARPU 表整数(円)コホート合計収益 / アクティブ顧客数。アクティブ顧客数 = 0 の場合は "-"
4. 収益成長率(%)表小数(当月総収益 / 前月総収益) - 1。前月収益 = 0 の場合は "-"

各表の先頭に太字の見出し行(例: ▼ 顧客数リテンション)を置き、表間に 1 行の空行を挟む。

DDL スキーマ追加(100_config/101_sys_config.js

  • setupAllSchemas の confSheet 登録ブロック(L795 PL_VAR 等の近く)に追加:
    if (!existKeys.includes('BIZ_COHO')) confSheet.appendRow(['BIZ_COHO', '', '69_biz_cohort_analysis', 'コホート分析(顧客リテンション/ARPU/収益成長率)']);
    
  • schemas オブジェクト(L826 以降)に追加(ヘッダーは 1 セルのみ。表データは writeCohortSheet_ が動的生成するため DDL では見出しのみ確保):
    'BIZ_COHO': { headers: ['コホート分析'], color: '#674ea7' },
    
  • 93_kpi_dashboard と同様に動的生成比率が高いシートだが、DDL 管理対象として明示的に登録する(F-28 KPI_SAAS と同方針)。シート作成の冪等性確保のため。

メニュー登録(000_infra/002_constants.js

Constants.MENU_DEFINITION'📋 サイドバー: 📊 マート更新' カテゴリ(002_constants.js L230)の items 末尾に追加:

{ label: '📈 コホート分析 更新', funcName: 'runCohortAnalysis', description: '42_trn_journal から獲得月×経過月のコホート分析を 69_biz_cohort_analysis に出力' },

カテゴリ名 '📋 サイドバー: 📊 マート更新'002_constants.js:230 実在名のコピー。造語禁止。

金額フィールド

  • 主たる集計は 税抜金額_実績JournalEntryDTO @property {number} 税抜金額_実績003_contracts.js:108)。
  • DTO 定義上は number だが、シート側で文字列として格納されているケースが過去複数例あるため(失敗パターン #21: 金額文字列混入)、集計時は必ず Utils.parseAmt(d['税抜金額_実績']) を通す。

影響範囲

区分ファイル変更内容影響度
新規600_report/611_cohort_analysis.js新規作成。runCohortAnalysis() + 内部ヘルパー 4 関数
追記のみ100_config/101_sys_config.jssetupAllSchemas 内の confSheet.appendRowschemas オブジェクトに各 1 行追加(L795 / L900 付近)低(DDL 再実行で反映)
追記のみ000_infra/002_constants.jsMENU_DEFINITION の「📋 サイドバー: 📊 マート更新」カテゴリに 1 項目追加(L232 付近)低(onOpen() 次回実行で反映)
新規出力69_biz_cohort_analysis(シート)新規シート生成。既存シートと衝突なし

他機能への影響: runCohortAnalysis()42_trn_journal読み取り専用 で参照するだけで、既存仕訳・サブ元帳・マート(P/LB/S・CF)への書き込みは一切行わない。したがって Action A / Action B / buildBudgetTrendDataMart 等の既存パイプラインへの影響はない。

注意事項

  1. Contracts.toDtoList() を追加で呼ばないJournalRepository.findAll() は内部の readSheetAsDtos_()200_data/202_repository.js:19-29)で既に DTO 変換済み。戻り値の .dtos をそのまま使う。二重変換しないこと。
  2. 列参照は必ずヘッダー名ベースd['発生日(P/L計上日)'] / d['収支区分'] / d['取引先名'] / d['税抜金額_実績'] のように DTO プロパティ名で参照する。列番号ハードコード禁止(CLAUDE.md コーディング規約)。
  3. 42_trn_journal は有効フラグ列を持たないJournalEntryDTO003_contracts.js:96-129)および DDL スキーマ(101_sys_config.js:860)どちらにも 有効フラグ は定義されていない。したがってフィルタ条件は 収支区分 === '収入' のみで可。他シート(32_wrk_invoice 等)の 有効フラグ=FALSE スキップの習慣でコードを書くと常に空テーブル出力になる失敗に繋がる ので注意。
  4. Utils.parseDateToYm() の戻り値 "" は必ずガード — パース不可の行を無視せずクロス集計に含めると、空文字列キーのセルが生成されて表が崩れる。該当行は Utils.logInfo('runCohortAnalysis', '日付パース失敗: trnId=' + d['取引ID']) でログ出力してスキップ。
  5. シート番号 69 の衝突リスク — F-28 ARR/MRR トラッキング仕様書(dev_F-28_arr_mrr_tracking.md)が 69_kpi_saas を予約済み。両案件とも spec-only の段階なので、先に実装する側が 69 を確保し、後発は別番号(例: 70)に調整する 方針とする。実装着手時に 101_sys_config.js の最新 schemas を確認し、69_kpi_saas が先行登録済みであれば本仕様を 70_biz_cohort_analysis / BIZ_COHO に読み替える。
  6. ファイル番号 610 は F-28/F-29 で予約済みdev_F-28_arr_mrr_tracking.md610_datamart_saas_kpi.jsdev_F-29_unit_economics.md610_datamart_uniteconomics.js を予約しており、F-29 側も競合している。F-30 は 611 とする。こちらも先着順で 612 以降への繰り下げを許容。
  7. 金額が文字列の可能性 — 理論上 税抜金額_実績 は数値型だが、実データで文字列混入が検出された場合は Utils.parseAmt() で数値変換する。MCP 実データ確認を推奨(後述「実データ検証」参照)。
  8. Expand の定義は暫定 — 個別顧客のアップセル/クロスセル追跡は本案件スコープ外。「コホート全体の当月総収益 / 前月総収益 - 1」= 収益成長率として算出する(後述「人間が検討すべき事項」参照)。

エッジケース

以下の条件で例外を throw せず、表示値(文字列 or 数値)をシートに書き込む、またはスキップしてログ記録する。

条件挙動理由
収入レコードが 0 件処理中断・SpreadsheetApp.getUi().alert('⚠️ 収入仕訳が 0 件です。コホート分析をスキップしました。') でユーザー通知・正常終了空テーブル出力を防ぐ
特定月に売上がない顧客その月は非アクティブ扱い(顧客数リテンション表で値 0/ARPU 表・収益成長率表は空)売上ベース(≠ 月次在籍)のリテンション判定の自然な帰結
チャーン後の復活(N ヶ月欠損後に再度売上)復活月からアクティブ再カウント(V 字回復挙動を許容)売上ベース集計の自然な帰結。厳密な「チャーン確定フラグ」は本案件スコープ外
ARPU 計算時にアクティブ顧客数 = 0表示値 "-"ゼロ除算防止。数値 0 だと「ARPU 0 円」と誤解されるため文字列で区別
収益成長率計算時に前月収益 = 0(または前月値が存在しない M0)表示値 "-"ゼロ除算防止。M0 は前月が存在しないため必ず "-"
Utils.parseDateToYm()"" を返す行Utils.logInfo('runCohortAnalysis', '日付パース失敗: trnId=' + d['取引ID']) でログ出力してスキップ日付不正データの混入防止。ハードクラッシュさせない
税抜金額_実績 が空欄・文字列Utils.parseAmt() で 0 or 数値に変換。結果 0 なら売上 0 として集計失敗パターン #21(金額文字列混入)への防御
取引先名 が空欄Utils.logInfo でログ出力してスキップ(コホート分類不能のため)空キーのマップ生成を防ぐ
同一取引先・同一月に複数の収入仕訳合算(加算)クロス集計の自然な挙動
獲得月のみ売上があり以降 0(単発顧客)M0 以降は非アクティブ正しい挙動(新規獲得後すぐチャーン)
観測月が未来月(まだ到来していない月)そもそもデータが存在しないため列自体が生成されないクロス集計の自然な帰結

実データ検証

実装前に MCP(Google Sheets MCP)で 42_trn_journal の以下を確認する。Phase 1 の机上確認で済ませず、実装着手直前に必ず MCP 実行すること(失敗パターン #3: 値の乖離で全件除外)。

確認項目目的確認方法例
収支区分 列の実値'収入' に末尾スペース・全角スペース等の混入がないかget_all_values収支区分 列のユニーク値を列挙
税抜金額_実績 列の型数値型か文字列型か。文字列ならカンマ区切りかランダムサンプル 10 行の型確認
発生日(P/L計上日) 列の型Date / "YYYY-MM-DD" / "YYYY/MM/DD" / "YYYY-MM" のどれかparseDateToYm が全て吸収するが実態把握のため確認
有効フラグ 列の実在存在しないことを確認(DTO定義・スキーマ定義とも存在しない前提)ヘッダー行に含まれないことを確認
取引先名の表記揺れ'株式会社XXX' vs 'XXX(株)' 等の表記揺れがないかユニーク値を列挙し、同一顧客が複数キーに分裂していないか確認
テスト対象月のデータ量最低 3 顧客 × 4 ヶ月分のテストデータが存在するか件数集計

関連ドキュメント

仕様書 / 参照元関連箇所
TODO_future.md §3.2.1 プロダクト事業メトリクスF-30 の案件定義
dev_F-28_arr_mrr_tracking.mdプロダクト事業メトリクスの姉妹案件。69_kpi_saas との番号衝突調整
dev_F-29_unit_economics.md同 姉妹案件。610_datamart_uniteconomics.js とのファイル番号衝突調整
dev_F-03_kpi_dashboard.md動的生成シート(93_kpi_dashboard)のクリア→再構築パターン・条件付き書式の実装リファレンス
CLAUDE.mdコーディング規約(列参照ルール・ファイル番号体系・シートキー登録方法)
000_infra/003_contracts.js:96-129JournalEntryDTO 定義
200_data/202_repository.js:259-298JournalRepository.findAll()
000_infra/004_utils.js:92-99Utils.parseDateToYm()
600_report/609_datamart_kpi.js動的シート生成・条件付き書式のリファレンス実装

人間が検討すべき事項

TODO_future.md からの転記事項と、仕様策定時に確定した暫定方針を列挙する。以下は実装後の運用で見直し余地がある項目。

  1. コホート粒度(月次 vs 四半期 vs 年次) — TODO_future.md 原文で言及されている論点。

    • 初版は月次で実装。四半期/年次への切り替えはパラメータ化せず、別途拡張案件として起票する。
    • 月次コホートは「表が肥大化する」(獲得月が増えるほど行数が増える)欠点があるため、データ量が一定量を超えた時点で粒度見直しを検討。
  2. Expand の定義(アップセル・クロスセル・追加ライセンスの区別) — TODO_future.md 原文で言及されている論点。

    • 初版はコホート全体の収益成長率として算出(コホートの当月総収益 / 前月総収益) - 1)。
    • 個別顧客のアップセル追跡(同一顧客内で「基本契約」「オプション」等の科目内訳を見る)は、科目名 / 摘要 の構造化が前提。現時点で科目マスタがプロダクトライン単位に整理されていないため、本案件スコープ外。
    • 将来的には 12_mst_partner × 科目階層 × PJ名 の組み合わせで Expand/Upsell/Cross-sell を分離する案を別案件化する。
  3. 顧客識別キー取引先名 のままで分析する(表記揺れは「実データ検証」で把握)。

    • 将来的には 取引先コードJournalEntryDTO.取引先コード)を第一キー、取引先名 を第二キーに昇格する選択肢もある。初版は単純化のため取引先名単独で分析。
  4. ヒートマップの色設定(リテンション率表) — 条件付き書式の閾値・色は実装者が判断する領域。

    • 推奨デフォルト: 緑系グラデーション・0%=白・100%=濃緑(#d9ead3#38761d)。
    • リテンション率が 100% を超えるケース(後述「NRR」)には別色(紫系など)を適用するかは運用フィードバックで調整。
  5. NRR / GRR への拡張 — 本仕様のリテンション率は「顧客数ベース」で 100% 上限。

    • 収益ベースで計算すれば NRR(Net Revenue Retention)が得られ、Expand を含めた 100%超の数値が取れる。本案件では簡易版として顧客数ベースに留め、NRR 表の追加は将来拡張とする。
  6. チャーン定義の厳密化 — 本仕様は「売上が観測されない = 非アクティブ」としか判定しない。

    • 「N ヶ月連続で売上なし = チャーン確定」のような厳密な定義は 12_mst_partner に契約終了日が入力されれば精度が上がる。契約マスタ整備(TODO_future.md の別案件)と合わせて検討。
  7. 観測月の範囲(全期間 vs 直近 N ヶ月) — 初版は全期間の観測月を横軸に出す。

    • 将来的にはパラメータ CFG_COHORT_MAX_MONTHS(例: 36 ヶ月)で列数を制限するオプションを検討(列数が表幅を超過する対策)。

実装プロンプト

以下のプロンプトを Claude Code に与えて実装させる。行頭 4 スペースインデントでコードブロックを開始するため、バッククォートで囲まず原文として渡す

あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 F-30「コホート分析」を実装してください。

## 実行前タスク(Read で固有名詞を確定してから実装に入ること)

- `000_infra/003_contracts.js` — `JournalEntryDTO` の全フィールド名(`発生日(P/L計上日)`・`収支区分`・`取引先名`・`税抜金額_実績` 等)を確認。**`有効フラグ` は定義されていないことを確認**(定義されていれば仕様書の前提が崩れるため要相談)
- `200_data/202_repository.js` — `JournalRepository.findAll()` の戻り値型が `{ headers: string[], dtos: JournalEntryDTO[] }` であることを確認。内部の `readSheetAsDtos_()` が DTO 変換済みのため `Contracts.toDtoList()` の追加呼び出しは不要
- `000_infra/004_utils.js` — `Utils.parseDateToYm()`(戻り値 `""` でパース失敗を示す)・`Utils.parseAmt()`・`Utils.logInfo()`・`Utils.toastResult()` のシグネチャを確認
- `100_config/101_sys_config.js` — `setupAllSchemas` の以下を確認:
  - `confSheet.appendRow([...])` の既存パターン(L783-L823 付近の `PL_VAR` / `KPI_DASH` 等)
  - `schemas` オブジェクトの既存パターン(L826 以降)
- `000_infra/002_constants.js` — `MENU_DEFINITION` の `'📋 サイドバー: 📊 マート更新'` カテゴリ(L230 付近)の `items` 配列構造を確認。**メニュー名は造語せず実在文字列のコピーのみ使う**
- `docs/dev/dev_F-28_arr_mrr_tracking.md` / `dev_F-29_unit_economics.md` — **先行実装の有無を確認**。
  - `69_kpi_saas` が既に `101_sys_config.js` に登録済みであれば、本実装は `70_biz_cohort_analysis` / `BIZ_COHO` に読み替える
  - `610_datamart_saas_kpi.js` / `610_datamart_uniteconomics.js` が既に存在すれば、本実装のファイル名 `611_cohort_analysis.js` はそのまま、なければ 610 に繰り上げ可

## 修正対象ファイル

1. `600_report/611_cohort_analysis.js` — 新規作成(衝突状況により 610 / 612 等へ調整可)
2. `100_config/101_sys_config.js` — `confSheet.appendRow` 1 行追加 + `schemas` オブジェクトに 1 エントリ追加(計 2 箇所のみ変更)
3. `000_infra/002_constants.js` — `MENU_DEFINITION` の '📋 サイドバー: 📊 マート更新' カテゴリ `items` 末尾に 1 項目追加

## 実装内容

### 1. `600_report/611_cohort_analysis.js`

```js
/**
 * F-30: コホート分析
 * 顧客獲得月 × 経過月(M0, M1, ...) のコホート分析表 4 種を
 * 69_biz_cohort_analysis シートに出力する。
 */
function runCohortAnalysis() {
  var FUNC = 'runCohortAnalysis';
  var lock = LockService.getScriptLock();
  if (!lock.tryLock(5000)) {
    SpreadsheetApp.getUi().alert('🚫 他のコホート分析処理が実行中です。しばらくしてから再実行してください。');
    return;
  }
  try {
    var result = JournalRepository.findAll();
    var incomes = result.dtos.filter(function(d) { return String(d['収支区分'] || '').trim() === '収入'; });
    if (incomes.length === 0) {
      SpreadsheetApp.getUi().alert('⚠️ 収入仕訳が 0 件です。コホート分析をスキップしました。');
      return;
    }

    var acqMap = buildAcquisitionMap_(incomes, FUNC);   // { 取引先名: 獲得月YYYY-MM }
    var revMatrix = buildRevenueMatrix_(incomes, FUNC); // { 取引先名: { YYYY-MM: 税抜金額_実績合計 } }
    var allMonths = collectAllMonths_(revMatrix);       // 観測月の昇順ユニーク配列
    var tables = buildCohortTables_(acqMap, revMatrix, allMonths);

    var sheet = Utils.getSheetByKey('BIZ_COHO', '69_biz_cohort_analysis');
    if (!sheet) {
      SpreadsheetApp.getUi().alert('🚨 69_biz_cohort_analysis シートがありません。先に setupAllSchemas を実行してください。');
      return;
    }
    writeCohortSheet_(sheet, tables);
    Utils.toastResult(FUNC, '✅ コホート分析を更新: ' + Object.keys(acqMap).length + ' 顧客 / ' + tables.cohorts.length + ' コホート');
  } catch (e) {
    Utils.logError(FUNC, e);
    SpreadsheetApp.getUi().alert('🚨 ' + e.message);
  } finally {
    lock.releaseLock();
  }
}
```

内部ヘルパーの実装ポイント:

- `buildAcquisitionMap_(dtos, FUNC)`:
  - 各 dto について `ym = Utils.parseDateToYm(dto['発生日(P/L計上日)'])` を取得
  - `ym === ''` ならスキップ + `Utils.logInfo(FUNC, '日付パース失敗: trnId=' + dto['取引ID'])`
  - `name = String(dto['取引先名'] || '').trim()`、空欄ならスキップ + `Utils.logInfo`
  - `map[name]` を `ym` との最小値で更新

- `buildRevenueMatrix_(dtos, FUNC)`:
  - `buildAcquisitionMap_` と同じガード条件で有効行を抽出
  - `amt = Utils.parseAmt(dto['税抜金額_実績'])`
  - `matrix[name][ym] = (matrix[name][ym] || 0) + amt` で加算

- `collectAllMonths_(revMatrix)`:
  - `Set` でユニークな年月を収集 → `Array.from(set).sort()` で昇順返却

- `buildCohortTables_(acqMap, revMatrix, allMonths)`:
  - 獲得月の昇順ユニーク配列 `cohorts = [...new Set(Object.values(acqMap))].sort()` を算出
  - 各 `cohort` について、最大経過月 `maxMi = Math.max(...allMonths で cohort 以降の月の個数 - 1)` を計算
  - 4 表をそれぞれ 2D 配列で構築し、戻り値 `{ cohorts, maxMi, customers, retention, arpu, growth }` に格納
  - 経過月計算は `monthsDiff(cohort, obsYm)` = `(obsY - cohY) * 12 + (obsM - cohM)`

- `writeCohortSheet_(sheet, tables)`:
  - `sheet.clear()` で完全クリア
  - 行オフセットを進めながら、見出し行 → ヘッダー行(`['コホート', 'M0', 'M1', ..., 'M' + maxMi]`)→ データ行 → 空行 の順で `setValues` する
  - リテンション率表・収益成長率表の数値セルは `sheet.getRange(r, c, h, w).setNumberFormat('0.0%;[Red]△ 0.0%;"-"')` で書式適用
  - リテンション率表に条件付き書式(緑系グラデーション 0%=白 `#ffffff` → 100%=濃緑 `#38761d`)を `SpreadsheetApp.newConditionalFormatRule().setGradientMinpointWithValue(...).setGradientMaxpointWithValue(...).build()` で設定

### 2. `100_config/101_sys_config.js`

`setupAllSchemas` 内の既存 `confSheet.appendRow` 群(L795 `PL_VAR` の近く)に追加:

```js
if (!existKeys.includes('BIZ_COHO')) confSheet.appendRow(['BIZ_COHO', '', '69_biz_cohort_analysis', 'コホート分析(顧客リテンション/ARPU/収益成長率)']);
```

`schemas` オブジェクト(L826 以降)に 1 行追加:

```js
'BIZ_COHO': { headers: ['コホート分析'], color: '#674ea7' },
```

※ **F-28 `KPI_SAAS` が既に `69_kpi_saas` を登録済みの場合** は、物理シート名を `70_biz_cohort_analysis` に変更し、本仕様書の該当箇所も合わせて書き換えること。

### 3. `000_infra/002_constants.js`

`MENU_DEFINITION` の `'📋 サイドバー: 📊 マート更新'` カテゴリ(L230 付近)の `items` 配列末尾に追加:

```js
{ label: '📈 コホート分析 更新', funcName: 'runCohortAnalysis', description: '42_trn_journal から獲得月×経過月のコホート分析を 69_biz_cohort_analysis に出力' },
```

## 制約

- `Contracts.toDtoList()` を追加呼び出ししない(`JournalRepository.findAll()` が `readSheetAsDtos_()` で DTO 変換済み)
- 列番号ハードコード禁止。ヘッダー名ベース(`d['発生日(P/L計上日)']` 等)で参照
- **`42_trn_journal` には `有効フラグ` 列が存在しない**。`dto['有効フラグ'] === false` のような条件を書かないこと(常に undefined で false 判定になり 0 件になる)
- `Utils.parseDateToYm()` の戻り値 `""` は必ずガードする
- メニュー名は `002_constants.js` を Read して確認した実在する文字列のみ使用
- `OrderRepository` / `InvoiceRepository` / `BankTxRepository` 等の他 Repository は本案件では不要(`42_trn_journal` のみ参照)

## 実データ検証(実装前に MCP 実行)

1. `42_trn_journal` の `収支区分` 列のユニーク値を列挙し、`'収入'` に末尾スペース等の混入がないことを確認
2. `税抜金額_実績` 列の型をランダムサンプル 10 行で確認(数値 or 文字列)。文字列の場合は `Utils.parseAmt()` 経由が必須
3. `発生日(P/L計上日)` 列のフォーマット確認
4. `取引先名` ユニーク値を列挙し、同一顧客が複数表記に分裂していないか確認

## 動作確認

1. `npm run push:dev` で dev 環境にデプロイ
2. GAS エディタで `setupAllSchemas()` を実行し `69_biz_cohort_analysis` タブがヘッダー付きで作成されることを確認
3. `42_trn_journal` に取引先 2〜3 社・3〜4 ヶ月分のテストデータを用意(手計算しやすい小さな値で)
4. GAS エディタで `runCohortAnalysis()` を直接実行
5. 手計算した獲得月マップ・各コホートのアクティブ顧客数・ARPU と、スクリプト出力が完全一致することを確認
6. `69_biz_cohort_analysis` シートに 4 表(顧客数リテンション / リテンション率 / ARPU / 収益成長率)が順に出力されていることを確認
7. リテンション率表に条件付き書式(緑系グラデーション)が適用されていることを確認
8. 収入レコード 0 件のケース(`42_trn_journal` を空にする or `収支区分` を全て `'支出'` に変更)でアラート通知が出ることを確認
9. `Utils.parseDateToYm` がパース不可となる不正日付を 1 行混入させ、該当行がスキップされてログに記録されることを確認
10. 同じ入力で 2 回連続実行し、結果が完全一致すること(冪等性)を確認

## 拡張思考の使用状況

| フェーズ | 拡張思考 | 備考 |
|---------|:--------:|------|
| 設計(実行前タスク) | あり | ファイル調査・固有名詞確定・衝突チェック |
| 清書(実装) | 最小限 | 仕様書で確定済みの内容の書き下し |

推奨実行モデル

工程推奨モデル理由
仕様書作成(本ドキュメント)Claude Opus 4.7姉妹案件 F-28/F-29 との番号衝突調整、エッジケース網羅、会計ロジック(売上ベースのリテンション定義)の妥当性確認に複数ファイル横断の設計判断が必要
Step 1: 611_cohort_analysis.js 新規実装Claude Sonnet 4.6既存パターン(609_datamart_kpi.js 等)の踏襲がベース。クロス集計・経過月インデックス計算・条件付き書式にやや判断要素あり
Step 2: 101_sys_config.js スキーマ追加Claude Haiku 4.5既存パターンに 2 行追加するだけ。判断要素なし
Step 3: 002_constants.js メニュー追加Claude Haiku 4.5既存 items 末尾に 1 行追加するだけ。判断要素なし
実装前 MCP 実データ検証Claude Sonnet 4.6列ユニーク値の確認と仕様書前提の整合性判断

変更履歴

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

仕様書作成プロンプト

展開して表示
【タイムアウト回避・実行原則(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(骨格 ~20行) / 2-2(概要〜注意事項 ~300行) / 2-3a(エッジケース〜人間検討事項 ~200行) / 2-3b(実装プロンプト〜変更履歴 ~250行) / 2-4(`<details>` プロンプト記録) に分割。1回の Write/Edit は約 300 行以内。
4. **各 Step で何を書くかを具体指示**: Phase 2 実行時に設計判断を持ち込まない。各 Step の内容は Phase 1 で完全確定させる。

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

---

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

以下のファイルを順番に Read し、Phase 2 で使う固有名詞・構造・行番号を **全て確定** させる。
名前や記憶から推測した瞬間に手を止めて Read すること(失敗パターン #18-#20: 1 回のコード未読から固有名詞誤記 3 件が同時発生した実例あり)。

### 1-A: 案件定義の読み込み
- `docs/_internal/TODO_future.md` — F-30 の行を検索し、案件名・概要・人間が検討すべき事項を取得

### 1-B: プロジェクト規約
- `CLAUDE.md` — コーディング規約(有効フラグ=FALSE スキップ、列参照ルール、ファイル番号体系)を確認

### 1-C: 既存仕様書フォーマット参照
- `docs/dev/dev_F-01_variance_analysis.md` — 大規模 FP&A 案件の仕様書構成を参照

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

1. **`000_infra/003_contracts.js`**
   - `JournalEntryDTO` の全フィールド名を確認(特に `発生日(P/L計上日)`・`収支区分`・`取引先名`・`税抜金額_実績`・`税込金額_実績`・`有効フラグ` の有無)
   - `有効フラグ` が DTO に定義されていない場合は「要調査」として注意事項に記載する

2. **`000_infra/004_utils.js`**
   - `Utils.parseDateToYm()` の引数型・戻り値(`""` を返すケースを確認)
   - `Utils.parseAmt()` の用途(金額フィールドが文字列の場合に必要)
   - `Utils.toastResult()` のシグネチャ(エラー通知手段として使用可否を確認)

3. **`200_data/202_repository.js`**
   - `JournalRepository.findAll()` の戻り値型が `{ headers: string[], dtos: JournalEntryDTO[] }` であることを確認
   - `readSheetAsDtos_()` が内部で DTO 変換済みであることを確認 → **`Contracts.toDtoList()` の追加呼び出しは不要**

4. **`100_config/101_sys_config.js`**
   - `setupAllSchemas`(または DDL スキーマ登録関数)の既存パターンを確認し、新シート `69_biz_cohort_analysis` の追加箇所を特定
   - `onOpen()` のメニュー登録コードを Read し、**実在するメニュー名**と追加位置を確定(メニュー名を造語しない)
   - `Utils.getSheetNameByKey()` が参照する `01_sys_config` へのシートキー登録パターンを確認(新シートキー名例: `BIZ_COHO`)

5. **`600_report/` ディレクトリの既存レポートファイル**(`601_datamart_ingest.js` 等)
   - シート初期化→データ書き込み→書式設定の既存パターンを確認
   - 新規実装ファイルの命名・配置先を決定(例: `609_cohort_analysis.js`、ファイル番号は既存の最大番号+1)

### 1-E: Phase 1 完了時点で以下を全て確定させること
- `JournalEntryDTO` に `有効フラグ` フィールドが存在するか(フィルタ方法を確定)
- 収益集計に使う金額フィールド名(`税抜金額_実績` 推奨。`003_contracts.js` の DTO 定義で確認)
- 新シートのシートキー名と `01_sys_config` への登録方法
- 実装関数名(例: `runCohortAnalysis()`)と実装ファイルパス
- メニュー登録先の実在するメニュー名(`onOpen()` を Read して確認)

---

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

出力先: `docs/dev/dev_F-30_cohort_analysis.md`
**【重要】1 回のツール呼び出しで全内容を出力せず、以下の Step に厳密に分割すること。**

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

### Step 2-2: 概要〜注意事項の追記 (Edit または Bash ~300行)
(本文は task_F-30.md 参照)

### Step 2-3a: エッジケース〜人間が検討すべき事項の追記 (Edit または Bash ~200行)
(本文は task_F-30.md 参照)

### Step 2-3b: 実装プロンプト〜変更履歴の追記 (Edit または Bash ~250行)
(本文は task_F-30.md 参照)

### Step 2-4: 仕様書作成プロンプトの記録 (Edit または Bash)
末尾に `<details><summary>展開して表示</summary>` ブロックを追加し、
この `<instruction>` 全文を記録する(再現性・監査性のため必須)。

---

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

### 3-A: `_config.json` への追記(必須)
`docs/_config.json` の §E.5(FP&A・レポーティング)セクションに以下を追加:
```json
{ "file": "dev/dev_F-30_cohort_analysis.md", "title": "E.5.X F-30 コホート分析" }

追記後は JSON 構文エラーがないことを確認すること。

3-B: changelog 追記

docs/_internal/changelog.md の先頭行(ヘッダー直後)に追記。

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

(task_F-30.md 参照)


</details>

---

## 📌 取り込み時の注記 (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)。
> 実装時にファイル番号の再割当が必要。