14.3.1 会议纪要为什么没人看
14.1 排了节点、14.2 定了通过/打回,但评审真正容易失控的是会后:会上说「描边改细一点」「这张头像留点边距」,散会就没人认领;过两周再问「上次那条改了吗」,谁也说不清。根因是纪要只有文字,没有结构——没有责任人、没有期限、没有状态,也没有「这条决策后来改了哪个资产」的链接。
解决办法是把走查记录写成带状态机的结构化 schema:每条问题有 owner(责任人)、due(期限)、decision(对应决策号),以及一条 history 状态序列。有了结构,三个量就能算出来:
- 闭环率:多少条真正
closed; - 逾期率:多少条
due已过、状态还没闭环; - 可追溯率:多少条的决策真的能在决策日志里查到。
14.3.2 D 层:走查记录与决策日志
下面这段是 D 层配置 config/sp14_walkthrough.json:7 条走查记录(每条带 history 状态序列)、6 条决策,以及合法跃迁表 transitions。样本里 W-007 的 decision 指向一个不存在的 D-099,用来证明可追溯性真的会拦:
{
"project": "stardust-inn",
"schemaVersion": 1,
"specRef": "config/visual_spec.json",
"note": "走查记录与决策留痕:每条 record 有 history(状态序列,末项为当前状态)。terminal 状态为 closed / wontfix。overdue = due < cutoff 且当前状态非 terminal。transitions 是合法状态跃迁表,history 中任一相邻对不在表内即记非法跃迁。untraced = 该记录的 decision 不在 decisions 的 id 集合里。全整数与字符串,逐位一致。",
"cutoff": "2026-10-10",
"terminal": ["closed", "wontfix"],
"records": [
{ "id": "W-001", "node": "in-production", "asset": "furn_chair_01", "owner": "artist-a", "due": "2026-10-05", "decision": "D-001", "history": ["open", "in-progress", "fixed", "verified", "closed"] },
{ "id": "W-002", "node": "in-production", "asset": "ui_frame", "owner": "artist-b", "due": "2026-10-08", "decision": "D-002", "history": ["open", "in-progress", "fixed", "verified", "closed"] },
{ "id": "W-003", "node": "pre-integration", "asset": "atlas_furn", "owner": "artist-c", "due": "2026-10-09", "decision": "D-003", "history": ["open", "in-progress", "wontfix"] },
{ "id": "W-004", "node": "pre-integration", "asset": "portrait_host_01", "owner": "artist-a", "due": "2026-10-06", "decision": "D-004", "history": ["open", "in-progress", "fixed"] },
{ "id": "W-005", "node": "in-production", "asset": "vfx_steam_01", "owner": "artist-d", "due": "2026-10-12", "decision": "D-005", "history": ["open", "in-progress", "fixed", "verified", "closed"] },
{ "id": "W-006", "node": "concept", "asset": "iso_tile_floor", "owner": "artist-b", "due": "2026-10-04", "decision": "D-006", "history": ["open", "in-progress", "fixed", "verified", "closed"] },
{ "id": "W-007", "node": "pre-integration", "asset": "dialogue_zh", "owner": "artist-c", "due": "2026-10-11", "decision": "D-099", "history": ["open", "in-progress", "fixed"] }
],
"decisions": [
{ "id": "D-001", "record": "W-001", "change": "spec-outline-1px" },
{ "id": "D-002", "record": "W-002", "change": "nineslice-inset-3px" },
{ "id": "D-003", "record": "W-003", "change": "atlas-slot-waiver" },
{ "id": "D-004", "record": "W-004", "change": "portrait-safearea-pad" },
{ "id": "D-005", "record": "W-005", "change": "vfx-duration-800ms" },
{ "id": "D-006", "record": "W-006", "change": "token-accent-align" }
],
"transitions": [
["open", "in-progress"], ["in-progress", "fixed"], ["fixed", "verified"], ["verified", "closed"],
["in-progress", "open"], ["fixed", "in-progress"], ["open", "wontfix"], ["in-progress", "wontfix"],
["closed", "reopened"], ["reopened", "in-progress"]
]
}
状态机的合法跃迁是这样一组:open → in-progress → fixed → verified → closed 是主干;wontfix 是「不改」的出口;fixed → in-progress 是返工回流;closed → reopened 是上线后复现。任何不在表内的相邻对都是非法跃迁。
14.3.3 S 层:闭环率 / 逾期率 / 可追溯率
下面这段是 S 层脚本,统计三个率并校验状态跃迁:
#!/usr/bin/env python3
# file: sim/sp14_walkthrough.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/sp14_walkthrough.py
"""14.3 走查记录与决策留痕的 S 层统计。
读 config/visual_spec.json(只读)与 config/sp14_walkthrough.json。
每条 record 的 history 末项即当前状态;terminal = closed / wontfix。
closed = 当前状态为 closed 的条数
resolved = 当前状态为 terminal 的条数
openCount = 未闭环条数(当前状态非 terminal)
overdue = due < cutoff 且未闭环的条数
closurePermille = closed * 1000 // total
resolvedPermille = resolved * 1000 // total
overduePermille = overdue * 1000 // total
untraced = decision 不在 decisions 的 id 集合里的记录
traceabilityPermille = (total - untraced) * 1000 // total
illegalTransitions = history 中不在 transitions 表内的相邻对 "<id>:<from>-><to>"
全部为整数与字符串,逐位一致。落 out/sp14_walkthrough.json 供 P 层跨语言对照。
"""
from __future__ import annotations
import json
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
SPEC = os.path.join(ROOT, "config/visual_spec.json")
CFG = os.path.join(ROOT, "config/sp14_walkthrough.json")
OUT = os.path.join(ROOT, "out")
def load(p):
with open(p, encoding="utf-8") as f:
return json.load(f)
def evaluate(cfg: dict):
terminal = set(cfg["terminal"])
allowed = {(t[0], t[1]) for t in cfg["transitions"]}
decision_ids = {d["id"] for d in cfg["decisions"]}
total = len(cfg["records"])
closed = resolved = overdue = 0
untraced, illegal = [], []
for r in cfg["records"]:
status = r["history"][-1]
if status == "closed":
closed += 1
if status in terminal:
resolved += 1
elif r["due"] < cfg["cutoff"]:
overdue += 1
if r["decision"] not in decision_ids:
untraced.append(r["id"])
for a, b in zip(r["history"], r["history"][1:]):
if (a, b) not in allowed:
illegal.append(f"{r['id']}:{a}->{b}")
open_count = total - resolved
violations = sorted([f"{rid}:UNTRACED" for rid in untraced] + illegal)
return {
"total": total, "closed": closed, "resolved": resolved, "openCount": open_count,
"overdue": overdue,
"closurePermille": closed * 1000 // total,
"resolvedPermille": resolved * 1000 // total,
"overduePermille": overdue * 1000 // total,
"untraced": untraced, "illegalTransitions": illegal,
"traceabilityPermille": (total - len(untraced)) * 1000 // total,
"violations": violations, "count": len(violations),
}
def main() -> int:
spec, cfg = load(SPEC), load(CFG)
r = evaluate(cfg)
summary = {"project": spec["project"], "specVersion": spec["specVersion"],
"cutoff": cfg["cutoff"], **r}
os.makedirs(OUT, exist_ok=True)
with open(os.path.join(OUT, "sp14_walkthrough.json"), "w", encoding="utf-8") as f:
json.dump(summary, f, ensure_ascii=False, indent=2, sort_keys=True)
f.write("\n")
print(f"total={r['total']} closed={r['closed']} resolved={r['resolved']} "
f"open={r['openCount']} overdue={r['overdue']}")
print(f"closurePermille={r['closurePermille']} resolvedPermille={r['resolvedPermille']} "
f"overduePermille={r['overduePermille']} "
f"traceabilityPermille={r['traceabilityPermille']}")
print(f"untraced={r['untraced']} illegalTransitions={r['illegalTransitions']}")
for v in r["violations"]:
print(f" VIOLATION {v}")
print("wrote out/sp14_walkthrough.json")
return 0
if __name__ == "__main__":
sys.exit(main())
实跑 /opt/homebrew/bin/python3 sim/sp14_walkthrough.py:
total=7 closed=4 resolved=5 open=2 overdue=1
closurePermille=571 resolvedPermille=714 overduePermille=142 traceabilityPermille=857
untraced=['W-007'] illegalTransitions=[]
VIOLATION W-007:UNTRACED
wrote out/sp14_walkthrough.json
读这组数字:7 条里 4 条真正 closed(闭环率 571‰),加上 1 条 wontfix 共 5 条已结案(714‰);W-004 的 due(10-06)已过 cutoff(10-10)却还停在 fixed,逾期率 142‰。untraced=['W-007'] 精确抓出「决策号查不到」的那条——可追溯率因此只有 857‰。
14.3.4 P 层:状态机合法性校验
下面这段是 P 层 TS 内核 core/src/sp14/walkthrough.ts,与 Python 同一套规则:统计三个率,并把 history 里每一对相邻状态与 transitions 表比对:
// file: core/src/sp14/walkthrough.ts
// tier: P
// verify: node --test 'core/test/sp14_*.test.ts'
// 14.3 走查记录与决策留痕的 TS 侧内核。与 sim/sp14_walkthrough.py 同一套规则:
// history 末项为当前状态,terminal 状态为 closed / wontfix;
// overdue = due < cutoff 且未闭环;untraced = decision 不在 decisions 的 id 集合;
// 相邻状态对不在 transitions 表内即非法跃迁。整数与字符串逐位一致。
export function validateTransitions(records: any[], transitions: string[][]): string[] {
const allowed = new Set(transitions.map((t) => `${t[0]}\u0000${t[1]}`));
const out: string[] = [];
for (const r of records) {
const h = r.history;
for (let i = 0; i + 1 < h.length; i += 1) {
if (!allowed.has(`${h[i]}\u0000${h[i + 1]}`)) out.push(`${r.id}:${h[i]}->${h[i + 1]}`);
}
}
return out;
}
export function evaluateWalkthrough(cfg: any): any {
const terminal: Set<string> = new Set(cfg.terminal);
const decisionIds: Set<string> = new Set(cfg.decisions.map((d: any) => d.id));
const total = cfg.records.length;
let closed = 0;
let resolved = 0;
let overdue = 0;
const untraced: string[] = [];
for (const r of cfg.records) {
const status = r.history[r.history.length - 1];
if (status === "closed") closed += 1;
if (terminal.has(status)) resolved += 1;
else if (r.due < cfg.cutoff) overdue += 1;
if (!decisionIds.has(r.decision)) untraced.push(r.id);
}
const illegal = validateTransitions(cfg.records, cfg.transitions);
const violations = [...untraced.map((id) => `${id}:UNTRACED`), ...illegal].sort();
return {
total, closed, resolved, openCount: total - resolved, overdue,
closurePermille: Math.floor((closed * 1000) / total),
resolvedPermille: Math.floor((resolved * 1000) / total),
overduePermille: Math.floor((overdue * 1000) / total),
untraced, illegalTransitions: illegal,
traceabilityPermille: Math.floor(((total - untraced.length) * 1000) / total),
violations, count: violations.length,
};
}
配套的 P 层测试 core/test/sp14_walkthrough.test.ts 读 Python 产物做跨语言对照,并用两条非法跃迁负例(open 直接跳 closed、跳过 verified 的 fixed → closed)证明跳步会被拦下:
// file: core/test/sp14_walkthrough.test.ts
// tier: P
// verify: node --test 'core/test/sp14_*.test.ts'
// 跨语言对照:读 out/sp14_walkthrough.json,断言闭环率/逾期率/可追溯率与非法跃迁
// 逐位一致。负例:非法状态跃迁(open 直接跳 closed、跳过 verified)必须 fail。
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 { evaluateWalkthrough, validateTransitions } from "../src/sp14/walkthrough.ts";
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
const cfg = JSON.parse(readFileSync(resolve(ROOT, "config/sp14_walkthrough.json"), "utf8"));
const py = JSON.parse(readFileSync(resolve(ROOT, "out/sp14_walkthrough.json"), "utf8"));
test("走查统计:TS 复算闭环率/逾期率/可追溯率与 Python 产物逐位一致", () => {
const mine = evaluateWalkthrough(cfg);
for (const k of ["total", "closed", "resolved", "openCount", "overdue",
"closurePermille", "resolvedPermille", "overduePermille", "traceabilityPermille", "count"]) {
assert.equal(mine[k], py[k], k);
}
assert.deepEqual(mine.untraced, py.untraced);
assert.deepEqual(mine.illegalTransitions, py.illegalTransitions);
assert.deepEqual(mine.violations, py.violations);
});
test("正例:样本全部走查记录的状态跃迁合法", () => {
assert.deepEqual(validateTransitions(cfg.records, cfg.transitions), []);
});
test("负例:非法跃迁(open 直接跳 closed)必须 fail", () => {
const records = [{ id: "X-1", history: ["open", "closed"] }];
assert.deepEqual(validateTransitions(records, cfg.transitions), ["X-1:open->closed"]);
});
test("负例:跳过 verified 的 fixed->closed 也必须 fail", () => {
const records = [{ id: "X-2", history: ["open", "in-progress", "fixed", "closed"] }];
assert.deepEqual(validateTransitions(records, cfg.transitions), ["X-2:fixed->closed"]);
});
实跑 node --test 'core/test/sp14_*.test.ts'(此处只列本节相关的 4 条;三节合计 11 条全绿):
✔ 走查统计:TS 复算闭环率/逾期率/可追溯率与 Python 产物逐位一致 (1.175542ms)
✔ 正例:样本全部走查记录的状态跃迁合法 (0.11ms)
✔ 负例:非法跃迁(open 直接跳 closed)必须 fail (0.081959ms)
✔ 负例:跳过 verified 的 fixed->closed 也必须 fail (0.065833ms)
ℹ tests 11
ℹ pass 11
ℹ fail 0
14.3.5 判读:状态机挡住的是「假装改完了」
状态机校验的价值不在「记录得漂亮」,而在堵住两类常见的偷懒:
- 跳步:
open直接跳closed——没经过fixed → verified,等于「口头说改完了」就结案。门禁要求状态必须逐级走,verified那一步强制有人真看过成品。 - 无声返工:
fixed → in-progress是合法回流(返工允许),但verified → fixed不在表内——一旦进入验证就不该再悄悄改,要改必须先reopened,留下痕迹。
三个率也要一起看:闭环率高不等于质量好(可能靠 wontfix 冲量),逾期率低不等于没积压(可能是期限定得松),可追溯率高不等于决策对(只证明查得到)。它们各量一个维度,缺一个都会误判。
14.3.6 边界与取舍
- 真实评审会上的口头决策、会后私下改动、跨系统资产变更需要真实环境(会议 / 版本控制系统 / 资产服务器),命令行不可复现,标 X;本节只对一份结构化记录做静态统计与状态机校验。
- 状态集合与跃迁表是**《星屑旅店》这一款治愈系休闲游戏**的取值;换项目可增删状态(如加
blocked),但「表内才合法、跳步即 fail」的规则不变。 - 三个率都是整数地板除的千分比、条数是整数,TS 与 Python 逐位一致;本节不涉及浮点派生量。
阅读导航:上一节:14.2 评审评分卡与通过/打回标准 · 下一节:15.1 资产版本治理:命名与分支 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。