MAS-079: 51タブをデータソース化(読み込み対応)
概要
| 項目 | 内容 |
|---|---|
| 案件ID | MAS-079 |
| カテゴリ | パイプライン / シミュレーション基盤 |
| Phase | Phase 2 |
| 優先度 | P2 (★) |
| 所要時間 | 3-4時間 |
| 対象ファイル | 200_data/202_repository.js(PipelineListRepository 新設)、000_infra/003_contracts.js(PipelineListDTO 追加)、600_report/602_datamart_main.js(51 タブ出力の clear→merge 化)、600_report/601_datamart_ingest.js(dmIngestPipelinePlanData_ で調整値を反映)、100_config/101_sys_config.js(DDL の 51 タブ列拡張・背景色) |
| 前提案件 | MAS-078(84 タブへのパイプライン合流・完了)、MAS-003(KPI ダッシュボード・完了) |
| 後続案件 | §4.8.2 シナリオ比較(Base/Best/Worst 切替 UI)、MAS-177 多年度基盤 |
目的
現状 51_list_pipeline_plan は「パイプライン展開明細のビュー(Read-Only)」として、マート更新のたびに listSheet.clear() → 自動生成結果で上書きされている。このためユーザーが特定案件の加重金額を手動で微調整したくても、次回マート更新で調整が全消去される。
本仕様では 51 タブを 双方向(Read/Write)データソースへ昇格させ、以下を実現する:
- 手動調整列を追加:
調整値_加重金額(税抜)・調整理由の 2 列(ユーザー入力枠) - マージロジック: マート更新時、既存調整値を Repository 経由で退避 → 自動再集計 → 調整値を適用 → シート出力
- 行ズレ耐性: 「管理 ID × 計上年月 × 明細区分(スポット/MRR-N)」のビジネス複合キーでマージ。並び順変動・新規行追加に耐える
- シミュレーション連携:
dmIngestPipelinePlanData_のevents.amt(→ P/L 計画・CF 計画に波及)にも調整値を反映し、51 タブの手動調整が全マートに伝播する
利用シナリオ
- 営業が「この案件は大口なので 50% ヨミでも確度 70% で計上したい」と判断 → 51 タブの調整値列に加重金額を直接上書き → 次回マート更新で P/L 計画・CF 計画・ランウェイがその前提で再計算
- 「このスポット案件はスライドするので今月分だけ 0 にする」 → 該当行の調整値に 0 を入力
現在のコード
1. 51_list_pipeline_plan 出力箇所(600_report/602_datamart_main.js:295-309)
// 5c. パイプライン展開明細を 51_list_pipeline_plan に出力
if (pipeResult.viewData.length > 0) {
var listSheet = ss.getSheetByName('51_list_pipeline_plan');
if (!listSheet) listSheet = ss.insertSheet('51_list_pipeline_plan');
listSheet.clear(); // ★ ここで既存調整値が消去される
var pipeHeaders = ['管理ID', '計上年月', '入金予定年月', 'PJ・案件名', '取引先名',
'契約形態', '売上科目', '確度', '元金額(税抜)', '加重金額(税抜)',
'入金ラグ(月)', '決済手段', '組織名', '摘要'];
var pipeOut = [pipeHeaders].concat(pipeResult.viewData);
listSheet.getRange(1, 1, pipeOut.length, pipeHeaders.length).setValues(pipeOut);
// ... 書式設定(L303-308)
}
問題: listSheet.clear()(L299)で既存データを無条件消去。ユーザーが手動入力した列があっても残らない。
2. 51 タブの行を生成している dmIngestPipelinePlanData_(601_datamart_ingest.js:363-479)
pipeResult.viewDataに push される各行は[mgrId, startYm (or curYm), sYm, pjName, vendor, typeStr, acc, prob, spot/mrr, weightedSpot/weightedMrr, lag, payMethod, orgName, 'スポット' or 'MRR N/Mヶ月目']- スポット 1 行 + MRR × duration 行を展開
- 同時に
events.push({ amt: weightedSpot/weightedMrr, ... })が P/L 計画・CF 計画の入力となる(L441, L465)
本仕様では events.amt も調整値で上書きする必要がある。
3. 既存のシート設定登録(100_config/101_sys_config.js:607)
if (!existKeys.includes('SYS_LIST_PIPE')) confSheet.appendRow(['SYS_LIST_PIPE', '', '51_list_pipeline_plan', '一覧_パイプライン展開明細(Read-Only)']);
シートキーは SYS_LIST_PIPE。「Read-Only」表記は本仕様で修正する必要あり。
4. 51 タブの列定義(現状は DDL 非管理・動的生成)
dmIngestPipelinePlanData_ がコード内でヘッダー文字列を直接定義しており、setupAllSchemas では DDL として管理されていない(CLAUDE.md「DDL (setupAllSchemas) で管理されないタブ」に 77_pj_raw は記載があるが 51 は未記載)。
本仕様で DDL 管理下に移す:
'SYS_LIST_PIPE': {
headers: ["管理ID","計上年月","入金予定年月","PJ・案件名","取引先名","契約形態",
"売上科目","確度","元金額(税抜)","加重金額(税抜)","入金ラグ(月)","決済手段",
"組織名","摘要","🔧調整値_加重金額(税抜)","🔧調整理由"],
color: "#674ea7"
}
5. 行の一意キー(マージ用ビジネス複合キー)
(管理ID, 計上年月, 摘要) の 3 値で一意。摘要 はスポット行なら 'スポット'、MRR 行なら 'MRR N/Mヶ月目'(N は連番)。
キー形式: `${mgrId}||${pYm}||${memo}`
例: 'PIP_0001||2026-08||スポット'
'PIP_0001||2026-08||MRR 1/12ヶ月目'
修正方針
全体像: 51 タブを DDL 管理下の双方向シートに昇格。PipelineListRepository を新設して「既存データの読み取り」「調整値マップ化」を担当、マート更新では clear せず、マージ後の結果で setValues する。dmIngestPipelinePlanData_ 側でも events.amt に調整を反映する。
Step 1: 51 タブの双方向化(DDL 列追加・背景色)
DDL 列追加(100_config/101_sys_config.js)
setupAllSchemasのschemas辞書(L645 付近)にSYS_LIST_PIPEを追加(既存 DDL 外タブから昇格)- ヘッダーは現行 14 列 + 🔧調整値_加重金額(税抜)・🔧調整理由 の計 16 列
- 🔧プレフィックスは「手動入力枠」の視認マーカー(CLAUDE.md / MAS-003 等の既存パターンに類似)
- 条件付き書式 or 初期背景色で 🔧列を薄黄色
#FFF2CCに設定。setupAllSchemas でのみ適用、ユーザーがマート更新で消えない設計(background はclear()で消えるが、本仕様は clear しないため保持)
シートキー説明文の修正
'SYS_LIST_PIPE' の `description` を
'一覧_パイプライン展開明細(Read-Only)'
→ '一覧_パイプライン展開明細(手動調整可・シミュレーション入力)'
Step 2: PipelineListRepository 新設と調整値マップ構築
000_infra/003_contracts.js への DTO 追加
/**
* 51_list_pipeline_plan — パイプライン展開明細(S-07 双方向化)
* @typedef {Object} PipelineListDTO
* @property {string} 管理ID
* @property {string} 計上年月 - "YYYY-MM"
* @property {string} 入金予定年月
* @property {string} PJ・案件名
* @property {string} 取引先名
* @property {string} 契約形態
* @property {string} 売上科目
* @property {number} 確度
* @property {number} 元金額(税抜)
* @property {number} 加重金額(税抜) - システム自動算出
* @property {number} 入金ラグ(月)
* @property {string} 決済手段
* @property {string} 組織名
* @property {string} 摘要 - "スポット" | "MRR N/Mヶ月目"
* @property {number} 調整値_加重金額(税抜) - 🔧ユーザー手動入力(空なら自動値採用)
* @property {string} 調整理由 - 🔧ユーザー手動入力
*/
200_data/202_repository.js への Repository 追加
// =====================================================================
// PipelineListRepository — 51_list_pipeline_plan(S-07 双方向データソース)
// =====================================================================
var PipelineListRepository = {
_getSheet: function() { return Utils.getSheetByKey('SYS_LIST_PIPE', '51_list_pipeline_plan'); },
findAll: function() { return readSheetAsDtos_(PipelineListRepository._getSheet()); },
/**
* ビジネス複合キー → 調整値の Map を返す(マージ用)
* キー: `${管理ID}||${計上年月}||${摘要}`
* 値: { override: number, reason: string } (override は数値、null のときは未入力)
*/
findAdjustmentMap: function() {
var map = {};
var sheet = PipelineListRepository._getSheet();
if (!sheet) return map;
var result = readSheetAsDtos_(sheet);
for (var i = 0; i < result.dtos.length; i++) {
var dto = result.dtos[i];
var mgrId = String(dto['管理ID'] || '').trim();
var pYm = Utils.parseDateToYm(dto['計上年月']);
var memo = String(dto['摘要'] || '').trim();
if (!mgrId || !pYm || !memo) continue; // キー構成要素が欠けたら除外
var key = mgrId + '||' + pYm + '||' + memo;
var rawOverride = dto['調整値_加重金額(税抜)'];
var override = null;
if (rawOverride !== '' && rawOverride !== null && rawOverride !== undefined) {
var n = Utils.parseAmt(rawOverride); // エッジケース 1: 不正値を 0 に正規化
if (isFinite(n)) override = n; // NaN 排除
}
var reason = String(dto['調整理由'] || '').trim();
if (override !== null || reason) {
map[key] = { override: override, reason: reason };
}
}
return map;
},
};
Step 3: マート更新のマージロジック(clear → merge 方式への変更)
dmIngestPipelinePlanData_ への調整値反映(601_datamart_ingest.js:363-479)
関数シグネチャに第 3 引数 adjustmentMap を追加:
function dmIngestPipelinePlanData_(ctx, sheetPipe, adjustmentMap) {
adjustmentMap = adjustmentMap || {};
// ... 既存ロジック
}
スポット行生成(既存 L441 前後):
var weightedSpot = Math.round(spot * prob);
var memoSpot = 'スポット';
var keySpot = mgrId + '||' + startYm + '||' + memoSpot;
var adjSpot = adjustmentMap[keySpot];
var finalSpot = (adjSpot && adjSpot.override !== null) ? adjSpot.override : weightedSpot;
if (spot > 0 && startYm <= lastMonth) {
if (finalSpot !== 0) {
events.push({ pYm: startYm, sYm: sYmSpot, acc: acc, amt: finalSpot, booked: false, isBsForce: null, noCash: false });
}
viewRows.push([mgrId, startYm, sYmSpot, pjName, vendor, typeStr, acc, prob, spot, weightedSpot, lag, payMethod, orgName, memoSpot,
(adjSpot && adjSpot.override !== null) ? adjSpot.override : '', // 調整値(空なら空文字)
(adjSpot && adjSpot.reason) ? adjSpot.reason : '']); // 調整理由
}
MRR 行生成(既存 L465 前後)も同様にキーを 'MRR ' + (m+1) + '/' + dur + 'ヶ月目' で構築。
viewRows の列数が 14 → 16 列に拡張される点に注意(後続の setValues の列数と整合)。
マート出力のマージ化(602_datamart_main.js:295-309 差し替え)
// 5c. パイプライン展開明細を 51_list_pipeline_plan に出力(S-07 マージ方式)
var listSheet = ss.getSheetByName('51_list_pipeline_plan');
if (!listSheet) listSheet = ss.insertSheet('51_list_pipeline_plan');
// 既存調整値マップを退避(clear 前)
var adjustmentMap = PipelineListRepository.findAdjustmentMap();
// 調整値マップを使って pipeResult を再生成(呼び出し順を前段に変更)
// ※ 602 の呼び出し順: 先に adjustmentMap を取得 → dmIngestPipelinePlanData_(ctx, sheetPipe, adjustmentMap)
// (L272-275 の呼び出し時点で adjustmentMap を渡す)
if (pipeResult.viewData.length > 0) {
var pipeHeaders = ['管理ID','計上年月','入金予定年月','PJ・案件名','取引先名','契約形態',
'売上科目','確度','元金額(税抜)','加重金額(税抜)','入金ラグ(月)','決済手段',
'組織名','摘要','🔧調整値_加重金額(税抜)','🔧調整理由']; // 16 列
var pipeOut = [pipeHeaders].concat(pipeResult.viewData);
// 既存セルを値のみクリア(書式・条件付き書式・幅は保持)
var lastRow = listSheet.getLastRow();
if (lastRow >= 2) {
listSheet.getRange(2, 1, lastRow - 1, 16).clearContent();
}
listSheet.getRange(1, 1, pipeOut.length, pipeHeaders.length).setValues(pipeOut);
// ヘッダー書式・黄色背景などの書式は既存コードを流用(ただし clear() は呼ばない)
listSheet.getRange(1, 1, 1, pipeHeaders.length).setBackground('#333333').setFontColor('#FFFFFF').setFontWeight('bold');
listSheet.setFrozenRows(1);
listSheet.getRange(2, 8, pipeOut.length - 1, 1).setNumberFormat('0%');
listSheet.getRange(2, 9, pipeOut.length - 1, 2).setNumberFormat('#,##0');
listSheet.getRange(2, 15, pipeOut.length - 1, 1).setNumberFormat('#,##0;[Red]△ #,##0;"-"'); // 🔧調整値列
// 🔧列(15,16)の薄黄色背景をデータ行全体に適用
listSheet.getRange(2, 15, pipeOut.length - 1, 2).setBackground('#FFF2CC');
}
呼び出し側(602_datamart_main.js:272-275)の変更
// 旧: pipeResult = dmIngestPipelinePlanData_(ctx, sheetPipe);
// 新:
var adjustmentMap = PipelineListRepository.findAdjustmentMap();
pipeResult = dmIngestPipelinePlanData_(ctx, sheetPipe, adjustmentMap);
planData = planData.concat(pipeResult.planData);
結果として波及するマート
planDataに push された events は P/L 計画(63/64)と CF 計画(82)の計算に反映される- したがって 51 タブの手動調整が P/L 計画・CF 計画にも伝播する
- MAS-008 Cash Runway は営業 CF(cfOp)を参照し、これは実績 + planData 由来の計画を含む → 間接的にランウェイにも反映
- MAS-078(84 タブ合流)は
21_bud_pipelineを直接読む別経路のため本仕様の影響を受けない(設計上の分離)
影響範囲
| 変更対象 | 変更内容 | 変更量 |
|---|---|---|
000_infra/003_contracts.js | PipelineListDTO 追加 | +25 行 |
200_data/202_repository.js | PipelineListRepository 追加(findAll / findAdjustmentMap) | +45 行 |
100_config/101_sys_config.js | schemas['SYS_LIST_PIPE'] 追加、説明文修正 | +5 行 |
600_report/601_datamart_ingest.js | dmIngestPipelinePlanData_ に第 3 引数・調整値適用ロジック | +30 行 |
600_report/602_datamart_main.js | 呼出側で findAdjustmentMap 取得、clear→clearContent(A2:P) へ変更、ヘッダー 14→16 列 | +15 行 / -3 行 |
- 既存動作への影響: 調整値が空の状態では従来と同じ出力(後方互換)
- DTO / DDL 変更: 51 タブの列が 14 → 16 に増える(
setupAllSchemas実行で既存シートに列追加) - P/L 計画・CF 計画・MAS-008 ランウェイへの間接波及: 調整値が入った行は自動計算値の代わりに採用される
- MAS-078(84 タブ)への影響なし: MAS-078 は 21_bud_pipeline を直接参照する別経路
注意事項
- 行ズレ防止のビジネス複合キー:
管理ID + 計上年月 + 摘要の 3 値で Map キーを構築。インデックス(行番号)ベースの照合は絶対禁止(失敗パターン #17 系) listSheet.clear()からclearContent(2:lastRow)へ変更必須:clear()は書式・条件付き書式・列幅まで消去する。本仕様ではヘッダー書式・🔧列背景色を保持したいため、値のみクリア(失敗パターン #11 系のデータロスト事故防止)setupAllSchemas実行時の既存データ保全: DDL 追加時にシートへ新規列が追加されるが、既存行のデータは消えない。手動調整値が消えないことを実機検証- 列インデックスのハードコード禁止: Repository 内も
dto['列名']でアクセス、直接row[14]のような固定インデックス禁止(失敗パターン #18 系) Utils.parseAmtの活用: 調整値の型不正('未定'/'要確認'等)はUtils.parseAmtで 0 にフォールバック。独自のNumber()直呼びは禁止- 調整理由の必須化はしない: ユーザー自由度を優先。ただし運用で必須化したい場合は
03_sys_paramsにLIST_PIPE_REASON_REQUIRED=trueを追加する余地を残す(人間検討事項) - 孤立調整値の警告: 元のパイプライン案件が削除されて調整値だけ残っているケースは
Utils.logInfoで警告ログのみ、次回マート更新で自動破棄(エッジケース 2) overrideがnullと0の区別:0は「0 円に上書き」を意味する正当な入力。null/空文字/NaNのみ「未入力」扱い。この区別を Repository で厳密に- キーの両端スペース:
管理ID/摘要の前後空白は.trim()で除去してからキー化。Excel ペースト時の trailing space に注意 - マージ適用順:
調整値 → override で採用→ 自動計算値を無視。本仕様では「上書きモード」を採用(加減算モードではない)。加減算モードは将来拡張余地 - テスト観点: (a) 調整値空 → 自動値採用、(b) 調整値
100000→ override 採用、(c) 調整値0→ 0 円採用、(d) 行削除(パイプライン元削除)→ 孤立警告・破棄、(e) 新規パイプライン行 → 調整値なしで自動値、(f) MRR 24 行中 3 行だけ調整 → 該当 3 行のみ override dmIngestPipelinePlanData_の後方互換: 第 3 引数を省略可能(adjustmentMap || {}でフォールバック)。既存呼び出し(テスト等)への影響なし
エッジケース
| # | 条件 | 処理 | 理由 |
|---|---|---|---|
| 1 | 調整値の型エラー('未定' / '要確認' / スペース等) | Utils.parseAmt(0) で 0 フォールバック、ただし override には反映せず null 扱い | NaN 波及防止、集計ロジックの堅牢性 |
| 2 | 元データの消滅(自動集計に該当管理 ID が存在しないが 51 タブに調整値だけ残る) | Utils.logInfo で警告ログを記録し、自動破棄(次回マート更新で行自体が消える) | 孤立データの累積を防ぎ、データ整合性を維持 |
| 3 | 調整値空欄 | 自動集計の 加重金額(税抜) をそのまま採用(override=null 扱い) | 既存動作との後方互換 |
| 4 | 調整値 = 0(正当な「ゼロ計上」入力) | 0 を override として採用(events.amt=0 → planData から除外される挙動も同時に) | 「0 円に上書き」の意思表明を尊重(null と 0 を厳密区別) |
| 5 | 調整値 = 負値(値引き・返品等) | そのまま override として採用 | 柔軟な運用を許容(人間検討事項 3 で方針確認) |
| 6 | 新規パイプライン行(既存 51 タブに対応行なし) | adjustmentMap に該当キーなし → 自動値を使用 | 初回登場案件は自動ベース |
| 7 | 管理ID が空(手動行追加時) | キー構築不能のため findAdjustmentMap で除外。Map に入らず、マート更新で消える | 管理 ID がなければ追跡不能 |
| 8 | 計上年月 が不正日付 | Utils.parseDateToYm が空を返す → Map に入らず消える | 日付整合性の維持 |
| 9 | MRR 24 行中 3 行だけ調整 | 該当 3 行のみ override、他 21 行は自動値 | 粒度管理の柔軟性 |
| 10 | 同キー重複(ユーザーが同一案件を複製入力) | findAdjustmentMap で後勝ち(Map の上書き挙動) | 決定的動作。重複検出は将来拡張 |
| 11 | setupAllSchemas 実行時の列追加 | 既存データは保持、新規列(🔧2 列)は空で追加される | 後方互換の必須要件 |
| 12 | 確度 100% 案件(S ヨミ・受注確定)は 51 タブに出ない(既存挙動) | 32_wrk_invoice に既に起票されている案件は dmIngestPipelinePlanData_ でスキップ(601_datamart_ingest.js:420-421)→ 51 タブに現れないため調整値も発生しない | 二重計上防止(MAS-078 と同原則) |
実データ検証(事前確認項目)
| 確認項目 | 確認方法 | 確認結果(Phase 1 で調査済) |
|---|---|---|
| 51 タブの現在の HEADERS(14 列) | 600_report/602_datamart_main.js:300 の pipeHeaders 配列 | ✅ ['管理ID','計上年月','入金予定年月','PJ・案件名','取引先名','契約形態','売上科目','確度','元金額(税抜)','加重金額(税抜)','入金ラグ(月)','決済手段','組織名','摘要'] 確認済 |
| 行の一意キー候補 | dmIngestPipelinePlanData_ の push ロジック(601_datamart_ingest.js:441, 465, 474) | ✅ 管理ID + 計上年月 + 摘要('スポット' or 'MRR N/Mヶ月目')で一意 |
| 51 タブが現在 DDL 管理下か | 100_config/101_sys_config.js の schemas 辞書 | ❌ 未管理。本仕様で SYS_LIST_PIPE を追加 |
| シートキー | 100_config/101_sys_config.js:607 | ✅ SYS_LIST_PIPE(既存) |
Utils.parseAmt / Utils.parseDateToYm の存在 | 000_infra/004_utils.js | ✅ 既存案件で使用実績あり |
dmIngestPipelinePlanData_ の呼出元 | 602_datamart_main.js:274 | ✅ 1 箇所のみ。第 3 引数追加の影響は局所的 |
listSheet.clear() の使用箇所 | 602_datamart_main.js:299 | ✅ 1 箇所のみ。clearContent(2:N) へ置換対象 |
readSheetAsDtos_ / writeDtosToSheet_ の private 性 | 200_data/202_repository.js:19, 38 | ✅ 同ファイル内のみ参照可。本仕様の Repository は同ファイル内に追加する必要あり |
実行前に MCP / 手動で追加確認すべき項目:
- 51 タブに現在入力されている行数の実測(数百行規模か)
- 確度列の実データ表記(
'S'/'A'/'60%'/0.6混在か) - ユーザーが 51 タブに独自の注釈列を追加していないか(DDL 実行時に消える懸念)
- MAS-003 KPI ダッシュボードなど、51 タブを読む別マートの有無
プロダクトポリシー
Human-in-the-Loop(手動調整枠の可視化)
- 🔧 プレフィックス: 手動入力可能な 2 列の見出しに
🔧を付与し、「システム計算ではなくユーザー入力枠」を明示 - 薄黄色背景
#FFF2CC: DDL 初期化時および毎回のマート更新時に 🔧列(O/P 列)のデータ行に背景を設定 - 調整理由列の運用: 空欄許容だが、月次レビュー時に「なぜ調整したか」を残す慣習を定着させる(レビューアーへの説明責任)
- 孤立調整値の警告:
Utils.logInfoで運用ログに残し、後日の監査を可能にする
シミュレーション連携の可視化
- マート更新後のダイアログまたはトースト(既存
ss.toastパターン)で「🔧 手動調整: N 件適用 / M 件孤立破棄」をサマリ表示 events.amtに反映された調整が P/L 計画・CF 計画に波及していることを明示(ユーザーへの情報提供)
整合性
- 51 タブの調整値と他マート(63_pl_plan / 82_cf_plan 等)は常に同時更新(同じマート更新ループ内で処理)されるため、不整合状態になるケースは原理的に発生しない
関連ドキュメント
| 仕様書 | 関連箇所 |
|---|---|
| spec/spec_pipeline_plan.md | パイプライン計画展開の業務仕様。本仕様の手動調整ルールをここに追記 |
| dev_mas-078_pipeline_cf_integration.md | 直前案件。MAS-078 は 84 タブ(日次 CF)に 21_bud_pipeline を直接合流。本仕様は 51 タブ経由で P/L 計画・CF 計画へ波及。両者の経路が独立していることを明記 |
| dev_mas-008_cash_runway.md | ランウェイは営業 CF(cfOp)を入力。本仕様の調整値が planData に波及する結果、ランウェイにも間接影響 |
| dev_mas-006_project_overhead_allocation.md | PJ 別 P/L。51 タブの調整が PJ 別 P/L の売上に波及する(同じ events を経由) |
| dev_mas-196_repo_contracts_test.md | Repository / Contracts / Utils 層のテスト基盤。本仕様の PipelineListRepository もここに追加 |
| dev_mas-192_repository_module_split.md | Repository パターン完全移行。本仕様は同パターンの素直な拡張 |
| CLAUDE.md | 列参照ヘッダー名ベース規約・Repository 経由原則・DDL 管理方針 |
| failure_patterns.md | #11(データロスト事故)、#17(Date 正規化)、#18-#20(Read 裏取り) |
| TODO_future.md | MAS-079 案件定義 |
人間が検討すべき事項
| # | 項目 | 詳細 |
|---|---|---|
| 1 | 手動調整値のリセットタイミング(TODO_future.md 転記) | 初期運用では永続保持(ユーザー手動クリアまで)。将来的に「月次締めで実績確定月の調整値を自動クリア」する案件(MAS-079-2 候補)を検討。リセット基準は Utils.parseDateToYm(計上年月) < boundaryMonthStr |
| 2 | 加減算モード vs 上書きモード | 本仕様は 上書きモード(override が自動値を置換)。別途「自動値 + 調整差分 = 最終値」の加減算モード(差分明示)も要望あり得る。03_sys_params.LIST_PIPE_MODE=override/delta でパラメータ化する余地 |
| 3 | 負値調整の意味付け | 値引き / 返品 / 受注減額等。業務で区別する場合は「🔧調整種別」列(3 列目追加)を検討 |
| 4 | 調整理由の必須化 | 初期は任意入力。監査要件で必須化する場合は、findAdjustmentMap で override!=null && !reason を警告ログ出力するオプション追加 |
| 5 | **51 タブへの手動行追加(管理 ID 手入力)**への対応 | 現状はキー無効で破棄。将来「51 タブだけで新規見込案件を起こす」運用なら、findAdjustmentMap を拡張して「調整値+新規案件」も planData に注入する拡張必要 |
| 6 | シナリオ別調整値(§4.8.2 シナリオ比較) | Base/Best/Worst の切替時に異なる調整値セットを使いたい要望あり得る。51 タブを 3 シート(51_list_pipeline_plan_base 等)に展開する選択肢 |
| 7 | 調整値の入力上限/下限バリデーション | データ検証(データ入力規則)で範囲外入力を弾くか、Repository 側で Number.isFinite 検査で弾くか。本仕様では後者(データ入力規則は設定せず、書込時検査) |
| 8 | 過去マート履歴の保持 | 現状はマート更新ごとに上書き。「調整前の自動値」を履歴として別シートに退避する機構があれば監査証跡強化。MAS-179(監査証跡)との統合を検討 |
| 9 | ユーザー教育 / ドキュメント化 | 🔧列は何でも入力できるが、実際にどう使うかの運用手順書は別途必要(docs/spec/spec_pipeline_plan.md に追記、または docs/ops/*.md で運用ガイド整備) |
実装プロンプト(Claude Code 用)
【タイムアウト回避・実行原則(v1.7)】
1. Phase 1(設計)では拡張思考フル活用・Read で裏取り。Phase 2(清書)の各 Step は最小限思考で書き下し。
2. 「〜作成します」等の text のみで tool_use なしに turn 終了しない。
3. 実装は 骨格 Write → 追記 Edit/Bash を分割実行。1 回あたり ~300 行以内。
4. 各 Step で書く内容を事前に洗い出してから tool_use へ進む。
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-079「51タブをデータソース化(読み込み対応)」を実装してください。
## 実行前タスク
以下のファイルを読み込み、既存構造を把握してください:
1. `docs/dev/dev_mas-079_tab51_data_source.md` — 本仕様書
2. `600_report/602_datamart_main.js` — 特に L272-275(`dmIngestPipelinePlanData_` 呼出)・L295-309(51 タブ出力ブロック)
3. `600_report/601_datamart_ingest.js` — `dmIngestPipelinePlanData_`(L363-479)。events.push / viewRows.push の構造
4. `200_data/202_repository.js` — `AccountRepository`(L304-350)パターン、`readSheetAsDtos_`(L19)
5. `000_infra/003_contracts.js` — DTO 定義パターン
6. `100_config/101_sys_config.js` — schemas 辞書(L645 付近)、既存 `SYS_LIST_PIPE` エントリ(L607)
7. `000_infra/004_utils.js` — `parseAmt` / `parseDateToYm` の挙動
8. `CLAUDE.md` — Repository 経由原則・ヘッダー名ベース・Human-in-the-Loop・DDL 管理方針
9. `docs/_internal/failure_patterns.md` — #11(データロスト)、#17(Date 正規化)、#18-#20(Read 裏取り)
## 修正対象ファイル
- `000_infra/003_contracts.js` — `PipelineListDTO` 追加
- `200_data/202_repository.js` — `PipelineListRepository` 追加
- `100_config/101_sys_config.js` — `schemas['SYS_LIST_PIPE']` 追加、`confSheet.appendRow` の description 変更
- `600_report/601_datamart_ingest.js` — `dmIngestPipelinePlanData_` の第 3 引数追加・調整値適用
- `600_report/602_datamart_main.js` — 呼出側で `findAdjustmentMap` 取得、出力ブロックの `clear()` を `clearContent(2:N)` へ
## 実装内容
### A. DDL 列追加(`100_config/101_sys_config.js`)
1. **schemas 辞書に `SYS_LIST_PIPE` を追加**(L645 付近、既存 `BUD_PIPE` 等と同じブロック):
'SYS_LIST_PIPE': {
headers: ["管理ID","計上年月","入金予定年月","PJ・案件名","取引先名","契約形態","売上科目","確度","元金額(税抜)","加重金額(税抜)","入金ラグ(月)","決済手段","組織名","摘要","🔧調整値_加重金額(税抜)","🔧調整理由"],
color: "#674ea7"
},
2. **`confSheet.appendRow` の description を変更**(L607):
if (!existKeys.includes('SYS_LIST_PIPE')) confSheet.appendRow(['SYS_LIST_PIPE', '', '51_list_pipeline_plan', '一覧_パイプライン展開明細(手動調整可・シミュレーション入力)']);
### B. DTO 追加(`000_infra/003_contracts.js`)
ファイル内の `JournalEntryDTO` or `BudgetDTO` の近傍に `PipelineListDTO` を `@typedef` として追加(本仕様書 Step 2 の JSDoc 参照)。
### C. Repository 追加(`200_data/202_repository.js`)
ファイル末尾(`AccountRepository` の後)に `PipelineListRepository` を追加:
var PipelineListRepository = {
_getSheet: function() { return Utils.getSheetByKey('SYS_LIST_PIPE', '51_list_pipeline_plan'); },
findAll: function() { return readSheetAsDtos_(PipelineListRepository._getSheet()); },
findAdjustmentMap: function() {
var map = {};
var sheet = PipelineListRepository._getSheet();
if (!sheet) return map;
var result = readSheetAsDtos_(sheet);
for (var i = 0; i < result.dtos.length; i++) {
var dto = result.dtos[i];
var mgrId = String(dto['管理ID'] || '').trim();
var pYm = Utils.parseDateToYm(dto['計上年月']);
var memo = String(dto['摘要'] || '').trim();
if (!mgrId || !pYm || !memo) continue;
var key = mgrId + '||' + pYm + '||' + memo;
var rawOverride = dto['調整値_加重金額(税抜)'];
var override = null;
if (rawOverride !== '' && rawOverride !== null && rawOverride !== undefined) {
var n = Utils.parseAmt(rawOverride);
if (isFinite(n)) override = n;
}
var reason = String(dto['調整理由'] || '').trim();
if (override !== null || reason) {
map[key] = { override: override, reason: reason };
}
}
return map;
},
};
※ DDL の `🔧` プレフィックス付き列名で HEADERS を書いた場合、DTO のキーも `🔧調整値_加重金額(税抜)` となる点に注意。`dto['🔧調整値_加重金額(税抜)']` でアクセス(または trim() でマッチするよう実装)。本仕様では**プレフィックス無しキー**でアクセスするため、headers を `"調整値_加重金額(税抜)"` で書き、背景色で手動入力枠を示す方が実装がシンプル。**🔧 はセル書式や条件付き書式で可視化**、HEADERS は非装飾名とする。
### D. `dmIngestPipelinePlanData_` 改修(`601_datamart_ingest.js`)
1. シグネチャ: `function dmIngestPipelinePlanData_(ctx, sheetPipe, adjustmentMap)` に変更。`adjustmentMap = adjustmentMap || {}` でフォールバック
2. スポット行 push 前に調整値を適用:
var memoSpot = 'スポット';
var keySpot = mgrId + '||' + startYm + '||' + memoSpot;
var adjSpot = adjustmentMap[keySpot];
var finalSpot = (adjSpot && adjSpot.override !== null) ? adjSpot.override : weightedSpot;
if (finalSpot !== 0 && startYm <= lastMonth) {
events.push({ pYm: startYm, sYm: sYmSpot, acc: acc, amt: finalSpot, booked: false, isBsForce: null, noCash: false });
}
viewRows.push([mgrId, startYm, sYmSpot, pjName, vendor, typeStr, acc, prob, spot, weightedSpot, lag, payMethod, orgName, memoSpot,
(adjSpot && adjSpot.override !== null) ? adjSpot.override : '',
(adjSpot && adjSpot.reason) ? adjSpot.reason : '']);
3. MRR 行も同様。キーは `mgrId + '||' + curYm + '||' + ('MRR ' + (m+1) + '/' + dur + 'ヶ月目')`
4. 旧挙動(`spot === 0` で events push スキップ)と新挙動(`finalSpot === 0` でスキップ)の差分に注意
### E. 呼出側改修(`602_datamart_main.js`)
1. **L272 前**に `adjustmentMap` 取得:
var adjustmentMap = PipelineListRepository.findAdjustmentMap();
2. **L274** の呼出に第 3 引数追加:
pipeResult = dmIngestPipelinePlanData_(ctx, sheetPipe, adjustmentMap);
3. **L295-309 の出力ブロック**を本仕様書 Step 3 のコード(clearContent 方式・16 列ヘッダー・黄色背景)に差し替え
### F. 孤立調整値の警告ログ
`adjustmentMap` のキー全体と、`dmIngestPipelinePlanData_` 内で実際にマッチしたキー集合を比較し、差分を警告ログ:
var matchedKeys = {}; // ensureMaster 内でセット
// ... ループ終了後
var orphans = [];
for (var k in adjustmentMap) if (!matchedKeys[k]) orphans.push(k);
if (orphans.length > 0) {
Utils.logInfo('dmIngestPipelinePlanData_', 'MAS-079 孤立調整値を破棄: ' + orphans.length + ' 件 [' + orphans.slice(0, 5).join(', ') + ...]');
}
## 制約
- **`listSheet.clear()` を絶対に呼ばない**(ヘッダー書式・背景色が消える。本仕様の clear-preserves-format が前提)
- **列インデックスハードコード禁止**、DTO キーは文字列で動的アクセス
- **`Utils.parseAmt`/`parseDateToYm` を必ず使用**(失敗パターン #17 継承)
- **`override === null` と `override === 0` を厳密区別**(0 は正当な入力)
- **第 3 引数フォールバック**: `adjustmentMap || {}` で既存呼出からの後方互換保証
- **DDL 実行で既存データが消えないことを検証**(実機テスト必須)
- **PipelineListRepository は `readSheetAsDtos_` 経由**(同ファイル内から呼ぶ)
- **孤立調整値は破棄、元データが生きていれば override 採用**
- **行の一意キーは `管理ID + 計上年月 + 摘要`**(インデックスベース禁止)
## エッジケース
1. 調整値型エラー → parseAmt フォールバック、override=null
2. 孤立調整値 → ログ警告・破棄
3. 調整値空欄 → 自動値採用
4. 調整値 0 → 0 を採用(null と厳密区別)
5. 調整値負値 → そのまま採用
6. 新規案件 → 調整なし・自動値
7. 管理 ID 空 → Map 非登録
8. 計上年月不正 → Map 非登録
9. MRR 部分調整 → 該当行のみ override
10. 同キー重複 → 後勝ち
11. DDL 実行時列追加 → 既存データ保持
12. 確度 100% 案件 → 51 タブに出ない(既存挙動)
## 実データ検証
- 51 タブの現行 14 列(`602_datamart_main.js:300`)確認済
- 行の一意キー `管理ID + 計上年月 + 摘要` 確認済
- `SYS_LIST_PIPE` シートキー既存(`101_sys_config.js:607`)
- `readSheetAsDtos_` / `writeDtosToSheet_` は private、同ファイル内追加必須
## 動作確認
`npm run push:dev` 後:
1. `setupAllSchemas` を実行し、51 タブに「🔧調整値_加重金額(税抜)」「🔧調整理由」列が追加されていること(ヘッダー行のみ)
2. マート更新(主要メニュー)を実行し、51 タブが展開されること(16 列)
3. 🔧列のデータ行に薄黄色背景 `#FFF2CC` が適用されていること
4. 特定行の「🔧調整値_加重金額(税抜)」に手動で 100000 を入力 → マート更新再実行 → 該当行の **override 採用**、他行は自動値のまま
5. 63_pl_plan・82_cf_plan の該当売上月が override 反映の金額に変化
6. MAS-008 Cash Runway のランウェイが計画側の変化を受けて変動(営業 CF 経由)
7. 🔧列に `未定` と入力 → `parseAmt` で 0 フォールバック → override=null で自動値採用(ログ警告なし、ただし理由列があれば保持)
8. 調整値 `0` を入力 → 該当行の events が除外され、該当月の計画売上がその分減少
9. 21_bud_pipeline から該当案件を削除 → 次回マート更新で該当行が 51 タブから消え、孤立ログに記録
10. 🔧列のみユーザーが手動でクリア → 次回マート更新で自動値に戻る(永続保持ではない)
### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|:--------:|------|
| Repository 追加 | なし | AccountRepository パターン踏襲 |
| findAdjustmentMap のキー構築 | あり | 3 値複合キーの正規化・null 厳密判定 |
| dmIngestPipelinePlanData_ の適用位置 | あり | events.push / viewRows.push の両経路への正確な差分適用 |
| clearContent の範囲 | なし | 仕様書で 2:N × 16 列と特定済 |
推奨実行モデル
| 工程 | 推奨モデル | 理由 |
|---|---|---|
| 仕様書作成(本ドキュメント) | Claude Opus 4.6 | Repository 新設 + マージ方式設計 + events.amt 波及の全マート確認 + 複合キー正規化の複合設計 |
| 実装 | Claude Sonnet 4.6 | 関数シグネチャ・マップ構造・差分適用箇所が仕様書で確定済み。override === null と 0 の厳密判定、DDL 実行時のデータ保全確認に中程度の判断 |
| 動作確認 | ユーザー手動 | setupAllSchemas 実行、51 タブへの手動入力、マート再更新、MAS-008 / MAS-006 への波及確認が必要 |
変更履歴
| 日付 | 変更内容 |
|---|---|
| 2026-04-18 | 初版作成 |
仕様書作成プロンプト(再現性・監査性のため記録)
仕様書作成プロンプト(再現性・監査性のため記録)
展開して表示
<instruction>
【タイムアウト回避・実行原則(v1.7・必ず遵守すること)】
Claude Code が Phase 2 で API ストリーム idle timeout を起こさないための装備:
1. **拡張思考の使い分け**:
- Phase 1(設計)では拡張思考をフル活用し、ファイル名形式・エッジケース一覧・Step 分割粒度・固有名詞(関数名/シート名/列名/行番号)を完全に確定させる。
- Phase 2(清書)の各 Step 内では拡張思考を最小限に抑え、Phase 1 で確定済みの内容の書き下しに徹する。出力途中で再考しない。
2. **テキスト報告の禁止**:
- 「〜を作成します」等の text のみで tool_use なしに turn を終了しない。
- 説明は 1 文以内。直ちに tool を呼ぶ。
3. **4-5 分割の Write/Edit 実行**:
- 仕様書作成は以下の Step に分けて実行する:
- 2-1 骨格 Write(~20行)
- 2-2 概要〜注意事項 Edit/Bash(~300行)
- 2-3a エッジケース〜人間検討事項 Edit/Bash(~200行)
- 2-3b 実装プロンプト〜変更履歴 Edit/Bash(~250行)
- 2-4 `<details>` にプロンプト全文記録 Edit/Bash(最重量・必ず独立 Step)
- 1 回の Write/Edit は約 300 行以内を目安にする。
4. **各 Step で何を書くかを具体指示**:
- 設計判断を Phase 2 実行時に持ち込まないよう、プロンプト内で指定された各 Step の内容(アーキテクチャ・エッジケース等)を忠実に書き下すこと。
======================================================================
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者兼仕様書ライターです。
CLIエージェントである「Claude Code」として、上記の原則と以下のフェーズに従い、案件 S-07「51タブをデータソース化(読み込み対応)」の開発仕様書を作成してください。
## Phase 1: 実行前タスク(必読・必ずツールを使用して順次実行)
(※テキストでの状況報告は一切行わず、直ちにツールの使用を開始してください)
1. `docs/_internal/TODO_future.md` を検索し、案件 **S-07** の「案件名」「概要」「期待される効果」「人間が検討すべき事項」を特定・完全に把握する。
2. `CLAUDE.md` と `docs/_internal/failure_patterns.md` を読む。
3. 51タブ(売上・パイプライン等)を出力している現在のマート生成ロジック(`600_report/` 配下の該当ファイル)を読む。
4. 51タブをデータソースとして読み込むために必要なデータアクセス層(`200_data/202_repository.js`、`000_infra/003_contracts.js`)を読む。
5. `docs/_internal/dev_spec_prompt_template.md` の Phase 2 構成と実装プロンプトフォーマットを読む。
6. ツール(MCP等)を使って、対象の 51タブ の DDL構成と実データを事前確認し、「自動算出される列」と「手動調整用として追加・利用すべき列」の境界や、行を一意に特定できる主キー(例: 取引先+案件ID など)を把握する。
## 既存実装の前提知識(車輪の再発明を防ぐ)
- シートからの読み込み・書き込みは Range を直接操作せず、必ず Repository を経由すること。今回の対応により、51タブ用の DTO と Repository クラス(`findAsMap()` または `findAll()` を備えるもの)を新設・拡張する必要がある。
- マート出力時は一度インメモリで「自動集計値」と「手動調整値」をマージ(結合)させてから、2次元配列として一括書き込みを行うこと。
## Phase 2: 仕様書の分割作成
出力先: `docs/dev/dev_mas-079_tab51_data_source.md`
**【重要】絶対に1回のツール呼び出しで全内容を出力せず、以下の Step 2-1 〜 2-4 に厳密に分割して実行してください。**
### Step 2-1: 骨格の作成 (File Write)
対象ファイルに、仕様書テンプレートに準拠した見出し(`## 概要`, `## 目的`, `## 現在のコード`, `## 修正方針` 等)の骨格のみを Write ツールで作成して保存してください。本文は空で構いません。
### Step 2-2: 前半セクションの追記 (File Edit または Bash)
「概要」「目的」「現在のコード」「修正方針」「影響範囲」「注意事項」を追記してください。以下を必ず含めること:
- **アーキテクチャの決定事項**:
- これまで「出力専用(読取専用)」だった51タブを、「双方向(Read/Write)」のデータソースに昇格させる。
- **マージロジック(最重要)**: マート更新時、いきなりシートをクリア(`clearContent()`)するのではなく、まず Repository 経由で既存の51タブのデータを読み込み、ユーザーが入力した「手動調整値」をインメモリの Map(行のユニークキーベース)に退避させる。その後、システムが自動集計した新しいベースデータに対して退避した手動調整値を適用(上書きまたは加減算)し、最終結果をシートに出力する。
- **行ズレ防止**: 調整値のインメモリキャッシュは、必ず「案件ID」や「取引先名」などのビジネス複合キーを用いて Map を構築し、行の並び順変動や新規行追加時に値がズレない設計とする。
- **シミュレーション計算への連携**: 51タブで手動調整された値は、後続のシミュレーション計算(P/L予測など)で優先して使用されるようにデータフローを修正する。
### Step 2-3a: エッジケース〜人間検討事項の追記 (File Edit または Bash)
「エッジケース」「実データ検証」「関連ドキュメント」「人間が検討すべき事項」を追記してください。
- **エッジケース(テーブル形式で必須)**:
1. **調整値の型エラー**: ユーザーが手動調整列に「未定」等の文字列や記号を入力した場合、`NaN` 波及を防ぐために `Utils.parseAmt()`等で 0 にフォールバックする。
2. **元データの消滅**: 元の自動集計データ(行)がパイプライン等から削除されたが、51タブ上に手動調整値だけが残っている場合(システム集計値がない場合は手動調整値も破棄して孤立データを防ぐ方針を推奨)。
3. **空欄の扱い**: 手動調整列が空欄の場合は、自動集計値をそのまま採用する。
- **プロダクトポリシー**:
- Human-in-the-Loop の観点から、手動調整が入力可能な列は、`setupAllSchemas` 等で背景色(例: 薄い黄色や青色)を変更し、「システム自動計算値ではなくシミュレーション入力枠である」ことが一目でわかる状態を担保する。
- **実データ検証**:
- Phase 1(Step 6)で確認した 51タブの主キーとなる列(一意に行を特定できるキー)の存在や、DDL追加が必要な手動調整用列の状況を記載する。
- **人間が検討すべき事項**:
- 手動調整値の「リセットタイミング」のルール(毎月月次締めで実績月をクリアするのか、ユーザーが手動でクリアするまで永続保持するのか)についての方針決定を促す記載。
### Step 2-3b: 実装プロンプト〜変更履歴の追記 (File Edit または Bash)
「実装プロンプト(Claude Code用)」「推奨実行モデル」「変更履歴」を追記してください。
- **実装プロンプト**:
- バッククォート(```)で囲まず、全行を行頭4スペースインデントで出力すること。
- 過去の失敗パターン(インデックスベースの行マッチング禁止、マート更新時の全消去によるデータロスト事故の防止など)を踏まえた注意事項を盛り込むこと。
- **変更履歴**: 当日の日付で「初版作成」と記載する。
### Step 2-4: 仕様書作成プロンプトの記録 (File Edit または Bash)
対象ファイルの末尾に `<details><summary>展開して表示</summary>` を設け、**この `<instruction>` タグの最初から最後まで(今あなたが読んでいるプロンプト全文)**を一言一句そのまま追記して `<details>` を閉じてください。
※この処理が最も出力トークンを消費し重いため、必ず独立したステップとして実行してください。
## Phase 3: `_config.json` への追記と構文チェック
1. `docs/_config.json` の該当箇所(FP&A・シミュレーション基盤等)に今回の仕様書へのリンクと説明を追記して保存。
2. 保存後、ターミナルで `node -e "require('./docs/_config.json')"` 等を実行し、JSONの構文エラー(カンマ抜け、括弧の不整合など)がないか自己チェック・修正する。
</instruction>