MAS-006: PJ別管理会計の共通費配賦(MAS-117 丸め誤差解消 統合)
概要
| 項目 | 内容 |
|---|---|
| 案件ID | MAS-006(実装済・リバース・ドキュメント化)+ MAS-117(未着手・丸め誤差解消の追加改修)統合版 |
| カテゴリ | FP&A / PJ別管理会計 |
| Phase | Phase 2 |
| 優先度 | P1(MAS-006 は完了扱いだが MAS-117 は P2★★) |
| 所要時間 | 2-3時間(既存コードの MAS-117 対応改修 + ガード追加) |
| 対象ファイル | 400_domain/420_project_profitability.js(既存、Step 3「共通費配賦」セクションの改修)、docs/spec/spec_project_accounting.md(仕様追記) |
| 前提案件 | MAS-006(実装済)、MAS-085(78_pj_pl と 92_fs_pl の整合性チェック・実装済 L761-789) |
| 後続案件 | MAS-007(配賦基準の多次元化:部門×PJなど)、MAS-008(Cash Runway:PJ別ランウェイ拡張) |
目的
MAS-006「PJ別管理会計の共通費配賦」は既に 420_project_profitability.js に実装済(buildProjectProfitability 関数)だが、以下 2 点が未対応で業務運用に支障をきたしている:
- 丸め誤差による全社一致の破綻(MAS-117): 各配賦先 PJ への按分時に
Math.round()を適用した結果、残差(元の共通費総額 − Σ各PJ配賦額)が 1 円〜439 円規模で累積し、MAS-085 の整合性チェック(L761-789)で全社 P/L(92_fs_pl)と PJ 別 P/L(78_pj_pl)の営業利益が一致しない問題が頻発 - エッジケースの堅牢性不足: 分母(配賦基準値の合計)が 0 の場合のゼロ除算、共通費自体がマイナス(経費戻入)の場合の符号保持、特定 PJ の基準値がマイナスの場合の下限補正が未実装
本仕様では既存実装の公式ドキュメント化と、MAS-117 の残差寄せ方式+各エッジケース対応を加えた改修設計を統合して提供する。ゼロからの作り直しではなく、リバースエンジニアリング済の現行ロジックに最小限の局所的改修を加える方針。
現在のコード
1. エントリ関数と全体フロー(400_domain/420_project_profitability.js)
関数 buildProjectProfitability() が L7 から始まり、3 段階で動作する:
| ステップ | 処理 | 行範囲 | 出力タブ |
|---|---|---|---|
| ステップ 1 | In-Memory データ展開(HC / Resource / INV から pjMap 構築) | L36-143 | —(内部) |
| ステップ 2 | 79_pj_monthly(限界利益の月次推移・配賦前)の出力 | L147-278 | 79_pj_monthly |
| ステップ 3 | 共通費配賦 + 78_pj_pl(PJ 横並び P/L)+ 77_pj_raw 出力 | L280-760 | 78_pj_pl / 77_pj_raw |
本仕様の改修対象はステップ 3 の L464-534「(I) 共通費配賦」ブロックに集中する。
2. 直接費 / 共通費の分離ロジック(L289-296、L502)
// (G) 配賦元PJ名の集合
var srcPjs = {}; // 配賦元
var dstPjs = {}; // 配賦先
for (var pn in projMaster) {
if (projMaster[pn].allocType === '配賦元') srcPjs[pn] = true;
else if (projMaster[pn].allocType === '配賦先') dstPjs[pn] = true;
}
14_mst_project(MST_PROJ)の 「配賦区分」列が判定基準配賦元= 共通費の発生元 PJ(本社・バックオフィス・全社費用)。例:指定なし_共通費など配賦先= 収益を生む実案件 PJ。按分対象対象外= 78_pj_pl に列表示しない PJ(例: 休眠案件)
3. 配賦基準(ドライバ)とルール(L510-528)
28_bud_allocation(BUD_ALLOC)の 科目名 × 按分方法 定義に従い、以下 5 方式で按分比率 ratio を算出:
| 方式 | 分母 | ソース |
|---|---|---|
売上高比(default / 営業外強制) | totalDstSales(全配賦先 PJ の売上高合計) | 32_wrk_invoice から集計 |
工数比 | totalDstWorkload(全配賦先 PJ の稼働率合計) | 27_bud_resource の稼働率(%) |
労務費比 | totalDstLabor(全配賦先 PJ の労務費合計) | 22_bud_headcount × 27_bud_resource |
均等割 | dstCount(配賦先 PJ 数) | — |
手動 | 28_bud_allocation の PJ 列別 rate(合計 100% 必須) | D 列以降 |
営業外損益(non_op_income / non_op_expense)は method='売上高比' に強制上書き(L511-513)。
4. 現状の配賦計算(L529)— MAS-117 の問題発生箇所
var allocated = Math.round(totalCost * ratio);
allocatedByPj[dp3][acct] = (allocatedByPj[dp3][acct] || 0) + allocated;
rawRows.push([dp3, '通期', acctInfo2.disp, acctInfo2.dispAcct, acct, -allocated, '配賦', '共通費(' + method + ')']);
問題: Math.round() 適用後の allocated を単純合計すると、元の totalCost と一致しない。配賦先 PJ が N 件あれば最大 ± N/2 円の誤差が発生(実運用で 1 円〜439 円の累積を観測、MAS-085 チェックで検出)。
5. 出力タブと MAS-085 整合性チェック(L761-789)
buildProjectProfitability 末尾で以下を検証:
var diff = Math.abs(opProfit78 - opProfit92);
if (diff > 1) { /* 警告ダイアログ */ }
本仕様 MAS-117 対応後: diff === 0 となる(ただし分母 0 のフォールバック分は ± 1 円以内の丸めを許容)。
6. 配賦元の符号処理(L504-506)— 潜在バグ
var totalCost = Math.abs(srcAccts[acct]);
if (totalCost === 0) continue;
問題: Math.abs() で絶対値化されるため、経費戻入(マイナス共通費)が正の配賦として扱われる(例: -100,000 の戻入が 100,000 として各 PJ に負担として按分される)。本仕様で符号保持に改修する。
修正方針
既存 buildProjectProfitability の「(I) 共通費配賦」ブロック(L464-534)に限定した局所改修。新規関数の追加ではなく既存ループ内の計算ロジックを書き換える。
Step 1: 残差寄せロジック(MAS-117 対応)
設計方針
1 科目 × 1 按分方法ごとに以下を行う:
- 全配賦先 PJ に対して
ratioを計算 - 各 PJ の
allocated = Math.round(totalCost * ratio)を一度配列に蓄積 - 残差
residue = totalCost - Σallocatedを計算 - 基準値(ratio 算出に使った分母要素:
dstSales[dp]等)が最大の PJにresidueを加算 - 加算後の値を最終配賦額として
allocatedByPjに記録
コード例(L502 からのループを以下に置換)
for (var srcPj in srcPjs) {
var srcAccts = pjAcctMap[srcPj] || {};
for (var acct in srcAccts) {
var totalCostSigned = srcAccts[acct]; // 符号保持(MAS-117 副次対応)
if (totalCostSigned === 0) continue;
var allocSecId = classifyAcct_(acct);
if (allocSecId === null) continue;
var isNonOp = (allocSecId === 'non_op_income' || allocSecId === 'non_op_expense');
var rule = allocRules[acct] || {};
var method = isNonOp ? '売上高比' : (rule.method || '売上高比');
// --- MAS-117: 分母ゼロ時のフォールバック ---
var fallbackApplied = false;
if (method === '売上高比' && totalDstSales <= 0) { method = '均等割'; fallbackApplied = true; }
if (method === '工数比' && totalDstWorkload <= 0) { method = '均等割'; fallbackApplied = true; }
if (method === '労務費比' && totalDstLabor <= 0) { method = '均等割'; fallbackApplied = true; }
var acctInfo2 = acctMaster[acct] || { disp: '不明', dispAcct: acct };
// --- MAS-117: 基準値の取得(負値は 0 で clamp)+ 各 PJ の配賦額算出 ---
var dstKeys = Object.keys(dstPjs);
var basisArr = [], allocArr = [];
var totalBasis = 0;
dstKeys.forEach(function(dp3) {
var b = 0;
if (method === '売上高比') b = Math.max((dstSales[dp3] || 0), 0);
else if (method === '工数比') b = Math.max((dstWorkload[dp3] || 0), 0);
else if (method === '労務費比') b = Math.max((dstLabor[dp3] || 0), 0);
else if (method === '均等割') b = 1;
else if (method === '手動') b = (rule.manual && rule.manual[dp3]) ? rule.manual[dp3] : 0;
basisArr.push(b);
totalBasis += b;
});
// 全基準値がゼロ(均等割フォールバック後も全てゼロはあり得ないが防御的に)
if (totalBasis <= 0) {
// 全 PJ に均等に割り当てる緊急フォールバック
for (var ei = 0; ei < dstKeys.length; ei++) basisArr[ei] = 1;
totalBasis = dstKeys.length;
}
// 各 PJ に按分(符号保持した totalCostSigned を使用)
var sumAlloc = 0;
for (var i = 0; i < dstKeys.length; i++) {
var ratio = basisArr[i] / totalBasis;
var a = Math.round(totalCostSigned * ratio);
allocArr.push(a);
sumAlloc += a;
}
// --- MAS-117 本丸: 残差を「基準値最大の PJ」に寄せる ---
var residue = totalCostSigned - sumAlloc;
if (residue !== 0) {
var maxIdx = 0;
for (var mi = 1; mi < basisArr.length; mi++) {
if (basisArr[mi] > basisArr[maxIdx]) maxIdx = mi;
}
allocArr[maxIdx] += residue;
}
// --- 配賦結果の記録 ---
for (var wi = 0; wi < dstKeys.length; wi++) {
var dp3 = dstKeys[wi];
var allocated = allocArr[wi];
allocatedByPj[dp3][acct] = (allocatedByPj[dp3][acct] || 0) + allocated;
// rawRows の符号は現行と整合: 配賦は PJ から見た費用(負値)として出力
// totalCostSigned が負(戻入)なら allocated も負 → rawRows 上は -allocated = 正 = 戻入
rawRows.push([dp3, '通期', acctInfo2.disp, acctInfo2.dispAcct, acct, -allocated, '配賦', '共通費(' + method + (fallbackApplied ? '・フォールバック' : '') + ')']);
}
}
}
Step 2: ゼロ除算ガードとフォールバック
Step 1 のコード内で fallbackApplied として既に実装済。方針:
| 条件 | 元の method | フォールバック method |
|---|---|---|
totalDstSales <= 0 かつ method='売上高比' | 売上高比 | 均等割 |
totalDstWorkload <= 0 かつ method='工数比' | 工数比 | 均等割 |
totalDstLabor <= 0 かつ method='労務費比' | 労務費比 | 均等割 |
method='手動' かつ manual 合計 = 0 | 手動 | 均等割 |
均等割フォールバック適用時は rawRows の「配賦区分」列に 共通費(売上高比・フォールバック) のように明示し、後日の追跡を可能にする。
Step 3: 配賦元符号保持・基準値マイナス補正
配賦元符号保持
現行 var totalCost = Math.abs(srcAccts[acct]);(L504)を var totalCostSigned = srcAccts[acct]; へ変更。経費戻入(マイナス)はそのまま負値として按分され、各 PJ への「マイナス配賦」(費用戻入)として記録される。
基準値マイナス補正
売上値引き等で特定 PJ の dstSales[dp] がマイナスになる場合、basisArr[i] を Math.max(value, 0) で 下限 0 に clamp。該当 PJ には配賦されず、残り PJ に redistribution される(合計 totalBasis が縮む)。
利益率(非加算)の扱い
現行 L185・L193・L219・L225 で s === 0 ? 0 : (gm / s) として合計から再計算済み(加算しない)。本改修では touch しない。失敗パターン #1(非加算指標の Total 壊れ)は既存実装で回避されている。
影響範囲
| 変更対象 | 変更内容 | 変更量 |
|---|---|---|
400_domain/420_project_profitability.js | L498-534「(I) 共通費配賦」ブロック全体を置換 | ~60 行(置換・実質追加は +30 行) |
docs/spec/spec_project_accounting.md | MAS-006 / MAS-117 対応の仕様追記 | +30 行 |
- 既存タブ構造への影響なし: 78_pj_pl / 79_pj_monthly / 77_pj_raw の列構成は不変
- 他関数・他モジュールへの影響なし:
buildProjectProfitability内の局所改修のみ - MAS-085 整合性チェック(L761-789)との関係: 本改修により
diff = 0となるため、警告ダイアログは原則発火しなくなる - rawRows の「配賦区分」文字列の後方互換:
共通費(売上高比)は不変、フォールバック時のみ末尾に・フォールバックが付く
注意事項
- 残差寄せの方向性(符号):
residue = totalCostSigned - sumAllocは負になる場合もある(丸めで過剰配賦時)。そのまま加算するため、最大 PJ に負の残差が乗ることもあり得る。これは意図された挙動(整合性優先) Math.max(value, 0)での clamp: 基準値の下限 0 補正はマイナス PJ を「除外」するのと同義。該当 PJ はそのラウンドで配賦を受けないが、符号方向の配賦がまったく発生しないわけではない(totalCostSigned側の符号は別軸)手動方式のゼロ合計は通常ありえない:loadAllocRules_(L842-868)の L865 でMath.abs(totalRate - 1.0) > 0.001をエラーとしているため、手動ルールでは合計 100% が保証済。ただし DDL 変更で設定が崩れた場合の防御として同じガードを残す配賦元PJ 自体の P/L は B 列「配賦元合計」に集約表示: 78_pj_pl の B 列は表示のみで、全社合計列(最右)には含めない(L692-698)。本改修は B 列の表示を変えない- 残差寄せの対象は「配賦先 PJ のみ」:
dstPjsのみをループ。配賦元 PJ には寄らない(自己寄せは無意味) rawRowsの-allocated符号: 従来どおり費用側は負値で記録(78_pj_pl 側でMath.abs()等で符号合わせ)。本改修で符号処理を変えた結果、戻入科目は正値で rawRows に現れる(想定どおり)- 配賦基準値の計算順序:
totalDstSales等は L466-495 で事前計算済。本改修はそれらの値を再利用するのみ(二重計算しない) - テスト観点(必須):
- 既存実運用データで MAS-085 整合性チェック(L779-787)の
diffが 0 に落ちること - 戻入科目(マイナス金額)を含む月で PJ 別 P/L が正しく符号反映されること
- 配賦先 PJ が 1 件のみのケース・売上 0 月(フォールバック発火)のケース
- 既存実運用データで MAS-085 整合性チェック(L779-787)の
- ログの強化(付随推奨): フォールバック発火回数と寄せた残差合計を
Utils.logInfoで出力すると運用監査に有用(実装プロンプト参照) - 関連 DDL シート
28_bud_allocation: DDL 更新で列がクリアされる可能性あり(CLAUDE.md「28_bud_allocation はDDLスキーマが3列のみ。D列以降のPJ別配賦率はDDL実行でクリアされる可能性あり」)。この既存リスクは本改修の範囲外だが、仕様書の「人間検討事項」に明記 - 列インデックスのハードコード禁止: 本改修範囲は
pjAcctMap/allocatedByPjの JSON キー操作のため列インデックス依存は無し。ただしrawRowspush 時の配列順序は現行 8 要素を厳守する(失敗パターン #18 系への配慮)
エッジケース
| # | 条件 | 表示値 / 処理 | 理由 |
|---|---|---|---|
| 1 | 全配賦先 PJ の売上高合計が 0(totalDstSales <= 0)で method='売上高比' | method を 均等割 にフォールバック、rawRows 配賦区分に ・フォールバック 付記 | ゼロ除算(Infinity)の防止。配賦漏れを起こさないため均等に振る |
| 2 | 同様に工数合計 0・労務費合計 0 | 同じく均等割フォールバック | 全方式で一貫したゼロ除算ガード |
| 3 | 手動配賦率の合計が 100% でない | loadAllocRules_ で throw(既存実装 L865) | 誤入力の早期検出。本改修では touch しない |
| 4 | 共通費自体がマイナス(経費戻入) | 符号保持で負値のまま按分、各 PJ もマイナス配賦として記録 | 現行 Math.abs は戻入を正の費用に誤変換していたため、本改修で是正 |
| 5 | 特定 PJ の基準値がマイナス(売上値引き等) | 下限 0 で clamp(該当 PJ の basisArr[i] = 0 → 配賦ゼロ、他 PJ に redistribution) | マイナス基準値は「配賦率を食う」誤配賦になるため除外 |
| 6 | 配賦先 PJ が 1 件のみ | 全額をその 1 件に集中(ratio=1、residue=0) | 最小構成でも算出可能 |
| 7 | 残差が非ゼロ(totalCostSigned - sumAlloc ≠ 0) | 基準値が最大の PJ に residue を加減算 | MAS-117 本丸。全社一致を 1 円単位で保証 |
| 8 | 残差が多数の PJ にまたがる 2 円以上の累積 | 1 件の最大 PJ に集約(分散しない) | 単純・決定的・追跡容易。人間検討事項 2 で方式の合意を取る |
| 9 | 配賦元 PJ の totalCostSigned = 0 | continue(スキップ) | 0 円の按分は無意味 |
| 10 | 営業外損益科目(non_op_income / non_op_expense) | method='売上高比' に強制上書き(既存 L511-513) | 営業外は売上比でのみ配賦する業務ルール。本改修でも維持 |
| 11 | 利益率(限界利益率 / 売上総利益率 / PJ 営業利益率) | 合計列から再計算(既存 L185, L193, L219, L225 で対応済) | 非加算指標。失敗パターン #1 の回避 |
| 12 | 全基準値が 0(防御的ガード:フォールバック後も起こり得ない理論値) | 全 PJ に均等割 | 異常時でも配賦漏れを起こさない最後の砦 |
実データ検証(事前確認項目)
| 確認項目 | 確認方法 | 確認結果(Phase 1 で調査済) |
|---|---|---|
既存 buildProjectProfitability の関数構造 | 400_domain/420_project_profitability.js を Read | ✅ L7-797 全体構造確認済 |
| 配賦ロジックの現在位置 | 同ファイル L464-534「(I) 共通費配賦」 | ✅ 本改修の対象ブロック特定済 |
Math.round(totalCost * ratio) のボトルネック | L529 | ✅ MAS-117 の問題箇所特定済 |
Math.abs(srcAccts[acct]) の符号消失 | L504-505 | ✅ 戻入処理の潜在バグ特定済 |
14_mst_project の「配賦区分」列の値 | projMaster[pn].allocType | ✅ 配賦元 / 配賦先 / 対象外 の 3 値 |
28_bud_allocation の按分方法 | allocRules[acct].method | ✅ 5 方式(売上高比・工数比・労務費比・均等割・手動) |
| MAS-085 整合性チェックの閾値 | L780 if (diff > 1) | ✅ 1 円以内は許容(本改修で完全 0 を目指す) |
78_pj_pl の全社合計列位置 | L691-698 | ✅ 最右列(B 列=配賦元合計は除外) |
実行前に MCP 等で追加確認すべき項目:
- 現在の
MAS-085警告ダイアログで出ている差額(1〜439 円)の実測値を取得し、本改修後に 0 円となることを比較検証 28_bud_allocationの手動方式適用中の科目一覧と、手動合計が 100% になっているかの最終確認(DDL クリア後の再設定漏れ検出)配賦元PJ の実名一覧(指定なし_共通費など以外に存在するか)- 戻入(マイナス共通費)の発生件数と代表科目
プロダクトポリシー
全社 P/L との完全一致が絶対基準
- 78_pj_pl(PJ 別 P/L 合計) = 92_fs_pl(全社 P/L)の営業利益は 1 円も狂わずに一致すること
- 本改修後は MAS-085 チェックの
diff = 0が期待される。1 円以上の差が発生した場合は MAS-085 アラートで検知・修正 - 残差寄せの影響で特定 PJ の値が「期待値」から最大 ± 1 円程度ズレるケースは許容される(全社一致を優先)
階層的な利益可視化
78_pj_pl は「配賦前の限界利益(直接利益)」と「配賦後の PJ 営業利益」を階層的に表示(plSections 配列、L540-552 既存実装):
売上高-売上原価= 売上総利益- 売上総利益 -
販管費(直接)-労務費(工数配賦)= 限界利益(配賦前・PJ の真の利益貢献) - 限界利益 -
共通費配賦= PJ 営業利益(配賦後・全社 P/L と一致) - ± 営業外損益 = PJ 経常利益
本改修で階層構造は維持、最下段の数値精度のみ向上。
Human-in-the-Loop(既存)
MAS-085 警告ダイアログは依然として有効。本改修後も diff > 1 を検出した場合は経理担当者に UI でアラート → 原因調査 → 28_bud_allocation の設定を見直す運用フローを継続。
関連ドキュメント
| 仕様書 | 関連箇所 |
|---|---|
| spec/spec_project_accounting.md | PJ 別 P/L・共通費配賦の業務仕様。本仕様追記先 |
| dev_mas-085_consistency_check.md | 78 タブと 92 タブの整合性チェック(本改修の検証ゲート) |
| dev_mas-192_repository_module_split.md | 420_project_profitability.js への分割・リネーム経緯(旧 402) |
| dev_mas-024_bep_analysis.md | 非加算指標の Total 独自計算パターン(本仕様の利益率再計算と同じ原則) |
| dev_mas-008_cash_runway.md | 本仕様の PJ 別 P/L を入力として PJ 別ランウェイを算出する将来案件の土台 |
| CLAUDE.md | 「PJ別配賦の判定は === "仕訳振替" の完全一致」「28_bud_allocation はDDLでD列以降クリア可能性あり」等の運用規約 |
| failure_patterns.md | #1(非加算指標の Total 壊れ)、#2(ゼロ除算)、#18-#20(Read 裏取り) |
| TODO_future.md | MAS-006(実装済・L233)と MAS-117(未着手・L185)の案件定義 |
人間が検討すべき事項
| # | 項目 | 詳細 |
|---|---|---|
| 1 | 残差寄せの方針(最大 PJ に寄せる vs 比率再計算) | 本仕様は MAS-117 TODO_future.md L185 の選択肢 A「最大 PJ に残差を寄せる」を採用。B 案「按分比率を整数で再計算(Hamilton 方式等)」との比較で以下の利点: (1) 実装が単純・決定的、(2) 追跡容易(寄せた PJ を rawRows で確認可能)、(3) 最大 PJ は金額インパクトが相対的に小さい。デメリットとして毎回同じ PJ に寄るため、見かけ上の「不公平感」が残る |
| 2 | 均等割フォールバック適用時のユーザー通知 | 本仕様では rawRows に ・フォールバック 付記のみ。より強い通知として (a) MAS-085 ダイアログに統合表示、(b) 77_pj_raw に専用列追加、(c) 79/78 タブにバッジ表示 の選択肢あり。初期運用で頻度を観察してから決定 |
| 3 | 基準値マイナスの clamp vs そのまま按分 | 本仕様は下限 0 クランプ。代替案として「マイナス基準値でも比率計算に参加させる(負の配賦=費用戻入効果)」もあり得るが、業務意図が不明確になるため非推奨 |
| 4 | 手動配賦率の 100% 合計チェック緩和 | 現行 loadAllocRules_ L865 で 0.1% 以上のズレで throw。運用で頻繁にエラーになる場合は warning に降格する選択肢あり(推奨せず、厳密性優先) |
| 5 | 28_bud_allocation のマイグレーション自動化 | DDL 更新で D 列以降がクリアされる既存リスク(CLAUDE.md 記載)への対応策として、マイグレーションスクリプト化(8XX_migration_*)。本改修範囲外 |
| 6 | 配賦方式のログ強化 | フォールバック発火回数・寄せた残差合計を Utils.logInfo で出力すると運用監査に有用。別改修として段階適用の選択肢 |
| 7 | 戻入(マイナス共通費)の検証カバレッジ | 実データで戻入がある月での PJ 別 P/L が符号正しく表示されるか。QA 観点で重点検証対象 |
| 8 | 配賦元 PJ の複数化対応 | 現行コードは srcPjs を辞書として扱うため複数配賦元に対応可。ただしユーザー教育と運用ドキュメントの整備は別途必要 |
| 9 | 月次配賦 vs 通期配賦 | 現行は通期配賦(年月='通期' で rawRows に格納、L460/L531)。月次配賦への移行は業務要件変更レベルで影響大。MAS-177(多年度基盤)と同期して検討 |
実装プロンプト(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-006「PJ別管理会計の共通費配賦」の MAS-117「丸め誤差解消」改修を実装してください。
## 実行前タスク
以下のファイルを読み込み、既存構造を把握してください:
1. `docs/dev/dev_mas-006_project_overhead_allocation.md` — 本仕様書
2. `400_domain/420_project_profitability.js` — 特に以下:
- L7-797 全体構造
- L464-534「(I) 共通費配賦」— 改修対象の中核
- L502-534 の配賦ループ
- L504 `Math.abs(srcAccts[acct])` — 符号消失バグ
- L529 `Math.round(totalCost * ratio)` — 残差発生箇所
- L761-789「MAS-085 整合性チェック」— 本改修の検証ゲート
- L842-868 `loadAllocRules_` — 手動配賦率の 100% チェック
3. `CLAUDE.md` — 「PJ別配賦ルール」、有効フラグ判定、列参照ヘッダー名ベース
4. `docs/_internal/failure_patterns.md` — #1(非加算 Total 壊れ)、#2(ゼロ除算)、#18-#20(Read 裏取り)
5. `docs/dev/dev_mas-085_consistency_check.md` — 既存整合性チェックの挙動
## 修正対象ファイル
- `400_domain/420_project_profitability.js` — **L498-534 の「(I) 共通費配賦」ブロックを置換**(その他は touch しない)
- `docs/spec/spec_project_accounting.md` — MAS-006 / MAS-117 対応の仕様追記
## 実装内容
### A. `420_project_profitability.js` の L498-534 置換
現行:
for (var srcPj in srcPjs) {
var srcAccts = pjAcctMap[srcPj] || {};
for (var acct in srcAccts) {
var totalCost = Math.abs(srcAccts[acct]);
if (totalCost === 0) continue;
// ... ratio 計算 ...
var allocated = Math.round(totalCost * ratio);
allocatedByPj[dp3][acct] = (allocatedByPj[dp3][acct] || 0) + allocated;
rawRows.push([dp3, '通期', acctInfo2.disp, acctInfo2.dispAcct, acct, -allocated, '配賦', '共通費(' + method + ')']);
}
}
以下に置換(本仕様書 Step 1 のコード例を逐語的に適用):
for (var srcPj in srcPjs) {
var srcAccts = pjAcctMap[srcPj] || {};
for (var acct in srcAccts) {
var totalCostSigned = srcAccts[acct];
if (totalCostSigned === 0) continue;
var allocSecId = classifyAcct_(acct);
if (allocSecId === null) continue;
var isNonOp = (allocSecId === 'non_op_income' || allocSecId === 'non_op_expense');
var rule = allocRules[acct] || {};
var method = isNonOp ? '売上高比' : (rule.method || '売上高比');
// ゼロ除算フォールバック
var fallbackApplied = false;
if (method === '売上高比' && totalDstSales <= 0) { method = '均等割'; fallbackApplied = true; }
if (method === '工数比' && totalDstWorkload <= 0) { method = '均等割'; fallbackApplied = true; }
if (method === '労務費比' && totalDstLabor <= 0) { method = '均等割'; fallbackApplied = true; }
var acctInfo2 = acctMaster[acct] || { disp: '不明', dispAcct: acct };
// 基準値配列 + 按分額配列の構築
var dstKeys = Object.keys(dstPjs);
var basisArr = [], allocArr = [];
var totalBasis = 0;
dstKeys.forEach(function(dp3) {
var b = 0;
if (method === '売上高比') b = Math.max((dstSales[dp3] || 0), 0);
else if (method === '工数比') b = Math.max((dstWorkload[dp3] || 0), 0);
else if (method === '労務費比') b = Math.max((dstLabor[dp3] || 0), 0);
else if (method === '均等割') b = 1;
else if (method === '手動') b = (rule.manual && rule.manual[dp3]) ? rule.manual[dp3] : 0;
basisArr.push(b);
totalBasis += b;
});
// 全基準値 0 の防御的フォールバック
if (totalBasis <= 0) {
for (var ei = 0; ei < dstKeys.length; ei++) basisArr[ei] = 1;
totalBasis = dstKeys.length;
}
// 按分額の算出
var sumAlloc = 0;
for (var i = 0; i < dstKeys.length; i++) {
var ratio = basisArr[i] / totalBasis;
var a = Math.round(totalCostSigned * ratio);
allocArr.push(a);
sumAlloc += a;
}
// 残差寄せ(MAS-117 本丸)
var residue = totalCostSigned - sumAlloc;
if (residue !== 0) {
var maxIdx = 0;
for (var mi = 1; mi < basisArr.length; mi++) {
if (basisArr[mi] > basisArr[maxIdx]) maxIdx = mi;
}
allocArr[maxIdx] += residue;
}
// 結果の記録
for (var wi = 0; wi < dstKeys.length; wi++) {
var dp3 = dstKeys[wi];
var allocated = allocArr[wi];
allocatedByPj[dp3][acct] = (allocatedByPj[dp3][acct] || 0) + allocated;
rawRows.push([dp3, '通期', acctInfo2.disp, acctInfo2.dispAcct, acct, -allocated, '配賦', '共通費(' + method + (fallbackApplied ? '・フォールバック' : '') + ')']);
}
}
}
### B. ログ強化(任意・推奨)
改修後、配賦ループの直後にフォールバック集計ログを追加:
Utils.logInfo(FUNC, 'MAS-006/MAS-117: 共通費配賦完了 (残差寄せ後、MAS-085 整合性は本体末尾で検証)');
### C. `docs/spec/spec_project_accounting.md` への追記
末尾または既存の配賦ルール セクションに、本仕様の「計算式・残差寄せ方針・フォールバック規約」を業務表現で記載。
## 制約
- **改修範囲は L498-534 のみ**。その他(ステップ 1・2・3 の他部分、79_pj_monthly 出力、77_pj_raw 出力、MAS-085 チェック)は touch しない
- **`rawRows` の配列順序と要素数(8 要素)は厳守**
- **符号方針**: `totalCostSigned` は符号保持、`allocArr[i]` も符号保持、`rawRows` は `-allocated` で従来どおり
- **`Math.round(totalCostSigned * ratio)` のまま**(`Math.floor` や `toFixed` に変えない)
- **フォールバック発火は `・フォールバック` 付記で明示**(既存運用と区別可能に)
- **列インデックスのハードコード禁止**(本範囲は JSON キー操作のため該当なしだが、念のため)
## エッジケース
1. 分母ゼロ(売上 0・工数 0・労務費 0)→ 均等割フォールバック
2. 全基準値 0(防御的) → 全 PJ に均等割
3. 戻入(マイナス共通費)→ 符号保持で負の配賦
4. 特定 PJ 基準値マイナス → 下限 0 clamp で除外、他 PJ に redistribution
5. 配賦先 1 件 → 全額そこに集中
6. 残差 ≠ 0 → 基準値最大 PJ に加減算
7. 営業外損益 → `method='売上高比'` 強制(既存ルール維持)
8. 手動配賦合計 ≠ 100% → `loadAllocRules_` で throw(既存ルール維持)
## 実データ検証
- `420_project_profitability.js:498-534` の現在コードを Read 済
- 既存 MAS-085 警告ダイアログで差額が 1〜439 円発生していることが確認済
- `totalDstSales` / `totalDstWorkload` / `totalDstLabor` は L466-495 で事前計算済
## 動作確認
`npm run push:dev` 後:
1. メニュー「💰 プロジェクト別 採算(限界利益) + 共通費配賦」を実行
2. **検証 (MAS-117 本丸)**: MAS-085 警告ダイアログ(`buildProjectProfitability` 末尾)が発火しない、または `diff = 0` でログに出る
3. **検証**: 78_pj_pl の全社合計列(最右)と 92_fs_pl の営業利益が完全一致
4. **検証**: 戻入(マイナス共通費)科目が PJ 別にマイナス配賦として正しく反映
5. **検証**: 売上 0 月(期首月等)でも配賦が均等割フォールバックで実行
6. **検証**: 77_pj_raw の配賦区分に `共通費(売上高比・フォールバック)` が該当月で出現
7. **検証**: 手動配賦(`method='手動'`)の科目で、従来と同じ比率(100% 基準)で按分され、残差は最大率 PJ に寄る
8. **検証**: 配賦先 PJ が 1 件のみの試験データで全額集中
9. **検証**: 28_bud_allocation の DDL 実行で D 列以降がクリアされた直後にランすると throw される(既存挙動、本改修で変えない)
### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|:--------:|------|
| 残差寄せアルゴリズム | あり | 符号保持と最大 PJ 選定の正確性検証 |
| ゼロ除算フォールバック | あり | 4 方式 × 分母 0 パターンの網羅 |
| 基準値 clamp | なし | `Math.max(value, 0)` の定型 |
| 既存コードへの置換 | なし | 仕様書で置換範囲が L498-534 と特定済 |
推奨実行モデル
| 工程 | 推奨モデル | 理由 |
|---|---|---|
| 仕様書作成(本ドキュメント) | Claude Opus 4.6 | リバース・ドキュメント化 + MAS-117 改修設計 + 多数のエッジケースの複合設計で高い推論力が必要 |
| 実装 | Claude Sonnet 4.6 | 仕様書で置換コードが完全定義済み。既存関数内の局所改修で中程度の判断(符号保持・clamp・残差寄せの正確な実装) |
| 動作確認 | ユーザー手動 | 実運用データで MAS-085 diff = 0 達成の確認が主。78 / 92 の突合と戻入ケースの目視確認 |
変更履歴
| 日付 | 変更内容 |
|---|---|
| 2026-04-18 | 初版作成(MAS-006 仕様の公式ドキュメント化および MAS-117 丸め誤差解消機能の追加) |
仕様書作成プロンプト(再現性・監査性のため記録)
仕様書作成プロンプト(再現性・監査性のため記録)
展開して表示
<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」として、上記の原則と以下のフェーズに従い、案件 MAS-006「PJ別管理会計の共通費配賦」および関連案件 MAS-117「PJ別配賦計算の丸め誤差解消」を統合した高度化開発仕様書を作成してください。
## Phase 1: 実行前タスク(必読・必ずツールを使用して順次実行)
(※テキストでの状況報告は一切行わず、直ちにツールの使用を開始してください)
1. `docs/_internal/TODO_future.md` を検索し、案件 **MAS-006**(実装済み)および **MAS-117**(未着手)の「概要」「期待される効果」「人間が検討すべき事項」を特定・完全に把握する。
2. `CLAUDE.md` と `docs/_internal/failure_patterns.md` を読む。
3. 既存の配賦ロジックが実装されている `400_domain/402_project_profitability.js` (※MAS-192移行済みの場合は `420_project_profitability.js` 等)を読み、現在の配賦基準(ドライバ:売上高、稼働率等)と共通費の抽出ロジックをリバースエンジニアリングして把握する。
4. 影響を受けるデータアクセス層(`200_data/202_repository.js` の関連する Repository と、`000_infra/003_contracts.js` の関連する DTO)を読む。
5. 関連する定数・マスタ(`000_infra/002_constants.js`、`100_config/101_sys_config.js`)を読み、科目マスタから「直接費」「共通費」を判定するルールを確認する。
6. `docs/_internal/dev_spec_prompt_template.md` の Phase 2 構成と実装プロンプトフォーマットを読む。
7. ツール(MCP等)を使って、対象の `78_pj_pl` (プロジェクト別損益)と `92_fs_pl` (全社損益)の実データを比較し、MAS-117 で指摘されている丸め誤差(端数不一致)が現在どの程度発生しているか事前検証する。
## 既存実装の前提知識(車輪の再発明を防ぐ)
- 本案件は「既存の配賦ロジックを正確にドキュメント化」しつつ、「MAS-117 の丸め誤差解消とエッジケースの堅牢化を仕様に組み込む」リファクタリング設計書です。ゼロから配賦エンジンを作り直すわけではありません。
- マートへの集計出力は、シートの Range を直接操作するのではなく、必ずインメモリで2次元配列を構築してから一括書き込みする既存アーキテクチャを踏襲すること。
## Phase 2: 仕様書の分割作成
出力先: `docs/dev/dev_mas-006_project_overhead_allocation.md`
**【重要】絶対に1回のツール呼び出しで全内容を出力せず、以下の Step 2-1 〜 2-4 に厳密に分割して実行してください。**
### Step 2-1: 骨格の作成 (File Write)
対象ファイルに、仕様書テンプレートに準拠した見出し(`## 概要`, `## 目的`, `## 現在のコード`, `## 修正方針` 等)の骨格のみを Write ツールで作成して保存してください。本文は空で構いません。
### Step 2-2: 前半セクションの追記 (File Edit または Bash)
「概要」「目的」「現在のコード」「修正方針」「影響範囲」「注意事項」を追記してください。以下を必ず含めること:
- **アーキテクチャの決定事項**:
- 全社の費用から、各PJに直接紐付く「直接費」と、紐付かない「共通費(間接費)」をどのように分離しているか(対象科目の条件)を明記。
- **配賦基準(ドライバ)**: 現在コードで採用されているドライバ(例:PJ別売上高比率、直接作業時間比率など)を明文化する。
- **端数処理の丸め誤差解消(MAS-117対応・最重要)**: 各PJに按分した金額を `Math.round()` 等で丸めた結果生じる「残差(元の共通費総額 − Σ各PJ配賦額)」を算出する。そして、この残差を「配賦基準値が最も大きい(最大)プロジェクト」に加減算して帳尻を合わせ、全社合計(92_fs_pl)と完全一致させるロジックを仕様として確定する。
### Step 2-3a: エッジケース〜人間検討事項の追記 (File Edit または Bash)
「エッジケース」「実データ検証」「関連ドキュメント」「人間が検討すべき事項」を追記してください。
- **エッジケース(テーブル形式で必須)**:
1. **ゼロ除算の防止**: 当月の全PJの配賦基準値(分母)合計が 0 の場合、共通費配賦でゼロ除算(Infinity)が発生しないよう、PJ数での均等割り等にフォールバックするロジック。
2. **共通費自体のマイナス**: 経費戻入などで共通費総額がマイナスの場合の配賦挙動(配賦率に従いそのままマイナス配賦)。
3. **基準値のマイナス**: 売上値引き等で特定PJの基準値がマイナスになる場合の除外・下限ゼロ補正ルール。
- **計算式・フィルタの設計要件**:
- 営業利益率などの「利益率」指標は比率(非加算項目)であるため、合計列やマート出力時の集計において単純加算(`filterValues`)を絶対に行わず、必ず配賦後の合計額から再計算(`filterWithRecalcTotal`相当)を行う方針を明記する。
- **プロダクトポリシー**:
- 全社P/LとPJ別P/Lの合計額が「1円の狂いもなく一致すること」をシステム要件の絶対基準とする。配賦前の「限界利益(直接利益)」と配賦後の「営業利益」を階層的に表示し、PJの真の利益貢献を可視化する。
- **実データ検証**:
- Phase 1(Step 7)で確認した 78タブと 92タブ間の実データにおける丸め誤差の状況を記載する。
- **人間が検討すべき事項**:
- 残差を最大PJに寄せる方式についての合意確認。
### Step 2-3b: 実装プロンプト〜変更履歴の追記 (File Edit または Bash)
「実装プロンプト(Claude Code用)」「推奨実行モデル」「変更履歴」を追記してください。
- **実装プロンプト**:
- バッククォート(```)で囲まず、全行を行頭4スペースインデントで出力すること。
- 既存コードに対する MAS-117(残差寄せ)対応およびゼロ除算ガードの追加リファクタリングを指示する内容として構成すること。
- **変更履歴**: 当日の日付で「初版作成(仕様の公式ドキュメント化および丸め誤差解消機能の追加)」と記載する。
### 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>