一、这份索引解决什么
卷二 48 节各自给了规则、参数与一份可跑脚本,但它们散在 16 章里。这份附录做三件事:把管线规则与公式汇成一张表,把工具链文件汇成一套命名约定,把六层复现命令汇成一段可以照着敲的清单。最后给出两段真跑的脚本——S 层清点器与 P 层索引内核——它们对同一份工作区算出的清单与摘要逐位一致,本身就是本卷「一份规范、多处消费」主张的一次小型演示。
二、卷二工具链命名约定
本卷与卷一共用工作区 prism,但所有文件一律用 ta<章号>_ 前缀:
| 目录 | 命名 | 例 |
|---|---|---|
config/ | ta<章号>_<用途>.json | config/ta10_safearea.json |
sim/ | ta<章号>_<用途>.py | sim/ta10_safearea.py |
core/src/ | ta<章号>/<用途>.ts | core/src/ta10/safearea.ts |
core/test/ | ta<章号>_<用途>.test.ts | core/test/ta10_safearea.test.ts |
web/ | build_ta<章号>_<用途>.mjs / verify_ta<章号>_<用途>.mjs | web/build_ta10_safearea.mjs |
唯一真源仍是 config/visual_spec.json(卷一落盘,本卷只读),6 个主题包在 config/sp16_themes.json。前缀严格不串卷:sp 是卷一、ta 是本卷、st 是卷三;清点脚本按路径前缀过滤,所以三卷的数字互不干扰。
# snippet: node --test 接收的是 glob 而非目录,且必须写全章号,否则会跨章误匹配
# 只跑第 10 章的测试(注意引号:让 node 自己展开 glob)
node --test 'core/test/ta10_*.test.ts'
# 跑本卷全部测试
node --test 'core/test/ta*.test.ts'
写 core/test/ta*_*.test.ts 才只命中本卷。另外 web/ 层的章号不在文件名首位(build_ta10_…),清点脚本要同时认这两套命名。
三、环境速查
| 项 | 本机实测值 | 说明 |
|---|---|---|
| Python | /opt/homebrew/bin/python3 = 3.14.6(/usr/bin/python3 = 3.9.6 无 numpy,不要用) | S 层一律用全路径 |
| numpy / pandas / Pillow | 2.5.1 / 3.0.3 / 12.3.0 已装;matplotlib / scipy / colour 均未装 | 图表纯 xml.etree 生成 SVG |
| Node / tsc | v26.5.0(strip-only:禁 enum / namespace / 参数属性 / 装饰器);tsc 未装 | P 层靠原生类型擦除,不做类型检查 |
| Playwright | 1.59.1(全局) | W 层需 NODE_PATH=/opt/homebrew/lib/node_modules + createRequire |
| DCC | 一个都没有 | 本卷 2.3 整节把这件事写成 X 范式 |
四、六层可运行性口径与复现命令
| 层 | 复现命令 |
|---|---|
| S 仿真层 | /opt/homebrew/bin/python3 sim/ta<章号>_*.py(逐个跑,产出 JSON / CSV / SVG) |
| P 内核层 | node --test 'core/test/ta<章号>_*.test.ts'(必须写全 glob) |
| W 网页层 | node web/build_ta10_safearea.mjs 产出 HTML,再 NODE_PATH=/opt/homebrew/lib/node_modules node web/verify_ta10_safearea.mjs |
| D 静态层 | 无独立命令:schema 由 sim/ 与 core/ 同时读入并校验 |
| X 不可复现 | 无命令:正文每处显式标注「需真实环境,命令行不可复现」 |
| B 基准 | /opt/homebrew/bin/python3 sim/ta16_bench.py(软告警,不硬失败) |
# snippet: 逐章逐个跑 S 层,避免 shell 把通配符展开成多个文件名
for f in sim/ta*.py; do /opt/homebrew/bin/python3 "$f" || exit 1; done
五、全卷规则表与公式索引
整数类(跨语言逐位一致)
| 量 | 公式 | 出处 |
|---|---|---|
| 对比度(×10) | contrast10 = (Lmax + 500) * 10 // (Lmin + 500) | 3.2 / 16.2 |
| 装箱率(千分比) | permille = placedArea * 1000 // atlasArea | 5.1 |
| mip 链字节 | bytes = Σ ceil(w / 2^i) * ceil(h / 2^i) * bpp | 5.2 |
| 显存总量 | vram = mipChainBytes * 格式乘数 // 1000 | 5.2 / 8.2 |
| 资源分级缩放(千分比) | scale = targetWidth * 1000 // designWidth,选 ratio >= scale 的最小档 | 10.2 |
| 合批缩减(千分比) | reducedPermille = (naive - sorted) * 1000 // naive | 10.3 |
| 中台年节省(×100) | annualSavingX100 = projectsAffected * repeatPersonDaysX100 - maintainX100 | 13.1 |
| 回本月数(×100) | paybackMonthsX100 = buildX100 * 12 * 100 // 年节省(年节省 ≤ 0 记 null) | 13.1 |
| 复用收益(×100) | reuseGainX100 = (consumers - 1) * localCostX100 | 13.2 |
| 采纳率(千分比) | adoptionPermille = activeUsers * 1000 // targetUsers | 13.3 |
| 节省人天(×100) | savedPersonDaysX100 = activeUsers * sessions * minutes * 100 // minutesPerPersonDay | 13.3 |
| 工具有效率(千分比) | efficiencyPermille = savedPersonDaysX100 * 1000 // maintainPersonDaysX100 | 13.3 |
| 关键路径 | 前向 EF = ES + dur、后向 LS = LF - dur、浮动 slack = LS - ES,全整数 | 14.2 |
| 外包总价 | total = Σ 件数 * 阶梯单价(档位),档位与单价全整数 | 15.1 |
| 风格距离(×10) | 五维风格向量的加权整数距离,对角为 0 | 16.2 |
| 浮点类(跨语言容差内一致) |
| 量 | 公式 | 声明容差 |
|---|---|---|
| sRGB → 线性 | c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4 | 相对误差 ≤ 1e-9 |
| 线性 → sRGB | 上式逆函数,同一组常数 | 相对误差 ≤ 1e-9 |
| OKLab | Ottosson 同一组矩阵常数(两次矩阵乘法 + 立方根) | 相对误差 ≤ 1e-9 |
| OKLCH | 由 OKLab 取 L / C = hypot(a,b) / H = atan2(b,a) | 相对误差 ≤ 1e-9 |
| 色盲模拟 | Brettel / Viénot–Mollon 1999 同一组矩阵,输出 8-bit 整数 | 通道差 ≤ 1 |
正文一律不写「三处颜色完全相同」,只写「容差内一致,实测最大偏差 N」。本卷实测的最大偏差量级为 4.9e-13(16.2 的 OKLab 复算),比声明容差 1e-9 小三个数量级——但那是实测结果,不是承诺。
六、S 层清点脚本:把这份索引重新跑出来
下面这段只登记本卷 ta 前缀的四个清单(config/ / sim/ / core/src/ / core/test/),按路径排序后逐个登记,算总数并给一个跨语言可复算的摘要。只登记静态文件路径,不登记产物(out/ 随时会被清空重建),所以同一份工作区在任何机器上跑出的清单与摘要都逐位一致:
#!/usr/bin/env python3
# file: sim/ta17_index.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/ta17_index.py
"""附录 A 的 S 层索引清点器(卷二):只登记**本卷 `ta` 前缀**的四个清单(config / sim /
core/src / core/test),按路径排序后逐个登记,算总数并给一个跨语言可复算的摘要(digest)。
只登记**静态文件路径**,不登记产物(out/ 下随时会被清空重建),所以同一份工作区在任何
机器上跑出的清单与摘要都逐位一致;前缀过滤让两卷的数字互不干扰。digest 用两个不同
偏移量的 FNV-1a 32 位拼成 16 位十六进制,TS 侧用 Math.imul 复算,故两侧必然相同。
"""
from __future__ import annotations
import json
import os
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
GROUPS = (
("config", "config", lambda rp: rp.startswith("config/ta")),
("sim", "sim", lambda rp: rp.startswith("sim/ta")),
("coreSrc", "core/src", lambda rp: rp.startswith("core/src/ta")),
("coreTest", "core/test", lambda rp: rp.startswith("core/test/ta")),
)
def listing(rel: str, pred) -> list[str]:
d = os.path.join(ROOT, rel)
out: list[str] = []
for dirpath, _dirs, files in os.walk(d):
for n in files:
rp = os.path.relpath(os.path.join(dirpath, n), ROOT)
if pred(rp):
out.append(rp)
return sorted(out)
def fnv1a(text: str, basis: int) -> int:
h = basis
for ch in text.encode("utf-8"):
h = ((h ^ ch) * 0x01000193) & 0xFFFFFFFF
return h
def digest_of(lines: list[str]) -> str:
joined = "\n".join(lines)
return f"{fnv1a(joined, 0x811C9DC5):08x}{fnv1a(joined, 0x7FFFFFFF):08x}"
def main() -> int:
groups: dict[str, list[str]] = {}
all_lines: list[str] = []
for key, rel, pred in GROUPS:
files = listing(rel, pred)
groups[key] = files
all_lines.extend(files)
spec = json.load(open(os.path.join(ROOT, "config/visual_spec.json"), encoding="utf-8"))
report = {
"project": spec["project"],
"volume": "art-ta",
"counts": {k: len(v) for k, v in groups.items()},
"total": len(all_lines),
"digest": digest_of(all_lines),
"groups": groups,
}
with open(os.path.join(ROOT, "out/ta17_index.json"), "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=2)
f.write("\n")
print(f"project={report['project']} volume={report['volume']} "
f"total={report['total']} digest={report['digest']}")
for k, _rel, _pred in GROUPS:
print(f" {k:<9} {report['counts'][k]:>3}")
print("wrote out/ta17_index.json")
return 0
if __name__ == "__main__":
sys.exit(main())
实跑 /opt/homebrew/bin/python3 sim/ta17_index.py(前缀过滤只认本卷 ta 文件,所以卷一、卷三落盘后这四个数字不会变):
project=stardust-inn volume=art-ta total=184 digest=afb5b8548e1399fe
config 45
sim 51
coreSrc 44
coreTest 44
wrote out/ta17_index.json
七、P 层索引校验内核:与 Python 侧逐位对拍
同一个摘要用 TypeScript 复算一遍。关键在两处:os.walk 的顺序不可依赖,所以两侧都先排序;JS 的 32 位乘法会丢精度,所以必须用 Math.imul:
// file: core/src/ta17/index.ts
// tier: P
// verify: node --test 'core/test/ta17_*.test.ts'
// 附录 A 的 P 层索引内核(卷二)。与 sim/ta17_index.py 同一套规则:只登记本卷 `ta` 前缀的
// 四个清单,按路径排序后拼接,用两个不同偏移量的 FNV-1a 32 位拼成 16 位十六进制摘要。
// 只登记静态文件路径,不登记产物,故两侧清单与摘要必然逐位一致。只允许可擦除 TS 语法。
import { readdirSync, statSync } from "node:fs";
import { join, relative, sep } from "node:path";
const GROUPS: { key: string; rel: string; pred: (rp: string) => boolean }[] = [
{ key: "config", rel: "config", pred: (rp) => rp.startsWith("config/ta") },
{ key: "sim", rel: "sim", pred: (rp) => rp.startsWith("sim/ta") },
{ key: "coreSrc", rel: "core/src", pred: (rp) => rp.startsWith("core/src/ta") },
{ key: "coreTest", rel: "core/test", pred: (rp) => rp.startsWith("core/test/ta") },
];
export function listing(root: string, rel: string, pred: (rp: string) => boolean): string[] {
const out: string[] = [];
const walk = (abs: string) => {
for (const name of readdirSync(abs)) {
const full = join(abs, name);
const rp = relative(root, full).split(sep).join("/");
if (statSync(full).isDirectory()) walk(full);
else if (pred(rp)) out.push(rp);
}
};
walk(join(root, rel));
return out.sort();
}
export function fnv1a(text: string, basis: number): number {
let h = basis >>> 0;
const bytes = Buffer.from(text, "utf8");
for (const b of bytes) h = Math.imul(h ^ b, 0x01000193) >>> 0;
return h;
}
export function digestOf(lines: string[]): string {
const joined = lines.join("\n");
const a = fnv1a(joined, 0x811c9dc5).toString(16).padStart(8, "0");
const b = fnv1a(joined, 0x7fffffff).toString(16).padStart(8, "0");
return a + b;
}
export function indexWorkspace(root: string): { counts: Record<string, number>; total: number; digest: string; groups: Record<string, string[]> } {
const groups: Record<string, string[]> = {};
const counts: Record<string, number> = {};
const all: string[] = [];
for (const g of GROUPS) {
const files = listing(root, g.rel, g.pred);
groups[g.key] = files;
counts[g.key] = files.length;
all.push(...files);
}
return { counts, total: all.length, digest: digestOf(all), groups };
}
配套测试 core/test/ta17_index.test.ts 一边读 Python 落盘的 out/ta17_index.json 做跨语言对照,一边用内存清单构造两条负例(摘要必须内容敏感、空清单必须有确定值)。实跑 node --test 'core/test/ta17_*.test.ts':
✔ 索引清点:TS 复算与 Python 产物逐位一致 (23.595542ms)
✔ 正例:四个清单都非空,且 total 等于四者之和 (18.619ms)
✔ 负例:摘要内容敏感,改一个字符必须换一个摘要 (0.1245ms)
✔ 负例:空清单有确定摘要,且与单元素清单不同 (0.084917ms)
ℹ tests 4
ℹ pass 4
ℹ fail 0
两侧的 total=184 与 digest=afb5b8548e1399fe 一致——这就是本卷反复强调的那句话的一次最小演示:同一份输入,两套语言,一个结论。更硬的一条证据在 16.2:同一份 config/sp16_themes.json 被卷一与卷二各自复算,风格向量与 6×6 距离矩阵的摘要 fa04e9418d9f3c2c 逐位相同。
八、一键复现与排错顺序(四步,不要跳步)
- 先跑环境自检:
/opt/homebrew/bin/python3 sim/ta00_env_check.py。它不通过,后面全是噪音。 - 再跑 S 层:
for f in sim/ta*.py; do /opt/homebrew/bin/python3 "$f" || exit 1; done。S 层失败通常是配置被改坏了,先看是哪一章的config/ta<章号>_*.json。 - 然后跑 P 层:
node --test 'core/test/ta*.test.ts'。P 层的跨语言对照失败几乎总是因为第 2 步没跑——它读的是 S 层落盘的 JSON。 - 最后跑 W 层与 B 层:
node web/build_ta10_safearea.mjs配verify_ta10_safearea.mjs跑浏览器断言;再跑/opt/homebrew/bin/python3 sim/ta16_bench.py看跨语言容差与阈值敏感性的软告警。
三个最常见的坑:用了系统 python3(无 numpy,报 ModuleNotFoundError);没写全 glob(node --test core/test/ 找不到任何 .ts,静默 0 测试);只跑 P 层没跑 S 层(对照文件不存在,报 ENOENT)。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。