16.3 交付物与卷三衔接
16.2 双案例回放:6 主题走同一条管线
证明了一条链能被同一套判据反复跑,但「跑得通」不等于「交得出去」。本节给卷二收尾:把全卷的产出列成一张交付物清单,并点名哪几批交给卷三《风格与品类美术实战》。清单本身是数据(D 层),校验它的是一段 S 层脚本与一份 P 层 TS 内核;判据只有四类——字段齐全、kind/status 合法、id 唯一、接手目标存在且已 done。卷三逐风格展开尚未落盘,本节只交代接口,不展开内容。
16.3.1 D 层:交付物清单的结构
清单把交付物分三类:spec-text(给人读的规范文本)、schema(机器校验的静态结构)、runnable-artifact(能真跑的产物)。每项四个必需字段 id / kind / producer / status;handoffToVolume3 只列卷三要接的那批 id,且只有 status: done 的才允许被接手。
{
"project": "stardust-inn",
"volume": "技术美术与管线工程",
"nextVolume": "风格与品类美术实战",
"note": "卷二交付物清单。kind 三类:spec-text=给人读的规范文本,schema=机器校验的静态结构,runnable-artifact=可跑产物。handoffToVolume3 列出卷三接手的那批——只有 status=done 的交付物才允许被接手,且接手目标必须真实存在于清单里。",
"requiredFields": ["id", "kind", "producer", "status"],
"allowedKinds": ["spec-text", "schema", "runnable-artifact"],
"allowedStatus": ["done", "in-progress", "planned"],
"deliverables": [
{ "id": "pipeline.checklist", "kind": "schema", "producer": "config/ta16_pipeline.json", "status": "done" },
{ "id": "assembly.gate", "kind": "runnable-artifact", "producer": "sim/ta16_pipeline.py", "status": "done" },
{ "id": "themes.six-pipeline", "kind": "runnable-artifact", "producer": "sim/ta16_six.py", "status": "done" },
{ "id": "bench.sensitivity", "kind": "runnable-artifact", "producer": "sim/ta16_bench.py", "status": "done" },
{ "id": "manifest.schema", "kind": "schema", "producer": "config/ta11_manifest.json", "status": "done" },
{ "id": "atlas.spec", "kind": "schema", "producer": "config/ta05_atlas.json", "status": "done" },
{ "id": "material.spec", "kind": "schema", "producer": "config/ta06_material.json", "status": "done" },
{ "id": "lod.spec", "kind": "schema", "producer": "config/ta07_lod.json", "status": "done" },
{ "id": "bones.spec", "kind": "schema", "producer": "config/ta08_bones.json", "status": "done" },
{ "id": "gate.rules", "kind": "schema", "producer": "config/ta12_rules.json", "status": "done" },
{ "id": "dependency.cycle", "kind": "runnable-artifact", "producer": "sim/ta04_cycle.py", "status": "done" },
{ "id": "handoff.checklist", "kind": "spec-text", "producer": "content/posts/books/技术美术与管线工程/art-ta-16-3-deliverable-handoff-volume3.md", "status": "done" }
],
"handoffToVolume3": [
"pipeline.checklist", "assembly.gate", "themes.six-pipeline", "manifest.schema",
"atlas.spec", "lod.spec", "gate.rules", "dependency.cycle"
]
}
16.3.2 S 层:清单完整性校验
check() 就是上面四类判据的直译;违例字符串排序去重后落 out/ta16_handoff.json,供 P 层逐位对照:
#!/usr/bin/env python3
# file: sim/ta16_handoff.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/ta16_handoff.py
"""卷二 16.3 交付物清单完整性 + 卷三接手的 S 层校验器。
读 config/ta16_deliverables.json,对卷二交付物清单做完整性校验,并校验交给卷三的那批目标:
字段齐全、kind/status 合法、id 唯一、handoff 目标存在且已 done。违例字符串排序去重后落
out/ta16_handoff.json 供 P 层跨语言对照。全是字符串与集合运算,逐位一致。
"""
import hashlib
import json
import os
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def check(handoff: dict) -> list:
req = handoff["requiredFields"]
kinds, statuses = set(handoff["allowedKinds"]), set(handoff["allowedStatus"])
items = handoff["deliverables"]
ids = [d["id"] for d in items]
known = set(ids)
viol = []
for d in items:
for f in req:
if not d.get(f):
viol.append(f"MISSING_FIELD:{d.get('id', '?')}:{f}")
if d.get("kind") not in kinds:
viol.append(f"BAD_KIND:{d.get('id', '?')}:{d.get('kind')}")
if d.get("status") not in statuses:
viol.append(f"BAD_STATUS:{d.get('id', '?')}:{d.get('status')}")
if ids.count(d.get("id")) > 1:
viol.append(f"DUPLICATE_ID:{d.get('id')}")
for target in handoff["handoffToVolume3"]:
if target not in known:
viol.append(f"DANGLING_HANDOFF:{target}")
continue
st = next(d["status"] for d in items if d["id"] == target)
if st != "done":
viol.append(f"HANDOFF_NOT_DONE:{target}:{st}")
return sorted(set(viol))
def digest(handoff: dict, viol: list) -> str:
parts = [f"{d['id']}:{d['kind']}:{d['status']}" for d in handoff["deliverables"]]
parts += list(handoff["handoffToVolume3"]) + viol
return hashlib.sha256(";".join(parts).encode("utf-8")).hexdigest()[:16]
def main() -> int:
h = json.load(open(os.path.join(ROOT, "config/ta16_deliverables.json"), encoding="utf-8"))
viol = check(h)
by_kind = {}
for d in h["deliverables"]:
by_kind[d["kind"]] = by_kind.get(d["kind"], 0) + 1
report = {"project": h["project"], "volume": h["volume"], "nextVolume": h["nextVolume"],
"deliverableCount": len(h["deliverables"]), "byKind": by_kind,
"handoffCount": len(h["handoffToVolume3"]), "handoff": h["handoffToVolume3"],
"violations": viol, "count": len(viol)}
report["digest"] = digest(h, viol)
with open(os.path.join(ROOT, "out/ta16_handoff.json"), "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=2, sort_keys=True)
f.write("\n")
print(f"deliverables={report['deliverableCount']} handoff={report['handoffCount']} "
f"violations={report['count']}")
for k in sorted(by_kind):
print(f" {k:<18} {by_kind[k]}")
for t in h["handoffToVolume3"]:
print(f" HANDOFF -> vol3 {t}")
for v in viol:
print(f" VIOLATION {v}")
print(f"digest={report['digest']}")
print("wrote out/ta16_handoff.json")
return 0
if __name__ == "__main__":
sys.exit(main())
实跑 /opt/homebrew/bin/python3 sim/ta16_handoff.py:
deliverables=12 handoff=8 violations=0
runnable-artifact 4
schema 7
spec-text 1
HANDOFF -> vol3 pipeline.checklist
HANDOFF -> vol3 assembly.gate
HANDOFF -> vol3 themes.six-pipeline
HANDOFF -> vol3 manifest.schema
HANDOFF -> vol3 atlas.spec
HANDOFF -> vol3 lod.spec
HANDOFF -> vol3 gate.rules
HANDOFF -> vol3 dependency.cycle
digest=2c44e82b3d92f5a0
wrote out/ta16_handoff.json
12 项交付物、8 项交给卷三、violations=0。三类分布是 schema 7 / runnable-artifact 4 / spec-text 1——多数是机器校验的静态结构,这正是「规范先行」的落点:清单、图集、材质、LOD、骨骼、门禁规则都是 schema,人读的只有这一节本身。
16.3.3 P 层:同一套判据的 TS 内核
TS 内核 core/src/ta16/handoff.ts 与 Python 的 check() 逐条对应;测试 core/test/ta16_handoff.test.ts 读 Python 产物做跨语言对照,并带三条负例——悬空接手目标、接手未完成交付物、缺字段与非法 kind:
// file: core/src/ta16/handoff.ts
// tier: P
// verify: node --test 'core/test/ta16_*.test.ts'
// 卷二 16.3 交付物清单完整性的 P 层内核。与 sim/ta16_handoff.py 同一套规则:字段齐全、
// kind/status 合法、id 唯一、handoff 目标存在且已 done。只允许可擦除 TS 语法。
export function checkDeliverables(h: any): string[] {
const req: string[] = h.requiredFields;
const kinds = new Set<string>(h.allowedKinds);
const statuses = new Set<string>(h.allowedStatus);
const items = h.deliverables;
const ids = items.map((d: any) => d.id);
const known = new Set<string>(ids);
const viol: string[] = [];
for (const d of items) {
for (const f of req) {
if (!d[f]) viol.push(`MISSING_FIELD:${d.id ?? "?"}:${f}`);
}
if (!kinds.has(d.kind)) viol.push(`BAD_KIND:${d.id ?? "?"}:${d.kind}`);
if (!statuses.has(d.status)) viol.push(`BAD_STATUS:${d.id ?? "?"}:${d.status}`);
if (ids.filter((x: string) => x === d.id).length > 1) viol.push(`DUPLICATE_ID:${d.id}`);
}
for (const target of h.handoffToVolume3) {
if (!known.has(target)) {
viol.push(`DANGLING_HANDOFF:${target}`);
continue;
}
const st = items.find((d: any) => d.id === target).status;
if (st !== "done") viol.push(`HANDOFF_NOT_DONE:${target}:${st}`);
}
return [...new Set(viol)].sort();
}
// file: core/test/ta16_handoff.test.ts
// tier: P
// verify: node --test 'core/test/ta16_*.test.ts'
// 卷二 16.3 跨语言对照:读 sim/ta16_handoff.py 落盘的 out/ta16_handoff.json,断言 TS 侧的
// 清单完整性校验与 Python 逐位一致,并比对 sha256 指纹。负例:悬空接手目标、接手未完成
// 交付物、缺字段与非法 kind,都必须真的 fail。
import test from "node:test";
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { checkDeliverables } from "../src/ta16/handoff.ts";
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(HERE, "../..");
const h = JSON.parse(readFileSync(resolve(ROOT, "config/ta16_deliverables.json"), "utf8"));
const py = JSON.parse(readFileSync(resolve(ROOT, "out/ta16_handoff.json"), "utf8"));
const clone = (): any => JSON.parse(JSON.stringify(h));
test("交付物清单完整性:TS 与 Python 违例集合逐位一致", () => {
assert.deepEqual(checkDeliverables(h), py.violations);
assert.deepEqual(py.violations, []);
assert.equal(py.deliverableCount, h.deliverables.length);
assert.equal(py.handoffCount, h.handoffToVolume3.length);
});
test("sha256 指纹跨语言一致", () => {
const parts = h.deliverables.map((d: any) => `${d.id}:${d.kind}:${d.status}`);
const all = [...parts, ...h.handoffToVolume3, ...checkDeliverables(h)];
const digest = createHash("sha256").update(all.join(";"), "utf8").digest("hex").slice(0, 16);
assert.equal(digest, py.digest);
});
test("负例:接手一个不存在的交付物 → DANGLING_HANDOFF", () => {
const b = clone();
b.handoffToVolume3.push("spec.ghost");
assert.ok(checkDeliverables(b).includes("DANGLING_HANDOFF:spec.ghost"));
});
test("负例:接手一个还没 done 的交付物 → HANDOFF_NOT_DONE", () => {
const b = clone();
b.deliverables.find((d: any) => d.id === "atlas.spec").status = "in-progress";
assert.ok(checkDeliverables(b).includes("HANDOFF_NOT_DONE:atlas.spec:in-progress"));
});
test("负例:缺字段与非法 kind 都必须 fail", () => {
const b = clone();
delete b.deliverables[0].producer;
b.deliverables[1].kind = "video";
const v = checkDeliverables(b);
assert.ok(v.includes("MISSING_FIELD:pipeline.checklist:producer"), v.join(","));
assert.ok(v.includes("BAD_KIND:assembly.gate:video"), v.join(","));
});
实跑 node --test 'core/test/ta16_*.test.ts'(本节 5 条):
✔ 交付物清单完整性:TS 与 Python 违例集合逐位一致 (0.897333ms)
✔ sha256 指纹跨语言一致 (0.377584ms)
✔ 负例:接手一个不存在的交付物 → DANGLING_HANDOFF (0.151542ms)
✔ 负例:接手一个还没 done 的交付物 → HANDOFF_NOT_DONE (0.106542ms)
✔ 负例:缺字段与非法 kind 都必须 fail (0.1175ms)
ℹ tests 14
ℹ pass 14
ℹ fail 0
前两条断言 TS 与 Python 的违例集合与 sha256 指纹逐位相同;三条负例分别把清单改坏一处,都必须被判 fail——校验器先被负例验过,才有资格去验别人的清单。
16.3.4 边界与取舍
- 真实资产的交接(把源文件、贴图、模型真正移交给卷三)需要真实工程与权限环境,命令行不可复现,标 X;本节只对清单本身做完整性账。
- 清单校验是静态一致性:它证明「谁交给谁、状态对不对」,不证明「那份产物的内容对不对」——内容由各章自己的 S/P 门禁负责。
handoffToVolume3只允许指向done的交付物,是为了挡住「把没做完的东西挂上交接牌」;这条规则与项目无关,可移植。
阅读导航:上一节:16.2 双案例回放:6 主题走同一条管线 · 下一节:回到目录 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。