概要

項目内容
案件IDMAS-003
カテゴリFP&A
PhaseP1
優先度★★
所要時間4-6時間(MVP 5 KPI + 条件付き書式)
対象ファイル600_report/609_datamart_kpi.js(新規)
100_config/101_sys_config.js01_sys_config への KPI_DASH 登録+メニュー追加)
CLAUDE.md(「DDLで管理されないタブ」に追記)
出力先タブ93_kpi_dashboard(新設)
前提案件MAS-024(BEP・完了)、MAS-020(前年度スナップショット・完了)、MAS-001(予実差異・完了)
一次資料docs/spec/spec_dashboard.md §6(KPI 一覧・レイアウト・GAS制約)

目的

売上総利益率・営業利益率・BEP・ランウェイ・労働分配率など経営指標を 1 枚に集約し、CFO が当月/前月/YTD/6ヶ月トレンドを一目で把握できる情報提供型ダッシュボードを新設する。既存の 61 (P/L実績単月)・62 (YTD)・71 (B/S) に実装済みの値を Spreadsheet 数式で参照するのみとし、GAS は DDL 初期化・HELPER 行生成・条件付き書式設定に限定する。

現在のコード(参照先)

spec_dashboard.md §6 の設計ドラフト

docs/spec/spec_dashboard.md §6 に 8 KPI のドラフトが既に存在する。本仕様書は当該 §6 を一次資料として踏襲しつつ、以下の差分を反映する。

差分項目ドラフト (§6)本仕様書
BEP 行の参照名「  BEP到達率」実装は「  安全余裕率 (%)」(603_datamart_pl.js:383)。こちらを採用
表示 KPI 数8個MVP=5個 (K1/K2/K4/K5/K6) + Phase 2=3個 (K3/K7/K8)
閾値管理言及なし03_sys_paramsCFG_KPI_* パラメータで管理(後述)
実装ファイル言及なし600_report/609_datamart_kpi.js 新規

既存実装の活用(車輪の再発明を禁ずる)

参照対象既存実装参照方法
K5 BEP・安全余裕率603_datamart_pl.js:271-399 の BEP セクション。61/62 タブに「📊 損益分岐点分析 (BEP)」行として出力INDEX+MATCH で「  BEP売上高」「  安全余裕率 (%)」行を拾う
前年比較の基盤66_pl_prev_year(MAS-020 savePlSnapshot()前月比ではなく YoY 比較を出す場合はこの行にアクセス(MVPでは前月比のみで YoY は任意)
P/L 合計行61_pl_monthly 上の「✨ 売上総利益」「✨ 営業利益」「✨ 経常利益」(603_datamart_pl.js:264 付近の fmtM.push('profit') で出力)同上 INDEX+MATCH(改行対策必須)
B/S 残高71_bs 上の「現金預金」「売掛金」等の科目行同上
マート更新起動点602_datamart_main.js:buildBudgetTrendDataMart()末尾で buildKpiDashboard() を呼び出す(HELPER 行と条件付き書式のみ更新、本体は数式)
描画パターンdmApplyDwhFormat_(), dmApplyYoyFormat_()本機能は独自レイアウトのため dmApplyDwhFormat_ は使わず、buildKpiDashboard() 内で setValues + newConditionalFormatRule を直接呼ぶ
動的生成タブの先例76_notes, 72_bs_snapCLAUDE.md「DDLで管理されないタブ」に 93_kpi_dashboard も追加

修正方針

アーキテクチャ決定

  1. 新規タブ方式: 93_kpi_dashboard を新設(既存 91/92 決算書は法定様式で目的が異なるため分離)
  2. 行・列リテラル埋め込み方式(Step 2 実装時に仕様変更): 当初は INDEX+MATCH+ARRAYFORMULA+SUBSTITUTE ベースで設計したが、絵文字・全角スペース・改行混在で MATCH が壊れる事例が多発。GAS 側で行番号と列番号を事前計算してリテラルとして数式に埋め込む方式に変更。INDEX(sheet!$C:$N, <row>, <col>) の形で生成し MATCH を廃止
  3. 月列は 12 列固定(C-N): 61_pl_monthly は MAS-020 で YoY 差異列(O-Z の △YYYY-MM)が追加されており、getLastColumn() がこれを含めて返す。列解決は必ず C:$N の 12 列に限定する。YoY 差異列の "-" 値を末尾非空セルと誤判定して列番号が崩れた事例あり
  4. 境界月の動的検出: 03_sys_paramsCFG_BOUNDARY_YM が基本だが未設定時は 61_pl_monthly の売上高行(  売上高 or  【売上高 計】)の末尾非空月から自動検出。ヘッダーが Date オブジェクトの場合に備え Utilities.formatDate()"yyyy-MM" に正規化
  5. ラベル比較の注意: String.trim()全角スペース (U+3000) も除去する。  売上高 を trim すると 売上高 になるため、比較する側も同等に trim するか、trim せず完全一致でマッチする
  6. B2 の文字列強制: 境界月 "2026-03"setValue(boundaryYm) するとスプレッドシートが日付 or 負数として解釈する問題あり。setNumberFormat('@').setValue("'" + boundaryYm)(apostrophe 前置)で強制文字列化
  7. SPARKLINE のデータ範囲: 同一シート内の HELPER 行(最下部の行 28-34)に過去 6 ヶ月値を展開して参照する(他シート参照は一部ケースで機能しない既知制約)
  8. 更新タイミング: 数式参照のため buildBudgetTrendDataMart() 実行で自動追随。末尾で buildKpiDashboard() を try/catch で呼び、KPI 描画失敗でもマート更新本体は成功扱い
  9. シートキー: KPI_DASH をシステム設定シート 01_sys_config(= Constants.CONFIG_SHEET)に登録する。setupAllSchemas() 内で confSheet.appendRow(['KPI_DASH', '', '93_kpi_dashboard', 'KPIダッシュボード'])。タブ名は Utils.getSheetNameByKey('KPI_DASH') \|\| '93_kpi_dashboard' で取得。000_infra/002_constants.jsSHEET_DEFAULTS触らない{ pattern, prefix, defaults } 形式で smartAddRow 行デフォルト値用)
  10. 動的生成タブ扱い: setupAllSchemas() では登録のみ、本体描画は buildKpiDashboard() に委譲(76_notes と同じ扱い)

表示 KPI の範囲

#KPI計算式Phaseデータソース
K1売上総利益率売上総利益 ÷ 売上高MVP61_pl_monthly
K2営業利益率営業利益 ÷ 売上高MVP61_pl_monthly
K3経常利益率経常利益 ÷ 売上高拡張61_pl_monthly
K4労働分配率人件費合計 ÷ 売上総利益MVP61_pl_monthly
K5BEP売上高 / 安全余裕率MAS-024 算出済みを参照MVP61_pl_monthly(BEPセクション)
K6ランウェイ (月数)現預金残高 ÷ 月間固定支出MVP71_bs + 61_pl_monthly(固定費合計)
K7売掛金回転日数売掛金残高 ÷ (売上高 ÷ 365)拡張71_bs + 62_pl_ytd
K8自動起票率自動起票INV数 ÷ 全INV数拡張32_wrk_invoice(起票種別列の存在に依存)

MVP = TODO_future.md に明記された 5 KPI(K1/K2/K4/K5/K6)。K3/K7/K8 は Phase 2 として仕様書末尾「人間が検討すべき事項」で実装判断を仰ぐ。

レイアウト(Step 2 実装版)

KPI 本体行の直下に、分子・分母・内訳科目を埋め込み表示する構成。単独の「計算の内訳」セクションは設けない(上表・下表の列ズレを回避するため)。

行 1:     [📊 KPIダッシュボード] (A1:F1 merge, 黒帯ヘッダー)
行 2:     [対象期間:] [<YYYY-MM>] [(前月比=前月との差分)]
行 3:     [KPI] [当月] [前月] [前月比] [YTD] [トレンド (6ヶ月)]
行 4:     K1 売上総利益率         [main: 黄色背景+太字+SPARKLINE]
行 5-6:     - 売上総利益 / 売上高 計 [sub: グレー文字]
行 7:     K2 営業利益率
行 8-9:     - 営業利益 / 売上高 計
行 10:    K4 労働分配率
行 11:      - 人件費合計            [sub: グレー文字]
行 12-17:     · 役員報酬〜雑給 6科目 [subsub: 更に薄いグレー]
行 18:      - 売上総利益 (分母)
行 19:    K5 BEP売上高
行 20:      - 固定費合計
行 21-22:     · うち人件費合計 / うち非人件費 [subsub]
行 23-24:   - 変動費合計 / 変動費率
行 23:    K5 安全余裕率             (派生値なので sub なし)
行 24:    K6 ランウェイ
行 25-26:   - 現預金残高 / 固定費合計 (分母)
行 27:    (空白)
行 28-34: HELPER (非表示): 月ラベル + K1〜K6 の過去6ヶ月値

行タイプ別の書式:

タイププレフィックス書式対象列
main(なし)黄色背景 #FFF2CC + 太字 + SPARKLINE 付きB-F 全列使用
sub - (半角スペース2+ハイフン)グレー文字 #555555B-D のみ (E/F 空白)
subsub · (半角スペース4+中黒)薄グレー文字 #888888B-D のみ

列定義(上表・埋め込み内訳 共通):

内容書式
A項目名 (KPI名 or sub/subsub ラベル)テキスト
B当月0.0%;[Red]△ 0.0%;"-" / 金額 #,##0;[Red]△ #,##0;"-" / 月数 0.0"ヶ月"
C前月同上
D前月比=B-C の IFERROR ラップ、表示は同フォーマット
EYTDmain 行のみ(sub/subsub は空白)
Fトレンドmain 行のみ。SPARKLINE は HELPER 行(B28-G34)を参照

閾値・条件付き書式

KPI条件背景色文字色パラメータキー
K1 売上総利益率< 閾値(デフォ 40%)#F4CCCC#CC0000CFG_KPI_GROSS_MARGIN_MIN
K2 営業利益率< 0%#F4CCCC#CC0000(閾値固定。赤字月は必ず警告)
K4 労働分配率> 閾値(デフォ 60%)#FCE5CD#B45F06CFG_KPI_LABOR_RATIO_MAX
K5 安全余裕率< 0%(BEP未達)#F4CCCC#CC0000(閾値固定)
K6 ランウェイ< 閾値(デフォ 6ヶ月)#F4CCCC#CC0000CFG_KPI_RUNWAY_MIN_MONTHS

03_sys_params 未設定時はデフォ値を使用。Constants.getParam('CFG_KPI_*', デフォルト値) で取得する。

数式の主要パターン(Step 2 実装版:行・列リテラル方式)

行番号と列番号は GAS 側で事前計算し、MATCH を使わずに INDEX(sheet!$C:$N, <行>, <列>) を生成する。

K1 売上総利益率(当月列、例: 売上総利益が行9、2026-03 が C:N 範囲の 8列目の場合):

=IFERROR(INDEX('61_pl_monthly'!$C:$N,9,8) / INDEX('61_pl_monthly'!$C:$N,5,8), "-")
  • 分子行(売上総利益 = 行 9)、分母行(売上高 計 = 行 5)、列(2026-03 = 8)は全て GAS の buildKpiContext_() が返す ctx.pl[label]ctx.colIdx から取得
  • GAS ヘルパー: kpiCellRefByIdx_(sheetName, rowNum, colIdx)INDEX(...) 部分を組み立てる

K4 労働分配率:

=IFERROR(
  IF(INDEX('61_pl_monthly'!$C:$N,9,8) <= 0, "N/A",
    (IFERROR(INDEX('61_pl_monthly'!$C:$N,15,8),0)  -- 役員報酬
    +IFERROR(INDEX('61_pl_monthly'!$C:$N,16,8),0)  -- 給料手当
    + ... 他4科目
    ) / INDEX('61_pl_monthly'!$C:$N,9,8)
  ),
  "-")
  • 人件費6科目(役員報酬・給料手当・賞与・法定福利費・福利厚生費・雑給)を各 IFERROR(..., 0) で合計
  • 不在科目(科目マスタに未登録または該当月に計上なし)は自動的に 0 扱い

K6 ランウェイ:

=IFERROR(
  IF(INDEX('61_pl_monthly'!$C:$N,<固定費行>,<当月列>) <= 0, "∞",
    INDEX('71_bs'!$C:$N,<現預金行>,<当月列>) / INDEX('61_pl_monthly'!$C:$N,<固定費行>,<当月列>)
  ),
  "∞")

固定費 = MAS-024 で算出された BEP セクションの「  固定費合計」行。減価償却費も含まれるため厳密な現金流出ではないが、MVP は単純固定費を使用(「人間が検討すべき事項」参照)。

SPARKLINE (K1 の例、HELPER 行 29 を参照):

=IFERROR(SPARKLINE($B$29:$G$29, {"charttype","line";"color","#1C4587"}), "")

HELPER 行 28 は月ヘッダー、行 29-34 が K1-K6 の過去 6 ヶ月値。

buildKpiDashboard() の責務

GAS 側でやることを限定する:

  1. タブが存在しなければ作成(93_kpi_dashboard
  2. タイトル・ヘッダー行を setValues で固定書き込み
  3. 各 KPI 行の数式 (setFormulas) を記述
  4. HELPER 行(過去 6 ヶ月値用)を生成。これも数式ベース
  5. 条件付き書式 (newConditionalFormatRule) を CFG_KPI_* パラメータを読んで設定
  6. 列幅・フォント (BIZ UDGothic) を固定
  7. HELPER 行を hideRows で非表示

本体の値計算は全て数式buildBudgetTrendDataMart() が 61/71 を更新すれば 93 は自動追随する。

影響範囲

変更ファイル変更量内容
600_report/609_datamart_kpi.js新規 ~470行buildKpiDashboard() + buildKpiContext_() + 行・列リテラル方式の数式ビルダー (kpiRatioByIdx_, kpiDirectByIdx_, kpiLaborRatioByIdx_, kpiRunwayByIdx_, kpiLaborSumRawFormula_, kpiFixedNonLaborFormula_) + renderKpiRows_ (main/sub/subsub 埋め込み) + HELPER 行生成 + applyKpiConditionalFormat_ (Step 3 で本実装予定)
100_config/101_sys_config.js+3行setupAllSchemas() 内の confSheet.appendRow 群(L440-470付近)に KPI_DASH を追加(FS_PL の直後・SS_NOTES の前)、onOpen() 内「📊 マート更新」メニューに buildKpiDashboard の項目追加
600_report/602_datamart_main.js+1行buildBudgetTrendDataMart() 末尾から buildKpiDashboard() 呼び出し
CLAUDE.md+1行「DDLで管理されないタブ」一覧に 93_kpi_dashboard を追加
docs/_config.json+1行§E.5 FP&A・レポーティング配下に dev_mas-003_kpi_dashboard.md を追加

000_infra/002_constants.js は変更しないSHEET_DEFAULTS はシートキーマッピングではなく smartAddRow 用の行デフォルト値管理({ pattern, prefix, defaults } オブジェクト配列)。シートキーは 01_sys_config に登録する。

既存動作への影響: なし。新規タブ追加のみ。61/62/71 の出力ロジックは変更しない。

注意事項

Step 2 実装で顕在化した落とし穴(失敗パターン #21-#24 候補、要 failure_patterns.md 登録)

#21 YoY 差異列による getLastColumn() の膨張 61_pl_monthly は MAS-020 実装で O-Z 列に △YYYY-MM(YoY 差異)が追加されており、getLastColumn() は 22 を返す。lastCol - 2 = 20 列分の範囲を取ると差異列の "-" 値を末尾非空セルと誤判定し、境界月列が壊れる事例が発生。月列は必ず 12 列固定(C-N)に限定

#22 String.trim() が全角スペースを除去 '  売上高'.trim()'売上高' になる(U+3000 が空白扱い)。セルラベル比較で trim を使う場合は比較文字列側も trim して前提を揃えるか、trim せず完全一致で行う。どちらか一方だけ trim すると永続的に不一致になる。

#23 setValue("2026-03") の自動パース スプレッドシートが文字列 "2026-03" を「2026 マイナス 3 = 2023」の負数として解釈し、△2026-03 のような表示になる事例あり。setNumberFormat('@').setValue("'" + value)(apostrophe 前置)で強制文字列化する。

#24 ARRAYFORMULA+SUBSTITUTE+MATCH の組み合わせの脆弱性 絵文字(例: ✨ 売上総利益 の U+2728)・全角スペース・改行が混在するラベルで MATCH が返値なしになる事例が多発。数式内 MATCH を完全廃止し、GAS 側で行番号を事前計算して埋め込む方式に変更した(設計段階で想定していた動的 MATCH 方式から実装変更)。

一般注意事項

  1. SPARKLINE の参照範囲: 同一シート内に HELPER 行を置くこと(行 28-34)。他シート直接参照だと一部ケースで描画されない
  2. 条件付き書式ルール上限(50/シート): MVP 5 KPI × 2 条件(赤・黄)= 10 ルールで余裕あり。拡張時も K7/K8 各 2 ルール追加なので上限問題なし
  3. DDL 動的生成タブ扱い: setupAllSchemas() の実行順で buildKpiDashboard() を呼ぶが、61/62/71 の更新前に呼ぶと数式のエラーが発生する。buildBudgetTrendDataMart() の末尾で try/catch で呼ぶ運用 (KPI 描画失敗でもマート更新本体は成功扱い)
  4. 固変区分の実データ乖離(MAS-024 失敗パターン踏襲): K6 の月間固定支出は MAS-024 の固定費を参照。固変区分未設定科目は MAS-024 と同様「固定費扱い(保守的推計)」
  5. filterWithRecalcTotal で非加算指標の Total が壊れる失敗パターン: KPI は全て非加算指標。Total (YTD) は 62_pl_ytd の YTD 行から独立参照
  6. 売上ゼロ月のゼロ除算(MAS-024 失敗パターン): 全ての率系 KPI は IFERROR(..., "-") でラップ
  7. 新規ファイル 609_datamart_kpi.js(MAS-192 失敗パターン踏襲): 600_report/ 配下に追加。既存関数 (dmApplyDwhFormat_, buildBudgetTrendDataMart) への参照は残し、呼び出し関係を grep で確認してから動作確認
  8. テスト対応: 90_test_results でテストする buildBudgetTrendDataMart() の下流に KPI 描画が追加されるため、テストの SKIP_PATTERNS に独立タブとして除外設定するか、専用テストケースを追加する判断

エッジケース

計算式があるため必須セクション。MVP で実装する 5 KPI のゼロ除算・異常値対応:

KPI条件表示値理由
K1 売上総利益率売上=0"-"IFERROR で吸収
K1 売上総利益率売上総利益 < 0(原価割れ)マイナス率を赤字表示実態を隠さない
K2 営業利益率売上=0"-"同上
K2 営業利益率営業利益 < 0(赤字)[Red]△ 0.0%条件付き書式で赤背景も発火
K4 労働分配率売上総利益 ≦ 0"N/A"分母が非正のため意味を持たない
K4 労働分配率100% 超[Red]100.0% 超 + 警告背景人件費 > 売上総利益
K5 BEP売上高MAS-024 側で "-" 出力(売上=0 や 変動費率≧100%)そのまま "-"MAS-024 の挙動を踏襲
K5 安全余裕率MAS-024 側で "-" 出力そのまま "-"同上
K5 安全余裕率< 0%(売上 < BEP)マイナス% + 赤背景赤字ライン
K6 ランウェイ固定費=0 または黒字経営(月間CF流入)"∞"分母≦0、定義上無限大
K6 ランウェイ< 6ヶ月月数 + 赤背景資金ショートリスク警告
前月比 (D列)前月 = "-" or "N/A""-"差分計算不可
YTD (E列)isActualOnly で空白月含むfilterValues 相当の挙動(数式側で空白月除外)加算可能指標(分子分母)は SUM、非加算指標(率)は YTD Total 行から独立参照
トレンド (F列)過去6ヶ月のうちゼロ除算月ありその月は空値(SPARKLINE が自動スキップ)IFERROR(計算, "")

フィルタ整合性(isActualOnly)

  • 61 P/L 実績タブは isActualOnly=true 時に境界月以降が空白化される
  • 当月列は直近実績月 = 境界月の1ヶ月前を参照する。境界月そのものは未確定データ
  • 境界月の動的取得は 03_sys_paramsCFG_BOUNDARY_YMConstants.getParam() で取得し、セル B2 に書き込んで数式から参照
  • YTD 列は 62_pl_ytd(実績 YTD タブ)を直接参照。非加算指標(率)は月別の合算ではなく 62 タブの YTD 行から計算

実データ検証(MCP で事前確認)

実装着手前に Google Sheets MCP で dev 環境のスプレッドシートに対して以下を検証する。結果次第で数式の MATCH 文字列や Phase 2 判定を調整:

#確認項目シート具体的にチェックする内容
V1P/L 合計行ラベルの完全一致61_pl_monthly「✨ 売上総利益」「✨ 営業利益」「✨ 経常利益」「  BEP売上高」「  安全余裕率 (%)」「  固定費合計」の厳密な文字列(絵文字・全角スペース含む)。改行 (\n) の有無
V2B/S 現預金科目名71_bs「現金預金」「現金及び預金」「当座預金」等の表記揺れ。K6 ランウェイの分子取得ラベル確定
V3B/S 売掛金科目名71_bs「売掛金」単独で MATCH するか、「売上債権」のような集約表記か
V4人件費科目の網羅性11_mst_account役員報酬・給料手当・賞与・法定福利費・福利厚生費・雑給 の 6 科目が全て登録されているか。大分類 or 表示区分 で一括絞り込み可能か
V5固変区分の実データ11_mst_account固変区分 列(第8列)で FV_FIX/FV_VAR/FV_NA の設定状況。未設定科目の数
V6自動起票列の存在確認32_wrk_invoice「起票種別」列があるか。無ければ K8 は Phase 2 先送り
V7既存タブ番号の衝突(list_sheets)93 番台タブが未使用であることを確認(現状 91_fs_bs, 92_fs_pl までの想定)
V8境界月の値03_sys_paramsCFG_BOUNDARY_YM の現値。当月/前月列で参照する相対計算のベース

関連ドキュメント

仕様書関連箇所
spec_dashboard.md §6一次資料。KPI 一覧・レイアウト・GAS制約
dev_mas-024_bep_analysis.mdK5 の参照元。BEP 算出ロジック・固変区分・「安全余裕率」の出力行ラベル
dev_mas-020_yoy_comparison.md66_pl_prev_year スナップショット方式(YoY 表示で利用可)、dmApplyYoyFormat_ の独自描画パターン
dev_mas-001_variance_analysis.md65_pl_variance 予実差異。KPI との棲み分け(KPI は率、variance は差額)
CLAUDE.mdコーディング規約・ヘッダー名ベースの列参照・動的生成タブ一覧
TODO_future.mdMAS-003 案件定義

人間が検討すべき事項

#項目詳細
1MVP vs 拡張の切り分けTODO_future.md 準拠の 5 KPI (K1/K2/K4/K5/K6) で MVP 着手。K3 経常利益率は数式追加のみで安価なため MVP 同梱してもよい
2K6 ランウェイの月間固定支出の定義(A) BEP セクションの「固定費合計」そのまま / (B) 減価償却費を控除した現金流出固定費 / (C) 過去 3~12 ヶ月の月間 CF 流出の平均。MVP は (A)、精度向上時は MAS-008(資金繰り予測高度化)の成果を利用
3閾値のシステムパラメータ化CFG_KPI_* として 03_sys_params に追加するか、コード内定数とするか。将来の SaaS 化(T-08 参照)を見据えパラメータ化を推奨
4前月比 vs YoYMVP は「前月比」のみ(D列)。YoY は Phase 2 で 66_pl_prev_year を参照する列を追加
5自動起票率(K8)の判定条件32_wrk_invoice に「起票種別」列が存在するか要 MCP 確認。無い場合は別列(例: 起票者メールアドレスが system@ 等)で判定するか、Phase 2 先送り
6トレンド SPARKLINE の期間MVP 6 ヶ月 vs 12 ヶ月。SPARKLINE は同一シート内 HELPER 行参照なので期間変更はデータ行の拡張で対応
7ダッシュボードの更新タイミング数式ベースなので buildBudgetTrendDataMart() 実行時に自動追随。独立したメニュー項目「📊 KPIダッシュボード再描画」は DDL 変更・閾値変更後のリセット用

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

以下 4 Step 構成。Step 1 → 2 → 3 → 4 の順で実行する。Step 間でユーザー確認を挟む。

Step 1: DDL・シートキー追加(Haiku)

あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-003「KPIダッシュボード/経営指標」の Step 1(DDL・シートキー登録)を実装してください。

## 実行前タスク

以下のファイルを **Read で実読**して既存パターンを把握する(Grep の部分マッチで推測しない):
- `CLAUDE.md` L158-164 — 「DDL (setupAllSchemas) で管理されないタブ」セクションの現行リスト
- `000_infra/004_utils.js` L23-32 — `Utils.getSheetNameByKey` が参照する `Constants.CONFIG_SHEET` の値
- `000_infra/002_constants.js` L7 — `Constants.CONFIG_SHEET = '01_sys_config'`
- `100_config/101_sys_config.js` L420-470 — `setupAllSchemas()` 冒頭で `confSheet = ss.getSheetByName(Constants.CONFIG_SHEET)` を取得した後に `confSheet.appendRow([key, '', tabName, displayName])` を繰り返す登録パターン(`PL_VAR`, `FS_PL`, `SS_NOTES` を参考)
- `100_config/101_sys_config.js` L331-337 — `onOpen()` 内「📊 マート更新」メニューの既存構造
- `docs/dev/dev_mas-003_kpi_dashboard.md` — 本仕様書

## 修正対象ファイル

1. `100_config/101_sys_config.js` への追記(2 箇所: DDL 登録とメニュー項目)
2. `CLAUDE.md` への追記(1 箇所: 動的生成タブ一覧)

**重要**: `000_infra/002_constants.js` の `SHEET_DEFAULTS` は**触らない**。構造が `{ pattern, prefix, defaults }` のオブジェクト配列で `smartAddRow` の行デフォルト値管理用のため、シートキーマッピングとは無関係。シートキーは `01_sys_config` シートに行として登録する。

## 実装内容

### 1-1. `101_sys_config.js` — `01_sys_config` に KPI_DASH 行を登録

`setupAllSchemas()` の L440-470 付近、`FS_PL` の直後・`SS_NOTES` の前に 1 行追加:

    if (!existKeys.includes('KPI_DASH')) confSheet.appendRow(['KPI_DASH', '', '93_kpi_dashboard', 'KPIダッシュボード']);

`confSheet` は `Constants.CONFIG_SHEET = '01_sys_config'` から取得済みのシート。ここへの appendRow がシートキーマッピングの実体。

### 1-2. `101_sys_config.js` — メニュー項目追加

`onOpen()` 内の「📊 マート更新」メニュー(L331 付近、既存の `プロジェクト別 採算(限界利益)の生成` の直後・`addSeparator()` の前)に 1 行追加:

    .addItem('📊 KPIダッシュボード再描画', 'buildKpiDashboard')

`buildKpiDashboard` 本体は Step 2 で実装するため、Step 1 時点ではクリック時にエラーで OK。

### 1-3. `CLAUDE.md` — 動的生成タブ一覧を更新

L158 付近「## DDL (setupAllSchemas) で管理されないタブ」セクションの現行リスト:

    03_sys_params, 75_ss_equity_changes, 76_notes,
    77_pj_raw, 78_pj_pl, 91_fs_bs, 92_fs_pl, 90_test_results

の `92_fs_pl,` の直後に `93_kpi_dashboard,` を挿入(カンマ区切り)。

## 制約

- `000_infra/002_constants.js` の `SHEET_DEFAULTS` には**絶対に追加しない**(仕様書内のアーキテクチャ決定 #6 参照)
- DDL のヘッダー定義(`schemas` オブジェクト、L474 付近)には Step 1 では追加しない。Step 2 で本体描画関数内でのみ書き込むため
- メニュー項目追加時、既存の他の `addItem` の順序を変更しない

## 動作確認

1. `npm run push:dev` でデプロイ
2. dev GAS のメニュー「🔧 開発・設定」→「全シートのスキーマとUIを最新化(DDL)」を実行(関数名 `setupAllSchemas`。「🔧 システム初期化」や「MST_CONF_SHEETS 初期化」というメニューは**存在しない**)
3. `01_sys_config` シート(`Constants.CONFIG_SHEET`)に `KPI_DASH / (空) / 93_kpi_dashboard / KPIダッシュボード` 行が追加されていること。**`03_sys_params` ではない**ので注意。この時点では B列(シートID_GID)は空
4. **「🔧 開発・設定 → シートのGID取得・リンク」を実行**(関数名 `initConfigs`)。`01_sys_config` の各行に対し、C列のシート名で実タブを探し、無ければ `insertSheet()` で新規作成し、B列に GID を書き込む。実行後:
   - `93_kpi_dashboard` タブが空タブとして新規作成されていること
   - `01_sys_config` の KPI_DASH 行の B列(シートID_GID)に数値が入っていること
5. メニュー「📊 マート更新」に「📊 KPIダッシュボード再描画」が表示されること(クリック時は `buildKpiDashboard is not defined` エラーで OK)

**補足**: 手順 4 の `initConfigs` を省略しても Step 2 の `buildKpiDashboard()` 内で `ss.getSheetByName(sheetName) || ss.insertSheet(sheetName)` によりタブは作成される。ただし Step 1 の範囲で「config 行追加 + 実タブ生成 + GID 紐付け」までを完結させるのが既存運用パターンのため、手順 4 を含めて検証する。

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

| フェーズ | 拡張思考 | 備考 |
|---------|:---:|------|
| 1-1~1-3 | なし | 既存パターンの模倣のみ |

Step 2: 数式生成ロジック(Sonnet)

あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-003 の Step 2(数式生成ロジック)を実装してください。

## 実行前タスク

以下を MCP (`mcp__google-sheets__get_sheet_data`) で dev スプレッドシートに対して**必ず先に実行**し、取得した文字列を数式にハードコードする:

1. `61_pl_monthly!A:A` 全行取得 → 「✨ 売上総利益」「✨ 営業利益」「✨ 経常利益」「  BEP売上高」「  安全余裕率 (%)」「  固定費合計」の**完全一致文字列**を確認(改行文字 `\n` や余計な全角スペースの有無)
2. `71_bs!A:A` 全行取得 → 現預金科目「現金預金」or「現金及び預金」等の実名、売掛金科目の実名を確認
3. `11_mst_account` 全行取得 → 人件費科目(役員報酬・給料手当・賞与・法定福利費・福利厚生費・雑給)の存在確認
4. `03_sys_params` → `CFG_BOUNDARY_YM` の現値

以下のファイルを読み込む:
- `docs/dev/dev_mas-003_kpi_dashboard.md` — 本仕様書(レイアウト・数式パターン・エッジケース)
- `docs/spec/spec_dashboard.md` §6 — 一次資料
- `600_report/603_datamart_pl.js:271-399` — BEP セクションの出力行ラベル。**実装が「安全余裕率 (%)」で出力しているので、仕様書のドラフトと異なる点に注意**
- `600_report/602_datamart_main.js:savePlSnapshot()` — GAS 関数の命名・構造の参考
- `000_infra/004_utils.js` — `Utils.getSheetNameByKey()` / `Utils.logInfo()` の使い方
- `000_infra/002_constants.js` — `Constants.getParam()` の使い方

## 修正対象ファイル

- `600_report/609_datamart_kpi.js` を新規作成
- `600_report/602_datamart_main.js` の `buildBudgetTrendDataMart()` 末尾で `buildKpiDashboard()` を呼ぶ 1 行を追加

## 実装内容

### 2-1. `609_datamart_kpi.js` の関数構成

    /**
     * =========================================================
     * 609_datamart_kpi.js — MAS-003: KPIダッシュボード
     * =========================================================
     * 93_kpi_dashboard タブに 5 KPI + 条件付き書式を配置する。
     * データは数式で 61/62/71 タブを参照する(GASは数式配置と書式のみ)。
     */

    function buildKpiDashboard() {
      var FUNC = 'buildKpiDashboard';
      try {
        var ss = getWebSpreadsheet_();
        var sheetName = Utils.getSheetNameByKey('KPI_DASH') || '93_kpi_dashboard';
        var sheet = ss.getSheetByName(sheetName) || ss.insertSheet(sheetName);
        sheet.clear();
        sheet.clearConditionalFormatRules();

        // タイトル・ヘッダー
        // 各 KPI 行の setFormulas
        // HELPER 行の setFormulas(過去6ヶ月 × 5 KPI = 30セル)
        // 列幅・書式
        // 条件付き書式(applyKpiConditionalFormat_)
        // HELPER 行を hideRows

        Utils.logInfo(FUNC, sheetName + ' 再描画完了');
      } catch (e) {
        Utils.logError(FUNC, e);
        throw e;
      }
    }

### 2-2. KPI 行の数式(抜粋)

**K1 売上総利益率** の当月列(B列):

    =IFERROR(
      INDEX('61_pl_monthly'!$B:$N,
            MATCH("✨ 売上総利益",
                  ARRAYFORMULA(SUBSTITUTE('61_pl_monthly'!$A:$A, CHAR(10), "")), 0),
            <当月列番号>)
      /
      INDEX('61_pl_monthly'!$B:$N,
            MATCH("売上高",
                  ARRAYFORMULA(SUBSTITUTE('61_pl_monthly'!$A:$A, CHAR(10), "")), 0),
            <当月列番号>),
      "-")

**K5 安全余裕率(MAS-024 既存を参照)**:

    =IFERROR(
      INDEX('61_pl_monthly'!$B:$N,
            MATCH("  安全余裕率 (%)",
                  ARRAYFORMULA(SUBSTITUTE('61_pl_monthly'!$A:$A, CHAR(10), "")), 0),
            <当月列番号>) / 100,
      "-")

MAS-024 は安全余裕率を整数%(例: `32`)で出力しているため `/100` で率に戻す。

**K6 ランウェイ**:

    =IFERROR(
      INDEX('71_bs'!$B:$N,
            MATCH("現金預金",  // ← V2 の MCP 結果に従って置換
                  ARRAYFORMULA(SUBSTITUTE('71_bs'!$A:$A, CHAR(10), "")), 0),
            <直近実績月列番号>)
      /
      INDEX('61_pl_monthly'!$B:$N,
            MATCH("  固定費合計",
                  ARRAYFORMULA(SUBSTITUTE('61_pl_monthly'!$A:$A, CHAR(10), "")), 0),
            <直近実績月列番号>),
      "∞")

### 2-3. HELPER 行(SPARKLINE 用)

最下部に 6 列(過去 6 ヶ月分)の HELPER 行を KPI ごとに配置。`SPARKLINE` はその範囲を参照:

    F4: =SPARKLINE($B$11:$G$11, {"charttype","line";"color","#1C4587"})

### 2-4. `buildBudgetTrendDataMart()` への組み込み

`602_datamart_main.js` の `buildBudgetTrendDataMart()` 末尾(return の直前)に以下を追加:

    // MAS-003: KPIダッシュボード再描画
    try { buildKpiDashboard(); } catch (e) { Utils.logError('buildBudgetTrendDataMart.kpi', e); }

## 制約

- 61/62/71 タブの出力ロジックは変更禁止
- MATCH 文字列は MCP で確認した実名を使う(絵文字・全角スペース含む完全一致)
- HELPER 行は `hideRows` で非表示にする。delete はしない(数式が壊れる)
- 条件付き書式は Step 3 で別関数 `applyKpiConditionalFormat_()` として実装。Step 2 では空実装で構わない
- K3 経常利益率・K7 売掛金回転日数・K8 自動起票率は Step 2 では実装しない(Phase 2)

## エッジケース

| 条件 | 期待動作 |
|------|---------|
| 売上=0 | K1/K2 の数式が `"-"` を返す(IFERROR) |
| 売上総利益≦0 | K4 は `"N/A"`(IF で分岐) |
| 固定費=0 | K6 は `"∞"` |
| MAS-024 BEP 行が未出力(科目マスタ未設定等) | K5 は `"-"`(IFERROR) |
| 当月列 = 境界月 | `isActualOnly` で空白 → IFERROR で `"-"` |
| 過去 6 ヶ月のうちゼロ除算月あり | SPARKLINE は空値をスキップ(IFERROR, "") |

## 実データ検証

実装後に再度 MCP で以下を確認:
- `93_kpi_dashboard` の B4(K1 当月値)が期待値になっているか
- MATCH が失敗して `#N/A` が出ていないか
- SPARKLINE が HELPER 行を参照して描画できているか

## 動作確認

1. `npm run push:dev` でデプロイ
2. メニュー「📊 マート更新 → 財務3表の更新」を実行 → 61/62/71 が更新された後、93 も連動して更新される
3. メニュー「📊 マート更新 → 📊 KPIダッシュボード再描画」を単独実行して再描画のみも動くか確認
4. K1 売上総利益率 の値が手計算と一致するか(1 ヶ月分)
5. K6 ランウェイ の月数が妥当な範囲か(1~24 ヶ月程度)
6. HELPER 行が非表示になっているか

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

| フェーズ | 拡張思考 | 備考 |
|---------|:---:|------|
| 数式設計(MATCH 組み立て) | あり | 改行対策・IFERROR ラッピング・列番号動的化 |
| HELPER 行のレイアウト | あり | SPARKLINE の参照範囲設計 |
| setFormulas 書き出し | なし | パターン反復 |
| 動作確認 | なし | チェックリスト |

Step 3: 条件付き書式適用(Sonnet)

あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-003 の Step 3(条件付き書式)を実装してください。

## 実行前タスク

- `docs/dev/dev_mas-003_kpi_dashboard.md` の「閾値・条件付き書式」表
- `docs/spec/spec_dashboard.md` §4.1 セマンティックカラー、§6.5 条件付き書式上限(50ルール/シート)
- `600_report/608_datamart_render.js:dmApplyYoyFormat_()` — `setConditionalFormatRules` ではなく直接色を塗るパターンも参考

## 修正対象ファイル

- `600_report/609_datamart_kpi.js`(Step 2 で作成済み)のみ

## 実装内容

`applyKpiConditionalFormat_(sheet)` を実装:

    function applyKpiConditionalFormat_(sheet) {
      var rules = [];

      // K1 売上総利益率 < CFG_KPI_GROSS_MARGIN_MIN
      var gmMin = Number(Constants.getParam('CFG_KPI_GROSS_MARGIN_MIN', 0.4));
      rules.push(
        SpreadsheetApp.newConditionalFormatRule()
          .whenNumberLessThan(gmMin)
          .setBackground('#F4CCCC').setFontColor('#CC0000')
          .setRanges([sheet.getRange('B4:E4')])
          .build()
      );

      // K2 営業利益率 < 0
      rules.push(
        SpreadsheetApp.newConditionalFormatRule()
          .whenNumberLessThan(0)
          .setBackground('#F4CCCC').setFontColor('#CC0000')
          .setRanges([sheet.getRange('B5:E5')])
          .build()
      );

      // K4 労働分配率 > CFG_KPI_LABOR_RATIO_MAX
      var lrMax = Number(Constants.getParam('CFG_KPI_LABOR_RATIO_MAX', 0.6));
      rules.push(
        SpreadsheetApp.newConditionalFormatRule()
          .whenNumberGreaterThan(lrMax)
          .setBackground('#FCE5CD').setFontColor('#B45F06')
          .setRanges([sheet.getRange('B6:E6')])
          .build()
      );

      // K5 安全余裕率 < 0
      rules.push(
        SpreadsheetApp.newConditionalFormatRule()
          .whenNumberLessThan(0)
          .setBackground('#F4CCCC').setFontColor('#CC0000')
          .setRanges([sheet.getRange('B8:E8')])  // 安全余裕率の行
          .build()
      );

      // K6 ランウェイ < CFG_KPI_RUNWAY_MIN_MONTHS
      var rwMin = Number(Constants.getParam('CFG_KPI_RUNWAY_MIN_MONTHS', 6));
      rules.push(
        SpreadsheetApp.newConditionalFormatRule()
          .whenNumberLessThan(rwMin)
          .setBackground('#F4CCCC').setFontColor('#CC0000')
          .setRanges([sheet.getRange('B9:E9')])
          .build()
      );

      sheet.setConditionalFormatRules(rules);
    }

`buildKpiDashboard()` 末尾で `applyKpiConditionalFormat_(sheet)` を呼ぶ。

## 制約

- `sheet.setConditionalFormatRules(rules)` で既存ルールを全置換する(`clearConditionalFormatRules()` を先に呼ぶ必要なし、`setConditionalFormatRules` が上書きする)
- `Constants.getParam()` のデフォルト値は数値型で渡す(文字列 `'0.4'` だと型エラー)
- ルール数は 5 個(MVP)。50 ルール上限まで余裕あり

## エッジケース

| 条件 | 期待動作 |
|------|---------|
| セル値が `"-"` / `"N/A"` / `"∞"`(テキスト) | `whenNumberLessThan` は発火しない(文字列は無視) |
| `CFG_KPI_*` が未設定 | デフォルト値を使用(`Constants.getParam` の第2引数) |
| 前月比列 (D列) に負値 | 書式は**数値の絶対値ではなく KPI 値そのもの**で判定(D列は前月比の差分なのでルール対象外) |

## 動作確認

1. `npm run push:dev` でデプロイ
2. 再描画後、K1 の当月値が 40% 未満の月があれば赤背景になっていること
3. `03_sys_params` に `CFG_KPI_GROSS_MARGIN_MIN = 0.5` を設定して再描画 → 閾値が切り替わっていること
4. K2 が 0% 未満(赤字月)で赤背景が発火すること

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

| フェーズ | 拡張思考 | 備考 |
|---------|:---:|------|
| 条件設計 | あり | 範囲指定・デフォルト値設計 |
| `newConditionalFormatRule` 反復 | なし | パターン反復 |

Step 4: 動作確認・_config.json 登録(Haiku)

あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-003 の Step 4(動作確認と仕様書登録)を実施してください。

## 実行前タスク

- `docs/_config.json` — §E.5 FP&A・レポーティングの既存ページ数を確認
- `docs/_internal/changelog.md` — 先頭エントリーのフォーマット確認

## 実装内容

### 4-1. `docs/_config.json` に登録

§E.5 の末尾に以下を追加(既存の E.5.X の次の番号で):

    { "file": "dev/dev_mas-003_kpi_dashboard.md", "title": "E.5.X MAS-003 KPIダッシュボード" }

### 4-2. `docs/_internal/changelog.md` 先頭に追記

    | 2026-04-17 | [dev_mas-003_kpi_dashboard.md](dev_mas-003_kpi_dashboard.md) | 初版作成。93_kpi_dashboard 新設、5 KPI の数式ベース実装と条件付き書式 |

### 4-3. 統合動作確認

1. `npm run push:dev` でデプロイ済み前提
2. 「📊 マート更新 → 財務3表の更新」を実行(`buildBudgetTrendDataMart`)
3. 93_kpi_dashboard タブが自動的に再描画されること
4. 以下のチェックリストを全て ✅:
   - [ ] 5 KPI (K1/K2/K4/K5/K6) が全て表示される
   - [ ] 当月/前月/前月比/YTD/トレンド の 5 列が揃っている
   - [ ] SPARKLINE が 6 ヶ月トレンドを描画している
   - [ ] HELPER 行が非表示
   - [ ] K1 売上総利益率が手計算(売上総利益 ÷ 売上高)と一致
   - [ ] K5 BEP売上高 と 安全余裕率 が 61 タブの BEP セクションと一致
   - [ ] K6 ランウェイの値が妥当(正の数値 or `"∞"`)
   - [ ] 条件付き書式: K1 < 40% で赤、K4 > 60% で黄、K6 < 6ヶ月で赤
   - [ ] `CFG_KPI_*` を `03_sys_params` で変更 → 再描画で閾値切替
   - [ ] 61/62/71 タブに変更なし(既存動作リグレッションなし)
5. テストランナー実行: `T4-03`, `T4-21` が KPI セクションを未登録科目と誤検知しないか確認(独立タブなので除外対象)

### 4-4. prod デプロイ

dev で検証完了後:

    npm run push:prod

prod の `03_sys_params` で `CFG_KPI_GROSS_MARGIN_MIN`, `CFG_KPI_LABOR_RATIO_MAX`, `CFG_KPI_RUNWAY_MIN_MONTHS` を必要に応じて設定。

### 4-5. git 操作

    git add 600_report/609_datamart_kpi.js 600_report/602_datamart_main.js \
            100_config/101_sys_config.js 000_infra/002_constants.js \
            CLAUDE.md docs/dev/dev_mas-003_kpi_dashboard.md \
            docs/_config.json docs/_internal/changelog.md
    git commit -m "feat(MAS-003): KPIダッシュボード新設(93_kpi_dashboard)"
    git push -u origin feat/MAS-003-kpi-dashboard
    # PR 作成後、ユーザー指示を待ってマージ

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

| フェーズ | 拡張思考 | 備考 |
|---------|:---:|------|
| 動作確認チェックリスト | なし | 機械的実行 |
| git 操作 | なし | 標準フロー |

推奨実行モデル

工程推奨モデル理由
仕様書作成(本作業)Opusspec_dashboard §6 の昇格、MAS-024/MAS-020/MAS-001 の既存実装との整合判断、KPI選定と閾値設計の会計判断
Step 1 DDL・シートキー追加Haiku既存パターン(PL_VAR, PL_PREV_YEAR)のコピペで済む
Step 2 数式生成ロジックSonnetINDEX/MATCH/SUBSTITUTE(CHAR(10)) の組み立て、IFERROR ラッピング、HELPER 行設計の中程度の判断
Step 3 条件付き書式SonnetnewConditionalFormatRule の範囲指定・閾値デフォルト設計、50ルール上限に収める設計判断
Step 4 動作確認・登録Haikuチェックリスト実行と _config.json / changelog 追記のみ

変更履歴

日付変更内容
2026-04-17初版作成。spec_dashboard.md §6 を正式な開発仕様書に昇格。MVP 5 KPI (K1/K2/K4/K5/K6) と Phase 2 拡張 (K3/K7/K8) に段階分割。MAS-024 の「安全余裕率」出力行ラベルをそのまま参照する方針を明示
2026-04-17Step 1 実装プロンプトと関連セクションを訂正。Grep だけで類推して書いた 3 件の誤りを修正: (1) 000_infra/002_constants.jsSHEET_DEFAULTS はシートキーマッピング用ではないため 002_constants.js の変更指示を削除 (2) シートキー登録先を 03_sys_params から正しい 01_sys_configConstants.CONFIG_SHEET)に修正 (3) 実在しないメニュー「🔧 システム初期化 → MST_CONF_SHEETS 初期化」を実在する「🔧 開発・設定 → 全シートのスキーマとUIを最新化(DDL)」に訂正
2026-04-17Step 1 動作確認手順に initConfigs(シートのGID取得・リンク)の実行ステップを追加。setupAllSchemas01_sys_config 行追加 → initConfigs で 93_kpi_dashboard タブ生成 + GID 紐付け、の既存運用パターンに合わせた。補足として Step 2 の buildKpiDashboard() が独立してタブ生成する動作も明記
2026-04-17Step 2 実装内容を反映。(1) MATCH/ARRAYFORMULA/SUBSTITUTE 方式を廃止し GAS 側で行・列番号を事前計算してリテラル埋め込み方式に変更 (絵文字・全角スペース・改行での壊れ回避) (2) 月列を C-N の 12 列固定に限定 (MAS-020 YoY 差異列との混同を回避) (3) 境界月は 03_sys_paramsCFG_BOUNDARY_YM 未設定時に売上高行末尾非空から動的検出 (4) 内訳を別テーブルから KPI 行直下の埋め込み (main/sub/subsub 3段) に再構成 (5) K5 BEP 配下に「うち人件費合計 / うち非人件費」の内訳行を追加。Step 2 で顕在化した #21-#24 の落とし穴を「注意事項」にも追記
2026-04-17prompt dispatch / receive workflow (dispatch_prompt.yml + receive_prompt.yml) の end-to-end 接続テスト用トリガー。内容の実質変更はなし