概要

項目内容
案件IDMAS-105
カテゴリPJ管理
PhaseP2
優先度★★
所要時間4-5時間
実装ステータス📝 仕様書段階・実装未着手 (2026-04-28 監査時点)
対象ファイル000_infra/002_constants.jsSHEET_DEFAULTS 変更)
200_data/202_repository.jsResourceRepository / HeadcountRepository 新設)
100_config/101_sys_config.jsBUD_RSCE DDL ヘッダー変更)
400_domain/420_project_profitability.js(労務費算出ロジックの置換)
templates/operations_sidebar.html(操作パネルに工数入力ガイドを追加・任意)
前提案件MAS-006(PJ別管理会計の共通費配賦・実装済み)
後続案件MAS-026(TDABC:時間主導型ABC)・MAS-027(M365監査ログ連携)

目的

現行の PJ 別損益(78_pj_pl / 79_pj_monthly)は 27_bud_resource70_bud_resource)の 稼働率 (%)22_bud_headcount の月額給与+社保会社負担分を掛けた「概算配賦」で労務費を算出している(400_domain/420_project_profitability.js L73-86)。これでは「8月に 80% の稼働」「9月に 50% の稼働」といった比率表現しか扱えず、以下の管理会計上の要請を満たせない。

  1. 時間単価(キャパシティレート)の可視化 — 時給換算の原価が経営会議で説明できない。
  2. MAS-026(TDABC)の前提データ — 「時間単価 × PJ別消費時間」による間接費配賦には実工数が必要。
  3. 「時間を食って赤字の PJ」の検出 — 売上 A ランクでも工数を消費している PJ を PJ 営業利益で負にマッピングできない。

本案件では、70_bud_resource「稼働率 (%)」から「工数 (h)」入力 に切り替え、22_bud_headcount の月額給与+社保料率から算出する 時間単価 × PJ 別工数 で PJ 別人件費を算出して 78_pj_pl に反映する。稼働率ベースの「ざっくり按分」から実工数ベースに進化させることで、MAS-026 / MAS-027 / MAS-039(ABC分析)の土台を築く。

現在のコード

1. 000_infra/002_constants.js L78(SHEET_DEFAULTS70_bud_resource エントリ)

{ pattern: '70_bud_resource', prefix: 'RSC_', defaults: { '稼働率(%)': 1.0, _dynamic: { '開始年月': 'nextYm' } } },
  • 稼働率(%) を 1.0(100%)で自動補完。smartAddRow / bulkAssignDefaults が新規行に適用。
  • _dynamic.開始年月: nextYm(= 翌月 YYYY-MM)を自動設定。

2. 100_config/101_sys_config.js L648(BUD_RSCE DDL ヘッダー)

'BUD_RSCE': { headers: ["有効フラグ","要員名","PJ・案件名","対象年月","稼働率(%)"], color: "#e69138" },

3. 000_infra/002_constants.js L76(SHEET_DEFAULTS22_bud_headcount エントリの社保料率)

{ pattern: '22_bud_headcount', prefix: 'EMP_',
  defaults: { ..., '健保料率': 0.05, '介護保険料率': 0, '厚年料率': 0.0915, '雇用保険料率': 0.0065, '子ども・子育て拠出金率': 0.0036, ..., '月額給与・報酬': 0, ... } },

社会保険料率は個別フィールドとして独立している(22_bud_headcount の DDL でも 健保料率 / 介護保険料率 / 厚年料率 / 雇用保険料率 / 子ども・子育て拠出金率 の 5 列が定義されている — L661)。一方、420_project_profitability.js社保会社負担率 という合算済みの単一列(社保会社負担率)を読み取るコードになっている(L44)。

4. 400_domain/420_project_profitability.js の現行労務費ロジック

(B) 人員ごとの月額人件費マップ作成 — L40-60

const idxIn = hcData[0].indexOf('開始年月'), idxOut = hcData[0].indexOf('終了年月'),
      idxName = hcData[0].indexOf('氏名・ポジション'), idxSal = hcData[0].indexOf('月額給与・報酬'),
      idxSoc = hcData[0].indexOf('社保会社負担率'), idxType = hcData[0].indexOf('雇用形態');
// ...
const totalLaborCost = sal + (empType !== '業務委託' ? Math.round(sal * soc) : 0);
// hcMap[name][ym] = totalLaborCost  // 月額人件費(=給与+社保)を月別に保持

(C) 労務費(配賦原価)の集計: Resource × Headcount — L73-86

const idxName = rsData[0].indexOf('要員名'), idxPj = rsData[0].indexOf('PJ・案件名'),
      idxYm = rsData[0].indexOf('対象年月'), idxRate = rsData[0].indexOf('稼働率(%)');
// ...
const baseSal = (hcMap[name] && hcMap[name][ym]) ? hcMap[name][ym] : 0;
pjObj[ym].laborCost += Math.round(baseSal * rate);

工数合計(共通費の「工数比」配賦用) — L469-482

if (sheetRsce) {
  var rsData = sheetRsce.getDataRange().getValues();
  var idxRsPj = rsData[0].indexOf('PJ・案件名'), idxRsRate = rsData[0].indexOf('稼働率(%)');
  for (var ri = 1; ri < rsData.length; ri++) {
    // ...
    if (rpj && dstPjs[rpj]) dstWorkload[rpj] = (dstWorkload[rpj] || 0) + rr;
  }
}

ここでも「稼働率」を工数相当として扱っているため、配賦ルール.method === '工数比' でも実質稼働率比と区別がない。

5. 200_data/202_repository.js

ResourceRepository / HeadcountRepository は未実装。420_project_profitability.js が直接 sheet.getDataRange().getValues()indexOf で列を引く旧パターン。新規 Repository は本案件のスコープ内で追加する。

修正方針

3 Step 構成で実装する。

Step 1 — スキーマ変更・Repository 新設

(1-a) 000_infra/002_constants.js L78(SHEET_DEFAULTS70_bud_resource エントリ)

// Before
{ pattern: '70_bud_resource', prefix: 'RSC_', defaults: { '稼働率(%)': 1.0, _dynamic: { '開始年月': 'nextYm' } } },
// After
{ pattern: '70_bud_resource', prefix: 'RSC_', defaults: { '工数(h)': 0, _dynamic: { '対象年月': 'nextYm' } } },
  • 稼働率(%)工数(h) に置き換え(デフォルト 0h)。
  • _dynamic.開始年月_dynamic.対象年月 に変更。70_bud_resource の実シートヘッダーでは列名は 対象年月(DDL: 101_sys_config.js L648)であり、開始年月 は実在しない列名だった(現行の defaults は DDL と齟齬があった)。

(1-b) 100_config/101_sys_config.js L648(BUD_RSCE DDL ヘッダー)

// Before
'BUD_RSCE': { headers: ["有効フラグ","要員名","PJ・案件名","対象年月","稼働率(%)"], color: "#e69138" },
// After
'BUD_RSCE': { headers: ["有効フラグ","要員名","PJ・案件名","対象年月","工数(h)"], color: "#e69138" },

既存データに対しては setupAllSchemas() によるヘッダー上書きが走る。既存の稼働率値は 800_ops/809_migration_resource_hours.js(要新設・本案件スコープ外オプション)で「稼働率 × 160h = 初期工数」として推定値を流し込むか、手動で工数入力をやり直す かを PO 判断で決定する(下記「人間が検討すべき事項」参照)。

(1-c) 200_data/202_repository.js への追加(末尾、AccountRepository.resetCache 関数定義の直後、L351 近辺)

OrderRepository(L107-146)と同一パターン。

// =====================================================================
// ResourceRepository — 70_bud_resource
// =====================================================================
var ResourceRepository = {
  _getSheet: function() { return Utils.getSheetByKey('BUD_RSCE', '70_bud_resource') || Utils.getSheetByKey('BUD_RSCE', '27_bud_resource'); },
  findAll: function() { return readSheetAsDtos_(ResourceRepository._getSheet()); },
  save: function(dtos) {
    var sheet = ResourceRepository._getSheet();
    if (!sheet) return;
    var headers = sheet.getRange(1, 1, 1, sheet.getMaxColumns()).getValues()[0].map(function(h){return String(h).trim();});
    writeDtosToSheet_(sheet, headers, dtos);
  },
  append: function(dtos) {
    var sheet = ResourceRepository._getSheet();
    if (!sheet) return 0;
    var headers = sheet.getRange(1, 1, 1, sheet.getMaxColumns()).getValues()[0].map(function(h){return String(h).trim();});
    return appendDtosToSheet_(sheet, headers, dtos, 1);
  },
};

// =====================================================================
// HeadcountRepository — 22_bud_headcount
// =====================================================================
var HeadcountRepository = {
  _getSheet: function() { return Utils.getSheetByKey('BUD_HC', '22_bud_headcount'); },
  findAll: function() { return readSheetAsDtos_(HeadcountRepository._getSheet()); },
  save: function(dtos) {
    var sheet = HeadcountRepository._getSheet();
    if (!sheet) return;
    var headers = sheet.getRange(1, 1, 1, sheet.getMaxColumns()).getValues()[0].map(function(h){return String(h).trim();});
    writeDtosToSheet_(sheet, headers, dtos);
  },
  append: function(dtos) {
    var sheet = HeadcountRepository._getSheet();
    if (!sheet) return 0;
    var headers = sheet.getRange(1, 1, 1, sheet.getMaxColumns()).getValues()[0].map(function(h){return String(h).trim();});
    return appendDtosToSheet_(sheet, headers, dtos, 1);
  },
};
  • _getSheet のシステムキー BUD_RSCE / BUD_HC01_sys_config に登録済み(101_sys_config.js L594・L599 で確認済み)。
  • ResourceRepository のフォールバック名は 70_bud_resource を優先し、旧タブ名 27_bud_resource も二段フォールバック(現行 420_project_profitability.js L16 と同じ扱い)。

(1-d) 03_sys_paramsCFG_MONTHLY_WORKING_HOURS を登録(シート作業)

パラメータキー備考
CFG_MONTHLY_WORKING_HOURS160月の標準労働時間。時間単価算出の分母。未登録でも Constants.getParam('CFG_MONTHLY_WORKING_HOURS', 160) のデフォルト 160h が効くため、登録は任意

Step 2 — 時間単価算出ロジックの実装

400_domain/420_project_profitability.js L40-60 の hcMap 構築ロジックを、HeadcountRepository を使った DTO ベースに置き換え、時間単価(円/h)を月別に算出するマップとして再定義する。

// 新規ヘルパー(ファイル末尾 L869 以降に追加)
function calcHourlyRate_(empDto, workingHours) {
  var sal = Utils.parseAmt(empDto['月額給与・報酬']);
  if (!sal || sal <= 0) return 0;
  if (!workingHours || workingHours <= 0) return 0;
  var empType = String(empDto['雇用形態'] || '').trim();
  // 業務委託は社保対象外
  if (empType === '業務委託') return Math.round(sal / workingHours);
  var healthRate   = Number(empDto['健保料率'])                || 0;
  var careRate     = Number(empDto['介護保険料率'])             || 0;
  var pensionRate  = Number(empDto['厚年料率'])                 || 0;
  var employRate   = Number(empDto['雇用保険料率'])             || 0;
  var childRate    = Number(empDto['子ども・子育て拠出金率'])   || 0;
  var socialRate = healthRate + careRate + pensionRate + employRate + childRate;
  var totalLaborCost = sal + Math.round(sal * socialRate);
  return Math.round(totalLaborCost / workingHours);
}
  • 分子 月額給与・報酬 + 会社負担社会保険料合計。社保合算は既存の DDL で定義された 5 料率を合計。
  • 分母 CFG_MONTHLY_WORKING_HOURSConstants.getParam('CFG_MONTHLY_WORKING_HOURS', 160))。
  • 業務委託(雇用形態 === '業務委託')は社保ゼロのまま単価計算(旧コード L47 の分岐を保持)。

人員×月の時間単価マップ構築(L40-60 を置換)

var workingHours = Constants.getParam('CFG_MONTHLY_WORKING_HOURS', 160);
var hcRateMap = {};     // [name][ym] = 時間単価 (円/h)
var hcResult = HeadcountRepository.findAll();
for (var i = 0; i < hcResult.dtos.length; i++) {
  var e = hcResult.dtos[i];
  var flag = e['有効フラグ'];
  if (flag === false || String(flag).toUpperCase() === 'FALSE') continue;
  var name = String(e['氏名・ポジション'] || '').trim();
  if (!name) continue;
  var rate = calcHourlyRate_(e, workingHours);
  if (rate <= 0) continue;
  var curYm = Utils.parseDateToYm(e['開始年月']);
  var endYm = Utils.parseDateToYm(e['終了年月']) || '2099-12';
  if (!curYm) continue;
  if (!hcRateMap[name]) hcRateMap[name] = {};
  var limit = 0;
  while (curYm <= endYm && curYm <= targetMonths[11] && limit < Constants.MONTH_ITERATION_LIMIT) {
    hcRateMap[name][curYm] = rate;
    curYm = Utils.addMonths(curYm, 1);
    limit++;
  }
}

Step 3 — PJ 別人件費計算と 78_pj_pl への書き込み

(3-a) 労務費集計(L73-86 の置換)

// ResourceRepository で DTO 取得
var rsResult = ResourceRepository.findAll();
for (var i = 0; i < rsResult.dtos.length; i++) {
  var r = rsResult.dtos[i];
  var flag = r['有効フラグ'];
  if (flag === false || String(flag).toUpperCase() === 'FALSE') continue;
  var name = String(r['要員名'] || '').trim();
  var pjObj = getPj(r['PJ・案件名']);
  var hours = Math.max(0, Utils.parseAmt(r['工数(h)']));  // マイナス値は 0 扱い
  var ym = Utils.parseDateToYm(r['対象年月']);
  if (!name || !pjObj || hours === 0 || !ym || !targetMonths.includes(ym)) continue;
  var rate = (hcRateMap[name] && hcRateMap[name][ym]) ? hcRateMap[name][ym] : 0;
  if (rate === 0) {
    Utils.logInfo('buildProjectProfitability', '時間単価=0のためスキップ: ' + name + '/' + ym);
    continue;
  }
  pjObj[ym].laborCost += Math.round(rate * hours);
}
  • 同一従業員 × 同一月 × 同一 PJ の複数行は +=工数の合算(Repository ループ内で自然に合算)。
  • 存在しない PJ コード/マスタ未登録従業員は pjObj / rate が 0 でスキップ、Utils.logInfo で警告。

(3-b) 共通費配賦「工数比」の分母を工数合計に置換(L469-482 の置換)

var dstWorkload = {};
var rsResult2 = ResourceRepository.findAll();  // 必要なら上段と共有化
for (var ri = 0; ri < rsResult2.dtos.length; ri++) {
  var r2 = rsResult2.dtos[ri];
  if (r2['有効フラグ'] === false || String(r2['有効フラグ']).toUpperCase() === 'FALSE') continue;
  var rpj = String(r2['PJ・案件名'] || '').trim();
  var rhr = Math.max(0, Utils.parseAmt(r2['工数(h)']));
  if (rpj && dstPjs[rpj]) dstWorkload[rpj] = (dstWorkload[rpj] || 0) + rhr;
}

'工数比' メソッドの配賦ロジック(L521)は変更不要(分母 totalDstWorkload の意味が稼働率合計から工数合計に変わるだけで、比率の意味は保たれる)。

(3-c) 78_pj_pl への冪等性書き込み

現行コードは sheet78.clear()setValues()全置換(L703)で、対象年月以外も含めて毎回丸ごと再生成する方式。これを維持する(シート丸ごと再描画は既存の Human-in-the-Loop ポリシー — 生成 → 目視確認 → 必要なら手動編集 — と整合)。

(3-d) 77_pj_raw のラベル更新(任意)

L460 の配賦ラベル '労務費(工数)' は既存と同じ表記で良いが、L459 の科目表示名 '🤖労務費(工数配賦)''🤖労務費(時間単価×工数)' に変更すると、ロジック変更が画面から読み取れて良い(変更は任意。既存フォーマット尊重ならスキップ)。

Human-in-the-Loop ポリシー(PO 判断ポイント)

計算結果を 78_pj_pl に直接書き込むか、中間シート 78_pj_pl_draft 経由で人間が承認してから本番反映する 2 段階フローにするかは PO 判断。本仕様書では 直接書き込みをデフォルト とし、後から 2 段階フローへ差し替えられるよう buildProjectProfitability() の末尾 sheet78.clear()setValues() 部分を独立関数 write78PjPl_(plOut) に抽出することだけを推奨する(今回のスコープでは関数抽出まで。実際の 2 段階フローは別案件)。

影響範囲

ファイル変更種別変更量
000_infra/002_constants.jsSHEET_DEFAULTS L78 のエントリ 1 行差し替え±1 行
100_config/101_sys_config.jsBUD_RSCE DDL ヘッダー L648 の 1 語差し替え±1 行
200_data/202_repository.jsResourceRepository / HeadcountRepository を末尾に 2 ブロック追加+50 行
400_domain/420_project_profitability.jsL40-86(労務費算出)・L469-482(工数集計)を置換、calcHourlyRate_ をファイル末尾に追加±80 行
templates/operations_sidebar.htmlL62-66 「📊 マート更新」に「工数単価算出(プレビュー)」ボタン追加は任意0-3 行
03_sys_paramsCFG_MONTHLY_WORKING_HOURS = 160 を 1 行追加(任意・シート直接編集)0-1 行

注意事項

  1. Constants.SHEET_DEFAULTS70_bud_resource エントリ変更は smartAddRow / bulkAssignDefaults の既存挙動に影響する稼働率(%) のデフォルトが消えるので、旧クライアントから新規行を追加すると 工数(h) が 0 で入る(意図通り)。一方、旧シートにはまだ 稼働率(%) 列が物理的に残るため、setupAllSchemas() でヘッダーを上書きする手順を忘れないこと。DDL 再実行前に、既存データのバックアップ(MAS-201 スプレッドシート定期バックアップ)を取得してから進める。

  2. 78_pj_pl は DDL (setupAllSchemas) で管理されない動的生成タブ(CLAUDE.md 「DDL で管理されないタブ」セクション)。ヘッダーや列数は buildProjectProfitability() が毎回動的に決めているため、本案件で列幅や列順を変えても DDL 側の変更は不要。ただし 77_pj_raw のカラムを追加する場合は L753 のヘッダー配列と行データ両方を変更する必要がある。

  3. 列参照はヘッダー名ベース (indexOf / Contracts.toDto)。列番号ハードコード禁止(CLAUDE.md コーディング規約)。本案件で DTO 方式に移行するため、indexOf('工数(h)')-1 を返さないよう、ヘッダー定義の変更順序を 「DDL → 実シートの setupAllSchemas() 再実行 → コード差し替え → npm run push:dev の順で行う。

  4. 有効フラグ = FALSE の行は全処理でスキップ(CLAUDE.md 規約)。Repository 経由で DTO を取得した後、必ず if (dto['有効フラグ'] === false || String(dto['有効フラグ']).toUpperCase() === 'FALSE') continue; を入れること。

  5. 22_bud_headcount社保会社負担率 列(合算列)は 420_project_profitability.js L44 の既存ロジックが参照しているが、DDL(L661)と SHEET_DEFAULTS には存在しない。つまり旧コードは実シートに手動で追加された派生列を参照していた可能性が高い。本案件では 5 料率合算方式に切り替えるため、派生列 社保会社負担率 への依存を完全に除去し、calcHourlyRate_ が 5 料率を個別に読むよう変更する。これにより DDL と実データの齟齬が解消される。

  6. MONTH_ITERATION_LIMITConstants.MONTH_ITERATION_LIMIT = 120)を使うこと。旧コード L55 の limit < 120 ハードコードは新規コードでは定数参照に統一する。

  7. Repository 追加順序に注意ResourceRepository / HeadcountRepository202_repository.js に追加するため、ファイル読込順で 420_project_profitability.js より前に評価される(200 < 420)。この順序を変えない限り、420_ 側から ResourceRepository.findAll() 呼び出しは問題なく動作する。

エッジケース

#条件動作理由
E01CFG_MONTHLY_WORKING_HOURS が 0 または未設定(Constants.getParam のデフォルト 160 でも満たされない異常値)calcHourlyRate_ は 0 を返す。全 PJ の労務費 0 で計算継続。Utils.logInfo('buildProjectProfitability', 'CFG_MONTHLY_WORKING_HOURS が 0 または未設定のため時間単価 = 0')ゼロ除算防止。運用ミスが全社停止を招かない防御策
E02従業員の 月額給与・報酬 が 0(無報酬役員等)calcHourlyRate_ は 0 を返す。その従業員の工数は 0 円で計算される無報酬役員や未支給月の正常ケース
E0370_bud_resource要員名22_bud_headcount に存在しないhcRateMap[name] が undefined → 労務費 0。Utils.logInfo で警告マスタ不整合の検知と処理継続の両立
E04存在しない PJ・案件名 が入力されたgetPj()null / projMaster に存在しない PJ をスキップ。Utils.logInfo で警告PJ マスタ不整合の検知
E05工数(h) にマイナス値が入力されたMath.max(0, Utils.parseAmt(...)) で 0 クランプ。計算継続不正値の防御的処理(集計を負に歪めない)
E06特定月の従業員合計工数が CFG_MONTHLY_WORKING_HOURS × 2(= 320h)を超過計算は実行するが Utils.logInfo で超過警告を出力入力ミスの早期検知(過剰工数の兆候)
E07同一従業員 × 同一月 × 同一 PJ のデータが複数行存在(日次/週次で細分入力した場合)全行の工数を合算して計算(pjObj[ym].laborCost += Math.round(rate * hours) を行ごとに累積)分割入力(日次/週次/月次のハイブリッド)を許容
E08対象年月targetMonths(当期 12 ヶ月)の範囲外targetMonths.includes(ym) でスキップ期外入力を集計から除外し、前期/来期データ混入を防ぐ
E0970_bud_resource稼働率(%) 列が物理的に残留している(DDL 再実行前)新コードは 工数(h) 列しか読まない。稼働率列は無視される(=すべての工数が 0 扱い)DDL 再実行の必要性を検知させる。setupAllSchemas() を必ず再実行する運用手順
E10雇用形態 === '業務委託' の要員社保ゼロで 時間単価 = 月額 / 工数。社保 5 料率は読まない業務委託は社保対象外という旧コード L47 の挙動を保持
E11従業員の 開始年月 が空白Utils.parseDateToYm""continue でスキップDTO 必須項目の欠損防御
E12終了年月 が空白`
E13配賦区分 === '配賦元' の PJ に工数入力getPj() 内で null を返しスキップ(L67)配賦元 PJ は 79_pj_monthly の対象外(既存ロジック保持)
E14buildProjectProfitability() の 2 回連続実行2 回目も同じ結果を全置換で上書き(sheet78.clear()setValues() は冪等)冪等性保証
E15一部の従業員のみ 5 料率 のうち一部が未入力(例: 健保 0.05 厚年 0 雇用 0.0065)未入力の料率は 0 として加算(欠損を 0 で扱う)→ 社保合算が過小評価される22_bud_headcount の入力不足を検知する責務は 22_bud_headcount 側のバリデーションに委ねる
E16同一人物名で 22_bud_headcount に複数行(適用年度が異なる等)同一 [name][ym] キーで後勝ちで上書き。注意が必要新旧給与の切り替え月は 開始年月 / 終了年月 で切られるべき。重複月を許容しない運用ルールを推奨
E17MONTH_ITERATION_LIMIT (= 120) を超えるレンジ(10 年以上)ループ上限で打ち切り無限ループ防止

実データ検証

MCP ツール等で本番/開発スプレッドシートを確認すべき項目:

  1. 70_bud_resource の実列構造 — DDL 定義 ["有効フラグ","要員名","PJ・案件名","対象年月","稼働率(%)"]101_sys_config.js L648)と実シートヘッダーの一致を確認。もし実シートに追加列(例: 作業内容 / 備考 等)が手動で足されていたら、DDL 再実行時にその列が 保持されるか破棄されるか を確認する(setupAllSchemas() の DDL 適用ポリシーに依存)。
  2. 22_bud_headcount の社会保険料フィールド名 — DDL(L661)に 健保料率 / 介護保険料率 / 厚年料率 / 雇用保険料率 / 子ども・子育て拠出金率 が定義されていることと、実シートで各列が全社員分入力されていることを確認。
  3. 社保会社負担率 列の存在有無 — 現行 420_project_profitability.js L44 は idxSoc = hcData[0].indexOf('社保会社負担率') を読んでいる。実シートで派生列として存在する場合、本案件後は参照されなくなるため列の論理削除(= 有効フラグ運用外)を検討
  4. 03_sys_paramsCFG_MONTHLY_WORKING_HOURS キーが存在するか — 存在しない場合は 160 がデフォルト。経営会議等で「標準月次工数」を変更したい場合は、本キーを明示的に登録する必要あり。
  5. 78_pj_pl の列構造 — 人件費セクション 'labor'plSections 配列 L546)がそのまま使われることを確認。ラベル '労務費 (工数配賦)' はそのまま。
  6. 28_bud_allocation按分方法='工数比' を使っている配賦ルール — 本案件で工数合計の意味が「稼働率の合計」から「工数(h)の合計」に変わるため、工数比 を使っている行が既存の配賦ルールに存在する場合、配賦結果の大きさが変わる可能性あり。変更前後の 78_pj_pl を並べて検証する。

関連ドキュメント

仕様書 / ドキュメント関連箇所
PRD プロダクトポリシーHuman-in-the-Loop: 自動計算 → 目視確認 → 承認の 3 段階フロー
MAS-006 PJ別管理会計の共通費配賦本案件の前提。78_pj_pl の共通費配賦ロジックの全体像
MAS-026 TDABC(時間主導型 ABC)本案件の後続案件。時間単価の算出ロジックはそのまま TDABC の入力になる
MAS-027 M365 監査ログ連携本案件の代替候補。工数入力を M365 ログから自動推定
ADR A.3 78 vs 79 タブの役割分担78_pj_pl(PJ 横並び P/L)と 79_pj_monthly(PJ 別採算一覧)の責務境界
B.3 統合テスト手順「マート更新テスト(P/L・B/S・CF)」に本案件のテストケースを追加する
CLAUDE.md変更時の動作確認テスト: 400_domain/420_project_profitability.js 変更 → PJ 別採算テスト

人間が検討すべき事項

  1. 工数の入力粒度(日次 / 週次 / 月次) — 本仕様書では月次集計を前提とし、同一 PJ × 同一月の複数行は合算する(E07)。日次入力を許容するなら 日付 列を追加する必要があり、スキーマ変更が発生する。
  2. 入力 UI の設計(シート直接入力 / フォーム / サイドバー) — 現状は 70_bud_resource シート直接編集。MAS-027(M365 ログから自動推定)との将来統合を見据え、今は「手入力のまま」か「専用フォーム」かを決める。
  3. 勤怠管理 SaaS との連携(MAS-099 は対象外扱いに再分類済み) — 給与計算 SaaS(freee 人事労務等)経由で勤怠実績を取り込む場合の I/F をどうするか。
  4. 計算結果の直接書き込み vs 2 段階確認フロー — 本仕様書ではデフォルトで直接書き込み(78_pj_pl を全置換)。Human-in-the-Loop ポリシー(CLAUDE.md・PRD)に照らすと、重要性の高い計算(PJ 営業利益の確定等)は中間シート 78_pj_pl_draft 経由で人間承認後に本番反映するのが望ましい。PO 判断待ち
  5. 70_bud_resource 既存データ(稼働率ベース)のマイグレーション方針 — 稼働率 0.8(80%)の行を「0.8 × 160h = 128h」として推定値を流し込むか、履歴をクリアして手動で工数入力をやり直すか。前者なら 800_ops/809_migration_resource_hours.js を新設(冪等・メニュー登録)する。PO 判断待ち
  6. 社保会社負担率 派生列の扱い — 実シートで手動運用されている可能性のある 社保会社負担率 列を、本案件後に削除するか残すか。残す場合は「表示用」と割り切って参照ロジックから完全に切り離す。
  7. CFG_MONTHLY_WORKING_HOURS の会社別設定 — 160h は 8h × 20 営業日のラフ値。実際には月によって営業日が 19-23 日で揺れる。月次で CFG_MONTHLY_WORKING_HOURS_2026_04 のように動的キーを増やすか、160h 固定で割り切るか。本仕様書は 160h 固定 を推奨(運用簡素化)。

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

Step 1: スキーマ変更・Repository 新設

あなたは GAS 会計システム (bizlp-gas-accounting) のシニア開発者です。
CLI エージェントである「Claude Code」として、以下の指示に従い、案件 MAS-105「工数入力・PJ原価連携」の **Step 1**(スキーマ変更・Repository 新設)を実装してください。

## 実行前タスク
1. `CLAUDE.md` のコーディング規約(列参照ルール・有効フラグ処理・Repository パターン)を確認。
2. `000_infra/002_constants.js` L73-87 の `SHEET_DEFAULTS` の構造を確認。
3. `200_data/202_repository.js` L107-146 の `OrderRepository` の実装パターンを確認。
4. `100_config/101_sys_config.js` L594-L661 の `BUD_RSCE` / `BUD_HC` DDL を確認。
5. 仕様書: `docs/dev/dev_mas-105_workforce_costing.md`

## 修正対象ファイル
- `000_infra/002_constants.js`(`SHEET_DEFAULTS` L78 のエントリ 1 行差し替え)
- `100_config/101_sys_config.js`(`BUD_RSCE` DDL ヘッダー L648 の 1 語差し替え)
- `200_data/202_repository.js`(末尾に `ResourceRepository` / `HeadcountRepository` 2 ブロック追加)
- 他ファイルの変更はしないこと

## 実装内容
1. `002_constants.js` L78 を以下に差し替え:
   `{ pattern: '70_bud_resource', prefix: 'RSC_', defaults: { '工数(h)': 0, _dynamic: { '対象年月': 'nextYm' } } },`
2. `101_sys_config.js` L648 の BUD_RSCE ヘッダーを `["有効フラグ","要員名","PJ・案件名","対象年月","工数(h)"]` に変更。
3. `202_repository.js` 末尾の `AccountRepository.resetCache` 関数の直後に、仕様書「Step 1 — (1-c)」の `ResourceRepository` と `HeadcountRepository` を追加する。
   - `_getSheet` のキーは `BUD_RSCE` / `BUD_HC`(登録済み)。フォールバック名は `70_bud_resource` / `22_bud_headcount`。
   - `findAll` / `save` / `append` の 3 メソッドを既存の `OrderRepository` と同一パターンで実装。

## 制約
- 列番号ハードコード禁止(ヘッダー名ベース)
- 有効フラグ = FALSE のスキップは Repository 利用側で実装(Repository 自体は生データを返す)
- 既存 Repository 関数(`OrderRepository` 等)の改変禁止
- ファイル読込順(200 < 420)に依存した宣言順序を崩さない

## エッジケース
仕様書のエッジケース E01-E17 のうち、Step 1 のスコープ内で対応するのは以下:
- E09: `70_bud_resource` に旧 `稼働率(%)` 列が残っている場合 → `setupAllSchemas()` 再実行で上書き

## 動作確認
1. `npm run push:dev` で開発環境にデプロイ
2. GAS エディタで `setupAllSchemas()` を実行 → `70_bud_resource` のヘッダーが `工数(h)` に変わっていることを確認
3. GAS エディタで `ResourceRepository.findAll()` を試行 → `{ headers, dtos }` が返ることを確認
4. 同様に `HeadcountRepository.findAll()` を試行

## 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---|:---:|---|
| `SHEET_DEFAULTS` の差し替え | なし | 仕様書で完全定義済み |
| DDL ヘッダーの変更 | なし | 仕様書で完全定義済み |
| `Repository` の新設 | あり(軽微) | 既存パターンへの準拠判断のみ |

Step 2: 時間単価算出ロジックの実装

あなたは GAS 会計システム (bizlp-gas-accounting) のシニア開発者です。
案件 MAS-105「工数入力・PJ原価連携」の **Step 2**(時間単価算出ロジック)を実装してください。Step 1 は完了している前提です。

## 実行前タスク
1. `CLAUDE.md`
2. `000_infra/004_utils.js` の `Utils.parseDateToYm` / `Utils.parseAmt` / `Utils.addMonths` / `Utils.logInfo` の仕様を確認
3. `000_infra/002_constants.js` の `Constants.getParam(key, defaultVal)` / `Constants.MONTH_ITERATION_LIMIT` を確認
4. `400_domain/420_project_profitability.js` L40-60 の現行 `hcMap` 構築ロジック(`社保会社負担率` 参照含む)を確認
5. 仕様書: `docs/dev/dev_mas-105_workforce_costing.md`

## 修正対象ファイル
- `400_domain/420_project_profitability.js` のみ

## 実装内容
1. **ファイル末尾 L869 以降に `calcHourlyRate_(empDto, workingHours)` を追加**:
   - `月額給与・報酬 <= 0` または `workingHours <= 0` → 0 を返す
   - `雇用形態 === '業務委託'` → `Math.round(月額 / workingHours)` を返す(社保ゼロ)
   - それ以外 → `社保率 = 健保料率 + 介護保険料率 + 厚年料率 + 雇用保険料率 + 子ども・子育て拠出金率`、`社保額 = Math.round(月額 × 社保率)`、`時間単価 = Math.round((月額 + 社保額) / workingHours)`
2. **L40-60 の `hcMap` 構築ロジックを置換**:
   - `HeadcountRepository.findAll()` で DTO 配列を取得
   - `有効フラグ` FALSE をスキップ
   - `workingHours = Constants.getParam('CFG_MONTHLY_WORKING_HOURS', 160)`
   - `calcHourlyRate_(e, workingHours)` で時間単価を算出
   - `hcRateMap[name][ym] = rate` を `開始年月` から `終了年月`(または `targetMonths[11]`)まで月単位で展開
   - ループ上限は `Constants.MONTH_ITERATION_LIMIT`
3. **既存の `idxSoc` / `社保会社負担率` への参照を完全削除**。DDL に存在しない派生列への依存を断つ。
4. **変数名**: 旧 `hcMap[name][ym] = totalLaborCost`(月額人件費)→ 新 `hcRateMap[name][ym] = hourlyRate`(時間単価)へ変更。Step 3 で `hcRateMap` を参照する。

## 制約
- 列番号ハードコード禁止(DTO のキー名で参照)
- 有効フラグ FALSE の行スキップ必須
- 業務委託分岐(`雇用形態 === '業務委託'` の社保ゼロ)を必ず保持
- 旧 `hcMap` 変数名は **全て `hcRateMap` に改名** (Step 3 で参照を揃えるため)

## エッジケース
仕様書の E01-E02, E10-E12, E15, E17 を満たすこと。特に `CFG_MONTHLY_WORKING_HOURS=0` での `calcHourlyRate_ → 0` の分岐を忘れない。

## 動作確認
1. `npm run push:dev`
2. GAS エディタで `buildProjectProfitability()` を実行(Step 3 未実装のため一時的に工数集計は 0 となるが、エラーなく完走することを確認)
3. `Utils.logInfo` ログで `hcRateMap` の値が月別に展開されていることを確認(`console.log(JSON.stringify(hcRateMap))` をデバッグ目的で一時追加しても良い)

## 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---|:---:|---|
| `calcHourlyRate_` の実装 | あり(軽微) | 5 料率合算の式と業務委託分岐 |
| `hcRateMap` 構築ループの書き換え | あり | 旧 `hcMap` の月別展開ロジックとの対応関係 |
| 派生列 `社保会社負担率` の完全除去 | あり | 互換コードを残さない判断 |

Step 3: PJ 別人件費計算と 78_pj_pl への書き込み

あなたは GAS 会計システム (bizlp-gas-accounting) のシニア開発者です。
案件 MAS-105「工数入力・PJ原価連携」の **Step 3**(PJ 別人件費計算と 78_pj_pl 書き込み)を実装してください。Step 1・2 は完了している前提です。

## 実行前タスク
1. `CLAUDE.md`
2. `000_infra/003_contracts.js` の `Contracts.toDto` / `Contracts.toDtoList` の仕様
3. `400_domain/420_project_profitability.js` の全体構造(特に L73-86 労務費集計、L463-496 工数合計、L536-746 78_pj_pl 書き込み)
4. 仕様書: `docs/dev/dev_mas-105_workforce_costing.md`

## 修正対象ファイル
- `400_domain/420_project_profitability.js` のみ
- `77_pj_raw` のラベル変更は任意(必要なら L459 `'🤖労務費(工数配賦)'` → `'🤖労務費(時間単価×工数)'`)

## 実装内容
1. **L73-86 の労務費集計を DTO ベースに置換**:
   - `ResourceRepository.findAll()` で DTO 取得
   - `有効フラグ` FALSE をスキップ
   - `hours = Math.max(0, Utils.parseAmt(r['工数(h)']))`(マイナス値は 0 クランプ)
   - `ym = Utils.parseDateToYm(r['対象年月'])` が `targetMonths` に含まれない場合スキップ
   - `rate = hcRateMap[name][ym]` が 0 の場合 `Utils.logInfo` で警告後スキップ
   - `pjObj[ym].laborCost += Math.round(rate * hours)` で合算
   - 合計工数が `CFG_MONTHLY_WORKING_HOURS × 2` を超える場合 `Utils.logInfo` で警告(E06)
2. **L469-482 の `dstWorkload` 集計を DTO ベースに置換**:
   - 同じく `ResourceRepository.findAll()` を再利用(Step 3-(a) で既に取得した結果を変数に保持して使い回しても良い)
   - `rhr = Math.max(0, Utils.parseAmt(r2['工数(h)']))`
   - `dstPjs[rpj]` 所属のみ `dstWorkload[rpj] += rhr`
3. **`78_pj_pl` は全置換維持**(`sheet78.clear()` → `setValues()`)。冪等性 OK。
4. **(任意・推奨)`78_pj_pl` への書き込み部分を独立関数に抽出**:
   - `write78PjPl_(plOut)` を追加(L702-746 を切り出し)
   - 将来 2 段階フロー(`78_pj_pl_draft` 経由)への差し替えを容易にする

## 制約
- 列番号ハードコード禁止
- 有効フラグ FALSE の行スキップ必須
- 旧 `rate = Number(row[idxRate])` の稼働率参照は完全削除
- `classifyAcct_` の `'🤖労務費(工数配賦)'` 分岐は、ラベルを変えるなら同時に修正(現状は変えない想定)

## エッジケース
仕様書の E01-E17 を全て満たすこと。特に:
- E03: `hcRateMap[name]` undefined で 0 処理(スキップ時にログ出力)
- E04: `projMaster` に無い PJ は `getPj` で null 返却(既存ロジック)
- E05: `Math.max(0, ...)` での 0 クランプ
- E07: 複数行の累積加算
- E14: 2 回連続実行で同結果(`sheet78.clear()` の冪等性)

## 動作確認
1. `npm run push:dev`
2. `70_bud_resource` に以下を投入:
   - 要員 A / PJ_X / 2026-04 / 40h
   - 要員 A / PJ_X / 2026-04 / 20h(分割入力テスト)
   - 要員 B / PJ_Y / 2026-04 / 160h
3. GAS エディタで `buildProjectProfitability()` を実行
4. `78_pj_pl` を開き、「労務費 (工数配賦)」セクションに以下を確認:
   - PJ_X の 2026-04 行: `要員Aの時間単価 × 60h` の値
   - PJ_Y の 2026-04 行: `要員Bの時間単価 × 160h` の値
5. 2 回連続実行して同じ値になることを確認(冪等性)
6. 工数 -5h を投入して E05 が発動することを確認
7. 存在しない PJ 名を投入して E04 が発動(`Utils.logInfo` 警告)することを確認

## 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---|:---:|---|
| L73-86 の労務費集計置換 | あり | 旧 `baseSal * rate` → 新 `rate * hours` の意味変換 |
| L469-482 の工数合計置換 | あり | 「工数比」配賦の分母の意味が変わる影響範囲分析 |
| `write78PjPl_` の関数抽出(任意) | あり | 将来の 2 段階フロー拡張を見越した設計判断 |
| エッジケース E01-E17 の実装 | あり | 17 ケース × 具体的な発動条件の特定 |

推奨実行モデル

工程推奨モデル理由
Step 1: スキーマ変更・Repository 新設Claude Sonnet既存 OrderRepository パターンへの準拠判断が中心。ロジック判断なし
Step 2: 時間単価算出ロジック実装Claude Sonnet会計ロジック(社保合算・業務委託分岐)の理解と既存関数の適用判断
Step 3: PJ 損益シート連携・冪等性実装Claude Opus複数ファイル横断(420_ / 77_pj_raw / 78_pj_pl)の設計判断、冪等性設計、エッジケース 17 件の網羅実装
仕様書作成(本ドキュメント)Claude Opus 4.7 (1M context)スキーマ変更の全社影響・DTO 変換・HITL ポリシー・MAS-026 / MAS-027 との後続連携を横断的に判断

変更履歴

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

仕様書作成プロンプト

展開して表示
【タイムアウト回避・実行原則(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 骨格Write(~20行) / 2-2 概要〜注意事項Edit/Bash(~300行) / 2-3a エッジケース〜人間検討事項Edit/Bash(~200行) / 2-3b 実装プロンプト〜変更履歴Edit/Bash(~250行) / 2-4 `<details>`にプロンプト全文記録Edit/Bash(最重量・必ず独立Step)
4. **各Stepで何を書くかを具体指示**: Phase 1で全固有名詞・行番号・設計判断を確定させ、Phase 2では書き下しのみ行う。

======================================================================
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者兼仕様書ライターです。
案件 S-33「工数入力・PJ原価連携」の開発仕様書を作成してください。
作成後は `docs/_config.json` の `nav` 配列の適切なセクションにも必ず追記してください。

---

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

### 1-A: 案件定義の読み込み
`docs/_internal/TODO_future.md` を検索し、S-33 の案件名・カテゴリ・Phase・優先度・概要・人間が検討すべき事項を取得する。

### 1-B: プロジェクト規約の読み込み
`CLAUDE.md` を読み込み、コーディング規約(列参照ルール・有効フラグ処理・Repositoryパターン)・ファイル番号体系・テスト手順を把握する。

### 1-C: 既存仕様書テンプレートの読み込み
`docs/dev/dev_mas-093_cf_actual_only.md`(既存パターン横展開)または `docs/dev/dev_mas-001_variance_analysis.md`(新機能)を1件読み込み、フォーマットを把握する。

### 1-D: 関連コードの調査

**読み方のルール(Grep vs Read)**: 仕様書に固有名詞(関数名・シート名・列名・定数名)を書く前は必ず `Read` で裏取りすること。「〜という名前だろう」と推測した瞬間に手を止めてReadする。Grepは「どこにあるか」の発見まで、「どう書くか」の判断は必ずReadで行う。

以下のファイルをこの順番でReadし、確認ポイントを全て把握してから Phase 2 に進むこと:

1. **`000_infra/002_constants.js`**
   - `Constants.SHEET_DEFAULTS` 配列内の `70_bud_resource` エントリ: `{ pattern, prefix, defaults }` の型と全フィールド名(現行の `稼働率(%)` や `開始年月` 等)を確認
   - `Constants.SHEET_DEFAULTS` 配列内の `22_bud_headcount` エントリ: 社会保険料率フィールド名(`健保料率`・`厚年料率`・`雇用保険料率`・`子ども・子育て拠出金率`・`介護保険料率`等)と `月額給与・報酬` フィールド名を確認
   - `Constants.ID_PREFIX_MAP` 内の `70_bud_resource` エントリ(プレフィックス `RSC_` と `isDate` 設定を確認)
   - `Constants.getParam(key, defaultVal)` の呼び出しシグネチャを確認(`03_sys_params` からパラメータ取得に使用)

2. **`000_infra/004_utils.js`**
   - `Utils.parseDateToYm`・`Utils.parseAmt`・`Utils.logInfo`・`Utils.logError` の引数・戻り値を確認

3. **`000_infra/003_contracts.js`**
   - `Contracts.toDto`・`Contracts.toDtoList`・`Contracts.toRow` の使い方を確認

4. **`200_data/202_repository.js`**
   - 既存Repository(`OrderRepository` 等)の `findAll` / `save` / `append` のパターン全体を確認し、新設 `ResourceRepository`・`HeadcountRepository` の実装テンプレートとして把握する
   - 内部ヘルパー `readSheetAsDtos_`・`writeDtosToSheet_`・`appendDtosToSheet_`・`findLastRow_` の仕様を確認

5. **`400_domain/420_project_profitability.js`**(※`402_`ではなく`420_`が正しいファイル番号)
   - 現行の稼働率ベース配賦ロジックの主要関数名・行番号・処理フローを特定する
   - `78_pj_pl` シートへの書き込みパターン(列構造・クリア方法)を特定する

6. **`100_config/101_sys_config.js`**
   - `onOpen()` のメニュー構造を確認し、工数計算実行メニューを追加する位置を特定する(メニュー名の固有名詞を実在する文字列で引用すること)
   - `70_bud_resource` と `22_bud_headcount` のシステムキー(`01_sys_config` シートに登録されているキー)を確認する(`Utils.getSheetByKey` の第1引数として使用)

---

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

**出力先**: `docs/dev/dev_mas-105_workforce_costing.md`(**S は大文字**)

### Step 2-1: 骨格の作成(File Write、~20行)
以下の見出しのみ。本文は空で可:
# S-33: 工数入力・PJ原価連携
## 概要
## 目的
## 現在のコード
## 修正方針
## 影響範囲
## 注意事項
## エッジケース
## 実データ検証
## 関連ドキュメント
## 人間が検討すべき事項
## 実装プロンプト(Claude Code 用)
## 推奨実行モデル
## 変更履歴
## 仕様書作成プロンプト

### Step 2-2: 概要〜注意事項の追記(File Edit または Bash heredoc、~300行)

Phase 1の調査結果に基づき以下を記述する(固有名詞はReadで確認済みの実在する文字列のみ使用):

- **概要テーブル**: 案件ID=S-33、対象ファイル(Phase 1で特定した実ファイル名を記載)、前提案件等
- **目的**: 稼働率ベース概算配賦から工数ベース実費計算への移行目的を1〜3文で記述
- **現在のコード**: `Constants.SHEET_DEFAULTS` の `70_bud_resource` エントリで確認した実際のフィールド名と `420_project_profitability.js` の現行配賦ロジック関数名・行番号を記載
- **修正方針**: 以下3Stepで記述
  - **Step 1: スキーマ変更・Repository新設**
    - `002_constants.js` の `Constants.SHEET_DEFAULTS` 内 `70_bud_resource` エントリ変更: `稼働率(%)` を `工数(h)` に変更し `対象年月` フィールドを追加(`{ pattern, prefix, defaults }` 型はReadで確認した構造に従うこと)
    - `202_repository.js` に `ResourceRepository`(`70_bud_resource` 用)と `HeadcountRepository`(`22_bud_headcount` 用)を**新設**。既存の `OrderRepository` のパターンに準拠し `findAll` / `save` / `append` の3メソッドを実装。`_getSheet()` のシステムキーはPhase 1で `101_sys_config.js` から確認した実在するキーを使用(未確認の場合はフォールバック名で実装し「要確認」と注記)
  - **Step 2: 時間単価算出ロジックの実装**
    - `420_project_profitability.js` に `calcHourlyRate_(empDto)` ヘルパー関数を新設
    - 算出式: `時間単価 = (月額給与・報酬 + 会社負担社会保険料合計) / CFG_MONTHLY_WORKING_HOURS`
    - 会社負担社会保険料合計 = Phase 1で `22_bud_headcount` エントリから確認した各率フィールド(`健保料率`・`厚年料率`・`雇用保険料率`・`子ども・子育て拠出金率`・`介護保険料率`)× `月額給与・報酬` の合算
    - `CFG_MONTHLY_WORKING_HOURS` は `Constants.getParam('CFG_MONTHLY_WORKING_HOURS', 160)` で取得(`03_sys_params` から読み込み。未設定時デフォルト 160)
  - **Step 3: PJ別人件費計算と `78_pj_pl` への書き込み**
    - 計算式: `PJ別人件費 = 時間単価 × PJ別工数(h)`(同一従業員・同一月・同一PJの複数行は工数を合算)
    - 冪等性保証: 対象年月の既存データを `78_pj_pl` からクリアしてから新規書き込み(クリア方法はPhase 1で `420_project_profitability.js` を確認した書き込みパターンに従う)
    - Human-in-the-Loopポリシー: 計算結果の直接書き込みか中間シート経由の2段階確認フローかはPO判断待ち(現状は直接書き込みをデフォルトとして実装し、後から2段階フローに差し替え可能な設計とする)
- **影響範囲**: `000_infra/002_constants.js`・`200_data/202_repository.js`・`400_domain/420_project_profitability.js`・`100_config/101_sys_config.js`(メニュー追加)の変更量概算
- **注意事項**:
  - `Constants.SHEET_DEFAULTS` の `70_bud_resource` エントリ変更は既存の `smartAddRow` / `bulkAssignDefaults` 挙動に影響する。スキーマ変更前に既存データのバックアップとマイグレーション方針(800番台スクリプトの新設要否)を確認すること
  - `78_pj_pl` は DDL (setupAllSchemas) で管理されない動的生成タブのため、スキーマ変更の影響範囲を事前確認すること(CLAUDE.md の「DDLで管理されないタブ」参照)
  - 列参照はヘッダー名ベース(`indexOf` / `buildHeaderIndex_`)。列番号ハードコード禁止(CLAUDE.md コーディング規約)
  - 有効フラグ = FALSE の行は全処理でスキップ(CLAUDE.md 規約)

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

- **エッジケーステーブル**(テーブル形式: 条件 / 動作 / 理由):

  | 条件 | 動作 | 理由 |
  |------|------|------|
  | `CFG_MONTHLY_WORKING_HOURS` が 0 または未設定 | 時間単価 = 0 として計算継続。`Utils.logInfo` で警告出力 | ゼロ除算防止 |
  | 従業員の月額給与・報酬が 0 | 時間単価 = 0 として計算継続 | 無報酬役員等の正常ケース |
  | `70_bud_resource` の従業員名が `22_bud_headcount` に存在しない | 該当工数を計算対象から除外。`Utils.logInfo` で警告出力 | マスタ不整合の検知 |
  | 存在しないPJコードが入力された場合 | 該当工数を計算対象から除外。`Utils.logInfo` で警告出力 | PJマスタ不整合の検知 |
  | 工数にマイナス値が入力された場合 | 0 として扱い計算継続 | 不正値の防御的処理 |
  | 特定月の従業員合計工数が `CFG_MONTHLY_WORKING_HOURS` × 2 を超過 | 計算は実行するが `Utils.logInfo` で超過警告を出力 | 入力ミスの可能性を通知 |
  | 同一従業員・同一月・同一PJのデータが複数行存在 | 全行の工数を合算して計算 | 分割入力を許容 |

- **実データ検証**(MCP等で確認すべき項目):
  - `70_bud_resource` の実際の列構造(DDL定義の `稼働率(%)` フィールドと実シートの一致確認)
  - `22_bud_headcount` の社会保険料フィールド名(`Constants.SHEET_DEFAULTS` の定義と実シートヘッダーの一致確認)
  - `03_sys_params` に `CFG_MONTHLY_WORKING_HOURS` キーが存在するか(存在しない場合は手動追加が必要)
  - `78_pj_pl` の列構造(人件費科目行の位置とクリア対象範囲の確認)

- **関連ドキュメント**: Phase 1で発見した関連仕様書・アーキテクチャドキュメントへのリンク

- **人間が検討すべき事項**:
  - `TODO_future.md` のS-33から転記した既存の検討事項(工数入力粒度・入力UI・勤怠管理システム連携等)
  - 【追加】計算結果の直接書き込みか2段階確認フロー(中間シート出力→人間確認・承認→本番反映)かの選択。Human-in-the-Loopポリシー(CLAUDE.md・PRD)に照らしPO判断が必要
  - 【追加】`70_bud_resource` 既存データ(稼働率ベース)のマイグレーション方針。スキーマ変更後の既存データ取り扱いをPOと合意し、必要に応じて `800_ops/` に冪等マイグレーションスクリプトを新設する

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

**【注意】実装プロンプトはバッククォートのコードブロックで囲まず、行頭4スペースインデントで出力すること。**

以下の内容を記述する:

- **実装プロンプト(Claude Code 用)**: Step分割(Step 1 / Step 2 / Step 3)それぞれを別プロンプトとして記述。各プロンプトには以下を含める:
  - 役割定義(シニア開発者として案件S-33 Step Nを実装)
  - 実行前タスク(読み込むべきファイルと確認ポイント)
  - 修正対象ファイルと変更種別(「のみ」「への追記」を明示)
  - 実装内容(具体的手順・コード例)
  - 制約(列番号ハードコード禁止・有効フラグFALSEスキップ必須・既存Repository関数の改変禁止等)
  - エッジケース(Step 2-3aのテーブルを転記)
  - 動作確認(`npm run push:dev` 後の検証手順)
  - 拡張思考の使用状況テーブル

- **推奨実行モデルテーブル**:

  | 工程 | 推奨モデル | 理由 |
  |------|-----------|------|
  | Step 1: スキーマ変更・Repository新設 | Claude Sonnet | 既存パターンへの準拠判断が必要 |
  | Step 2: 時間単価算出ロジック実装 | Claude Sonnet | 会計ロジックの理解と既存関数の適用判断が必要 |
  | Step 3: PJ損益シート連携・冪等性実装 | Claude Opus | 複数ファイル横断の設計判断・冪等性設計が必要 |

- **変更履歴テーブル**: `| 2026-04-19 | 初版作成 |`

### Step 2-4: 仕様書作成プロンプトの記録(File Edit または Bash)
末尾に `<details><summary>展開して表示</summary>` ブロックを設け、この `<instruction>` タグで囲まれたプロンプト全文を記録する。

---

## Phase 3: 保存と記録

### 3-A: `docs/_config.json` への追記(必須)
`docs/_config.json` の `nav` 配列の §E.5(FP&A・レポーティング)に以下を追加する:
{ "file": "dev/dev_mas-105_workforce_costing.md", "title": "E.5.X S-33 工数入力・PJ原価連携" }
追記後、JSON構文エラーがないことを `cat docs/_config.json | python3 -m json.tool` 等で確認する。

### 3-B: changelog への追記
`docs/_internal/changelog.md` の先頭行(ヘッダーの直後)に追記:
| 2026-04-19 | [dev_mas-105_workforce_costing.md](dev_mas-105_workforce_costing.md) | 初版作成。稼働率ベースから工数ベース実費計算への移行仕様。ResourceRepository・HeadcountRepository新設、420_project_profitability.jsのロジック置換。 |

### 3-C: コミット&プッシュ
git add docs/dev/dev_mas-105_workforce_costing.md docs/_internal/changelog.md docs/_config.json
git commit -m "docs: S-33 工数入力・PJ原価連携の開発仕様書を作成

稼働率ベースから工数ベース実費計算への移行仕様。
ResourceRepository・HeadcountRepository新設、420_project_profitability.jsのロジック置換。

https://claude.ai/code/session_XXXXX"
git push -u origin {現在のブランチ}