15.2 成本结构与超支归因
15.1 算出的合计是报价时的数——按四种计价结构签合同时预期的账。结算是实际花掉的数。两者之差就是超支,而超支最没用的形态是一个孤零零的总数:它只说「多花了」,不说「多花在哪」。本节把成本结构写清,再把超支分解到可归因的桶里。
这一节与卷一 7.3 预算超支的归因与裁剪顺序 是同一条纪律的两个面:那一节做的是面数预算的超支归因(超了多少面、该先裁谁),本节做的是成本的超支归因(超了多少钱、钱花在哪类原因上)。两者共用同一条铁律——归因桶之和必须恰好等于差额,不许有「其它」这种兜底桶。本节所有金额仍是本套构造的示例参数,与真实报价无关。
15.2.1 成本结构:四个加项
一个包从签约到结算,成本由四项相加而成:
| 加项 | 含义 | 整数算法 |
|---|---|---|
| 基线 | 15.1 算出的 total,签约时冻结 | baseline |
| 变更 | 签约后追加的需求(改规格、加资产) | change |
| 超计划返工 | 实际返工超出签约时预估返工的部分 | extraRework |
| 管理分摊 | 项目管理、协调、验收的开销 | baseline × overheadPermille // 1000 |
即 actual = baseline + change + extraRework + overhead。把管理分摊写成基线的千分比而不是固定值,是因为协调成本随包的大小增长——大包耗的沟通与验收工时本来就多。注意 change 与 extraRework 是结算时才知道的实测量,不是预估;一旦签约后追加需求,基线就冻结不动,追加全部落进 change。
15.2.2 超支归因:桶必须恰好覆盖差额
超支 overrun = actual - baseline = change + extraRework + overhead。右边三项就是三个归因桶,它们之和恒等于超支——这不是巧合,是结构决定的:所有加项都在基线之上。但真正的纪律在于桶的集合必须穷尽且互斥:任何一笔实际支出都必须落进且只落进一个桶;如果算出来桶之和 ≠ 超支,说明有一笔钱没被归类,模型就是错的,必须报错而不是四舍五入抹平。
归因的价值在于行动不同:change 高说明需求管理有问题,extraRework 高说明验收标准或规格传达有问题,overhead 高说明包切得太碎或协调太重。三个桶指向三套完全不同的整改动作。
15.2.3 D 层:成本行配置
下面这份 D 层配置声明管理分摊率与每个包的追加项。baseline 显式写出来,且必须等于 15.1 算出的 total——S 层会逐包断言,防止两处各写一遍导致漂移:
{
"project": "stardust-inn",
"chapter": 15,
"specVersion": 1,
"note": "15.2 成本结构与超支归因的示例参数。overheadPermille(管理分摊千分比)、change(变更追加)、extraRework(超出计划的返工追加)与 baseline(报价基线)都是本套构造的示例,与任何真实报价无关;baseline 必须等于 15.1 计价模型算出的 total,S 层会逐包断言。金额以「分」为最小整数单位。",
"currencyUnit": "cent",
"overheadPermille": 60,
"lines": [
{"package": "pkg_atlas", "baseline": 2400000, "change": 0, "extraRework": 0},
{"package": "pkg_concept", "baseline": 1350000, "change": 180000, "extraRework": 90000},
{"package": "pkg_furniture", "baseline": 276000, "change": 24000, "extraRework": 12000},
{"package": "pkg_ui_icons", "baseline": 172800, "change": 0, "extraRework": 6400}
]
}
15.2.4 S 层:从基线到结算
下面这段是 S 层脚本,先断言每行 baseline 等于 15.1 的 total,再叠加三个加项、分解超支。全整数、地板除:
#!/usr/bin/env python3
# file: sim/ta15_cost.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/ta15_cost.py
"""15.2 的 S 层成本器:把 15.1 的计价基线(out/ta15_pricing.json)当作「报价基线」,
叠加变更追加、超计划返工与管理分摊,得到结算实际成本,再把超支分解到三个可归因桶。
成本结构(全整数,单位「分」):
actual = baseline + change + extraRework + overhead
overhead = baseline * overheadPermille // 1000
overrun = actual - baseline = change + extraRework + overhead
归因桶 change / extraRework / overhead 之和必须恰好等于 overrun(不许有「其它」兜底桶)。
先断言 config/ta15_cost.json 里每行 baseline 等于 15.1 算出的 total,杜绝两处各写一遍。
落 out/ta15_cost.json 供 P 层对照。
"""
import hashlib
import json
import os
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def main() -> int:
cost = json.load(open(os.path.join(ROOT, "config/ta15_cost.json"), encoding="utf-8"))
pricing = json.load(open(os.path.join(ROOT, "out/ta15_pricing.json"), encoding="utf-8"))
base_by_id = {p["id"]: p["total"] for p in pricing["packages"]}
# 单一真源:baseline 必须等于 15.1 的 total,否则两处数字会各自漂移。
for ln in cost["lines"]:
want = base_by_id.get(ln["package"])
if want is None:
print(f"FAIL 未知包 {ln['package']}")
return 1
if ln["baseline"] != want:
print(f"FAIL {ln['package']} baseline={ln['baseline']} != 15.1 total={want}")
return 1
rate = cost["overheadPermille"]
rows, base_t, change_t, rework_t, oh_t = [], 0, 0, 0, 0
for ln in sorted(cost["lines"], key=lambda x: x["package"]):
base = ln["baseline"]
overhead = base * rate // 1000
actual = base + ln["change"] + ln["extraRework"] + overhead
overrun = actual - base
if ln["change"] + ln["extraRework"] + overhead != overrun:
print(f"FAIL {ln['package']} 归因桶不覆盖超支")
return 1
base_t += base
change_t += ln["change"]
rework_t += ln["extraRework"]
oh_t += overhead
rows.append({"id": ln["package"], "baseline": base, "change": ln["change"],
"extraRework": ln["extraRework"], "overhead": overhead,
"actual": actual, "overrun": overrun})
lines = [f"{r['id']}:{r['baseline']}:{r['actual']}:{r['overrun']}" for r in rows]
digest = hashlib.sha256("\n".join(lines).encode("utf-8")).hexdigest()[:16]
actual_t = sum(r["actual"] for r in rows)
overrun_t = sum(r["overrun"] for r in rows)
rep = {"project": cost["project"], "currencyUnit": cost["currencyUnit"],
"overheadPermille": rate, "baselineTotal": base_t, "actualTotal": actual_t,
"overrunTotal": overrun_t,
"buckets": {"change": change_t, "extraRework": rework_t, "overhead": oh_t},
"bucketSum": change_t + rework_t + oh_t, "digest": digest, "lines": rows}
with open(os.path.join(ROOT, "out/ta15_cost.json"), "w", encoding="utf-8") as f:
json.dump(rep, f, ensure_ascii=False, indent=2)
f.write("\n")
print(f"overheadPermille={rate} baselineTotal={base_t} actualTotal={actual_t} overrunTotal={overrun_t}")
for r in rows:
print(f" {r['id']:14s} base={r['baseline']:8d} change={r['change']:7d} "
f"rework={r['extraRework']:7d} oh={r['overhead']:7d} actual={r['actual']:8d} over={r['overrun']:7d}")
print(f"buckets change={change_t} extraRework={rework_t} overhead={oh_t} sum={change_t + rework_t + oh_t}")
print(f"digest={digest}")
print("wrote out/ta15_cost.json")
return 0
if __name__ == "__main__":
sys.exit(main())
实跑 /opt/homebrew/bin/python3 sim/ta15_cost.py:
overheadPermille=60 baselineTotal=4198800 actualTotal=4763128 overrunTotal=564328
pkg_atlas base= 2400000 change= 0 rework= 0 oh= 144000 actual= 2544000 over= 144000
pkg_concept base= 1350000 change= 180000 rework= 90000 oh= 81000 actual= 1701000 over= 351000
pkg_furniture base= 276000 change= 24000 rework= 12000 oh= 16560 actual= 328560 over= 52560
pkg_ui_icons base= 172800 change= 0 rework= 6400 oh= 10368 actual= 189568 over= 16768
buckets change=204000 extraRework=108400 overhead=251928 sum=564328
digest=1461ed3be924dbc4
wrote out/ta15_cost.json
baselineTotal=4198800 正好等于 15.1 的 grandTotal——两节读的是同一份模型,不是各算各的。超支 564328 分分解成 change=204000 / extraRework=108400 / overhead=251928,三桶之和 564328 恰好覆盖,没有一分钱落在桶外。pkg_concept(概念设计)的 change 最高,说明这一包的需求最不稳定——这正是按人天计价的包最常见的超支形态。
15.2.5 P 层:成本内核
下面这段是 P 层 TS 内核,与上面的 Python 逐条对应——同一个管理分摊率、同一套断言与 digest 口径:
// file: core/src/ta15/cost.ts
// tier: P
// verify: node --test 'core/test/ta15_*.test.ts'
// 15.2 的 P 层成本内核。与 sim/ta15_cost.py 同一套成本结构、同一份 config/ta15_cost.json。
// 全整数(分):overhead 用 Math.floor(baseline*rate/1000),两侧逐位一致。
// 只允许可擦除 TS 语法。
import { createHash } from "node:crypto";
export type CostLine = { package: string; baseline: number; change: number; extraRework: number };
export type CostRow = {
id: string; baseline: number; change: number; extraRework: number;
overhead: number; actual: number; overrun: number;
};
export function attribute(lines: CostLine[], overheadPermille: number, baseById: Record<string, number>): CostRow[] {
const rows: CostRow[] = lines.map((ln) => {
const want = baseById[ln.package];
if (want === undefined) throw new Error(`未知包 ${ln.package}`);
if (ln.baseline !== want) throw new Error(`${ln.package} baseline=${ln.baseline} != 15.1 total=${want}`);
const overhead = Math.floor((ln.baseline * overheadPermille) / 1000);
const actual = ln.baseline + ln.change + ln.extraRework + overhead;
const overrun = actual - ln.baseline;
if (ln.change + ln.extraRework + overhead !== overrun) throw new Error(`${ln.package} 归因桶不覆盖超支`);
return { id: ln.package, baseline: ln.baseline, change: ln.change, extraRework: ln.extraRework, overhead, actual, overrun };
});
rows.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
return rows;
}
export function rowLines(rows: CostRow[]): string[] {
return rows.map((r) => `${r.id}:${r.baseline}:${r.actual}:${r.overrun}`);
}
export function digest16(lines: string[]): string {
return createHash("sha256").update(lines.join("\n"), "utf8").digest("hex").slice(0, 16);
}
15.2.6 跨语言对照:digest 逐位一致
P 层测试同时读 15.1 与 15.2 的 Python 产物,断言 TS 复算逐位一致,并用一条负例守住「基线不许与 15.1 漂移」:
// file: core/test/ta15_cost.test.ts
// tier: P
// verify: node --test 'core/test/ta15_*.test.ts'
// 跨语言对照:读 out/ta15_pricing.json(15.1 基线)与 out/ta15_cost.json(15.2 结算),断言
// TS 侧复算逐位一致。负例:baseline 与 15.1 total 不一致必须报错,而不是静默算出一个错数。
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { attribute, rowLines, digest16 } from "../src/ta15/cost.ts";
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
const cost = JSON.parse(readFileSync(resolve(ROOT, "config/ta15_cost.json"), "utf8"));
const pricing = JSON.parse(readFileSync(resolve(ROOT, "out/ta15_pricing.json"), "utf8"));
const py = JSON.parse(readFileSync(resolve(ROOT, "out/ta15_cost.json"), "utf8"));
test("成本:TS 复算与 Python 产物逐位一致", () => {
const baseById: Record<string, number> = {};
for (const p of pricing.packages) baseById[p.id] = p.total;
const rows = attribute(cost.lines, cost.overheadPermille, baseById);
assert.deepEqual(rows, py.lines);
assert.equal(digest16(rowLines(rows)), py.digest);
assert.equal(rows.reduce((n, r) => n + r.baseline, 0), py.baselineTotal);
assert.equal(rows.reduce((n, r) => n + r.actual, 0), py.actualTotal);
assert.equal(rows.reduce((n, r) => n + r.overrun, 0), py.overrunTotal);
console.log(`cost digest = ${py.digest}`);
});
test("归因:三个桶之和恰等于超支总额", () => {
const b = py.buckets;
assert.equal(b.change + b.extraRework + b.overhead, py.overrunTotal);
assert.deepEqual(b, { change: 204000, extraRework: 108400, overhead: 251928 });
});
test("单一真源:15.1 的 grandTotal 等于 15.2 的 baselineTotal", () => {
assert.equal(pricing.grandTotal, py.baselineTotal);
assert.equal(pricing.grandTotal, 4198800);
});
test("负例:baseline 与 15.1 total 不一致必须报错", () => {
const baseById: Record<string, number> = { pkg_x: 100 };
const bad = [{ package: "pkg_x", baseline: 999, change: 0, extraRework: 0 }];
assert.throws(() => attribute(bad, 60, baseById), /baseline/);
});
实跑 node --test 'core/test/ta15_cost.test.ts':
cost digest = 1461ed3be924dbc4
✔ 成本:TS 复算与 Python 产物逐位一致
✔ 归因:三个桶之和恰等于超支总额
✔ 单一真源:15.1 的 grandTotal 等于 15.2 的 baselineTotal
✔ 负例:baseline 与 15.1 total 不一致必须报错
ℹ pass 4
ℹ fail 0
两侧 digest 都是 1461ed3be924dbc4,且两侧都独立断言了 grandTotal == baselineTotal——跨语言一致与跨节一致同时成立。
15.2.7 边界:真实超支不可复现
本节算的是给定这份示例配置的超支分解。但真实的超支发生在真实项目里:需求方临时改主意、供应商换人、汇率变动、验收拖期——需真实环境(真实项目与合同),命令行不可复现。这些事件无法从任何模型推出来,本节也不假装能预测它们。
本节能给的是一件更朴素的东西:一套把超支「记账」的格式。有了它,项目结束后你至少能回答「这笔钱花在哪一类原因上」,而不是只有一个「超了」的结论。下一节把最常引发返工的那个环节——验收——做成一台可执行的门。
阅读导航:上一节:15.1 外包切分与计价模型 · 下一节:15.3 供应商验收与质量门 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。