PPysolate SpineREAD THE CODE · UNDERSTAND THE SYSTEM
课程与章节
CODE READING / 38

把讲义与源码装进网站

prepare.mjs / readings.mjs

本章目录

本章要回答的问题: 执行 pnpm build 后,一篇仓库里的 Markdown 和一组允许公开的源码,怎样变成带导航、代码链接和检查结果的静态站点?

调用者是 site/package.json 的 build 脚本。它先在 Node.js 进程中运行 scripts/prepare.mjs,再交给 Astro,最后运行 scripts/verify.mjs。本章跟的是第一段:输入是仓库根目录里的文档、源码和几份清单;输出是 .generated/ 下供 Astro 消费的 Markdown、导航 JSON、源码 JSON 和覆盖摘要。浏览器尚未参与,Go/Wasm runtime 也没有被执行。

1. 构建进程先取得边界

site/scripts/prepare.mjs · L1–L7 · 打开完整源码

此文件的私有运行环境规则已在公开展示中隐藏;行号保持对应本机源码。

/* Public view: redacted lines; not a complete executable copy. */ // Explicit publishing boundary: only listed docs/source, never the whole repository.
import fs from 'node:fs';
import path from 'node:path';
import { symbolsFor } from './symbols.mjs';
import { readings, readingSources } from './readings.mjs'; import { goPatternDocs, goPatternSources } from './go-patterns.mjs'; import { deepDocs, deepSources } from './deep.mjs';
import { expandExcerpts, verifyCoverage } from './excerpts.mjs';
const root = path.resolve('..');

prepare.mjs 的开头把发布工作限制在明确的数据流里。Node 的 fs 负责读写,path 负责在仓库根和站点目录之间拼路径;symbolsFor 只为源码页提取声明目录,expandExcerpts 与 verifyCoverage 负责讲义中的源码片段。root = path.resolve('..') 的含义取决于调用位置:标准命令从 site 目录启动,所以 .. 才是仓库根,而不是访问者浏览器的当前目录。

这一点决定了后面的所有输入:脚本读取的是本机仓库文件,页面构建只是之后消费生成数据。若在仓库根直接运行这个脚本,路径基准就不同,不能把“脚本存在”误解为“任何目录都能启动”。

2. 两张清单把内容和源码分开

site/scripts/prepare.mjs · L8–L37 · 打开完整源码

const docs = [
  ['foundations', '前置课程:从刚入门开始', 'docs/foundations/index.md', '只默认基本编程,先建立理解实现所需的知识地图。', 'basics'],
  ['basics-tools', '工程工具与数据', 'docs/foundations/engineering-basics.md', '终端、文件、依赖、JSON 与测试分别在做什么。', 'basics'],
  ['basics-go', 'Go 语言入门', 'docs/foundations/go-language.md', '从函数和变量读到 struct、方法与接口。', 'basics'],
  ['basics-values', 'Go 的值与内存', 'docs/foundations/go-values.md', '指针、slice、map、拷贝与生命周期。', 'basics'],
  ['basics-execution', '程序怎样运行', 'docs/foundations/execution-stack.md', '编译、解释、CPython、Wasm 与 Host/Guest。', 'basics'],
  ['basics-memory', '内存与跨语言 ABI', 'docs/foundations/memory-abi.md', '从 bit/byte 到指针、buffer 与引用计数。', 'basics'],
  ['basics-concurrency', '并发、等待与取消', 'docs/foundations/concurrency.md', 'goroutine、channel、可见性和任务收尾。', 'basics'],
  ['basics-plm', 'PLM 的思考基础', 'docs/foundations/plm-reasoning.md', '依赖、AST、只读快照与交付时机。', 'basics'],
  ['basics-linux', 'Ubuntu 构建与离线演示', 'docs/foundations/linux-build-and-offline.md', 'WSL、交叉编译、构建缓存与 network namespace。', 'basics'],
  ['basics-web', 'Web 与发布(可选)', 'docs/foundations/web-basics.md', '浏览器、HTTP、Pages,以及 noindex 的边界。', 'basics'],

  ['research-positioning', 'Pysolate 定位:云端 Agent 执行', 'docs/research/cloud-agent-execution.md', '轻重执行分工、驻留成本与固定资源下的任务容量。', 'research'],
  ['research-reuse', '研究探索:生成代码复用', 'docs/research/reusable-code-exploration.md', '从问题定义、方法方向到实验数据与后续设想。', 'research'],
  ['research-memory', '内存优化历程:每个新增 Guest 的成本', 'docs/research/memory-optimization.md', '从引擎冗余、COW 写放大到分配器布局,记录配对数据与失败方案。', 'research'],
  ['guide', '学习顺序与完整路径', 'docs/learning-guide.md', '从两行 Python 到 Host 边界,逐步读懂实现。'],
  ['flow', '调用与状态图', 'docs/execution-flow.md', '区分 Guest 控制流、Host 任务与 Future。'],
  ['plm', 'Whole-program PLM', 'docs/whole-program-plm.md', '依赖、控制流与原调用处的结果交付。'],
  ['prefix', '流式 Prefix', 'docs/prefix.md', '接收完整语句,复用同一个 Guest 与 Future。'],
  ['prepared-copy', '可选:Prepared full-copy', 'docs/prepared-copy.md', '先理解干净基线与全量私有副本。'],
  ['cow', 'Linux COW:共享与隔离', 'docs/cow.md', '虚拟内存、memfd、同地址映射与私有写入。'],
  ['examples', '五个边界例子', 'docs/walkthrough-examples.md', '独立、依赖、分支、错误和正常回退。'],
  ['python-provider', 'Python 工具实现', 'docs/python-provider.md', 'Guest 经固定 Go 桥调用 native Python worker。'],
  ['python-editor', 'Python IDE 类型提示', 'docs/python-editor.md', '保留原始源码,用静态声明识别注入工具。'],
  ['core-demos', '四个核心 Demo', 'docs/core-demos.md', '普通调用、PLM、prefix、隔离与 prepared 复用。'],
  ['demo-walkthrough', '扩展:报告与通知', 'docs/demo-walkthrough.md', '一个 Python 程序,三个 Host 结果,沿调用链理解执行与状态信息。'],
  ['interview', '演示与协作练习', 'docs/interview-playbook.md', '运行入口、讲解顺序和未解答的小需求。'],
  ['questions', '六张追问卡', 'docs/question-cards.md', '带源码入口的问题,不替你写标准答案。'],
  ['build', '构建与来源', 'docs/build-and-provenance.md', '实际 Wasm 的打包边界与使用条件。'], ['meal-curriculum', '饭点课分课路线', 'docs/meal-curriculum.md', '流程课与知识小点怎样对应代码带读。'],
  ['repository', '仓库说明', 'README.md', '命令、实现范围与文件导航。'],

docs 是页面记录数组。每条记录依次保存 slug、标题、Markdown 路径、描述和可选 section。前置课程明确使用 basics,没有 section 的记录稍后会落到 implementation。这些记录决定“哪些文档成为页面、页面叫什么、属于哪组”,并不执行文档里的 Go 或 Python。

sourceFiles 是另一条发布边界。它列出 Host、Guest、测试、示例、构建脚本、依赖记录以及部分站点配置。路径写进数组只代表“构建时可以读取并加工”,不等于文件会被整个仓库扫描,更不等于它会作为可执行脚本送到浏览器。把两张清单分开,后续就能分别生成讲义页面和源码索引。

3. 合并章节来源,登记每个 URL

site/scripts/prepare.mjs · L38–L59 · 打开完整源码

];
const sourceFiles = [
  'cmd/python-provider/main.go', 'cmd/python-provider/main_test.go', 'examples/python_provider.py',
  'providers/report.py', 'examples/typings/__builtins__.pyi', 'pyrightconfig.json',
  'cmd/walkthrough/main.go', 'cmd/walkthrough/main_test.go', 'examples/notification.py', 'demos/walkthrough.sh', 'demos/core.sh',
  'runner.go', 'bridge.go', 'future.go', 'prefix.go', 'prepared.go',
  'cow.go', 'cow_linux.go', 'cow_other.go', 'cow_test.go', 'cow_linux_test.go',
  'guest/bootstrap.py', 'guest/plm.py', 'guest/prefix.py', 'guest/runtime.c',
  'cmd/demo/main.go', 'runner_test.go', 'plm_test.go', 'prefix_test.go', 'prepared_test.go',
  'cmd/demo/main_test.go', 'guest/test_plm.py', 'guest/test_prefix.py',
  'examples/lookup.py', 'examples/order.py', 'examples/dependency.py', 'examples/branch.py',
  'examples/error_delivery.py', 'examples/fallback.py', 'build-guest.sh', 'Makefile',
  'tools/setup-build-inputs.py', 'tools/build-inputs.lock.json',
  'go.mod', 'go.sum', 'LICENSE', '.gitignore', 'tools/offline-check.sh', 'tools/read-local.sh', 'site/public/_headers',
];
docs.unshift(...readings.map(([slug,title,file,description])=>[slug,title,`docs/code-reading/${file}.md`,description,'reading'])); docs.unshift(['order-tour','先跟完一次 order.py','docs/order-function-tour.md','按函数职责和调用目的读正常执行路径。','reading']);
sourceFiles.push(...readingSources.filter(name=>!sourceFiles.includes(name))); docs.push(...goPatternDocs, ...deepDocs); sourceFiles.push(...goPatternSources(root), ...deepSources); docs.push(['trading-demo','模拟交易:控制流与工具许可','docs/trading-demo.md','带时间戳观察安全读取、分支和本地下单。']); sourceFiles.push('examples/trading.py','cmd/trade-demo/main.go','cmd/trade-demo/market.go','cmd/trade-demo/main_test.go','cmd/trade-demo/market_test.go'); sourceFiles.push(...['run.sh','trade.sh','01-price-skip.sh','02-cash-skip.sh','03-host-denied.sh','04-approved.sh','05-prefix.sh','06-plm-source.sh'].map(name=>'demos/'+name));
const id = name => name.replace(/[^a-z0-9]+/gi, '-').replace(/^-|-$/g, '').toLowerCase();
const routes = new Map(docs.map(([slug,,file]) => [file, `/${slug}/`]));
for (const file of sourceFiles) routes.set(file, `/code/${id(file)}/`);
routes.set('site/public/plm-flow.svg', '/plm-flow.svg'); routes.set('site/public/deep-memory-domains.svg', '/deep-memory-domains.svg');
routes.set('site/public/robots.txt', '/robots.txt');

这一段把静态课程的多个来源合成同一份 docs 和 sourceFiles:

  • readings 是代码带读主线;order-tour 被放到最前面作为一条完整示例路径。
  • goPatternDocs 与 deepDocs 加入其他专题页面,并分别通过自己的函数计算源码边界。
  • trading-demo 加入模拟交易文档;交易示例的 Python、Go、测试和 demos/ 下的快捷脚本也进入源码清单。它们是发布数据,不是这里直接启动的演示。
  • id(name) 将路径中的非字母数字字符压成短横线并转小写。routes 以仓库相对路径为键,值分别是讲义 URL 或 /code/<id>/。因此文档里的相对链接可以被转换成站内链接,而不需要作者手填最终 URL。

routes 还登记两个 SVG 和 robots.txt,并把本地构建辅助脚本映射到构建说明页。这个映射只影响链接解析;它不会放宽源码白名单。真正的源码页候选仍然来自 sourceFiles。

4. 读取源码时先做隐私处理

site/scripts/prepare.mjs · L60–L82 · 打开完整源码

此文件的私有运行环境规则已在公开展示中隐藏;行号保持对应本机源码。

routes.set('tools/rebuild-guest.py', '/build/'); // Private machine defaults stay local.
/* [REDACTED: private operational rule] */
function check(text, name) {
  for (const pattern of forbidden) if (pattern.test(text)) throw new Error(`Private operational detail in ${name}: ${pattern}`);
}
// Only the privacy-rule implementations have a visibly redacted public view.
// Other source files still fail closed if private operational details appear.
const ruleFiles = new Set(['site/scripts/prepare.mjs', 'site/scripts/verify.mjs']);
const sources = [...new Set(sourceFiles)].map(name => {
  let code = fs.readFileSync(path.join(root,name),'utf8'), redacted = false;
  if (ruleFiles.has(name)) {
    code = code.split('\n').map(line=>{
      try { check(line,name); return line; }
      catch { redacted = true; return '/* [REDACTED: private operational rule] */'; }
    }).join('\n');
    if (redacted) code = '/* Public view: redacted lines; not a complete executable copy. */ '+code;
  }
  check(code,name);
  const ext = path.extname(name);
  const lang = ({'.go':'go','.py':'python','.c':'c','.sh':'bash','.mjs':'js','.ts':'ts','.astro':'astro','.css':'css','.json':'json'})[ext] || 'text';
  return {id:id(name),name,code,lang,redacted,symbols:symbolsFor(name,code)};
});
const sourceMap = new Map(sources.map(s=>[s.name,s]));

forbidden 是发布前的拒绝规则。check 对一段文本逐条测试,命中就抛异常,因此普通源文件遇到私有运行环境细节会让准备阶段停止。

ruleFiles 是唯一允许出现可见脱敏视图的例外集合:prepare.mjs 和 verify.mjs 本身需要在讲义中解释规则,但规则内容不能原样公开。脚本按行读取文件,命中的行替换为明确的 [REDACTED: private operational rule] 标记,并在有脱敏时加上“不是完整可执行副本”的说明。随后仍对替换后的结果调用 check,所以脱敏不是绕过最终边界。

每份源文件最终变成一个对象:

仓库路径 → { id, name, code, lang, redacted, symbols }

[...new Set(sourceFiles)] 在构造对象前去重,避免交易来源同时由主线清单和演示附加项登记时生成重复源码页。code 是准备后的公开文本,lang 由扩展名选择高亮语言,symbols 由声明扫描器产生。sourceMap 让片段提取按路径取到同一个对象;usedLines 则在整个构建过程中累计哪些原始行已经被讲义展示。

5. 一篇 Markdown 如何交接给 Astro

site/scripts/prepare.mjs · L83–L99 · 打开完整源码

const usedLines = new Map();
fs.rmSync('.generated', { recursive: true, force: true });
fs.mkdirSync('.generated/docs', { recursive: true });
for (const [order, [slug,title,file,description,section = 'implementation']] of docs.entries()) {
  let body = fs.readFileSync(path.join(root,file), 'utf8');
  body = body.replace(/\]\(([^)]+)\)/g, (whole, target) => {
    if (/^(https?:|#|mailto:)/.test(target)) return whole;
    const [name,fragment] = target.split('#');
    const resolved = path.posix.normalize(path.posix.join(path.posix.dirname(file),name));
    const route = routes.get(resolved);
    if (!route) throw new Error(`Unlisted publication link: ${file} -> ${target}`);
    return `](${route}${fragment ? '#'+fragment : ''})`;
  });
  body = expandExcerpts(body,sourceMap,usedLines);
  check(body,file);
  body = body.replace(/^# [^\n]*\n/, '');
  fs.writeFileSync(`.generated/docs/${slug}.md`, `---\ntitle: ${JSON.stringify(title)}\norder: ${order}\nsection: ${JSON.stringify(section)}\ndescription: ${JSON.stringify(description)}\n---\n${body}`);

脚本先删除并重建 .generated/docs,不会删除人工维护的 docs/。随后按 docs 的顺序读取每篇 Markdown,做三步转换。

第一步是链接重写。外部 URL、页内锚点和邮件链接保持不变;其他相对路径相对于当前 Markdown 文件所在目录解析,再去 routes 查找。找不到登记项就立即失败,避免站点里出现看似正确、实际没有目标的链接。这个顺序把“文档作者写的路径”转换成“静态页面能访问的路径”。

第二步是源码片段展开。expandExcerpts 只从已经建立的 sourceMap 取文件,校验行号范围,然后把真实源码放进 Markdown 代码围栏,同时写入原始文件名、行号范围、完整源码链接和 CSS 所需的 --line-start。源码不经过手抄或重新排版;脱敏状态也会随片段显示提醒。

第三步是发布边界检查、顶层标题移除和文件写入。页面模板已经会输出标题,所以脚本移除 Markdown 的第一个一级标题;frontmatter 记录标题、顺序、section 和描述。最终产物形如 .generated/docs/<slug>.md,由 Astro 的 docs 内容集合读取。

6. 生成物是下一阶段的输入

site/scripts/prepare.mjs · L100–L106 · 打开完整源码

}
const coverage = verifyCoverage(sourceMap,readingSources,usedLines);
fs.writeFileSync('.generated/reading-coverage.json', JSON.stringify(coverage));
console.log(`Reading excerpts cover ${coverage.length} source files, ${coverage.reduce((n,f)=>n+f.covered,0)} nonblank lines.`);
fs.writeFileSync('.generated/sources.json', JSON.stringify(sources));
fs.writeFileSync('.generated/navigation.json', JSON.stringify(docs.map(([slug,title,,description,section = 'implementation'])=>({slug,title,description,section}))));
console.log(`Publishing ${docs.length} documents and ${sources.length} source views; no runtime artifacts.`);

verifyCoverage 检查清单中的每个源码文件,其非空行是否至少被某个片段展示。它只证明“代码没有漏贴”,不证明中文解释正确。通过后,脚本写出四类交接结果:

  • .generated/docs/*.md:带 frontmatter、已改写链接和真实代码的页面输入;
  • .generated/reading-coverage.json:首页显示的覆盖摘要;
  • .generated/sources.json:源码索引与单文件源码页的数据;
  • .generated/navigation.json:侧栏、分组和章节前后导航的数据。

最后两条日志报告覆盖文件数和页面/源码视图数。它们是构建进程的结果,不是运行时接口,也不代表站点已经通过最终 QA;astro build 和 verify.mjs 还在后面。

7. 主线章节清单如何参与装配

site/scripts/readings.mjs · L1–L20 · 打开完整源码

// Ordered chapters: one execution path first, advanced mechanisms afterwards.
export const readings = [
  ['read', '代码带读:沿执行路径理解', 'index', '先理解职责与状态,再读完整源码块。'],
  ['read-entry', '入口:参数、工具与模式', 'entry', 'cmd/demo/main.go'],
  ['read-runner', 'Runner:创建与运行', 'runner', 'runner.go'],
  ['read-guest-c', '跨入 C 与 CPython', 'guest-c', 'guest/runtime.c'],
  ['read-guest-python', 'Python 执行约定', 'guest-python', 'guest/bootstrap.py'],
  ['read-bridge', 'Host 工具边界', 'bridge', 'bridge.go'],
  ['read-runtime-tests', '用测试核对普通执行', 'runtime-tests', 'runner_test.go'],
  ['read-example-tests', 'CLI 示例的测试', 'example-tests', 'cmd/demo/main_test.go'],
  ['read-examples', '六个用户程序', 'examples', 'examples/*.py'],
  ['read-trading', '交易演示:从读取到条件下单', 'trading', '交易程序、Host 状态与对应测试'],
  ['read-demo-scripts', '快捷脚本:组织可重复的演示', 'demo-scripts', 'demos/*.sh'],
  ['read-futures', 'Future:本轮任务登记与交付', 'futures', 'future.go'],
  ['read-plm-pass', 'PLM:沿 AST 找安全提前点', 'plm-pass', 'guest/plm.py'],
  ['read-plm-tests', 'PLM 的真实执行测试', 'plm-tests', 'plm_test.go'],
  ['read-plm-python-tests', 'AST 改写的单元测试', 'plm-python-tests', 'guest/test_plm.py'],
  ['read-prefix-host', 'Host 怎样接收源码片段', 'prefix-host', 'prefix.go'],
  ['read-prefix-guest', 'Guest 怎样解析和领取', 'prefix-guest', 'guest/prefix.py'],
  ['read-prefix-tests', 'Prefix 的真实执行测试', 'prefix-tests', 'prefix_test.go'],

readings 是有序的四元组:slug、标题、Markdown 短名和描述。prepare.mjs 把短名补成 docs/code-reading/<file>.md,再追加 reading section。数组顺序就是代码带读侧栏和章节前后关系的输入;它不会调用同名 Go 函数,也不会选择 runtime 的执行模式。

开头先建立普通执行:入口、Runner、C/Python Guest、Host bridge 和普通执行测试,之后进入示例。新增的 read-trading 把交易程序、Host 状态和测试接到主线,read-demo-scripts 再解释 demos/*.sh 如何组织可重复的演示;然后才转向 Future 与 PLM。这个顺序对应一次从 CLI 到 Guest、再回到 Host 的真实理解路径。每一项仍需有对应 Markdown 文件,否则读取文档时会在准备阶段失败。

site/scripts/readings.mjs · L21–L43 · 打开完整源码

  ['read-prefix-python-tests', 'Prefix 的解析测试', 'prefix-python-tests', 'guest/test_prefix.py'],
  ['read-prepared', '初始化与内存恢复的分岔', 'prepared', 'prepared.go'],
  ['read-prepared-tests', 'Full-copy 的行为测试', 'prepared-tests', 'prepared_test.go'],
  ['read-cow-interface', 'COW 的小接口', 'cow-interface', 'cow.go'],
  ['read-cow-linux', 'Linux COW:逐步跟进系统调用', 'cow-linux', 'cow_linux.go'],
  ['read-cow-platform', '不支持的平台怎样报错', 'cow-platform', 'cow_other.go'],
  ['read-cow-kernel-tests', '验证映射与释放', 'cow-kernel-tests', 'cow_linux_test.go'],
  ['read-cow-guest-tests', 'COW 上的真实 Python', 'cow-guest-tests', 'cow_test.go'],
  ['read-go-module', 'Go 依赖与校验记录', 'go-module', 'go.mod / go.sum'],
  ['read-build-entry', 'Makefile:选择构建任务', 'build-entry', 'Makefile'],
  ['read-build-inputs', '准备本地编译工具链', 'build-inputs', 'tools/setup-build-inputs.py / lock'],
  ['read-guest-build', '链接并打包真实 Guest', 'guest-build', 'build-guest.sh'],
  ['read-offline', '隔离子进程网络', 'offline', 'tools/offline-check.sh'],
  ['read-local-reader', '启动静态阅读服务', 'local-reader', 'tools/read-local.sh'],
  ['read-site-content', 'Astro 内容与页面路由', 'site-content', '内容集合与普通页面'],
  ['read-site-source-view', '源码页的行号与函数导航', 'site-source-view', '源码页面与符号提取'],
  ['read-site-styles', 'CSS:布局与手机阅读', 'site-styles', 'site/src/styles/global.css'],
  ['read-site-validation', '检查页面与发布边界', 'site-validation', 'site/scripts/verify.mjs'],
  ['read-site-config', '站点依赖与搜索索引规则', 'site-config', 'package.json / headers / robots'],
  ['read-site-pipeline', '把讲义与源码装进网站', 'site-pipeline', 'prepare.mjs / readings.mjs'],
  ['read-site-layout', '代码优先的页面外壳', 'site-layout', 'Docs.astro / 首页 / 阅读样式'],
  ['read-site-excerpts', '源码片段提取与覆盖检查', 'site-excerpts', 'excerpts.mjs / 对应测试'],
];

后半段继续 prefix、prepared、COW、依赖和构建工具,最后才读站点自身。清单的结尾仍以 ]; 关闭 readings 数组,说明章节顺序到此冻结;标题和描述属于教学导航,真正的源码是否发布由下一份 readingSources 清单另行决定,二者不能互相替代。

read-site-* 这些条目把站点实现也纳入同一条代码阅读链。这样读者能从真实站点数据流回看本章,而不是把网站当作无法追踪的黑盒。

8. 源码发布清单与读取边界

site/scripts/readings.mjs · L44–L69 · 打开完整源码


// Complete source coverage accompanies responsibility-level explanations.
export const readingSources = [
  'cmd/demo/main.go', 'runner.go', 'bridge.go', 'guest/runtime.c', 'guest/bootstrap.py',
  'runner_test.go', 'cmd/demo/main_test.go',
  'examples/lookup.py', 'examples/order.py', 'examples/dependency.py',
  'examples/branch.py', 'examples/error_delivery.py', 'examples/fallback.py',
  'future.go', 'guest/plm.py', 'plm_test.go', 'guest/test_plm.py',
  'prefix.go', 'guest/prefix.py', 'prefix_test.go', 'guest/test_prefix.py',
  'prepared.go', 'prepared_test.go', 'cow.go', 'cow_linux.go', 'cow_other.go',
  'cow_linux_test.go', 'cow_test.go', 'go.mod', 'go.sum', 'Makefile',
  'tools/setup-build-inputs.py', 'tools/build-inputs.lock.json', 'build-guest.sh',
  'tools/offline-check.sh', 'tools/read-local.sh',
  'site/src/content.config.ts', 'site/src/pages/[slug].astro',
  'site/src/pages/code/[id].astro', 'site/src/pages/code/index.astro',
  'site/src/pages/404.astro', 'site/src/styles/global.css',
  'site/scripts/symbols.mjs', 'site/scripts/verify.mjs', 'site/package.json',
  'site/public/_headers', 'site/public/robots.txt',
  'site/scripts/prepare.mjs', 'site/scripts/readings.mjs',
  'site/src/layouts/Docs.astro', 'site/src/pages/index.astro',
  'site/src/styles/code-reading.css', 'site/scripts/excerpts.mjs', 'site/scripts/excerpts.test.mjs',
  'examples/trading.py', 'cmd/trade-demo/main.go', 'cmd/trade-demo/market.go',
  'cmd/trade-demo/main_test.go', 'cmd/trade-demo/market_test.go',
  'demos/run.sh', 'demos/trade.sh', 'demos/01-price-skip.sh', 'demos/02-cash-skip.sh',
  'demos/03-host-denied.sh', 'demos/04-approved.sh', 'demos/05-prefix.sh', 'demos/06-plm-source.sh',
];

readingSources 是覆盖检查和源码发布使用的明确列表。它包含 runtime 主线、测试、示例、构建输入,以及本章站点实现文件;末尾再加入交易示例的五个源码文件和 demos/ 的八个快捷脚本。prepare.mjs 先把基础 sourceFiles 与它合并,再用 goPatternSources、deepSources 和交易 demo 扩展来源集合。

清单现在覆盖 67 个文件,并由 prepare.mjs 在构建时统一去重;重复登记交易 sources 不会产生重复源码页。清单数字是当前源码输入的事实,不代表覆盖检查已经运行成功,最终摘要仍由 verifyCoverage 产生。

这里有三个重要约束:

  1. 文档记录决定页面顺序,源码清单决定可公开的文件;新增一个文件时需要同时考虑两条边界。
  2. expandExcerpts 只能读取 sourceMap 中的文件,verifyCoverage 只能验证 readingSources 中的目标;任意仓库路径不会因为出现在 Markdown 中就自动成为发布内容。
  3. 这套管线只生成站点输入,不创建 Wasm、不运行 Guest、不承担访问鉴权。隐私规则和静态产物的最后检查见 构建后验证,布局消费这些 JSON 的路径见 页面外壳。

读完本章,应能回答:一次 pnpm build 中哪些数据在 Node 阶段产生?为什么讲义链接和源码片段必须经过白名单?.generated/docs、sources.json 与 navigation.json 分别交给谁?