这一页解决一个问题:把卷五 16 章散落在正文里的配置键、复现命令与产物清单收进一处。它不重复正文的推导,只保证你想自己跑一遍时有唯一入口。
一、这份索引解决什么
卷五的可运行性主张只有一句话:一份 config,三种消费——同一份 pebble/config/ 里的整数配置,被 Python 仿真、TypeScript 内核与浏览器单文件原型同时读入。这句话要能被检验,就必须能回答三个问题:
哪些文件属于这一卷:本卷工具链文件名一律带 g<章号>_ 前缀,与前四卷的 ch / e / l / o 互不重叠。每个数字从哪来:正文里出现的每个参数都能追到 config/g<章号>_*.json 的一个键,且全为整数(金额用「分」,比例用「千分比」)。怎么复现:每一档有一条确定的命令行,跑完产出 JSON / CSV / SVG 或测试结果。
下面三节分别给出命名约定、环境前提与逐层复现命令;第六、七节给出两个可跑的索引脚本,把这份索引本身变成可验证的产物。
二、卷五工具链命名约定
本卷四种形态的命名如下(NN 为两位章号):
config/g03_matrix.json 每章一份或多份整数配置表(D 层)
sim/g03_build_matrix.py Python 仿真脚本(S 层),一律用 /opt/homebrew/bin/python3
core/src/g03/platform.ts TypeScript 内核模块(P 层),每章一个 gNN/ 目录
core/test/g03_matrix.test.ts 内核测试,glob 必须写全 g03_*.test.ts
web/build_g05_text_fit.mjs 浏览器原型构建脚本(W 层,前缀在第二位)
web/verify_g05_text_fit.mjs 无头验收脚本,产出 out/shots/g05_text_fit.png
注意 web/ 层前缀不在首位(build_g05_… / verify_g05_…),清点脚本必须同时认这两套写法,否则 W 层会被统计成 0。测试 glob 也必须写全章号:
# snippet: 五卷的测试 glob 前缀互不重叠,node --test 不会跨卷误匹配
node --test 'core/test/ch00_*.test.ts' # 卷一
node --test 'core/test/e03_*.test.ts' # 卷二
node --test 'core/test/l03_*.test.ts' # 卷三
node --test 'core/test/o03_*.test.ts' # 卷四
node --test 'core/test/g03_*.test.ts' # 卷五
不要写成裸 g*:node --test 接收的是 glob 而非目录,economy 等共享文件名也以 e 开头,裸通配会跨卷误匹配。
三、环境速查
| 项 | 值 | 说明 |
|---|---|---|
| Python | /opt/homebrew/bin/python3(3.14.6) | 带 numpy / pandas / PIL;系统 /usr/bin/python3 是 3.9.6,不要用 |
| Node | 26 及以上 | 原生跑 TypeScript,但是 strip-only:禁 enum / namespace / 参数属性 / 装饰器 |
| Playwright | 全局已装 | W 层验收用 NODE_PATH=/opt/homebrew/lib/node_modules |
| 图表 | 纯 Python xml.etree | 本机没有 matplotlib,SVG 一律手写 |
| 包管理 | 无 | 不建 package.json、不装 npm 包,原型与仿真均零依赖 |
两条必须记住的陷阱:python3 有两个,正文与脚本一律写全路径 /opt/homebrew/bin/python3;date 必须严格早于构建时刻,否则 Hugo 的 buildFuture = false 会静默跳过该页并把指向它的链接变成 REF_NOT_FOUND。
四、六层可运行性口径与复现命令
| 层 | 含义 | 复现命令 | 产物 |
|---|---|---|---|
| S 仿真 | Python 数值仿真 | /opt/homebrew/bin/python3 sim/g03_build_matrix.py --config config/ --out out/ | out/*.json + .csv + .svg |
| P 内核 | TypeScript 逻辑内核 | node --test 'core/test/g03_*.test.ts' | 测试结果(pass / fail) |
| W 网页 | 单文件 HTML 原型 | node web/build_g05_text_fit.mjs → NODE_PATH=/opt/homebrew/lib/node_modules node web/verify_g05_text_fit.mjs web/dist/g05.html | web/dist/g05.html + 截图 |
| D 静态 | 配置表与清单 | node --test 'core/test/g03_*.test.ts' 中的 schema 用例 | 校验结果 |
| X 不可复现 | 真实平台后台 / 审核 / 到账 / 税务结论 | 无 | 正文逐处标注 |
| B 基准 | 扰动下的稳定性 | 与同章 S 脚本一同跑 | JSON + 软告警(不硬失败) |
跑 S 层时不要用通配符:python3 sim/g03_*.py 只会执行第一个脚本,其余被当成参数吃掉。逐个跑,或用 for 循环:
# snippet: 逐章逐个跑 S 层,避免 shell 把通配符展开成多个文件名
for f in sim/g03_*.py; do /opt/homebrew/bin/python3 "$f" --config config/ --out out/ || exit 1; done
五、全书公式与参数索引
每个参数都能追到一个配置键;下表按章给出主配置与关键参数(全部为整数)。
- 第 1–2 章
g01_platform_tiers.json(playRatePermille/tiers)、g01_share_terms.json(revenueMix)、g01_selection.json(budgetManDays/dimensions)、g02_sdk_matrix.json(formatKeys/weights)、g02_submission.json(materialKeys/rejectionReasons)、g02_platform_features.json(featureKeys/fallbackCost)。 - 第 3–4 章
g03_channels.json(base/features)、g03_matrix.json(baseSizeKb/assetTiers)、g03_consistency.json(errorRollbackPermille/goldenSourceRev)、g04_adformats.json、g04_pure_iaa.json(sessionsPerDauPermille/publisherSharePermille)、g04_revenue_mix.json(totalDau/daysPerMonth)。 - 第 5–6 章
g05_i18n.json(baseLocale/keyRules)、g05_langpack.json(chains/packBytes)、g05_textfit.json(containerWidthPx/baseWidthPx)、g06_taboo.json(rules)、g06_variants.json(markets)、g06_format.json(locales/fx/samples)。 - 第 7–8 章
g07_pseudo.json(padCount/charWidthPx)、g07_qa.json(checks/results)、g07_cost.json(translateFenPerWord/reviewFenPerHour/layoutFenPerKey)、g08_lang_metric.json(minSampleNewUsers)、g08_event_creative.json(languages/events)、g08_minor_lang.json(budgetFen/horizonMonths)。 - 第 9–10 章
g09_data_map.json(regimes/fields)、g09_kids_policy.json(ageBands/platformFlags)、g09_consent_transfer.json(purposes/transfers)、g10_att_link.json(attStates/skanFields)、g10_alt.json(seed/noisePermille)、g10_dp_agg.json(budget/minCount)。 - 第 11–12 章
g11_payout_methods.json(methods)、g11_fx_fee.json(commissionPermille/pairs)、g11_tax_form.json(forms/withholding)、g12_content_rights.json(regions/contents)、g12_sanction_regions.json(tierPolicy)、g12_fraud.json(seed/cohorts/sweepThresholds)。 - 第 13–14 章
g13_ua_caliber.json(spendFen/settleModels)、g13_creative_compliance.json(severity/regions)、g13_roi_stop_loss.json(seed/stopLoss)、g14_rank_structure.json(signals)、g14_competitor_localization.json(dimensionWeights)、g14_genre_region.json(affinityPermille)。 - 第 15–16 章
g15_timeline.json(sharedTasks)、g15_landing.json(items)、g15_data_iter.json(seed/volatilityPermille)、g16_manifest.json(chapters)、g16_fullchain.json(netArpdauThresholdDeciFen)、g16_finale.json(readyThresholdPermille/chain/unknowns)。
三条硬规则贯穿全部配置:不用浮点(金额用「分」、比例用「千分比」);除法一律整数地板除;随机数必须可播种(core/ 内禁用 Math.random(),TS 侧幂运算用 BigInt)。
六、S 层清点脚本:把这份索引重新跑出来
下面这段脚本扫描 pebble/ 下全部 g<章号>_ 文件,按章与目录分层统计,产出 out/g17_manifest.json / .csv / .svg。它同时是这份索引的自检工具:跑一遍就知道有没有哪一章的文件被漏算。
它只读一份配置 config/g17_manifest.json:prefix(卷五为 g)、chapters(16)与 dirs(config / sim / core/src / core/test / web / web/dist / out),全整数、无浮点、无随机。脚本 sim/g17_manifest.py(与工作区文件逐字节一致):
#!/usr/bin/env python3
# file: sim/g17_manifest.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/g17_manifest.py --config config/ --out out/
"""清点卷五工具链:扫描 pebble/ 下属于本卷的文件,按章与目录分层统计,
产出 out/g17_manifest.json / .csv / .svg。纯标准库,全整数,无随机。
三种命名形态都要认:config / sim / core/test 是 `<前缀><章号>_…`(前缀在首位),
web 层是 `build_<前缀><章号>_…` / `verify_<前缀><章号>_…`(前缀在第二位),
`core/src/` 下每章是一个 `<前缀><章号>/` 目录(里面的 .ts 文件不带前缀)。
目录按名归章并递归统计其中的常规文件,不把目录本身记成一条——卷四附录 E
那版脚本没有这一步,把目录当成文件计数、行数记 0,故两卷数字不可直接相加。
"""
import argparse
import json
import os
import xml.etree.ElementTree as ET
def line_count(path):
try:
with open(path, encoding='utf-8') as fh:
return fh.read().count('\n') + 1
except (UnicodeDecodeError, OSError):
return 0
def chapter_of(fname, prefix):
"""返回该条目归属的两位章号;不属于本卷工具链则返回 None。"""
for pre in (prefix, f"build_{prefix}", f"verify_{prefix}"):
if fname.startswith(pre):
head = fname[len(pre):len(pre) + 2]
if len(head) == 2 and head.isdigit():
return head
return None
def count_entry(dpath, fname):
"""把一个条目折算成 (文件数, 行数):目录按名归章并递归统计其中的常规文件。"""
full = os.path.join(dpath, fname)
if not os.path.isdir(full):
return 1, line_count(full)
n = lines = 0
for sub in sorted(os.listdir(full)):
subfull = os.path.join(full, sub)
if os.path.isfile(subfull):
n += 1
lines += line_count(subfull)
return n, lines
def write_svg(path, rows, peak):
h, w = 220, 40 + 30 * len(rows)
root = ET.Element('svg', {'xmlns': 'http://www.w3.org/2000/svg', 'width': str(w),
'height': str(h), 'viewBox': f'0 0 {w} {h}'})
ET.SubElement(root, 'text', {'x': '8', 'y': '16', 'font-size': '11'}).text = \
'files per chapter (g* tools)'
for i, r in enumerate(rows):
x = 24 + i * 30
bh = 0 if peak == 0 else (160 * r['files']) // peak
ET.SubElement(root, 'rect', {'x': str(x), 'y': str(190 - bh), 'width': '20',
'height': str(bh), 'fill': '#47a'})
ET.SubElement(root, 'text', {'x': str(x + 10), 'y': '205', 'font-size': '9',
'text-anchor': 'middle'}).text = r['chapter']
ET.ElementTree(root).write(path, encoding='utf-8', xml_declaration=True)
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--config', default='config/')
ap.add_argument('--out', default='out/')
args = ap.parse_args()
with open(os.path.join(args.config, 'g17_manifest.json'), encoding='utf-8') as fh:
cfg = json.load(fh)
root = os.path.dirname(os.path.abspath(args.config.rstrip('/')))
rows, total_files, total_lines = [], 0, 0
for ch in range(1, cfg['chapters'] + 1):
ch_id = f'{ch:02d}'
by_dir, ch_files, ch_lines = {}, 0, 0
for d in cfg['dirs']:
dpath = os.path.join(root, d)
n = 0
for fname in sorted(os.listdir(dpath)) if os.path.isdir(dpath) else []:
if chapter_of(fname, cfg['prefix']) == ch_id:
nf, nl = count_entry(dpath, fname)
n += nf
ch_lines += nl
by_dir[d] = n
ch_files += n
rows.append({'chapter': ch_id, 'files': ch_files, 'lines': ch_lines, 'byDir': by_dir})
total_files += ch_files
total_lines += ch_lines
digest = ','.join(f"{r['chapter']}:{r['files']}:{r['lines']}" for r in rows)
digest += f';files={total_files},lines={total_lines}'
peak = max((r['files'] for r in rows), default=0)
os.makedirs(args.out, exist_ok=True)
with open(os.path.join(args.out, 'g17_manifest.csv'), 'w', encoding='utf-8') as fh:
fh.write('chapter,files,lines\n')
for r in rows:
fh.write(f"{r['chapter']},{r['files']},{r['lines']}\n")
write_svg(os.path.join(args.out, 'g17_manifest.svg'), rows, peak)
out = {'version': cfg['version'], 'game': cfg['game'], 'observedAt': cfg['observedAt'],
'rows': rows, 'totalFiles': total_files, 'totalLines': total_lines,
'peakFiles': peak, 'digest': digest}
with open(os.path.join(args.out, 'g17_manifest.json'), 'w', encoding='utf-8') as fh:
json.dump(out, fh, ensure_ascii=False, indent=2)
print(json.dumps({'totalFiles': total_files, 'totalLines': total_lines,
'peakFiles': peak, 'digest': digest}, ensure_ascii=False))
if __name__ == '__main__':
main()
真跑输出(/opt/homebrew/bin/python3 sim/g17_manifest.py --config config/ --out out/):
01:17:1382,02:21:1486,03:21:1184,04:15:815,05:16:937,06:18:1235,07:17:1000,08:18:1357,09:21:1449,10:20:1346,11:11:610,12:21:1702,13:19:1268,14:17:1177,15:17:1099,16:18:2177;files=287,lines=20224
core/src/ 是三种命名形态里唯一按目录归章的:每章一个 gNN/ 目录,里面的 .ts 文件不带章号前缀。清点脚本按目录名归章并递归统计其中的常规文件,不把目录本身记成一条——卷四附录 E 那版脚本没有这一步,把目录当成文件计数、行数记 0;本版修掉了这个口径缺陷,故两卷的清点数字不可直接相加。
七、P 层索引校验内核:与 Python 侧逐字符对拍
清点逻辑在 TypeScript 侧重写了一遍(core/src/g17/index_audit.ts),用同一份配置扫同一个工作区,摘要必须与 Python 侧逐字符一致。这是「一份逻辑两种宿主」在附录里的最小落点。核心函数摘录:
// snippet: 索引校验内核的核心函数(完整文件见 core/src/g17/index_audit.ts)
/** 返回该文件名归属的两位章号;不属于本卷工具链则返回 null。 */
export function chapterOf(fname: string, prefix: string): string | null {
for (const pre of [prefix, `build_${prefix}`, `verify_${prefix}`]) {
if (fname.startsWith(pre)) {
const head = fname.slice(pre.length, pre.length + 2);
if (isTwoDigits(head)) return head;
}
}
return null;
}
// …(类型声明、lineCount、countEntry 与逐章循环略;
// lineCount 等价于 count('\n') + 1,countEntry 对 gNN/ 目录递归统计其中的 .ts;
// 完整文件见 core/src/g17/index_audit.ts)
let digest = rows.map((r) => `${r.chapter}:${r.files}:${r.lines}`).join(",");
digest += `;files=${totalFiles},lines=${totalLines}`;
const peakFiles = rows.reduce((m, r) => (r.files > m ? r.files : m), 0);
验收命令与真实结果(node --test 的 6 个用例):
# snippet: 与 Python 侧清点逐字符对拍
node --test 'core/test/g17_index_audit.test.ts'
✔ 跨语言一致:TS 清点摘要 == Python 金标准 ✔ 章号齐全、无空章、合计自洽
✔ 峰值章等于逐章文件数的最大值 ✔ 归章认三套前缀;错前缀返回 null
✔ core/src 一档统计的是 .ts 文件而非目录本身 ✔ 行数口径等价于 count('\n') + 1
ℹ tests 6 ℹ pass 6 ℹ fail 0
八、一键复现与排错顺序
从零复现卷五全部产物,按这个顺序走:
/opt/homebrew/bin/python3 sim/ch00_env_check.py—— 确认解释器、numpy / pandas 与 Node 版本。/opt/homebrew/bin/python3 sim/g17_manifest.py --config config/ --out out/—— 清点工具链,确认文件数与行数落在预期区间。- 逐章跑 S 层:
for f in sim/g<NN>_*.py; do /opt/homebrew/bin/python3 "$f" --config config/ --out out/; done(不要用裸通配符)。 node --test 'core/test/g<NN>_*.test.ts'—— P 层逐章跑;W 层两节用build_g05/verify_g05与build_g07/verify_g07。node --test 'core/test/g17_index_audit.test.ts'—— 回到这份索引本身做一次跨语言对拍。
排错顺序(按「先怀疑自己的判据」排):
- 数字对不上:先确认跑的是本卷前缀。
mg_run*.py的--book是装饰性参数,只读单槽out/blocks.json,先mg_extract.py --book 跨平台发行与出海覆盖,否则跑的是上一卷的块。 - W 层断言随机失败:
mg_runweb.py的截图断言扫全局out/shots/*.png,每卷开工前清空该目录;无头 Chromium 冷启动首个页面读 canvas 像素可能恒空白,断言写>= 1 px并重试一次。 - P 层报
ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX:Node 26 是 strip-only,把enum/namespace/ 参数属性 / 装饰器换掉。 - 测试 glob 命中别的卷:写全
g<章号>_*.test.ts,不要写裸g*。 - 链接 404:站内单段
/<topic>/是死链(普通文章在根级/<slug>/,栏目页在/posts/<topic>/);含中文的 slug 必须核对public/产物,Hugo 会把路径里的 Latin 字符小写化。 - 构建报
REF_NOT_FOUND:检查date是否早于构建时刻;--quiet会掩盖这类错误却仍 exit 0,必须用非 quiet 构建。
卷四的同类索引见附录 E:全书公式、参数与数据集索引 ;本卷的定位与分工见卷五目录 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。