概要

項目内容
案件IDMAS-015
カテゴリFP&A・レポーティング / UX
PhaseP3
優先度
所要時間3-4時間(Repository 新設 + マート 1 本 + 条件付き書式)
対象ファイル000_infra/003_contracts.jsResourceDTO 追記)
200_data/202_repository.jsResourceRepository 追記)
100_config/101_sys_config.jsRSC_HEAT システムキー登録 + MENU_DEFINITION に 1 項目追加 — 002_constants.js
000_infra/002_constants.jsMENU_DEFINITION📋 サイドバー: 📊 マート更新 カテゴリに 1 項目追加)
600_report/610_datamart_resource.js(新規作成)
出力先タブ69_rsc_heatmap(動的生成・DDL 管理外)
データソース70_bud_resource(システムキー BUD_RSCE
一次資料docs/_internal/TODO_future.md §3.2 F-15

目的

70_bud_resource に月次で蓄積される要員稼働率データを集計し、新規タブ 69_rsc_heatmap にメンバー × 月のヒートマップ形式で出力する。閾値超過セルを条件付き書式で赤/黄着色することで、個別メンバーの過負荷 (>120%)・余力 (<100%)・空き月を一覧視覚化し、PM / PMO が月次でリソース配分を判断できるようにする。

一次資料(TODO_future.md)の期待効果:

  • リソース配分の最適化
  • 「稼働率の目標値(100% or 80%)」の設定判断材料

本案件は 既存 70_bud_resource の再集計レポート層の新設 のみで、70_bud_resource 自体のスキーマ変更・既存 420_project_profitability.js の計算ロジック変更は伴わない(S-33「工数入力化」とは独立)。

現在のコード

新規実装のため既存実装はない。関連する既存資産は以下の通り。

データソース: 70_bud_resource の実スキーマ(確定情報)

100_config/101_sys_config.js:837 の DDL 定義:

'BUD_RSCE': { headers: ["有効フラグ","要員名","PJ・案件名","対象年月","稼働率(%)"], color: "#e69138",
  validations: {
    "稼働率(%)": { type: 'range', min: 0, max: 1, helpText: '0〜1の小数で入力してください(例: 0.05 = 5%)' }
  }
},

重要: 実スキーマは 5 列のみで、終了年月 列は存在しない。1 行 = 1 メンバーの 1 PJ・1 ヶ月の稼働率。対象年月 列が単一月を保持するため、本案件では期間展開 (開始〜終了の月別展開) は不要。同一メンバー × 同一月の複数 PJ 行は集計時に合算する。

値域: 稼働率(%) は 0.0〜1.0 の小数(1.0 = 100%)。見出しに (%) とあるが格納値は比率(decimal)。本仕様書の閾値・表示も同じスケールで扱う。

参考: 既存の集計実装(400_domain/420_project_profitability.js:73-86

buildProjectProfitability()70_bud_resource を PJ 別労務費算出のために既に読み込んでいる。列名の参照方法(rsData[0].indexOf('要員名') 等)と有効フラグのスキップ処理はこのコードのパターンを踏襲する:

const idxName = rsData[0].indexOf('要員名'), idxPj = rsData[0].indexOf('PJ・案件名'),
      idxYm = rsData[0].indexOf('対象年月'), idxRate = rsData[0].indexOf('稼働率(%)');
for(let i=1; i<rsData.length; i++){
  const row = rsData[i];
  if(row[0] === false || String(row[0]).toUpperCase() === 'FALSE') continue;
  // ...
}

参考: 動的生成タブの先例(93_kpi_dashboard

600_report/609_datamart_kpi.js:21-53buildKpiDashboard()93_kpi_dashboardss.insertSheet(name) で生成し毎回 sheet.clear() + sheet.clearConditionalFormatRules() してから再描画するパターン。setupAllSchemas() では 01_sys_config へのシステムキー登録のみ行い、DDL schemas マップには登録しない(= 動的生成扱い)。

参考: 条件付き書式の先例(600_report/609_datamart_kpi.js:476-522 / 600_report/606_datamart_daily_cf.js:178-184

SpreadsheetApp.newConditionalFormatRule().whenNumberGreaterThan(x).setBackground('#F4CCCC').setFontColor('#CC0000').setRanges([range]).build() を配列で組み立て、sheet.setConditionalFormatRules(rules) で一括適用する。

修正方針

アーキテクチャ決定

  1. 新規タブ方式: 69_rsc_heatmap を新設する。番号帯 69 は「60 番台=財務帳票 (61-68) の直前」で、リソース系レポートとして空き番号。93_kpi_dashboard と同じく 動的生成タブ(DDL 管理外) として扱う。
  2. Repository 層追加(読み取り専用): ResourceRepository.findAll() のみ実装する(save() / append() は本案件のスコープ外)。既存 420_project_profitability.js は直接 getDataRange().getValues() で読み込んでいたが、本案件では OrderRepository と同じ DTO 経由パターンで追加する(将来の再利用性のため)。
  3. 集計ロジックは合算のみ: 70_bud_resource の各行は既に月粒度のため、{ 要員名: { 'YYYY-MM': totalRate } } のマップに加算していくだけ。期間展開 (addMonths ループ) は不要。
  4. 表示期間はパラメータ化: Constants.getParam('HEATMAP_PAST_MONTHS', 6)Constants.getParam('HEATMAP_FUTURE_MONTHS', 12) で過去月数・将来月数を外部設定化。基準は本日を含む月 (today の YYYY-MM)
  5. 月列は期間から算出: getLastColumn() 等の動的列範囲取得は禁止(failure_patterns.md #21 対策)。月列数 = pastMonths + 1 + futureMonthsGAS 側で計算してから setValues() する。
  6. 閾値・色分けは条件付き書式: セル値(合算稼働率)に対し whenNumberGreaterThan(threshWarn) → 赤、whenNumberGreaterThan(threshSafe) → 黄 の 2 ルール。セル値自体の書き換えはしない(合算値をそのまま表示)。
  7. メニューは宣言的定義 (MENU_DEFINITION) に追記: 000_infra/002_constants.jsMENU_DEFINITION📋 サイドバー: 📊 マート更新 カテゴリに { label: '🔥 リソース稼働率ヒートマップ更新', funcName: 'buildResourceHeatmap', description: '69 タブにメンバー × 月のヒートマップを再生成' } を追加する。ui.createMenu は直接編集しない(onOpen() L323 は MENU_DEFINITION を走査して自動生成する実装のため)。

Step 1: ResourceDTO の追加(000_infra/003_contracts.js 末尾)

BudgetDTO と同一の @typedef スタイルで追加する。フィールド名は 100_config/101_sys_config.js:837 の DDL ヘッダーと完全一致させる:

/**
 * 70_bud_resource — 要員稼働率(PJ別・月次)
 * @typedef {Object} ResourceDTO
 * @property {boolean}     有効フラグ
 * @property {string}      要員名
 * @property {string}      PJ・案件名
 * @property {Date|string} 対象年月          - "YYYY-MM" または Date 型(月初)
 * @property {number}      稼働率(%)          - 0.0〜1.0 の小数 (1.0 = 100%)
 */

Step 2: ResourceRepository の追加(200_data/202_repository.js 末尾)

OrderRepository のパターンを完全踏襲する。本案件では findAll() のみ実装(読み取り専用):

// =====================================================================
// ResourceRepository — 70_bud_resource (読み取り専用)
// =====================================================================

var ResourceRepository = {

  /** @private */
  _getSheet: function() {
    // 既存の 420_project_profitability.js L16 と同じフォールバック順
    var sheet = Utils.getSheetByKey('BUD_RSCE', '70_bud_resource');
    if (sheet) return sheet;
    return SpreadsheetApp.getActiveSpreadsheet().getSheetByName('27_bud_resource');
  },

  /**
   * 全要員稼働率レコードを DTO 配列で取得する。
   * @returns {{ headers: string[], dtos: ResourceDTO[] }}
   */
  findAll: function() {
    return readSheetAsDtos_(ResourceRepository._getSheet());
  },
};

システムキー BUD_RSCE100_config/101_sys_config.js:78101_sys_config に登録済み(DDL 既存エントリ)。第 2 引数のフォールバックは 70_bud_resource を優先し、旧タブ名 27_bud_resource も保険で試す(420 の既存実装に合わせる)。

Step 3: 100_config/101_sys_config.js の修正(setupAllSchemas() のシステムキー登録のみ)

setupAllSchemas()confSheet.appendRow ブロック(L770-824)に 1 行追加する:

// 60番台レポート系の近くに挿入
if (!existKeys.includes('RSC_HEAT')) confSheet.appendRow(['RSC_HEAT', '', '69_rsc_heatmap', 'リソース稼働率ヒートマップ']);

schemas マップ(L826-)には登録しない69_rsc_heatmap は DDL 管理外の動的生成タブとして扱い、buildResourceHeatmap() が毎回 clear() + 再構築する(93_kpi_dashboard / 76_notes / 72_bs_snap と同じ扱い)。これにより CLAUDE.md の「DDL (setupAllSchemas) で管理されないタブ」リストへの追記も必要(後述「関連ドキュメント」参照)。

Step 3-B: メニュー追加(000_infra/002_constants.jsMENU_DEFINITION

MENU_DEFINITION 配列(L206-324)の 📋 サイドバー: 📊 マート更新 カテゴリ(L230-239)の items末尾追加 する:

{ label: '🔥 リソース稼働率ヒートマップ', funcName: 'buildResourceHeatmap', description: '69 タブにメンバー × 月の稼働率ヒートマップを再生成' },

onOpen()100_config/101_sys_config.js:323)は Constants.MENU_DEFINITION を走査して ui.createMenu / addItem を自動生成するため、onOpen() 本体の編集は不要。

Step 4: 600_report/610_datamart_resource.js(新規作成)

/**
 * =========================================================
 * 610_datamart_resource.js — F-15: リソース稼働率ヒートマップ
 * =========================================================
 * 70_bud_resource(要員名 × PJ × 対象年月 × 稼働率)を集計し、
 * 69_rsc_heatmap に「メンバー × 月」のヒートマップを出力する。
 *
 * - 月列: 本日月を基準に [過去 HEATMAP_PAST_MONTHS ヶ月 ~ 将来 HEATMAP_FUTURE_MONTHS ヶ月]
 * - セル値: 合算稼働率 (0.0 以上の小数、1.2 超は過負荷)
 * - 着色: 条件付き書式で >threshSafe 黄 / >threshWarn 赤
 */

function buildResourceHeatmap() {
  var FUNC = 'buildResourceHeatmap';
  try {
    Utils.logInfo(FUNC, '処理開始');
    var ss = getWebSpreadsheet_();
    var sheetName = Utils.getSheetNameByKey('RSC_HEAT') || '69_rsc_heatmap';
    var sheet = ss.getSheetByName(sheetName) || ss.insertSheet(sheetName);

    sheet.clear();
    sheet.clearConditionalFormatRules();

    // (1) パラメータ取得
    var pastMonths   = Number(Constants.getParam('HEATMAP_PAST_MONTHS', 6));
    var futureMonths = Number(Constants.getParam('HEATMAP_FUTURE_MONTHS', 12));
    var threshSafe   = Number(Constants.getParam('HEATMAP_THRESHOLD_SAFE', 1.0));
    var threshWarn   = Number(Constants.getParam('HEATMAP_THRESHOLD_WARNING', 1.2));

    // (2) 月列ラベル生成(本日を含む月を基準)
    var today = new Date();
    var baseYm = Utilities.formatDate(today, 'JST', 'yyyy-MM');
    var targetMonths = [];
    for (var i = -pastMonths; i <= futureMonths; i++) {
      targetMonths.push(Utils.addMonths(baseYm, i));
    }

    // (3) データ読み込み&合算
    var result = ResourceRepository.findAll();
    var rateMap = {};     // { 要員名: { 'YYYY-MM': 合算稼働率 } }
    var memberSet = {};   // 順序保持用
    result.dtos.forEach(function(dto) {
      var flag = dto['有効フラグ'];
      if (flag === false || String(flag).toUpperCase() === 'FALSE') return;
      var name = String(dto['要員名'] || '').trim();
      if (!name) return;
      var ym = Utils.parseDateToYm(dto['対象年月']);
      if (!ym || targetMonths.indexOf(ym) === -1) return;
      var rate = Utils.parseAmt(dto['稼働率(%)']);
      if (!rateMap[name]) { rateMap[name] = {}; memberSet[name] = true; }
      rateMap[name][ym] = (rateMap[name][ym] || 0) + rate;
    });

    // (4) 出力配列組み立て
    var memberNames = Object.keys(memberSet).sort();
    var headerRow = ['要員名'].concat(targetMonths);
    var dataRows = memberNames.map(function(name) {
      var row = [name];
      targetMonths.forEach(function(ym) {
        row.push(rateMap[name][ym] || 0);
      });
      return row;
    });
    var outValues = [headerRow].concat(dataRows);

    // (5) 書き込み
    var rows = outValues.length;
    var cols = headerRow.length;
    sheet.getRange(1, 1, rows, cols).setValues(outValues);

    // (6) 書式設定
    sheet.getRange(1, 1, 1, cols).setBackground('#434343').setFontColor('#FFFFFF').setFontWeight('bold');
    sheet.setFrozenRows(1);
    sheet.setFrozenColumns(1);
    if (dataRows.length > 0) {
      sheet.getRange(2, 2, dataRows.length, targetMonths.length).setNumberFormat('0.0%;[Red]△ 0.0%;"-"');
    }
    sheet.getDataRange().setFontFamily('BIZ UDGothic');
    sheet.autoResizeColumns(1, 1);
    sheet.setColumnWidths(2, targetMonths.length, 80);

    // 余分な列・行を削除
    if (sheet.getMaxColumns() > cols) sheet.deleteColumns(cols + 1, sheet.getMaxColumns() - cols);
    if (sheet.getMaxRows() > rows) sheet.deleteRows(rows + 1, sheet.getMaxRows() - rows);

    // フィルター
    var ef = sheet.getFilter(); if (ef) ef.remove();
    if (rows > 1) sheet.getRange(1, 1, rows, cols).createFilter();

    // (7) 条件付き書式
    if (dataRows.length > 0) {
      var dataRange = sheet.getRange(2, 2, dataRows.length, targetMonths.length);
      var rules = [
        // 過負荷 (>threshWarn, 例 120%) → 赤
        SpreadsheetApp.newConditionalFormatRule()
          .whenNumberGreaterThan(threshWarn)
          .setBackground('#F4CCCC').setFontColor('#CC0000').setBold(true)
          .setRanges([dataRange]).build(),
        // 上限近傍 (>threshSafe, 例 100%) → 黄
        SpreadsheetApp.newConditionalFormatRule()
          .whenNumberGreaterThan(threshSafe)
          .setBackground('#FCE5CD').setFontColor('#B45F06')
          .setRanges([dataRange]).build(),
      ];
      sheet.setConditionalFormatRules(rules);
    }

    Utils.logInfo(FUNC, sheetName + ' 再描画完了 (メンバー=' + memberNames.length + ', 月=' + targetMonths.length + ')');
  } catch (e) {
    Utils.logError(FUNC, e);
    throw e;
  }
}

影響範囲

ファイル変更量内容
000_infra/003_contracts.js+8 行ResourceDTO@typedef 追記(末尾)
000_infra/002_constants.js+1 行MENU_DEFINITION 📋 サイドバー: 📊 マート更新 に 1 項目追加
100_config/101_sys_config.js+1 行setupAllSchemas()appendRow ブロックに RSC_HEAT 登録
200_data/202_repository.js+20 行ResourceRepository 新規追加(末尾、findAll のみ)
600_report/610_datamart_resource.js+130 行新規ファイル
CLAUDE.md+1 行「DDL (setupAllSchemas) で管理されないタブ」リストに 69_rsc_heatmap を追加

既存ファイルへの影響(確認済み・影響なし):

  • 400_domain/420_project_profitability.js: 70_bud_resource を直接 getDataRange() で読み込み続ける(PJ 別労務費算出は変更しない)。ResourceRepository 経由に置換するかは将来検討事項(本案件のスコープ外)。
  • 600_report/601〜609_datamart_*.js: 変更なし。buildBudgetTrendDataMart() からの連鎖呼び出しも本案件では追加しない(ヒートマップは独立メニューから手動実行)。

注意事項

  1. 列参照はヘッダー名ベースDTO['要員名'] / DTO['対象年月'] 等で参照する。getLastColumn() や列番号ハードコード禁止(CLAUDE.md コーディング規約)。
  2. 有効フラグ=FALSE の行は全処理でスキップする。row[0] === false || String(row[0]).toUpperCase() === 'FALSE' の 2 条件判定を必ず実施(420_project_profitability.js:78 の既存パターンに合わせる)。DTO['有効フラグ'] 経由でも同じ判定を行う。
  3. 動的列範囲取得禁止。月列数は pastMonths + 1 + futureMonths から算出してハードコードする。sheet.getLastColumn() は既存書式が残っていた場合に想定外の列数を返す(failure_patterns.md #21 対策)。
  4. 年月ラベル比較は正規化後に行うUtils.parseDateToYm() が返す値は "YYYY-MM" 形式で確定しているが、targetMonths と比較する前に Utils.parseDateToYm() を通してから indexOf する。文字列比較する場合は .replace(/[\s ]+/g, '') で全角スペース対策を行う(failure_patterns.md #22 対策)。
  5. 03_sys_params パラメータ参照は Constants.getParam() 経由PropertiesService 直接呼び出し禁止)。初回呼び出し時にシートをスキャンしてキャッシュされるため、ヒートマップ再生成ごとのコストは低い。シートへ年月文字列を書く場合は apostrophe 前置または setNumberFormat('@') でテキスト化(failure_patterns.md #23 対策)。本案件では targetMonthsヘッダー行に書くが、setValues で書いた後に背景色等のみを設定し、自動で日付型解釈されないよう事前に setNumberFormat('@') しておくと安全(上記コードの (5) と (6) の間に sheet.getRange(1, 2, 1, targetMonths.length).setNumberFormat('@') を追加する実装を推奨)。
  6. MENU_DEFINITION / setupAllSchemas() / onOpen() への追記は Read で実在コードを確認してから行う。記憶・Grep 部分ヒットだけで固有名詞(メニュー名・カテゴリ名・定数名・関数名)を書かない(failure_patterns.md #18-#20 対策)。本仕様書ではカテゴリ名 📋 サイドバー: 📊 マート更新000_infra/002_constants.js:230 を Read して確定している。
  7. 閾値のスケール: 70_bud_resource稼働率(%)0.0-1.0 の小数(DDL バリデーション min: 0, max: 1)。閾値デフォルト HEATMAP_THRESHOLD_SAFE=1.0 / HEATMAP_THRESHOLD_WARNING=1.2 も同じスケール。「80% を標準とする運用」に切り替える場合はパラメータで 0.8 / 1.0 に再設定する(「人間が検討すべき事項」参照)。
  8. 数値フォーマット 0.0%: 0.0-1.0 を % 表示するためセル書式を 0.0%;[Red]△ 0.0%;"-" にする。生の小数値を書き込んでいるため、ユーザーが手動で変更できる値は書式だけで、条件付き書式の閾値は小数の生値(1.0 = 100%)で比較する。
  9. 同一メンバー × 同一月の複数 PJ 行は合算する。例: 山田PJ-A=0.5PJ-B=0.7 を同月に持つ場合、ヒートマップのセル値は 1.2(= 120%)になる。PJ 別内訳を表示しない設計は意図的(過負荷の総合判定が目的)。PJ 別内訳が必要な場合は将来追加案件で扱う(「人間が検討すべき事項」参照)。

エッジケース

条件表示値 / 動作理由
70_bud_resource にデータ行がない(または有効フラグ=FALSE のみ)69_rsc_heatmap にヘッダー行(要員名 + 月列)のみ出力して正常終了。条件付き書式もスキップNPE・空 setRanges エラー防止。dataRows.length === 0 で 2 段目以降の書き込みをスキップ
稼働率が数値でない(文字列混入・空欄)Utils.parseAmt()0 として扱い、合算に 0 加算データ品質保護。parseAmttypeof val === 'number' 判定で既存の小数値は保持、空欄・非数値は 0 に
Utils.parseDateToYm()"" を返す(年月不正)そのレコードを集計から除外 (!ym チェック)不正データのマップ混入防止
対象年月 が表示期間外 (targetMonths に含まれない)そのレコードを集計から除外 (indexOf === -1 チェック)列の膨張防止。古い過去月や遠い将来月のデータは捨てる
要員名が空文字 (.trim() === '')そのレコードを集計から除外ヒートマップ行が空名で埋まるのを防止
同一メンバー × 同一月の複数 PJ 行合算して 1 セルに 0.5 + 0.7 = 1.2 のように格納。閾値超で着色過負荷の総合判定が本案件の目的
合算稼働率が 1.0(100%)ぴったりwhenNumberGreaterThan(threshSafe=1.0)不等号のため着色されない(白)「ちょうど 100%」は理想配分として許容。過負荷のみ検出する仕様
合算稼働率が 1.2(120%)ぴったりwhenNumberGreaterThan(threshWarn=1.2) は不等号のため赤着色されない(黄のまま)黄と赤の境界は「Warning 閾値を越えた瞬間から赤」を厳密適用
合算稼働率が 0(アサイン無し月)セル値 0 を書き込み。書式 "-" により - で表示(0.0%;[Red]△ 0.0%;"-" の third part が適用)空き月を明示。null/空白より視認性が高い
Constants.getParam() が未設定で defaultVal を返すデフォルト pastMonths=6, futureMonths=12, threshSafe=1.0, threshWarn=1.2 で動作初回パラメータ未設定でも実用上動作する
69_rsc_heatmap タブが既に存在するsheet.clear() + clearConditionalFormatRules() してから再描画冪等性確保。前回の余分な行・列・書式も deleteRows/deleteColumns で物理削除
RSC_HEAT システムキーが 01_sys_config 未登録Utils.getSheetNameByKey が未定義を返し、フォールバック '69_rsc_heatmap' でタブ名を解決。存在しなければ ss.insertSheet(name) で新設setupAllSchemas() 未実行でも動作するよう保険を掛ける
70_bud_resource が未作成(シート自体が存在しない)ResourceRepository._getSheet()27_bud_resource フォールバックも試し、最終的に null を返す。readSheetAsDtos_{ headers: [], dtos: [] } を返すため、ヘッダーのみのヒートマップが出力されるNPE 防止。サンドボックス環境でも落ちない

実データ検証

70_bud_resource の実スキーマ確認(Phase 1 完了事項)

確認項目確認結果情報源
DDL ヘッダー定義["有効フラグ","要員名","PJ・案件名","対象年月","稼働率(%)"](5 列)100_config/101_sys_config.js:837
メンバー名列要員名(列 B)同上
期間列対象年月(列 D)単一月終了年月 は存在しない同上。docs/dev/dev_S-33_workforce_costing.md:116 も同定義を記載
稼働率列稼働率(%)(列 E)、値域 0.0〜1.0 の小数DDL validations: { "稼働率(%)": { type: 'range', min: 0, max: 1 } }
ID 列列 C(= PJ・案件名 の前に ID 列がない DDL 定義)。※PJ・案件名 列の後に 300_ui/301_ui_assist.js:120smartAddRowRSC_NNNN を自動採番するロジックあり。DDL ヘッダーに ID 列は明示されていない(列 C に入る可能性あり・要 MCP 確認)002_constants.js:105 { pattern: '70_bud_resource', prefix: 'RSC_', digit: 4, isDate: false }
有効フラグ列列 A(全予算系シート共通)。row[0] で判定同 DDL 定義 + 420_project_profitability.js:78
SHEET_DEFAULTS_dynamic.開始年月DDL ヘッダー 対象年月 との齟齬あり(002_constants.js:78'開始年月': 'nextYm' と書かれているが実ヘッダーは 対象年月)。本案件では使用しない(smartAddRow の自動デフォルト値用。実データの列名は DDL 定義の 対象年月 に従う)docs/dev/dev_S-33_workforce_costing.md:110 でも同齟齬を既知として記載済

実装前に MCP 実行すべき確認項目

  1. 70_bud_resource シートの実際の 1 行目を取得し、DDL 定義と一致するか照合する(DDL 未実行環境で旧列名が残留していないか)。
  2. 01_sys_configsystemkey_gas 列に BUD_RSCERSC_HEAT が登録されているか確認する(RSC_HEAT は本案件で新規追加)。
  3. 03_sys_params の既存キー一覧を取得し、HEATMAP_* で始まるキーの重複がないことを確認する(本案件で新規キー 4 つを追加: HEATMAP_PAST_MONTHS / HEATMAP_FUTURE_MONTHS / HEATMAP_THRESHOLD_SAFE / HEATMAP_THRESHOLD_WARNING)。

03_sys_params 新規パラメータ(本案件で追加)

キーデフォルト値用途
HEATMAP_PAST_MONTHS6基準月から過去何ヶ月を表示するか
HEATMAP_FUTURE_MONTHS12基準月から将来何ヶ月を表示するか
HEATMAP_THRESHOLD_SAFE1.0黄色着色の下限閾値(これを超えると黄色)
HEATMAP_THRESHOLD_WARNING1.2赤着色の下限閾値(これを超えると赤)

Constants.getParam()defaultVal 引数の型に応じて Number() / String() 変換するため、数値で取得される(本コードでは Number() で明示変換)。

関連ドキュメント

仕様書関連箇所
CLAUDE.md変更時の動作確認テスト: 600_report/6*_datamart_*.jsマート更新テスト。コーディング規約(ヘッダー名ベース列参照・有効フラグ FALSE 行スキップ)
docs/dev/dev_S-33_workforce_costing.md70_bud_resource の「稼働率 (%)」→「工数 (h)」置換案件。S-33 実施後は本仕様の DTO['稼働率(%)'] 参照を調整する必要がある(依存関係: S-33 が本案件より先に入った場合、集計ロジックを「工数 / 基準工数 (例 160h)」で比率化するよう修正する)
docs/dev/dev_F-03_kpi_dashboard.md動的生成タブ・条件付き書式・Constants.getParam の先例
docs/dev/dev_F-12_headcount_simulation.mdMENU_DEFINITION への項目追加パターン・ss.insertSheet() の動的タブ生成パターン
docs/_internal/failure_patterns.md #18-#20仕様書記述時の「Read で実在コードを確認」原則(メニュー名・定数名・シート名)
docs/_internal/failure_patterns.md #21-#23getLastColumn() 禁止、全角スペース対策、年月文字列の apostrophe 前置
docs/_internal/TODO_future.md §3.2 F-15案件定義。期待される効果「リソース配分の最適化」、検討事項「稼働率の目標値(100% or 80%を標準とするか)」

CLAUDE.md の「DDL (setupAllSchemas) で管理されないタブ」リスト(CLAUDE.md 末尾近辺)に 69_rsc_heatmap を追記する必要がある。現在の記載は「03_sys_params, 75_ss_equity_changes, 76_notes, 77_pj_raw, 78_pj_pl, 91_fs_bs, 92_fs_pl, 93_kpi_dashboard, 90_test_results」(参考列挙。実際の記載は CLAUDE.md を Read して書き換えること)。

人間が検討すべき事項

  1. 稼働率の目標値(100% or 80% を標準とするか) — TODO_future.md 原文。
    • 100% 基準: HEATMAP_THRESHOLD_SAFE=1.0, HEATMAP_THRESHOLD_WARNING=1.2(本仕様書のデフォルト)。「100% を正常、120% 超は過負荷」。
    • 80% 基準: HEATMAP_THRESHOLD_SAFE=0.8, HEATMAP_THRESHOLD_WARNING=1.0。「80% を理想、100% 超は黄、120% 超は赤」のように 2 段スライド可能(ただし実装では HEATMAP_THRESHOLD_WARNING を 1.0 にするとほぼ全月が赤になるため、黄+赤境界を別途調整)。
    • 推奨初期値: 100% 基準で運用開始し、1-2 ヶ月運用後に「余力可視化が弱い」と感じたら 80% 基準に切り替える(パラメータ変更のみで済む)。
  2. HEATMAP_PAST_MONTHS / HEATMAP_FUTURE_MONTHS のデフォルト値 — 本仕様書は 6/12 を採用。「過去半年 + 将来 1 年」は四半期/半期の採用計画レビューに合う粒度。採用計画の粒度が四半期主体であれば 3/9 に縮小することも検討。
  3. PJ 別内訳の表示有無 — 本仕様書は「メンバー × 月」のみ。PJ 別分解(例: 山田/PJ-A, 山田/PJ-B を別行で表示)の要望があれば、別案件(F-15 Phase 2 相当)として切り出す。現状の合算表示は「誰が過負荷か」の判定に特化しており、誰が何に時間を使っているかの可視化には追加実装が必要。
  4. 69_rsc_heatmap を DDL 管理タブにするか、動的生成タブにするか — 本仕様書は 動的生成93_kpi_dashboard パターン)を採用。理由は (a) 月列が HEATMAP_PAST_MONTHS / HEATMAP_FUTURE_MONTHS の設定で可変のため DDL 固定ヘッダーに馴染まない、(b) メンバー数が変動する。固定ヘッダーで管理したい場合は setupAllSchemas()schemas マップに追加する(その場合は月列が固定範囲、かつメンバー行のフィルタで絞り込む設計に変更)。
  5. 420_project_profitability.jsResourceRepository — 本案件では 420 は変更しない。将来的に全 Repository で DTO 経由の読み取りに統一するなら、別リファクタ案件として切り出す。S-33(工数入力化)と合わせて実施すると効率的。
  6. buildBudgetTrendDataMart() との連携有無 — 本仕様書は独立メニューから手動実行のみ。マート更新 (buildBudgetTrendDataMart) 末尾から自動呼び出しする設計も可能だが、ヒートマップは「リソース計画の変更時にレビューする」用途のため頻度が低く、自動連鎖させるとマート更新時間を延ばす。独立呼び出しを推奨。

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

あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 F-15「リソース稼働率のヒートマップ可視化」を実装してください。
大規模実装のため以下の 4 Step に分けて実施してください。

## 実行前タスク(各ファイルで確認すべきポイントを明記)

**Grep は「どこにあるか」の発見まで。型・固有名詞・呼び出し経路の確認は必ず Read で行う(failure_patterns.md #18-#20 対策)。**

- `000_infra/002_constants.js`:
  - `SHEET_DEFAULTS` の `70_bud_resource` エントリ (L78) — `{ pattern, prefix, defaults }` 構造と prefix `RSC_` を確認
  - `ID_PREFIX_MAP` の `70_bud_resource` エントリ (L105) — `digit: 4, isDate: false`
  - `Constants.getParam(key, defaultVal)` (L147) — 03_sys_params からの読み込み仕様、defaultVal 型で Number/String 変換
  - `MENU_DEFINITION` の `📋 サイドバー: 📊 マート更新` カテゴリ (L230-239) — このカテゴリの items 末尾にメニュー 1 項目を追加する
- `000_infra/003_contracts.js`:
  - `BudgetDTO` の `@typedef` 形式を Read — `ResourceDTO` の雛形として使用
  - `Contracts.toDto` ファクトリ — ヘッダー配列 + 行配列から DTO を生成
- `000_infra/004_utils.js`:
  - `Utils.parseDateToYm(val)` (L92) — Date 型・"YYYY-MM"・"YYYY/MM"・"YYYY年MM月" 対応、パース不可は `""`
  - `Utils.addMonths(ymStr, months)` (L127) — "YYYY-MM" の月加減算
  - `Utils.parseAmt(val)` (L191) — 非数値は 0
  - `Utils.getSheetByKey(key, fallbackName)` (L40) — `Utils.getSheetNameByKey` → `ss.getSheetByName(fallback)` の順
- `200_data/202_repository.js`:
  - `readSheetAsDtos_` (L19) — 内部ヘルパー `{ headers, dtos }` を返す
  - `OrderRepository` (L107-146) — `_getSheet` / `findAll` / `save` / `append` の雛形として完全踏襲(本案件は `findAll` のみ実装)
- `100_config/101_sys_config.js`:
  - `setupAllSchemas()` (L749-) 内の `confSheet.appendRow` ブロック (L770-824) — ここに `RSC_HEAT` システムキー登録行を追加
  - `schemas` マップ (L826-) の `'BUD_RSCE'` エントリ (L837) — DDL ヘッダー `["有効フラグ","要員名","PJ・案件名","対象年月","稼働率(%)"]` を確認。**`69_rsc_heatmap` はこの schemas マップには追加しない**(動的生成タブ扱い)
  - `onOpen()` (L323) — `Constants.MENU_DEFINITION` を走査する実装。**`onOpen` 本体は編集不要**。メニュー追加は `000_infra/002_constants.js` の `MENU_DEFINITION` のみ修正
- `400_domain/420_project_profitability.js`:
  - L73-86 の `70_bud_resource` 既存読み込みロジック(有効フラグ判定 + 列インデックス取得パターン)を参考にする
- `600_report/609_datamart_kpi.js`:
  - L21-53 の `buildKpiDashboard()` — 動的生成タブの `clear + clearConditionalFormatRules + 再描画` パターン
  - L476-522 の `applyKpiConditionalFormat_` — `SpreadsheetApp.newConditionalFormatRule()` の連鎖記法・`setConditionalFormatRules()` 一括適用
- `600_report/606_datamart_daily_cf.js`:
  - L178-184 — `whenNumberLessThan` での条件付き書式。`setRanges([sheet.getRange(...)])` のパターン
- MCP で以下を確認する:
  1. `70_bud_resource` の実ヘッダー(`対象年月` が単一月であること・`終了年月` が存在しないこと)
  2. `01_sys_config` の既存システムキー(`RSC_HEAT` の重複がないこと)
  3. `03_sys_params` の既存キー一覧(`HEATMAP_*` キーの重複がないこと)

## 修正対象ファイル

- `000_infra/003_contracts.js`(`ResourceDTO` の `@typedef` 追記)
- `000_infra/002_constants.js`(`MENU_DEFINITION` `📋 サイドバー: 📊 マート更新` カテゴリに 1 項目追加)
- `200_data/202_repository.js`(`ResourceRepository` 新設・末尾追記)
- `100_config/101_sys_config.js`(`setupAllSchemas()` の `appendRow` ブロックに `RSC_HEAT` 登録)
- `600_report/610_datamart_resource.js`(新規作成)
- `CLAUDE.md`(「DDL (setupAllSchemas) で管理されないタブ」リストに `69_rsc_heatmap` 追記)

## Step 1: ResourceDTO の追加

`000_infra/003_contracts.js` の末尾(`BudgetDTO` の `@typedef` の直後、または `PartnerDTO` のさらに下の適切な位置)に追記する。
フィールド名は MCP で確認した `70_bud_resource` の実ヘッダー名を使用すること(推測・造語禁止):

```js
/**
 * 70_bud_resource — 要員稼働率(PJ別・月次)
 * @typedef {Object} ResourceDTO
 * @property {boolean}     有効フラグ
 * @property {string}      要員名
 * @property {string}      PJ・案件名
 * @property {Date|string} 対象年月
 * @property {number}      稼働率(%)          - 0.0〜1.0 の小数 (1.0 = 100%)
 */
```

## Step 2: ResourceRepository の追加

`200_data/202_repository.js` の末尾に追記する。`OrderRepository` パターンを完全に踏襲する:

```js
// =====================================================================
// ResourceRepository — 70_bud_resource (読み取り専用)
// =====================================================================

var ResourceRepository = {

  /** @private */
  _getSheet: function() {
    var sheet = Utils.getSheetByKey('BUD_RSCE', '70_bud_resource');
    if (sheet) return sheet;
    return SpreadsheetApp.getActiveSpreadsheet().getSheetByName('27_bud_resource');
  },

  /**
   * 全要員稼働率レコードを DTO 配列で取得する。
   * @returns {{ headers: string[], dtos: ResourceDTO[] }}
   */
  findAll: function() {
    return readSheetAsDtos_(ResourceRepository._getSheet());
  },
};
```

本案件は読み取り専用のため `save()` / `append()` は実装しない。

## Step 3-A: setupAllSchemas() のシステムキー登録

`100_config/101_sys_config.js` の `setupAllSchemas()` 内、60 番台レポートタブの `appendRow` ブロック(`PL_VAR` 登録 L795 付近)の近くに 1 行追加する:

```js
if (!existKeys.includes('RSC_HEAT')) confSheet.appendRow(['RSC_HEAT', '', '69_rsc_heatmap', 'リソース稼働率ヒートマップ']);
```

**`schemas` マップには追加しない**(動的生成タブ扱い。`93_kpi_dashboard` と同じパターン)。

## Step 3-B: MENU_DEFINITION への項目追加

`000_infra/002_constants.js` の `MENU_DEFINITION` 配列、`category: '📋 サイドバー: 📊 マート更新'` のブロックの `items` 末尾に 1 項目追加する:

```js
{ label: '🔥 リソース稼働率ヒートマップ', funcName: 'buildResourceHeatmap', description: '69 タブにメンバー × 月の稼働率ヒートマップを再生成' },
```

`onOpen()` は編集不要(`MENU_DEFINITION` を走査する実装のため自動反映)。

## Step 4: buildResourceHeatmap() の実装

`600_report/610_datamart_resource.js` を新規作成する。実装の骨格は仕様書 §修正方針 Step 4 を参照:

```js
function buildResourceHeatmap() {
  var FUNC = 'buildResourceHeatmap';
  try {
    Utils.logInfo(FUNC, '処理開始');
    var ss = getWebSpreadsheet_();
    var sheetName = Utils.getSheetNameByKey('RSC_HEAT') || '69_rsc_heatmap';
    var sheet = ss.getSheetByName(sheetName) || ss.insertSheet(sheetName);
    sheet.clear();
    sheet.clearConditionalFormatRules();

    var pastMonths   = Number(Constants.getParam('HEATMAP_PAST_MONTHS', 6));
    var futureMonths = Number(Constants.getParam('HEATMAP_FUTURE_MONTHS', 12));
    var threshSafe   = Number(Constants.getParam('HEATMAP_THRESHOLD_SAFE', 1.0));
    var threshWarn   = Number(Constants.getParam('HEATMAP_THRESHOLD_WARNING', 1.2));

    var baseYm = Utilities.formatDate(new Date(), 'JST', 'yyyy-MM');
    var targetMonths = [];
    for (var i = -pastMonths; i <= futureMonths; i++) {
      targetMonths.push(Utils.addMonths(baseYm, i));
    }

    var result = ResourceRepository.findAll();
    var rateMap = {}, memberSet = {};
    result.dtos.forEach(function(dto) {
      var flag = dto['有効フラグ'];
      if (flag === false || String(flag).toUpperCase() === 'FALSE') return;
      var name = String(dto['要員名'] || '').trim();
      if (!name) return;
      var ym = Utils.parseDateToYm(dto['対象年月']);
      if (!ym || targetMonths.indexOf(ym) === -1) return;
      var rate = Utils.parseAmt(dto['稼働率(%)']);
      if (!rateMap[name]) { rateMap[name] = {}; memberSet[name] = true; }
      rateMap[name][ym] = (rateMap[name][ym] || 0) + rate;
    });

    var memberNames = Object.keys(memberSet).sort();
    var headerRow = ['要員名'].concat(targetMonths);
    var dataRows = memberNames.map(function(name) {
      var row = [name];
      targetMonths.forEach(function(ym) { row.push(rateMap[name][ym] || 0); });
      return row;
    });
    var outValues = [headerRow].concat(dataRows);

    var rows = outValues.length, cols = headerRow.length;
    // 月ラベル列を事前にテキスト形式に (failure_patterns #23 対策)
    sheet.getRange(1, 2, 1, targetMonths.length).setNumberFormat('@');
    sheet.getRange(1, 1, rows, cols).setValues(outValues);
    sheet.getRange(1, 1, 1, cols).setBackground('#434343').setFontColor('#FFFFFF').setFontWeight('bold');
    sheet.setFrozenRows(1);
    sheet.setFrozenColumns(1);
    if (dataRows.length > 0) {
      sheet.getRange(2, 2, dataRows.length, targetMonths.length)
        .setNumberFormat('0.0%;[Red]△ 0.0%;"-"');
    }
    sheet.getDataRange().setFontFamily('BIZ UDGothic');
    sheet.autoResizeColumns(1, 1);
    sheet.setColumnWidths(2, targetMonths.length, 80);
    if (sheet.getMaxColumns() > cols) sheet.deleteColumns(cols + 1, sheet.getMaxColumns() - cols);
    if (sheet.getMaxRows() > rows) sheet.deleteRows(rows + 1, sheet.getMaxRows() - rows);
    var ef = sheet.getFilter(); if (ef) ef.remove();
    if (rows > 1) sheet.getRange(1, 1, rows, cols).createFilter();

    if (dataRows.length > 0) {
      var dataRange = sheet.getRange(2, 2, dataRows.length, targetMonths.length);
      var rules = [
        SpreadsheetApp.newConditionalFormatRule()
          .whenNumberGreaterThan(threshWarn)
          .setBackground('#F4CCCC').setFontColor('#CC0000').setBold(true)
          .setRanges([dataRange]).build(),
        SpreadsheetApp.newConditionalFormatRule()
          .whenNumberGreaterThan(threshSafe)
          .setBackground('#FCE5CD').setFontColor('#B45F06')
          .setRanges([dataRange]).build(),
      ];
      sheet.setConditionalFormatRules(rules);
    }
    Utils.logInfo(FUNC, sheetName + ' 再描画完了 (メンバー=' + memberNames.length + ', 月=' + targetMonths.length + ')');
  } catch (e) {
    Utils.logError(FUNC, e);
    throw e;
  }
}
```

## Step 5: CLAUDE.md 追記

CLAUDE.md 末尾近辺の「## DDL (setupAllSchemas) で管理されないタブ」セクションに `69_rsc_heatmap` を追加する(既存の列挙順に合わせて挿入):

```
動的に生成・上書きされるタブ:
03_sys_params, 69_rsc_heatmap, 75_ss_equity_changes, 76_notes,
77_pj_raw, 78_pj_pl, 91_fs_bs, 92_fs_pl, 93_kpi_dashboard, 90_test_results
```

## 制約

- 列参照はヘッダー名ベース(`DTO['要員名']` / `indexOf` 等)。列番号ハードコード禁止(CLAUDE.md 規約)
- 有効フラグ列が `FALSE` の行は集計から除外(`row[0] === false || String(row[0]).toUpperCase() === 'FALSE'` の 2 条件判定)
- `getLastColumn()` での動的列範囲取得禁止(月列は `pastMonths + 1 + futureMonths` から算出)
- ラベル文字列の正規化は `.replace(/[\s ]+/g, '')` で全角スペースも除去
- 月ラベル(`targetMonths`)をヘッダー行に書く前にその範囲を `setNumberFormat('@')` でテキスト型に固定(`-` 記号を減算演算子として誤解釈されるのを防ぐ)
- 既存の `601〜609_datamart_*.js` / `420_project_profitability.js` は変更しない
- `MENU_DEFINITION` / `setupAllSchemas()` / `onOpen()` への追記は必ず Read で実在コードを確認してから行う(記憶・推測禁止)

## エッジケース

仕様書 §エッジケース テーブル全件を実装でカバーすること(データ 0 件・不正値・表示期間外・合算 >100% 等)。

## 実データ検証

実装前に MCP で以下を確認する:
- `70_bud_resource` の実ヘッダー列名(ResourceDTO フィールド確定、`終了年月` が無いことの再確認)
- `01_sys_config` のシステムキー(`RSC_HEAT` 重複なし)
- `03_sys_params` の既存キー一覧(`HEATMAP_*` 重複なし)

## 動作確認

1. `npm run push:dev` でデプロイする
2. GAS エディタで `setupAllSchemas()` を実行し、`01_sys_config` に `RSC_HEAT` / `69_rsc_heatmap` / `リソース稼働率ヒートマップ` が登録されることを確認
3. スプレッドシートを再読み込みし、`📋 サイドバー: 📊 マート更新` サブメニュー(または対応する統合サイドバー)に「🔥 リソース稼働率ヒートマップ」が出現することを確認
4. GAS エディタで `buildResourceHeatmap()` を直接実行する
5. `69_rsc_heatmap` シートが生成(またはクリア&再作成)され、列 A=要員名、列 B 以降=月列、データセル=合算稼働率(0.0〜1.0+ の小数、書式は `0.0%`)になっていること
6. `03_sys_params` に `HEATMAP_PAST_MONTHS`=6 / `HEATMAP_FUTURE_MONTHS`=12 / `HEATMAP_THRESHOLD_SAFE`=1.0 / `HEATMAP_THRESHOLD_WARNING`=1.2 を追加し、再実行後に値に応じた着色となることを確認
7. `70_bud_resource` に同一メンバー × 同一月の複数 PJ 行を追加し(例: `山田` × `PJ-A` × `2026-05` × `0.5` と `山田` × `PJ-B` × `2026-05` × `0.7`)、セル値が合算値 1.2 になり **黄色着色**されることを確認
8. さらに稼働率 1.3 のレコードを追加し、該当セルが **赤+太字着色**されることを確認
9. `70_bud_resource` のデータ行をすべて論理削除(有効フラグ=FALSE)した状態で実行し、`69_rsc_heatmap` がヘッダー行のみ出力され、条件付き書式がセットされない(エラーにならない)ことを確認
10. 表示期間外のレコード(例: `2020-01` のデータ)が `69_rsc_heatmap` に出現しないことを確認

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

| フェーズ | 拡張思考 | 備考 |
|---------|---------|------|
| Step 1: ResourceDTO 追加 | なし | DDL 確認済みフィールドの転記 |
| Step 2: ResourceRepository 追加 | なし | OrderRepository パターンの完全踏襲 |
| Step 3-A: setupAllSchemas に 1 行追加 | なし | 挿入位置は同族 `PL_VAR` 近傍 |
| Step 3-B: MENU_DEFINITION に 1 項目追加 | なし | カテゴリと items 位置は事前確定 |
| Step 4: buildResourceHeatmap() 実装 | あり | 月列生成・合算集計・条件付き書式の 3 ブロックの相互接続 |
| Step 5: CLAUDE.md 追記 | なし | 既存リストへの 1 語追加 |

推奨実行モデル

工程推奨モデル理由
仕様書作成(本ドキュメント)Claude Opus 4.6実データと SHEET_DEFAULTS の齟齬(終了年月 列の不在)を発見し、開始年月/終了年月 前提の task プロンプトを再設計する判断が必要
Step 1: ResourceDTO 追加Claude Haiku 4.5DDL 確認済みフィールドの転記のみ。判断要素なし
Step 2: ResourceRepository 追加Claude Haiku 4.5OrderRepository パターンの完全踏襲。判断要素なし
Step 3-A: setupAllSchemas 追加Claude Haiku 4.51 行挿入。PL_VAR 近傍に配置するだけ
Step 3-B: MENU_DEFINITION 追加Claude Haiku 4.51 項目追加。カテゴリと位置は仕様書で確定済み
Step 4: buildResourceHeatmap() 実装Claude Sonnet 4.6月列生成・合算・条件付き書式の組合せ。既存パターンの応用だが書式化・エッジケースの判断が必要
Step 5: CLAUDE.md 追記Claude Haiku 4.5既存リストに 1 語追加

変更履歴

日付変更内容
2026-04-21初版作成。70_bud_resource は 1 行 1 月の 対象年月 スキーマ(終了年月 不存在)であることを Phase 1 で確認し、期間展開不要の単純合算ロジックで設計。動的生成タブ(93_kpi_dashboard パターン)として 69_rsc_heatmap を新設。ResourceDTO / ResourceRepository(読み取り専用)・MENU_DEFINITION 追記・600_report/610_datamart_resource.js 新規の 5 ファイル構成。閾値は 03_sys_paramsHEATMAP_* 4 キーで外部設定化(デフォルト: 過去 6 ヶ月・将来 12 ヶ月・黄 100%・赤 120%)。failure_patterns #18-#23 の直接対策(Read による固有名詞確認・getLastColumn 禁止・全角スペース正規化・setNumberFormat('@') による年月文字列の保護)を注意事項に明記

仕様書作成プロンプト

展開して表示
【タイムアウト回避・実行原則(v1.7・必ず遵守すること)】
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 の内容を Phase 1 で完全確定させてから書き出しに移る。

======================================================================
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者兼仕様書ライターです。
案件 F-15「リソース稼働率のヒートマップ可視化」の開発仕様書を作成してください。
作成後は `docs/_config.json` の `nav` 配列の §E.5(FP&A・レポーティング)セクションに必ず追記してください。

---

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

以下のファイルをすべて Read し、Phase 2 で使う固有名詞・型・行番号を確定させる。
**Grep は「どこにあるか」の発見まで。型・固有名詞・呼び出し経路の確認は必ず Read で行う。推測で仕様書に固有名詞を書かない(failure_patterns.md #18–#20 の直接対策)。**

### 1-A: 案件定義の読み込み
- `docs/_internal/TODO_future.md` — F-15 の案件名・概要・期待される効果・人間が検討すべき事項を取得する

### 1-B: プロジェクト規約の読み込み
- `CLAUDE.md` — ファイル番号体系・コーディング規約(特に「データアクセス」節)を確認する

### 1-C: 既存仕様書テンプレートの読み込み
- `docs/dev/` 配下の新機能(FP&A・レポート)系仕様書を 1 件 Read しフォーマットを把握する(例: `dev_F-01_variance_analysis.md`)

### 1-D: 関連 GAS コードの調査(Read 必須・各ファイルで確認ポイントを明記)

1. **`000_infra/002_constants.js`**:
   - `SHEET_DEFAULTS` 内 `pattern: '70_bud_resource'` エントリを Read し、定義されている**実際の列名**(既知: `稼働率(%)`, `開始年月`)を記録する。`終了年月` 列の有無は SHEET_DEFAULTS に記載がないため、**1-D-5 の DDL 定義で必ず確認する(未確認のまま仕様書に書かない)**。
   - `ID_PREFIX_MAP` の `70_bud_resource` エントリ(既知: `prefix: 'RSC_'`, `isDate: false`)を確認する。
   - `Constants.getParam(key, defaultVal)` の仕様を確認する — `03_sys_params` シートからパラメータを読み込むヘルパー。`HEATMAP_PAST_MONTHS` 等の取得に使用する。

2. **`000_infra/003_contracts.js`**:
   - `BudgetDTO` の定義形式(`@typedef` フィールド列挙スタイル)と `Contracts.toDto` / `toRow` ファクトリの使い方を Read し、`ResourceDTO` 追加の雛形を把握する。

3. **`000_infra/004_utils.js`**:
   - `Utils.parseDateToYm(val)` — 対応フォーマット(Date 型・"YYYY-MM"・"YYYY/MM"・"YYYY年MM月")と戻り値(パース不可 → `""`)を確認する。
   - `Utils.addMonths(ymStr, months)` — "YYYY-MM" に月数を加減算する仕様を確認する。
   - `Utils.parseAmt(val)` — 数値変換の仕様(非数値 → 0)を確認する。

4. **`200_data/202_repository.js`**:
   - `readSheetAsDtos_`, `appendDtosToSheet_` の内部ヘルパー仕様を Read する。
   - `OrderRepository` の全コードを Read し、`_getSheet()` での `Utils.getSheetByKey(sysKey, fallbackName)` の呼び出し形式を把握する。`ResourceRepository` を同一パターンで設計するため、**sysKey 文字列は次の 1-D-5 で確認する**。

5. **`100_config/101_sys_config.js`**(最重要・必ず Read する):
   - `Utils.getSheetByKey()` に渡すシステムキーの登録箇所を Read し、`70_bud_resource` に対応するシステムキー文字列(`'BUD_RSC'` または類似)を確認する。**未確認のまま仕様書に書かない**。
   - `setupAllSchemas()` の実装を Read し、既存シートスキーマの DDL 登録形式(列定義・書式・バリデーション)を把握する。`69_rsc_heatmap` を DDL 管理シートとして追加するか、`buildResourceHeatmap()` が毎回再作成する動的生成シートとするかを設計判断するための情報を得る(参考: `93_kpi_dashboard` は DDL 非管理・動的生成として CLAUDE.md に明記)。
   - `onOpen()` の `ui.createMenu` を Read し、**実在するメニュー名と階層構造**を把握する(造語禁止・failure_patterns.md #20 の直接対策)。

6. **`600_report/` 配下の代表的なファイル**(例: `600_report/601_datamart_ingest.js`):
   - 既存データマートモジュールの関数命名規則・出力シートへの直接書き込みパターン・`ConditionalFormatRuleBuilder` の使用例(あれば)を Read し、`610_datamart_resource.js` の設計に反映する。

### 1-E: 実データ検証(MCP で実態確認)
- **`70_bud_resource` シートの実際のヘッダー列名**を MCP で取得し、DDL 定義・`SHEET_DEFAULTS` と照合する。
  - 必須確認項目: メンバー名列(列名を正確に特定)・`開始年月`・`終了年月`(存在するか)・`稼働率(%)`・ID 列名(RSC_NNNN 形式)・有効フラグ列(存在するか)
  - この結果を元に `ResourceDTO` の `@typedef` フィールド名を確定する。
- **`03_sys_params` シートの既存キー一覧**を MCP で取得し、`HEATMAP_*` キーの重複がないことを確認する。

---

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

出力先: `docs/dev/dev_F-15_resource_heatmap.md`
**絶対に 1 回のツール呼び出しで全内容を出力しない。以下の Step に分割して実行すること。**

### Step 2-1: 骨格の作成(File Write, ~20行)
(見出しのみ Write)

### Step 2-2: 前半セクションの追記(概要〜注意事項、~300行)

### Step 2-3a: エッジケース〜人間検討事項の追記(~200行)

### Step 2-3b: 実装プロンプト〜変更履歴の追記(~250行)

### Step 2-4: 仕様書作成プロンプトの記録(`<details>` ブロックに追記)

---

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

### 3-A: `docs/_config.json` へのナビゲーション登録(必須)
`docs/_config.json` の §E.5(FP&A・レポーティング)セクションに追記する。

### 3-B: `docs/_internal/changelog.md` への追記

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

(プロンプト本文は要約版。完全版は tasks/prompts/task_F-15.md.done を参照)


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

ℹ️ 別ドラフト存在: 本仕様には実装アプローチの異なる別版(予算シート BUD_RSCE 駆動 + ステップ分割実装プロンプト型・638行)が origin/docs/F-15-resource-heatmap-spec ブランチに保全されている。 本ファイルは概要記述のより詳細な 754行版を採用。実装時に両版を突合すること。