MAS-117: PJ別配賦計算の丸め誤差解消
概要
| 項目 | 内容 |
|---|---|
| 案件ID | MAS-117 |
| カテゴリ | バグ修正 / PJ別損益 |
| Phase | Phase 2 |
| 優先度 | P2(★★) |
| 所要時間 | 30分 〜 1時間 |
| 対象ファイル | 400_domain/420_project_profitability.js(L498-534 の「(I) 共通費配賦」ブロックのみ) |
| 前提案件 | MAS-085(78_pj_pl と 92_fs_pl の営業利益整合性チェック・420_project_profitability.js L761-789 に実装済) |
| 関連仕様書 | dev_mas-006_project_overhead_allocation.md — 配賦ロジック全体を含む高度化統合版(本仕様書は MAS-117 の丸め誤差解消に絞ったミニマル改修の記述) |
目的
PJ別損益マート(78_pj_pl)の共通費配賦計算で各PJに按分するたびに Math.round() で発生する1円未満の丸め誤差が累積し、配賦先PJ数ぶんだけ残差(元の共通費総額 − Σ各PJ配賦額)が積み上がる。実運用では最大 ¥439 の残差が MAS-085 整合性チェック(diff > 1)で検出されている。
本改修では最大剰余法(Largest Remainder Method)による残差寄せを導入し、78_pj_pl の全社合計営業利益と全社P/L(92_fs_pl)の営業利益を1円の狂いもなく完全一致させる。
現在のコード
対象関数: buildProjectProfitability()(400_domain/420_project_profitability.js L7-797)
改修対象ブロック: (I) 共通費配賦(L464-534)、特に按分計算の核となる L516-533 のループ。
丸め誤差の発生箇所(L516-533 抜粋)
for (var dp3 in dstPjs) {
var ratio = 0;
if (method === '売上高比') {
ratio = totalDstSales > 0 ? (dstSales[dp3] || 0) / totalDstSales : 0;
} else if (method === '工数比') {
ratio = totalDstWorkload > 0 ? (dstWorkload[dp3] || 0) / totalDstWorkload : 0;
} else if (method === '労務費比') {
ratio = totalDstLabor > 0 ? (dstLabor[dp3] || 0) / totalDstLabor : 0;
} else if (method === '均等割') {
ratio = dstCount > 0 ? 1 / dstCount : 0;
} else if (method === '手動') {
ratio = (rule.manual && rule.manual[dp3]) ? rule.manual[dp3] : 0;
}
var allocated = Math.round(totalCost * ratio); // ← L529: 独立丸め
allocatedByPj[dp3][acct] = (allocatedByPj[dp3][acct] || 0) + allocated;
rawRows.push([dp3, '通期', acctInfo2.disp, acctInfo2.dispAcct, acct, -allocated, '配賦', '共通費(' + method + ')']);
}
問題: 各PJへの Math.round(totalCost * ratio) が独立に丸められるため、N件の配賦先PJがあれば最大 ± N/2 円の残差が生じる。残差は 78_pj_pl の全社合計列(L691-698で再計算)と 92_fs_pl の営業利益(dmCalcPl_() が算出した元値)のズレとして現れ、MAS-085 チェック(L779-780、閾値 ±1円)で diff > 1 となる。
配賦基準(ドライバ)
| 方式 | 分母変数 | 事前計算位置 |
|---|---|---|
売上高比(default / 営業外強制) | totalDstSales | L487-489 |
工数比 | totalDstWorkload | L483, L495 |
労務費比 | totalDstLabor | L491-493 |
均等割 | dstCount | L485 |
手動 | rule.manual[dp3](合計100%必須) | loadAllocRules_ |
各PJの基準値(dstSales[dp3] / dstWorkload[dp3] / dstLabor[dp3])は L484-496 で事前計算済み。本改修ではこれらを再利用する。
MAS-085 整合性チェックの現状(L761-789)
同じ buildProjectProfitability 関数内で 77/78 タブ出力直後に実行される(200_data/201_data_validator.js ではなく 420_project_profitability.js に同居)。diff > 1 のとき SpreadsheetApp.getUi().alert() で警告ダイアログを表示する。
冪等性設計の現状
78_pj_pl は関数実行のたびに clear() されず、sheet.getRange(1, 1, r, c).setValues(plOut) で左上から上書きされる(L735 付近)。77_pj_raw も同様に getRange().setValues(rawOut) で上書き(L755-758)。既存行数より出力行数が少ない場合、古い行が末尾に残存するリスクは従来どおり本改修の対象外。
修正方針
**最大剰余法(Largest Remainder Method)**による残差寄せを L516-533 のループに適用する。1科目 × 1按分方法ごとに以下の手順で按分する。
アルゴリズム
- 基準値配列の構築: 各配賦先PJの配賦基準値(
dstSales[dp3]等)を配列basisArrに格納。totalBasis = Σ basisArrを算出 - 仮配賦額の算出:
allocArr[i] = Math.round(totalCost * basisArr[i] / totalBasis)を各PJに計算(Math.roundのまま維持・既存挙動との差分最小化) - 残差の算出:
residue = totalCost − Σ allocArr(通常は|residue| ≤ 配賦先PJ数/2円) - 残差寄せ:
basisArrが最大のインデックスmaxIdxを走査で特定し、allocArr[maxIdx] += residue - 記録: 既存コードと同じ形式で
allocatedByPjとrawRowsに格納
置換コード例(L498-534 の置換イメージ)
// 配賦元PJの科目別金額を各配賦先PJに按分(S-45: 最大剰余法で残差寄せ)
var allocatedByPj = {};
for (var dp2 in dstPjs) allocatedByPj[dp2] = {};
for (var srcPj in srcPjs) {
var srcAccts = pjAcctMap[srcPj] || {};
for (var acct in srcAccts) {
var totalCost = Math.abs(srcAccts[acct]);
if (totalCost === 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 acctInfo2 = acctMaster[acct] || { disp: '不明', dispAcct: acct };
// (a) 基準値配列の構築
var dstKeys = Object.keys(dstPjs);
var basisArr = [];
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;
});
// (b) 配賦基準合計 ≤ 0 は配賦処理をスキップ(ゼロ除算回避)
if (totalBasis <= 0) continue;
// (c) 各PJの仮配賦額を算出
var allocArr = [];
var sumAlloc = 0;
for (var i = 0; i < dstKeys.length; i++) {
var ratio = basisArr[i] / totalBasis;
var a = Math.round(totalCost * ratio);
allocArr.push(a);
sumAlloc += a;
}
// (d) 残差寄せ: 基準値が最大のPJに差額を集約(S-45 本丸)
var residue = totalCost - 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;
}
// (e) 結果の記録(既存フォーマットを厳守)
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 + ')']);
}
}
}
既存挙動との差分(最小化)
| 項目 | 既存 | 改修後 | 差分 |
|---|---|---|---|
| 丸め関数 | Math.round | Math.round | 変更なし |
rawRows の8要素順序 | 保持 | 保持 | 変更なし |
Math.abs(srcAccts[acct])(符号消失) | あり | あり(本仕様では touch しない) | 変更なし(※戻入処理は MAS-006 仕様の範囲) |
営業外の method='売上高比' 強制 | あり(L511-513) | あり | 変更なし |
基準値下限 0 clamp(Math.max(..., 0)) | なし | あり | 新規 |
| 配賦基準合計 ≤ 0 のスキップ | なし(ratio=0でゼロ配賦) | continue で該当科目をスキップ | 新規(ゼロ除算回避の防御的ガード) |
| 残差寄せ | なし | 基準値最大PJに加減算 | 新規(MAS-117 本丸) |
影響範囲
| 変更対象 | 変更内容 | 変更量 |
|---|---|---|
400_domain/420_project_profitability.js | L498-534「(I) 共通費配賦」ブロックの置換 | ~35行(置換・実質追加 +10行) |
| 他ファイル | 変更なし | — |
- 既存タブ構造への影響なし:
78_pj_pl/79_pj_monthly/77_pj_rawの列構成は不変 - 他関数・他モジュールへの影響なし:
buildProjectProfitability内の局所改修のみ - MAS-085 整合性チェック(L761-789)との関係: 本改修により
diff = 0となり、警告ダイアログは原則発火しなくなる(閾値 ±1円は維持) rawRowsの「配賦区分」文字列:共通費(売上高比)等、従来フォーマットを維持(本仕様ではフォールバック付記は導入しない)
注意事項
- 残差の帰属先が構成で変わる: 基準値が最大のPJに差額を集約するため、PJ構成や月次売上の変動で残差の寄り先PJが月ごとに変わる。会計上は許容範囲(最大でも ±配賦先PJ数/2 円)だが、PJ別損益の「期待値」との乖離が1円単位で生じる点は運用合意事項として記録する(人間検討事項 #2 参照)
- 冪等性:
78_pj_pl/77_pj_rawはsetValues()で左上から上書き。既存行クリアは行わない(本改修の範囲外・既存挙動維持)。関数の再実行で配賦結果が重複することはない(allocatedByPjは関数内で新規生成) 28_bud_allocationのDDLクリアリスク: CLAUDE.md「28_bud_allocation はDDLスキーマが3列のみ。D列以降のPJ別配賦率はDDL実行でクリアされる可能性あり」。配賦率マスタはDDL実行後に再確認が必要。本改修は既存マスタをloadAllocRules_経由で読むのみで、マスタ自体は touch しない- 列参照はヘッダー名ベース: CLAUDE.md「列参照はヘッダー名ベース (
indexOf/buildHeaderIndex_)。列番号ハードコード禁止」。本改修はpjAcctMap/allocatedByPjの JSON キー操作のため列インデックス依存は無いが、rawRowspush 時の配列順序(8要素)は既存順を厳守 - 有効フラグ=FALSEのスキップ: CLAUDE.md 「有効フラグ=FALSE の行は全処理でスキップ」。本改修は上位で事前集計済の
dstSales/dstWorkload/dstLabor/srcAcctsを再利用するのみで、シート直読はしないため該当フィルタは既存コード(L476等)で担保済 Math.abs(srcAccts[acct])の符号消失(既知の潜在バグ): 経費戻入(マイナス共通費)が絶対値化される既存挙動は本仕様では touch しない。符号保持対応は MAS-006 統合仕様 dev_mas-006_project_overhead_allocation.md で扱う- ゼロ除算フォールバック: 本仕様では「配賦基準合計 ≤ 0」の科目をスキップする最小実装のみ採用。
均等割へのフォールバック付記は MAS-006 統合仕様で扱う - MAS-085 整合性チェックは別ファイルではなく同居: 本改修の動作確認対象となる整合性チェックは
200_data/201_data_validator.jsではなく400_domain/420_project_profitability.jsL761-789(関数末尾)に同居している
エッジケース
| # | 条件 | 表示値 / 処理 | 理由 |
|---|---|---|---|
| 1 | 配賦基準合計がゼロ(全PJ売上ゼロ等、totalBasis ≤ 0) | 当該科目の配賦処理をスキップ(continue)。共通費は未配賦のまま残る | ゼロ除算(ratio = x/0 = Infinity)防止。配賦漏れは MAS-085 整合性チェックで検出可能 |
| 2 | 配賦基準合計がマイナス(マイナスPJが支配して合計 ≤ 0) | 同上(同じ totalBasis ≤ 0 条件でスキップ) | 会計上マイナス配賦率は業務意味が不明確。配賦しない方が安全 |
| 3 | 配賦対象の共通費がゼロ(totalCost === 0) | 処理スキップ(既存 L506 の continue を維持) | 0円の按分は無意味 |
| 4 | 配賦対象PJが1件のみ(dstKeys.length === 1) | そのPJに100%配賦(ratio=1)、residue=0 | 丸め誤差は発生しない。残差寄せループは走るが加算値 0 |
| 5 | 特定PJの基準値がマイナス(売上値引き等) | 下限 0 で clamp(Math.max(value, 0)、basisArr[i]=0)。該当PJへの配賦はゼロ、他PJで redistribution | マイナス基準値は「配賦率を食う」誤配賦を招くため除外 |
| 6 | 残差が非ゼロ(totalCost - sumAlloc ≠ 0) | 基準値が最大のPJに residue を加減算(正負両方あり得る) | MAS-117 本丸。全社一致を1円単位で保証 |
| 7 | 残差がマイナス(Math.round の結果が過剰配賦) | そのまま負値を最大PJに加算(値が減る) | 意図どおり。整合性優先の設計 |
| 8 | 基準値最大PJが複数(タイ) | ループで最初に遭遇したインデックスに集約(決定的) | 非決定性を避ける。業務影響は最大 ± N/2 円以内 |
| 9 | 営業外損益科目(non_op_income / non_op_expense) | method='売上高比' に強制上書き(既存 L511-513 を維持) | 営業外は売上比でのみ配賦する業務ルール |
| 10 | 配賦先PJが0件(dstKeys.length === 0) | totalBasis = 0 となり条件 #1 に吸収されスキップ | 配賦不能。異常データの防御的ガード |
実データ検証
- MAS-085 既存アラート値: MAS-006 統合仕様記載のとおり、実運用で1円〜439円の残差を観測。本改修で
diff = 0到達が期待値 - 新規マスタデータ依存なし: 既存の
14_mst_project・28_bud_allocation・11_mst_account・32_wrk_invoice・27_bud_resource・22_bud_headcountを読み取るのみ - 事前計算値の再利用:
totalDstSales/totalDstWorkload/totalDstLabor/dstSales/dstWorkload/dstLaborは既存ループ(L466-495)で算出済。本改修では二重計算せず再利用
関連ドキュメント
| ドキュメント | 関連箇所 |
|---|---|
| dev_mas-006_project_overhead_allocation.md | MAS-006 配賦ロジック全体 + MAS-117 + 符号保持・フォールバック明示付記の統合高度化版。本仕様書は MAS-117 部分のみのミニマル独立記述 |
| dev_mas-085_consistency_check.md | 78タブ vs 92タブ営業利益の整合性チェック(本改修の検証ゲート)。実装位置は 420_project_profitability.js L761-789 |
| spec/spec_project_accounting.md | PJ別P/L・共通費配賦の業務仕様 |
| CLAUDE.md | 「28_bud_allocation はDDLでD列以降クリア可能性あり」「列参照はヘッダー名ベース」「有効フラグFALSEスキップ」等の運用規約 |
| _internal/TODO_future.md | MAS-117 案件定義(L221、MAS-006 仕様書統合) |
人間が検討すべき事項
| # | 項目 | 詳細 |
|---|---|---|
| 1 | 端数処理方式の選定(決定事項) | TODO_future.md L221 の選択肢から「A: 基準値が最大のPJに残差を全額加算する最大剰余法」を採用。本仕様書で決定事項として確定(即実装可)。代替案「B: 按分比率を整数で再計算(Hamilton 方式)」と比較した採用理由は、(1) 実装が単純・決定的、(2) 追跡容易(寄せたPJは allocArr[maxIdx] の加算値で確認可)、(3) 最大PJは金額インパクトが相対的に小さい |
| 2 | 差額の帰属先が月次で変わる運用合意 | PJ構成や月次売上の変動で「基準値最大PJ」が月ごとに変わり、残差の寄り先PJが変動する。理論上限は「配賦先PJ数 − 1 円」程度のため実務上の影響は軽微だが、PJ別P/Lの期待値との乖離が発生する点を経理/FP&A担当者と合意する |
| 3 | PJ数が多い場合の動作確認 | 配賦先PJが数十件規模の場合でも残差寄せが正しく1件に集約されることを確認(理論上は件数に依存しないが念のため) |
| 4 | 符号保持・フォールバック付記の統合改修への発展 | 本仕様書のミニマル改修後、MAS-006 統合仕様(dev_mas-006_project_overhead_allocation.md)で定義された符号保持(Math.abs 除去)と均等割フォールバック付記(共通費(...・フォールバック))の追加改修を段階適用するかを判断 |
実装プロンプト(Claude Code 用)
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-117「PJ別配賦計算の丸め誤差解消」を実装してください。
## 実行前タスク(コンテキスト読み込み)
1. `400_domain/420_project_profitability.js` を読み、以下を確認する:
- L464-534「(I) 共通費配賦」ブロック(改修対象の中核)
- L498-534 の配賦ループ(L516-533 の按分計算)
- L505 `Math.abs(srcAccts[acct])`(本仕様では touch しない)
- L529 `Math.round(totalCost * ratio)`(丸め誤差発生箇所)
- L761-789「MAS-085 整合性チェック」(本改修の検証ゲート。同じファイル内に同居)
2. `CLAUDE.md` の以下規約を確認:
- 列参照はヘッダー名ベース(列番号ハードコード禁止)
- 有効フラグ=FALSE の行は全処理でスキップ
- 28_bud_allocation のDDL実行でD列以降がクリアされる可能性
3. 関連仕様書: `docs/dev/dev_mas-085_consistency_check.md` / `docs/dev/dev_mas-006_project_overhead_allocation.md`
## 修正対象ファイル
`400_domain/420_project_profitability.js` のみ(L498-534 の「(I) 共通費配賦」ブロック置換)。他ファイル・他ブロックは touch しない。
## 実装内容
L498-534 の配賦ループを最大剰余法に変更する:
1. 各配賦先PJの基準値 `basisArr` を `Math.max(value, 0)` で下限0 clamp しつつ配列化、`totalBasis = Σ basisArr` を算出
2. `totalBasis <= 0` の科目は `continue`(ゼロ除算回避)
3. 各PJの仮配賦額を `allocArr[i] = Math.round(totalCost * basisArr[i] / totalBasis)` で計算し、`sumAlloc = Σ allocArr` を算出
4. 残差 `residue = totalCost − sumAlloc` を算出
5. `basisArr` が最大のインデックス `maxIdx` を走査で特定し、`allocArr[maxIdx] += residue`
6. 結果を `allocatedByPj` と `rawRows` に格納(`rawRows` の8要素順序は既存フォーマットを厳守)
### 置換コードの完全形は `docs/dev/dev_mas-117_allocation_rounding_fix.md` の「修正方針 > 置換コード例」を参照。
## 制約
- 改修範囲は L498-534 のみ。MAS-085 チェック(L761-789)・79/78/77 タブの出力ロジック・`classifyAcct_` 等は touch しない
- `rawRows` の配列順序(8要素)を厳守。「配賦区分」列は `共通費(売上高比)` 等、既存フォーマットを維持(本改修ではフォールバック付記しない)
- `Math.round(totalCost * ratio)` を維持(`Math.floor` / `Math.ceil` / `toFixed` に変えない)
- 営業外損益の `method='売上高比'` 強制(既存 L511-513)を維持
- `Math.abs(srcAccts[acct])` は本改修では touch しない(符号保持は MAS-006 統合仕様の範囲)
- 列参照はヘッダー名ベース(列番号ハードコード禁止)
- 有効フラグ=FALSE の行をスキップするロジックを維持(既存 L476 等)
## エッジケース
| 条件 | 動作 |
|------|------|
| `totalBasis <= 0`(配賦基準合計が0以下) | `continue` で該当科目をスキップ |
| `totalCost === 0`(共通費が0) | `continue` で該当科目をスキップ(既存挙動維持) |
| 配賦先PJ = 1件 | 全額を1件に集中、`residue = 0` |
| 特定PJの基準値マイナス | `Math.max(value, 0)` で下限0 clamp、他PJに redistribution |
| 残差が負 | そのまま最大PJに負値を加算(意図どおり) |
| 最大PJが複数タイ | ループで最初に遭遇したインデックスに集約(決定的) |
## 動作確認
1. `npm run push:dev` でデプロイ
2. メニュー「💰 プロジェクト別 採算(限界利益) + 共通費配賦」を実行
3. **検証(MAS-117 本丸)**: MAS-085 警告ダイアログ(`buildProjectProfitability` L761-789 内)が発火しない(`diff = 0`)ことを確認
4. **検証**: `78_pj_pl` の全社合計列(最右)と `92_fs_pl` の「✨ 営業利益」が完全一致することをスプレッドシート上で目視確認
5. **検証**: 売上0PJが存在する月(期首等)で関数を実行し、エラーなく処理が完了することを確認
6. **検証**: `77_pj_raw` の「配賦区分」が既存フォーマット `共通費(売上高比)` 等で維持されていることを確認
### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|:-------:|------|
| ファイル読み込み・L498-534 の挿入位置特定 | あり | 配賦ループの構造把握と境界の正確な特定 |
| 残差寄せアルゴリズム | あり | 符号・最大PJ選定・タイ処理の正確性検証 |
| ゼロ除算ガード(`totalBasis <= 0`) | なし | 定型の早期 return |
| 実装(置換コードの書き下し) | なし | 仕様書の置換コード例を逐語的に適用 |
推奨実行モデル
| 工程 | 推奨モデル | 理由 |
|---|---|---|
| 仕様書作成(本ドキュメント) | Claude Opus 4.6 | 配賦ロジックの理解と残差寄せアルゴリズムの設計判断 |
| 実装(MAS-117) | Claude Sonnet 4.6 | 置換範囲が仕様書で完全定義済み。既存パターンへの適用と挿入位置(L498-534)の特定が中程度の判断 |
| 動作確認 | ユーザー手動 | メニュー実行 + 78/92 タブの目視突合 + MAS-085 ダイアログの非発火確認が必要 |
変更履歴
| 日付 | 変更内容 |
|---|---|
| 2026-04-19 | 初版作成(最大剰余法による残差寄せの独立ミニマル仕様。MAS-006 統合仕様 dev_mas-006_project_overhead_allocation.md の MAS-117 部分を抽出・ミニマル化) |
仕様書作成プロンプト(再現性・監査性のため記録)
展開して表示
<instruction>
【タイムアウト回避・実行原則(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(骨格 ~20行) / 2-2(概要〜注意事項 ~300行) / 2-3a(エッジケース〜人間検討事項 ~200行) / 2-3b(実装プロンプト〜変更履歴 ~250行) / 2-4(`<details>`記録・最重量・必ず独立Step)に分割。
4. **各Stepで何を書くかを具体指示**: Phase 1で確定済みの内容を清書するだけ。出力時に設計判断を再考しない。
======================================================================
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者兼仕様書ライターです。
案件 S-45「PJ別配賦計算の丸め誤差解消」の開発仕様書を作成してください。
作成後は `docs/_config.json` の `nav` 配列の適切なセクションにも必ず追記してください。
---
## Phase 1: 実行前タスク(テキスト報告禁止。即座にツール実行)
### 1-A: 案件定義の読み込み
`docs/_internal/TODO_future.md` を検索し、**S-45** の行から以下を取得する:
- 案件名・カテゴリ・Phase・優先度・概要・期待効果・人間が検討すべき事項
### 1-B: プロジェクト規約の読み込み
`CLAUDE.md` を読み込み、以下を把握する:
- `28_bud_allocation` タブの注意事項(D列以降のPJ別配賦率はDDL実行でクリアされる可能性)
- コーディング規約の「列参照はヘッダー名ベース」「有効フラグ=FALSEのスキップ」ルール
- マイグレーション/冪等性に関する規約
### 1-C: 既存仕様書テンプレートの読み込み
`docs/dev/dev_mas-085_consistency_check.md` を1件読み込み、フォーマットを把握する(S-45は整合性チェックと連動する数値計算案件のため)。
### 1-D: 関連コードの調査(Grep → 必ずReadで裏取り)
**【Grep vs Readの原則】** 以下の全調査で、Grepは「どこにあるか」の発見まで。「どう書くか」の判断は必ずReadで行う。関数名・変数名・配列構造を記憶や推測で書かず、Read後に確認した文字列だけを仕様書に引用する。
調査対象ファイル(フルパスを使用):
1. **`400_domain/420_project_profitability.js`**(主要修正対象)
- 共通費を各PJに配賦しているループ処理の関数名・行番号を特定する
- 配賦基準として使用している指標(売上高か別の値か)を確認する
- 配賦結果を `78_pj_pl` に書き込む処理の冪等性設計(既存行クリアの有無)を確認する
- `Math.round` / `Math.floor` / `Math.ceil` 等の丸め処理の現状を確認する
2. **`200_data/201_data_validator.js`**(動作確認に使用)
- S-13で実装された78タブ vs 92タブの営業利益整合性チェック関数名・行番号を特定する(存在しない場合は「要調査」とメモする)
3. **`docs/dev/dev_mas-085_consistency_check.md`**(関連仕様書)
- 整合性チェックの呼び出し方・確認手順を把握する
### 1-E: Phase 1の確定事項をメモ
Read完了後、以下を確定させてからPhase 2に進む:
- 修正対象関数の正確な名前と行番号
- 配賦基準指標の正確な変数名/列名
- 整合性チェック関数の正確な呼び出し方
- 冪等性設計の現状(クリア処理があるか否か)
---
## Phase 2: 仕様書の分割作成
**出力先**: `docs/dev/dev_mas-117_allocation_rounding_fix.md`
### Step 2-1: 骨格の作成(Write)
以下の見出しのみ、本文空で作成する(~20行):
MAS-117: PJ別配賦計算の丸め誤差解消
概要
目的
現在のコード
修正方針
影響範囲
注意事項
エッジケース
実データ検証
関連ドキュメント
人間が検討すべき事項
実装プロンプト(Claude Code 用)
推奨実行モデル
変更履歴
仕様書作成プロンプト
### Step 2-2: 前半セクションの追記(Edit または Bash heredoc、~300行)
以下の内容を順番に埋める。Phase 1で確定した固有名詞(関数名・行番号)をリテラルで使用する:
- **[概要テーブル]** 案件ID=S-45 / カテゴリ=バグ修正 / 対象ファイル=`400_domain/420_project_profitability.js` / 前提案件=S-13 / (Phase・優先度・所要時間はTODO_future.mdから転記)
- **[目的]** PJ別損益(`78_pj_pl`)の共通費配賦計算で生じる1円未満の丸め誤差の累積を解消し、`78_pj_pl`の営業利益合計と全社P/L(`92_fs_pl`)の営業利益を完全に一致させる。
- **[現在のコード]** Phase 1のReadで特定した修正対象関数のコードスニペットを引用し、ファイル名と行番号を明記する。現状では単純な比率乗算後に`Math.round`(または丸め未処理)のまま各PJに割り振っており、複数PJへの配賦後に合計が元の共通費総額からずれる点を示す。
- **[修正方針]** 最大剰余法(Largest Remainder Method)による端数処理に変更する。以下のアルゴリズムを具体的に記述する:
1. 各PJへの配賦額を `Math.floor(共通費総額 × PJ配賦基準 / 配賦基準合計)` で仮計算する
2. 全PJの仮配賦額合計を算出する
3. `残差 = 共通費総額 − 仮配賦額合計` を計算する(1円以上の差額が生じる場合あり)
4. Phase 1で確認した配賦基準(売上高 or 実際の指標名)が最大のPJを特定する
5. そのPJの配賦額に残差を全額加算する
- Phase 1で特定した修正対象関数内の具体的な修正箇所(行番号)を示したコード例を記載する
- **[影響範囲]** `400_domain/420_project_profitability.js` のみ変更。データマート `78_pj_pl` の計算結果に影響。`92_fs_pl` や `201_data_validator.js` には変更なし。
- **[注意事項]**
1. 配賦基準が「最大」のPJに差額を集約するため、PJ構成が変わると差額の帰属先が変わる(許容範囲の設計判断として記録)
2. Phase 1で確認した冪等性設計の現状を記載する(既存行クリアがある/ない、それぞれの場合の注意点)
3. `28_bud_allocation` タブのD列以降はDDL実行でクリアされる可能性があるため、配賦率マスタはDDL実行後に再確認が必要(CLAUDE.md記載事項)
4. 列参照はヘッダー名ベースで行い、列番号ハードコード禁止(コーディング規約)
### Step 2-3a: エッジケース〜人間検討事項の追記(Edit または Bash、~200行)
- **[エッジケース]** 以下の観点でテーブルを作成する:
| 条件 | 表示値/動作 | 理由 |
|------|------------|------|
| 配賦基準合計がゼロ(全PJ売上ゼロ等) | 配賦処理をスキップ、共通費は未配賦のまま | ゼロ除算回避 |
| 配賦基準合計がゼロ以下(マイナスPJが支配) | 同上 | ゼロ除算回避(会計上マイナス配賦は不整合を招く) |
| 配賦対象の共通費がゼロ | 処理スキップ | 計算負荷回避 |
| 配賦対象PJが1件のみ | そのPJに100%配賦、残差=0 | 丸め誤差は発生しない |
| 残差がマイナスになるケース(`Math.ceil`混在等) | 要確認(Phase 1のRead結果に基づき記載) | 実装によっては残差がマイナスになる可能性 |
- **[実データ検証]** 不要(数値計算ロジックの変更のみ。マスタデータ依存なし)。
- **[関連ドキュメント]**
| ドキュメント | 関連箇所 |
|------------|---------|
| `docs/dev/dev_mas-085_consistency_check.md` | 78タブ vs 92タブの整合性チェック実装仕様 |
- **[人間が検討すべき事項]** TODO_future.mdから転記した事項 + 以下を追記:
- 端数処理方式の選定: 「配賦基準額が最大のPJに差額を全額加算する」最大剰余法を採用する(本仕様書で決定事項とする。即実装可)
- 差額の上限: 配賦PJ数−1円が理論上限のため実務上の影響は軽微だが、PJ数が多い場合の動作を念のため確認すること
### Step 2-3b: 実装プロンプト〜変更履歴の追記(Edit または Bash、~250行)
- **[実装プロンプト]** 以下の内容を**行頭4スペースインデント**(バッククォートで囲まない)で出力する:
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 S-45「PJ別配賦計算の丸め誤差解消」を実装してください。
## 実行前タスク
- `400_domain/420_project_profitability.js` を読み込み、共通費配賦関数の現在の丸め処理(行番号を特定)を確認する
- `200_data/201_data_validator.js` を読み込み、78タブ vs 92タブ整合性チェック関数の呼び出し方を確認する
## 修正対象ファイル
`400_domain/420_project_profitability.js` のみ
## 実装内容
共通費配賦ループを最大剰余法に変更する:
1. 各PJの仮配賦額を Math.floor(共通費 × PJ基準 / 基準合計) で計算
2. 残差 = 共通費総額 − 仮配賦額合計 を算出
3. 配賦基準が最大のPJを特定し、そのPJの配賦額に残差を加算
4. 修正後のコードが Phase 1で確認した関数の正確な行番号に収まるよう挿入位置を明示する
## 制約
- `200_data/201_data_validator.js` を変更しない
- 列参照はヘッダー名ベース(列番号ハードコード禁止)
- 有効フラグ=FALSE の行をスキップするロジックを維持する
## エッジケース
| 条件 | 動作 |
|------|------|
| 配賦基準合計 ≤ 0 | 配賦処理をスキップ(return or continue) |
| 共通費 = 0 | 配賦処理をスキップ |
| PJ数 = 1 | 残差=0のためそのまま全額配賦 |
## 動作確認
1. `npm run push:dev` でデプロイ
2. GASエディタから PJ別配賦計算関数を実行し、`78_pj_pl` に書き込まれた配賦額の合計と配賦元の共通費総額が一致することをスプレッドシート上で手動確認
3. `200_data/201_data_validator.js` の整合性チェック関数(Phase 1で特定した関数名)を実行し、78タブ vs 92タブの営業利益差異がゼロになることを確認
4. 売上ゼロPJが存在するテスト月で関数を実行し、エラーなく処理が完了することを確認
### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|---------|------|
| 実行前タスク(Read) | あり | 挿入位置・行番号の特定 |
| 実装(Write/Edit) | なし | 上記で確定した内容の清書 |
- **[推奨実行モデル]**
| 工程 | 推奨モデル | 理由 |
|------|----------|------|
| 実装(S-45) | Claude Sonnet 4.6 | 挿入位置の特定と既存パターンへの適用が必要 |
- **[変更履歴]**
| 日付 | 変更内容 |
|------|---------|
| 2026-04-19 | 初版作成 |
### Step 2-4: 仕様書作成プロンプトの記録(Edit または Bash・最重量・必ず独立Step)
仕様書末尾の「## 仕様書作成プロンプト」セクションに、この `<instruction>` 全文を `<details><summary>展開して表示</summary>` ブロックで囲んで追記する。
---
## Phase 3: 保存・登録・コミット
### 3-A: `docs/_config.json` にナビゲーション登録(必須)
`docs/_config.json` の `nav` 配列の §E.2(バグ修正・バリデーション)セクションに追加する:
```json
{ "file": "dev/dev_mas-117_allocation_rounding_fix.md", "title": "E.2.X MAS-117 PJ別配賦計算の丸め誤差解消" }
追加前に必ず git pull origin main して最新のJSONを取得し、JSON構文エラーが発生しないことを確認する。
3-B: docs/_internal/changelog.md への追記
先頭行(ヘッダー直後)に追記する:
| 2026-04-19 | [dev_mas-117_allocation_rounding_fix.md](dev_mas-117_allocation_rounding_fix.md) | 初版作成。PJ別配賦に最大剰余法を適用し丸め誤差を解消 |
3-C: コミット&プッシュ
git add docs/dev/dev_mas-117_allocation_rounding_fix.md docs/_config.json docs/_internal/changelog.md
git commit -m "docs: MAS-117 PJ別配賦計算の丸め誤差解消の開発仕様書を作成
78_pj_pl の共通費配賦に最大剰余法を適用し、92_fs_pl との営業利益差異をゼロにする。
修正対象: 400_domain/420_project_profitability.js のみ。
https://claude.ai/code/session_XXXXX"
git push -u origin docs/dev-MAS-117
```