上一节把工具链排成了层,也证明了「分层不是文档、是门禁」;但层排得再整齐,也回答不了最后一个问题:这些工具各自被用得多不多、养起来贵不贵。站内 game/industry
的工业化与外包短文讲的是组织与流程的道理;本节只算「这个工具值不值得养」这笔整数账——DevOps / CI 平台本身不是本节内容。
13.3 工具的采纳率与维护成本
13.3.1 两个轴,缺一不可
一个工具值不值得养,要同时看两件事:采纳率(有多少目标用户真的在用)与工具有效率(每投入 1 人天维护,换回多少人天节省)。两个轴各带一条下限,就落进四个象限:
| 采纳率 \ 有效率 | ≥ 下限 | < 下限 |
|---|---|---|
| ≥ 下限 | promote 推广 | keep 保留,先压维护成本 |
| < 下限 | observe 观察,先做推广 | retire 下架 |
三个量全是整数(地板除):adoptionPermille = activeUsers×1000 // targetUsers;savedPersonDaysX100 = activeUsers×sessions×minutes×100 // minutesPerPersonDay;efficiencyPermille = savedPersonDaysX100×1000 // maintainPersonDaysX100(×1000 表示倍率,1000 = 1.0 倍)。真实使用数据需真实团队环境,命令行不可复现(X);本节全部为构造的示例参数。
13.3.2 D 层:采纳与维护参数
{
"version": 1,
"project": "stardust-inn",
"note": "13.3 工具采纳率与维护成本账。targetUsers = 目标用户数;minutesPerPersonDay = 一人天分钟数(480)。每个工具:activeUsers = 年活跃使用人数;sessionsPerUserYear = 每人每年使用次数;minutesSavedPerSession = 每次节省分钟;maintainPersonDaysX100 = 年维护人天×100(修复 + 适配 + 支持)。全部为本套构造的示例参数,真实使用数据需真实团队环境,命令行不可复现(X)。判据(全整数地板除):adoptionPermille = activeUsers×1000 // targetUsers;savedPersonDaysX100 = activeUsers×sessions×minutes×100 // 480;efficiencyPermille = savedPersonDaysX100×1000 // maintainPersonDaysX100(工具有效率,×1000 表示倍率)。决策:promote(高采纳高效率) / keep(高采纳低效率) / observe(低采纳高效率) / retire(低采纳低效率)。",
"targetUsers": 20,
"minutesPerPersonDay": 480,
"adoptionFloorPermille": 500,
"efficiencyFloorPermille": 1000,
"tools": [
{ "id": "t-naming", "name": "命名校验", "activeUsers": 18, "sessionsPerUserYear": 40, "minutesSavedPerSession": 15, "maintainPersonDaysX100": 800 },
{ "id": "t-atlas", "name": "图集校验", "activeUsers": 12, "sessionsPerUserYear": 30, "minutesSavedPerSession": 20, "maintainPersonDaysX100": 600 },
{ "id": "t-dashboard", "name": "生产看板", "activeUsers": 16, "sessionsPerUserYear": 200, "minutesSavedPerSession": 2, "maintainPersonDaysX100": 2400 },
{ "id": "t-vfx", "name": "特效检查", "activeUsers": 4, "sessionsPerUserYear": 20, "minutesSavedPerSession": 25, "maintainPersonDaysX100": 300 },
{ "id": "t-color", "name": "取色助手", "activeUsers": 3, "sessionsPerUserYear": 10, "minutesSavedPerSession": 8, "maintainPersonDaysX100": 500 },
{ "id": "t-legacy", "name": "旧导出脚本", "activeUsers": 5, "sessionsPerUserYear": 15, "minutesSavedPerSession": 6, "maintainPersonDaysX100": 900 }
]
}
13.3.3 S 层:采纳账、节省账、有效率账
#!/usr/bin/env python3
# file: sim/ta13_adoption.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/ta13_adoption.py
"""13.3 工具的采纳率与维护成本:把「这个工具值不值得养」算成一笔整数账。
读 config/ta13_adoption.json。对每个工具算(全部整数地板除):
① 采纳率 adoptionPermille = activeUsers×1000 // targetUsers;
② 年节省人天×100 savedPersonDaysX100 = activeUsers×sessions×minutes×100 // minutesPerPersonDay;
③ **工具有效率 efficiencyPermille = savedPersonDaysX100×1000 // maintainPersonDaysX100**(倍率×1000);
④ 决策:promote / keep / observe / retire(采纳率与有效率两轴,各带下限)。
核心观点:**只看一个轴一定会误判**。只看采纳率会把高效但没人知道的 t-vfx 砍掉(它该观察推广);
只看有效率会把高采纳的 t-dashboard 砍掉(它该保留、优化维护成本)。两轴一起才成立。
产出 out/ta13_adoption.json,另落 canonical 摘要与 sha256 前 16 位供 P 层对照。
真实使用数据需真实团队环境,命令行不可复现(X)。"""
import hashlib
import json
import os
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
CONFIG = os.path.join(ROOT, "config/ta13_adoption.json")
OUT = os.path.join(ROOT, "out")
def analyze(cfg):
mppd, target = cfg["minutesPerPersonDay"], cfg["targetUsers"]
af, ef = cfg["adoptionFloorPermille"], cfg["efficiencyFloorPermille"]
rows = []
for t in cfg["tools"]:
ad = t["activeUsers"] * 1000 // target
sd = t["activeUsers"] * t["sessionsPerUserYear"] * t["minutesSavedPerSession"] * 100 // mppd
eff = sd * 1000 // t["maintainPersonDaysX100"] # 有效率 = 节省人天×100 ×1000 // 维护人天×100
v = ("promote" if ad >= af else "observe") if eff >= ef else ("keep" if ad >= af else "retire")
rows.append({"id": t["id"], "name": t["name"], "adoptionPermille": ad,
"savedPersonDaysX100": sd, "maintainPersonDaysX100": t["maintainPersonDaysX100"],
"efficiencyPermille": eff, "verdict": v})
kept = [r for r in rows if r["verdict"] != "retire"]
canon = json.dumps(
[{"id": r["id"], "adoptionPermille": r["adoptionPermille"],
"savedPersonDaysX100": r["savedPersonDaysX100"],
"efficiencyPermille": r["efficiencyPermille"], "verdict": r["verdict"]} for r in rows],
separators=(",", ":"), ensure_ascii=False)
ts = sum(r["savedPersonDaysX100"] for r in rows)
tm = sum(r["maintainPersonDaysX100"] for r in rows)
ks = sum(r["savedPersonDaysX100"] for r in kept)
km = sum(r["maintainPersonDaysX100"] for r in kept)
return {
"version": cfg["version"], "project": cfg["project"], "targetUsers": target,
"minutesPerPersonDay": mppd, "adoptionFloorPermille": af, "efficiencyFloorPermille": ef,
"toolCount": len(rows), "rows": rows,
"verdictCounts": {v: sum(1 for r in rows if r["verdict"] == v)
for v in ("promote", "keep", "observe", "retire")},
"totalSavedPersonDaysX100": ts, "totalMaintainPersonDaysX100": tm,
"portfolioEfficiencyPermille": ts * 1000 // tm,
"retainedSavedPersonDaysX100": ks, "retainedMaintainPersonDaysX100": km,
"retainedEfficiencyPermille": ks * 1000 // km,
"retireIds": [r["id"] for r in rows if r["verdict"] == "retire"],
"canonical": canon, "digest": hashlib.sha256(canon.encode("utf-8")).hexdigest()[:16],
}
def main():
rep = analyze(json.load(open(CONFIG, encoding="utf-8")))
os.makedirs(OUT, exist_ok=True)
with open(os.path.join(OUT, "ta13_adoption.json"), "w", encoding="utf-8") as f:
json.dump(rep, f, ensure_ascii=False, indent=2)
f.write("\n")
print(f"tools={rep['toolCount']} target={rep['targetUsers']} "
f"adoptionFloor={rep['adoptionFloorPermille']} efficiencyFloor={rep['efficiencyFloorPermille']}")
for r in rep["rows"]:
print(f" {r['id']:<11} adoption={r['adoptionPermille']:<5} savedX100={r['savedPersonDaysX100']:<5} "
f"maintX100={r['maintainPersonDaysX100']:<5} eff={r['efficiencyPermille']:<5} {r['verdict']}")
print(f"verdicts={rep['verdictCounts']} retire={rep['retireIds']}")
print(f"totalSavedX100={rep['totalSavedPersonDaysX100']} totalMaintX100={rep['totalMaintainPersonDaysX100']}")
print(f"portfolioEfficiency={rep['portfolioEfficiencyPermille']} "
f"retainedEfficiency={rep['retainedEfficiencyPermille']}")
print(f"digest={rep['digest']}")
print("wrote out/ta13_adoption.json")
return 0
if __name__ == "__main__":
raise SystemExit(main())
实跑 /opt/homebrew/bin/python3 sim/ta13_adoption.py:
tools=6 target=20 adoptionFloor=500 efficiencyFloor=1000
t-naming adoption=900 savedX100=2250 maintX100=800 eff=2812 promote
t-atlas adoption=600 savedX100=1500 maintX100=600 eff=2500 promote
t-dashboard adoption=800 savedX100=1333 maintX100=2400 eff=555 keep
t-vfx adoption=200 savedX100=416 maintX100=300 eff=1386 observe
t-color adoption=150 savedX100=50 maintX100=500 eff=100 retire
t-legacy adoption=250 savedX100=93 maintX100=900 eff=103 retire
verdicts={'promote': 2, 'keep': 1, 'observe': 1, 'retire': 2} retire=['t-color', 't-legacy']
totalSavedX100=5642 totalMaintX100=5500
portfolioEfficiency=1025 retainedEfficiency=1341
digest=c2ea30a827afb891
wrote out/ta13_adoption.json
整条组合一年节省 5642(56.42 人天),维护投入 5500(55 人天),组合有效率 1025‰——刚好过 1.0 倍。砍掉两个 retire 工具后,留下四个工具的有效率升到 1341‰:分母少了 500 + 900 = 1400 维护人天,分子只少了 50 + 93 = 143 节省人天,于是整条账立刻从「勉强保本」变成「赚三成」。这就是维护成本的性质:它摊在所有人头上,但只有少数人受益的工具最贵。
13.3.4 P 层内核
// file: core/src/ta13/adoption.ts
// tier: P
// verify: node --test 'core/test/ta13_*.test.ts'
// 13.3 工具采纳率与维护成本内核:把「值不值得养」算成一笔整数账。
//
// 关键设计:**判定必须两轴(采纳率 × 有效率)**。只看采纳率会砍掉高效但没人知道的工具;
// 只看有效率会砍掉高采纳但维护昂贵的工具。两轴各带下限,落在四个象限里。
// 全部整数:人天×100、千分比、倍率×1000,除法一律整数地板除。
// 只允许可擦除 TS 语法(禁 enum / namespace / 参数属性 / 装饰器)。
export type ToolUsage = {
id: string;
name: string;
activeUsers: number;
sessionsPerUserYear: number;
minutesSavedPerSession: number;
maintainPersonDaysX100: number;
};
export type Verdict = "promote" | "keep" | "observe" | "retire";
/** 采纳率(千分比):活跃用户 × 1000 // 目标用户。 */
export function adoptionPermille(t: ToolUsage, targetUsers: number): number {
return Math.floor((t.activeUsers * 1000) / targetUsers);
}
/** 年节省人天×100 = 活跃用户×次数×分钟×100 // 一人天分钟数。 */
export function savedPersonDaysX100(t: ToolUsage, minutesPerPersonDay: number): number {
const minutes = t.activeUsers * t.sessionsPerUserYear * t.minutesSavedPerSession;
return Math.floor((minutes * 100) / minutesPerPersonDay);
}
/** 工具有效率(倍率×1000)= 节省人天×100 × 1000 // 维护人天×100。 */
export function efficiencyPermille(t: ToolUsage, minutesPerPersonDay: number): number {
return Math.floor((savedPersonDaysX100(t, minutesPerPersonDay) * 1000) / t.maintainPersonDaysX100);
}
/** 两轴四象限决策。 */
export function verdict(
adoption: number, efficiency: number, adoptionFloor: number, efficiencyFloor: number,
): Verdict {
const hiA = adoption >= adoptionFloor;
const hiE = efficiency >= efficiencyFloor;
if (hiA && hiE) return "promote";
if (hiA && !hiE) return "keep";
if (!hiA && hiE) return "observe";
return "retire";
}
/** canonical 摘要:键序与 Python 侧一致,供 digest 对拍。 */
export function canonical(rows: {
id: string; adoptionPermille: number; savedPersonDaysX100: number;
efficiencyPermille: number; verdict: Verdict;
}[]): string {
return JSON.stringify(rows.map((r) => ({
id: r.id, adoptionPermille: r.adoptionPermille, savedPersonDaysX100: r.savedPersonDaysX100,
efficiencyPermille: r.efficiencyPermille, verdict: r.verdict,
})));
}
13.3.5 P 层测试:负例是核心
// file: core/test/ta13_adoption.test.ts
// tier: P
// verify: node --test 'core/test/ta13_*.test.ts'
// 采纳率与维护成本断言:TS 侧独立复算 config/ta13_adoption.json,与 Python(sim/ta13_adoption.py)
// 落盘的 out/ta13_adoption.json 逐位比对 + digest 对照。两条负例守住「单轴判定会误砍」。
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { createHash } from "node:crypto";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { adoptionPermille, savedPersonDaysX100, efficiencyPermille, verdict, canonical } from "../src/ta13/adoption.ts";
import type { ToolUsage, Verdict } from "../src/ta13/adoption.ts";
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
const cfg = JSON.parse(readFileSync(resolve(ROOT, "config/ta13_adoption.json"), "utf8"));
const py = JSON.parse(readFileSync(resolve(ROOT, "out/ta13_adoption.json"), "utf8"));
const tools = cfg.tools as ToolUsage[];
const mppd = cfg.minutesPerPersonDay as number;
const target = cfg.targetUsers as number;
const af = cfg.adoptionFloorPermille as number;
const ef = cfg.efficiencyFloorPermille as number;
const sha16 = (s: string): string => createHash("sha256").update(s, "utf8").digest("hex").slice(0, 16);
const rows = tools.map((t) => {
const ad = adoptionPermille(t, target);
const eff = efficiencyPermille(t, mppd);
return { id: t.id, adoptionPermille: ad, savedPersonDaysX100: savedPersonDaysX100(t, mppd),
efficiencyPermille: eff, verdict: verdict(ad, eff, af, ef) };
});
const row = (id: string) => rows.find((r) => r.id === id) as {
adoptionPermille: number; efficiencyPermille: number; verdict: Verdict;
};
test("跨语言:采纳率 / 节省 / 有效率 / 决策逐位一致,canonical 与 digest 一致", () => {
assert.equal(rows.length, py.toolCount);
for (const r of rows) {
const t = py.rows.find((x: { id: string }) => x.id === r.id);
assert.equal(r.adoptionPermille, t.adoptionPermille, r.id);
assert.equal(r.savedPersonDaysX100, t.savedPersonDaysX100, r.id);
assert.equal(r.efficiencyPermille, t.efficiencyPermille, r.id);
assert.equal(r.verdict, t.verdict, r.id);
}
const canon = canonical(rows);
assert.equal(canon, py.canonical);
assert.equal(sha16(canon), py.digest);
assert.equal(py.digest, "c2ea30a827afb891");
});
test("组合账:砍掉两个低效工具后有效率从 1025‰ 升到 1341‰", () => {
assert.equal(py.portfolioEfficiencyPermille, 1025);
assert.deepEqual(py.retireIds, ["t-color", "t-legacy"]);
assert.equal(py.retainedEfficiencyPermille, 1341);
assert.ok(py.retainedEfficiencyPermille > py.portfolioEfficiencyPermille);
});
test("负例:只看采纳率会把高效但没人用的 t-vfx 砍掉", () => {
const vfx = row("t-vfx");
assert.ok(vfx.adoptionPermille < af);
assert.ok(rows.filter((r) => r.adoptionPermille < af).map((r) => r.id).includes("t-vfx"));
assert.equal(vfx.verdict, "observe"); // 有效率 1386 ≥ 1000,该观察推广
});
test("负例:只看有效率会把高采纳的 t-dashboard 砍掉", () => {
const dash = row("t-dashboard");
assert.ok(dash.efficiencyPermille < ef);
assert.ok(rows.filter((r) => r.efficiencyPermille < ef).map((r) => r.id).includes("t-dashboard"));
assert.equal(dash.verdict, "keep"); // 采纳率 800 ≥ 500,该保留
});
test("边界:两轴都在下限之上才 promote;都在之下才 retire", () => {
assert.equal(verdict(af, ef, af, ef), "promote");
assert.equal(verdict(af - 1, ef - 1, af, ef), "retire");
assert.equal(verdict(af, ef - 1, af, ef), "keep");
assert.equal(verdict(af - 1, ef, af, ef), "observe");
});
实跑 node --test 'core/test/ta13_adoption.test.ts':
✔ 跨语言:采纳率 / 节省 / 有效率 / 决策逐位一致,canonical 与 digest 一致
✔ 组合账:砍掉两个低效工具后有效率从 1025‰ 升到 1341‰
✔ 负例:只看采纳率会把高效但没人用的 t-vfx 砍掉
✔ 负例:只看有效率会把高采纳的 t-dashboard 砍掉
✔ 边界:两轴都在下限之上才 promote;都在之下才 retire
ℹ tests 5
ℹ pass 5
ℹ fail 0
两条负例才是这一节的重点:t-vfx 采纳率只有 200‰(低于 500 的下限),但有效率 1386‰——它高效,只是没人知道,该 observe 推广而不是 retire;t-dashboard 采纳率 800‰(高于下限),但有效率只有 555‰——它人人都在用,只是维护太贵,该 keep 并压维护成本而不是下架。只看采纳率会把前者误砍,只看有效率会把后者误砍;两轴一起,digest=c2ea30a827afb891 与 Python 侧逐位一致,结论才立得住。
阅读导航:上一节:13.2 工具链的分层与复用 · 下一节:14.1 产能度量:人天与吞吐 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。