16.3 交付物与卷三衔接

16.2 证明了一条链能被同一套判据反复跑,但「跑得通」不等于「交得出去」。本节给卷二收尾:把全卷产出列成一张交付物清单,点名哪几批交给卷三《风格与品类美术实战》,并用一段 S 层脚本与一份 P 层 TS 内核校验清单完整性——字段齐全、kind/status 合法、id 唯一、接手目标存在且已 done。

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 主题走同一条管线 · 下一节:回到目录 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「gameart」更多文章

  1. 附录 A:风格参数表、量化规则与数据集索引
  2. 9.3 等距资产的制作约束
  3. 9.2 等距 tile 与遮挡规则