Skip to content

与参考仓 Z 的基建交叉对比 ​

定位:一次性调研的结论台账,不是教程。记「Z 有什么、我们缺什么、值不值得搬、搬的话落在哪」。 读者:要动本仓基建(门禁 / CI / 文档体系 / 工具链)的人与 agent。 权威边界:结论以本仓代码为准;Z 的做法只作为参照,它自己的文档与评审也有过期之处(见「可信度声明」)。 前提:Z 是另一个更成熟的私有内部 monorepo,本页不写它的仓库名与路径 —— 本仓文档站是公开的。 下文凡 Z · xxx 形式的引用都指 Z 仓内的文件;不带前缀的路径指本仓。


一、怎么做的 ​

开了 8 路互不重叠的只读子调研,每路负责一块基建,硬约束三条:只读(不许 install/build/test/git 写)、每条结论带 文件:行号、读不到就写「未核实」不许推测。分工:

路范围
1门禁编排、门禁台账、反向断言、负向验证器
2CI 流水线、发版编排、流水线配置的自校验
3文档体系与 agent 指引
4本地 ESLint 规则、tsconfig 分层、vitest 预设、AST 工具
5构建产物门禁、密钥 / 权限 / 许可证闸
6包标准、依赖与锁文件治理
7git 钩子、提交纪律、命令行公共层、开发体验
8提案 / 调研文档、部署配置、类型与 schema

8 路报告我逐条复核过,下面第二节是复核结果 —— 那一节比结论本身更值得先读。


二、可信度声明:3 条假阳性、1 条要打折、1 条是我自己的锅 ​

调研报告不能直接采信。逐条复核后:

报告的说法复核结果
「catalog 三处写 242,实测只有 147 条,已经烂掉 95 条」❌ 假阳性,它自己数错。pnpm-workspace.yaml 的 catalog: 段是 99–351 行(下一个顶层键在 352 行),实际 243 条;它取的 197–343 只是这段的中段。差的那 1 条是本次新增的 eslint-plugin-turbo
「release scope 不在 commitlint 白名单 ⇒ 发版记账提交会被 hook 拒」❌ 假阳性。packages/tooling/commitlint-config/index.ts 的 SCOPES 里 'release' 就在第 27 行,上面还专门有一段注释解释它「不是包 scope、是发版记账提交专用」。它引用的行号范围 :2-28 恰好包含第 27 行,是被上方注释带偏的
「lint:workflows 在 actionlint 缺失时静默 return 0,是门禁缺口」⚠️ 要打折。.github/workflows/workflow-lint.yml 确实存在且会下载并执行 actionlint,实现里那句「CI 里才是权威闸门」是准确的、有兜底的有意设计。但它指出的次生问题成立:packages/tooling/scripts/src/ci/prepush.ts 只在失败时回放子进程输出,所以本地看到的是绿灯而那一段其实什么都没跑
「本仓归档评审否决过『门禁图写在 TS』,所以别做 profile 台账」⚠️ 需澄清。被否的是带 needs/after/allowFailure 的依赖图形态;Z 的实际实现只有 186 行,是按触发面取用的名册,两者不是一回事,引用那条否决来反对它是误读
apps/docs/src/zh-CN/content/monorepo/release.md 里电池表只列 11 条、且有一段「lint-root 绝不能交给 turbo」的论证✅ 真,而且是我上一轮留下的债://#lint:root 定义出来之后那段论证的前提就没了,我改了代码与测试却漏改这份文档

教训(与本仓既有纪律同源):子调研的价值在于「把面铺开、把 file:line 找出来」,不在于它的判断。凡是要写进结论的数字与「真问题」,必须自己再跑一次判据。


三、分档结论 ​

📌 还剩哪些(一眼版) ​

已经全部落地:A1–A4(立刻做)· B1–B5(值得做)· C1 / C2 / C3 / C4 / C5(结构性五项全做完)。

还没做的只剩一条(登记在架构待办事项里,按 id 搜):

剩什么待办表里的 id为什么现在没做
安全观察两条待办表的「两处安全观察」要改 apps/admin/src,在本轮范围铁律之外(只登记不动手)

决定不做:B6(后端 i18n key 集合门禁)—— 理由与解冻条件见 §五 的 Q5。

无需动作:D(参考仓更弱,不要反向学)· E(明确不值得搬)。

同族候选,还没登记成条目:C2 里剩下的 script-matrix / package-json-fields / 目录索引双向对账 —— 连"要不要做"都还没定,所以它们不在待办表里(不在表里 ≠ 已经做完,这里写明以免误读)。

A. 立刻做(半天以内,且已亲手验证) ​

#事项证据代价
A1CI ↔ 门禁名册的对账断言 ✅ 已做全仓没有任何测试读 .github/workflows/ci.yml(唯二提到它的地方是把它当示例路径字符串用);prepush.test.ts 的 7 条断言全是单向的 ⇒ 删掉一整段门禁没有任何测试会红。这是唯一 0 覆盖率的接线面半天
A2hooks:check 那句假陈述 ✅ 已做(真补进发版电池第 0 步)lefthook.yml 顶部注释写「装没装上用 pnpm hooks:check 机械核对(发版第 0 步也会调它)」,但 packages/tooling/release/ 里零引用、.github/ 零引用、prepush.ts 零引用 —— 全仓只有那句注释与它的脚本定义5 分钟(改注释)/半小时(真加进发版电池)
A3release.md 的电池表与过期论证 ✅ 已做见第二节最后一行20 分钟
A4subject-case 关掉 ✅ 已做唯一已确认在伤害日常提交的:该规则把「大写拉丁字母开头的 subject」判成 sentence-case 一律拒绝。实测:一次会话内被拒 4 次(都是大写拉丁字母开头的 subject),每次都只能改成中文开头5 分钟

B. 值得做(缺口明确 + 有机械判据) ​

#事项为什么
B1bin ↔ 根脚本 ↔ 门禁表 的接线门禁 ✅ 已做(gate-wiring.test.ts 的「二段跳」一段)根脚本经 walnut-* bin 二段跳,全靠人工对齐;重命名一个 bin 会让整段门禁静默消失。已核对:今天 0 漂移,所以上了就是绿的
B2check-lockfile 的版本锁步(L5) ✅ 已做(pnpm lint:lockfile)「catalog: 的解析版本 == catalog 声明值」。本仓风险比 Z 大:catalog 243 条 + catalogMode: strict。但落地时发现原设计会「恒关」,见下方 §4.6 的踩坑记录 —— 最终改成比对 HEAD 里那一对
B3文档三件套(归档归宿列 / 占位页处置 / 文档规范页) ✅ 已做archive/index.md 里「未执行」与「已执行」两篇长得一样;frontend/features/ 下有 7 个 1 行的占位页;「文档该长什么样」散在四处
B4nginx 安全响应头 ✅ 已做(pnpm lint:nginx-headers + post-verify 硬条件)实测 deploy/nginx/ 下 add_header 只有 2 条且都不是安全头(HSTS / X-Frame-Options / X-Content-Type-Options / Referrer-Policy 全缺),而 CSP 插件在构建配置里是注释掉的 ⇒ 这一面目前无人兜。动手时又实测出一条更重的:api.conf(API 域名的入口)一个安全头都没有 —— 它和 frontend.conf 是两个并列 server 块,nginx 的 add_header 不跨块继承。于是这条按「配置面(推送前静态查)+ 运行面(部署后真发头)」两端落地
B5产物去密体检 ✅ 已做(pnpm lint:dist,CI 紧跟 admin 构建;源码侧另有 pnpm lint:secrets,见 §4.5)产物侧门禁为 0(apps/admin/package.json 的 build 只有 vite build)。Z 把它列为性价比最高的一条,且规则表与它 CI 的密钥扫描同源。落地时最花功夫的不是写规则,是删规则:3 条看起来天经地义的规则在真产物上分别是 1 / 8 / 5 次误报(详见 §4.5)
B6后端 i18n key 集合门禁apps/server/apps/api/src/i18n/ 下两组语言包是纯文件真源,缺 key 只表现为线上回退成 key 串(当前实测两侧完全对齐)

C. 结构性但更大 ​

登记与落地情况:C1–C5 五条全部已落地(2026-09-23 当天登记当天做完三条)。对比页保留逐块对比与理由,条目里放判据与前置。

⚠️ C2 的初稿有一处说得不准:「本仓 0 条机械规则」——其实 lint:turbo-cache 的第 1 条不变量早就在查「每个包都必须有非空 tags」了(那一半是 B2 同期做的)。本轮真正补的是更细的三条:tag 形态、平台 tag 与目录组一致、以及「boundaries.tags 每条规则的 key 都得有包声明」的反向断言 —— 因为「非空」只挡「整个忘写」,挡不住「写了个不会匹配任何规则的 tag」(同样是静默豁免)。

#事项说明
C1命令行公共层 ✅ 已做(lib/cli.ts 的 runCli + 门禁全部改走 lib/log.ts)Z 有 verdict(三态退出码归一)/gate-ui(unmet() 抛前置条件错)/argv(统一参数解析)/errors 基类 + 唯一映射。⚠️ 初稿的描述要更正:本仓那一层大半本来就有 —— lib/log.ts(TTY/管道两条正确路径)与 lib/errors.ts(PreconditionError 广泛使用),真正的缺口是「门禁没在用它」:12 个文件 77 处裸 console.*,失败还是 return 1/2 + 自己打印,而 ViolationError 定义了却全仓零使用(死类)。落地:runCli 成为错误→退出码的唯一映射点(0/1/2 + 其它异常→1),15 个 bin 从 process.exit(main()) 改为 await runCli(main),门禁改成抛错 + 打明细、结语由 runCli 统一。⚠️ 初稿说的绑定项(改输出层就必须同时换掉 process.exit)已一并处理 —— 而且方向正好相反:log.ts 在管道下走同步写,所以真正要消除的是 console.* 的异步路径
C2包标准的机械门禁(最小骨架) ✅ tags 与 exports-shape 已做Z 有 22 条机械规则(turbo-tags / exports-shape / script-matrix / package-json-fields / 目录索引双向对账 …)。本仓不是 0 条(见上方更正):lint:turbo-cache 已有 8 条不变量,其中第 1 条就是「每个包 turbo.json 的 extends + tags」;2026-09-23 又补了 exports-shape(pnpm lint:exports,四条不变量:目标存在 / 通配命中 / types 条件在前 / 与顶层 main·types 不矛盾)—— 本仓共享包不构建就被消费,exports 写错 = 解析到别的入口或类型静默丢失,而这一面此前零判据。剩下的候选:script-matrix、package-json-fields、目录索引双向对账
C3根脚本白名单 ✅ 已做(只搬前两段)根脚本(调研时 43 条,现已 46 条)零约束。Z 用「白名单 + 形态正则 + 理由」三件套。落地时只搬「名字形态 + 值里不许有 shell 连接子」,并只对 lint:* 收了紧 —— 全量白名单会变成一份要不停维护的名单(本仓根脚本合法地直接调 tsc / rimraf / syncpack / docker / cross-env)
C4turbo 缓存跨 CI run ✅ 已做(ci.yml 的 quality job)实测全仓无 actions/cache。落地时把方案换了一个范围,ROI 完全不同:整份 .turbo/cache 是 385 MB(build 产物占绝大部分,搬运费吃掉收益),而 quality job 里那份只有几 MB —— 质量门三类任务(lint / types:check / test)的 outputs 是空的,条目只是元数据;实测冷跑 144s → 命中 0.13s(43/43 FULL TURBO,本地口径)。⚠️ CI 实测又补了两条:① 命中已实测 —— run #17 三个质量门步骤全 0s(无缓存基线 69 / 41 / 10s),另有 actions/caches API 上 last_accessed_at 被刷新的独立证据;② 「只缓存质量门」第一版并不成立 —— Docs build (dead-link check) 也是 turbo 任务,4.6 MB 的 VitePress 产物跟着上了缓存(归档 4.7 MB → 9.8 MB),现已用 --cache-dir 把它挡在外面并把缓存 key 升到 v2。⚠️ 初稿那条风险比想的低:turbo 的条目按 task hash 校验,恢复到不匹配的只会 miss、不会误用;而且本仓每次 push 都在用同一套(prepush 的 turbo run lint:root / types:check 本来就命中本地缓存)。真正决定"会不会误用"的是 hash 覆盖面 —— 那正是 lint:turbo-cache 那组不变量的职责(docs 构建那一面本仓早已覆盖,见 apps/docs/turbo.json 的 inputs 注释;run #16 还顺带证明了「改了就真跑、没改就命中」)。前提实测:CI 注入的 CI / TURBO_SCM_BASE / TURBO_SCM_HEAD / GITHUB_ACTIONS 都不改变 task hash。详见 Turbo 缓存边界 第七节
C5本地 ESLint 规则 ✅ 已做(两条)Z 有 13 条 + 每条配 RuleTester 正反语料。落地取「本仓有成文约定却零判据」的两条:script-header 与 script-exit-code(0/1/2 三态)。⚠️ 初稿那句「存量已满足、上线即绿」对 script-header 是错的 —— 登记时逐个数过:bin/*.ts 一个 JSDoc 文件头都没有;落地实测 13 个 bin 里 2 个连注释都没有,已补。退出码那条实测 1515 个文件零违规,确实上线即绿

D. 参考仓更弱:不要反向学(无需动作) ​

议题本仓为什么更强
环境变量管理Z 把第三方 AppKey/Sign 以 VITE_(Vite 的公开标记)明文入库,它自己的调研文档就把这列为待整改项;本仓是 dotenvx 逐值加密 + 密文入库
nginxZ 那份是 listen 80 + 裸 IP + 零安全响应头的历史文件;本仓已有 TLS1.2/1.3 + 密码套件 + brotli。它记录的两个坑在本仓都不构成问题(2026-09-23 核实):① 「运行时配置类资源禁缓存」没有对应物 —— 本仓是构建期注入 env 的纯 SPA,产物里没有任何运行时配置文件(实测 apps/admin/dist 只有 assets/ static/ vendor/ + favicon + index.html,无 .json);② 「SPA fallback 的内部重定向环」静态读下来不成立(frontend-server.conf 的 try_files 第三段直接命中 /index.html 这个真实文件,不会再回退一次;⚠️ 本机无 Docker,这条是读配置得出的,不是跑出来的)
ADR 治理Z 索引表没有 Status 列;本仓是四值枚举 + 双向逐字校验的门禁
发版电池Z 6 条、无 id;本仓每条带稳定 id、支持 --skip-gates=<id>,且整表被测试逐行钉住(条数以 release/steps.ts 那张表为准,本页不抄数量)
.claude/skills frontmatter本仓有 when_to_use / paths,Z 多数没有
仓库级 TODO本仓每条带「核实:」结论;Z 是纯勾选清单
空面/尺度反向断言lint:doc-budget 的「预算用不到一半也算失败」正是 Z 缺的那类判据
--affected 的空集陷阱Z 记过这个坑;本仓已经防住(显式 TURBO_SCM_BASE/HEAD + 「有变更却 0 个受影响包 ⇒ 红」自检)

E. 明确不值得搬(无需动作) ​

  • 逐脚本六字段元数据登记(Z 的 manifest.json + 159 KB 的 check-scripts.ts 断言群):它成立的前提是 Z 有约 170 个脚本文件;本仓的脚本面比它小一个量级(src/lib 的纯逻辑 + bin/ 下的门禁入口),登记成本高于收益。只摘其中不依赖 manifest 的两段(= B1 与 A1)。
  • 把门禁名册写成 gates.json(JSON):在本仓门禁表的规模下(两三处触发面、十几段),JSON 只带来三项成本(不能写注释、拿不到类型检查、要额外形状校验层)。
  • Jenkins / Groovy 判据全家:本仓是 GitHub Actions,且我们有真 YAML 解析器,同类校验在 GHA 上比在 Jenkins 上更容易做。
  • 混淆门禁:Z 自己的审计已把它的判据判为不可靠(判据与结论无因果关系);本仓混淆默认关闭。
  • 许可证门禁:它守的是「交付物不含 AGPL/GPL/SSPL」这类对外交付合规;本仓是 MIT 模板,无私有化交付。
  • 多 app 产物合并 / 地图运行时扫描 / 浏览器性能指标:无对应业务面。
  • 权限标识的前端 AST 扫描:本仓前端不持有权限字面量(从后端接口拿字符串数组),照搬扫不到东西。
  • no-hardcoded-chinese 棘轮:本仓中文是产品默认语言,且文案真源在后端数据库,硬开会被无视。
  • 厚层 README(39 KB 级):本仓等价内容已在 packages/tooling/scripts/README.md + 文档站两页 + 门禁表里,再加一份会形成第三个家。

F. 只登记不动手(业务代码,本轮范围外) ​

已登记为待办表的「两处安全观察」(含核实结论与"为什么不能照抄改法")。本节保留原始观察。

  • apps/admin/src/utils/window/open.ts 第 24 行的 window.open 没带 noopener(同文件另外两处都带了)。登记时又核实出两件原文没写的事:① 带 noopener 时 window.open 返回 null,而调用点 apps/admin/src/views/auth/src/shared/other.vue 接住返回值用来关弹窗 ⇒ "照抄加上 noopener"会让弹窗关不掉;② 好在该流程走 SSE 收登录结果,不依赖 window.opener,所以加 noopener 不会打断登录。正确改法要连"怎么关窗"一起重新设计。
  • no-dangerous-html 类规则一旦启用会立刻命中 4 个 v-html(apps/admin/src/components/Global/AI/**),另有 apps/admin/src/views/auth/index.vue 第 26 / 31 行的 2 处 innerHTML = ''(是清空而不是注入)—— 全在业务代码里,按本仓「一排开始误报的门禁等于没有门禁」的口径,现在不该开。⚠️ 本节初稿写的是"7 处",登记时逐个数了一遍:4 + 2 = 6,初稿的数字是错的。

四、逐块详细对比(8 块) ​

4.1 门禁编排与反向断言 ​

  • Z 的模式:Z · packages/tooling/gates.json 把「门禁名册」与「触发面 profile」分开,每个面显式声明自己的成员与 external(不在名册里但属于该面的检查)。断言群做的是双向:名册 → 真实接线,以及 CI 侧脚本 → 名册(否则必须写明 runner-exempt: <原因>)。
  • 本仓:packages/tooling/scripts/src/ci/prepush.ts 是一张扁平的表 + 并行 runner,没有触发面维度;prepush.test.ts 只做单向(argv[0] 能落到真实根脚本)。
  • 差距的两个方向:
    1. 漏失效(A1):CI 与发版电池对这张表没有任何约束;
    2. 误报风险:Z 自己也踩过 —— 它的 external 字段一度变成「可以往里塞东西」的口袋,后来补了形状断言才收住。照搬时必须同时补断言。
  • 负向验证:Z 把「注入错误证明门禁会红」做成了可重跑的工具(带 HEAD 对账、写后读回、信号钩子还原),本仓是「一次性脚本手工注入 + 结果写进文档」——不可重跑。它的安全壳值得学(finally 不覆盖 SIGINT/SIGTERM,中途被杀会把注入留盘,下次运行反而把它当基线「还原」回去),但要在下一次手工注入之前先落壳。

4.2 CI 与发版编排 ​

  • Z 用 55 KB 的脚本来校验自己的流水线定义(断言「不该消失的阻塞门禁还在」、禁止形、引用的根脚本必须存在)。本仓唯一的 workflow 校验是 actionlint —— 它只做语法,语义层零覆盖。
  • 回答一个关键可行性问题:「secrets 不得出现在 if 里」这类断言在 GHA 上能不能做?能,而且比 Jenkins 容易(我们是真 YAML,可按 job/step 结构判,不必做 Groovy 文本切片)。
  • 本仓比 Z 强的两处:发版电池的结构(见 D 表);--affected 的空集自检。
  • 本仓缺的:CI 语义断言(= A1);turbo 缓存跨 run(= C4)。

4.3 文档体系 ​

  • Z 的核心资产不是目录,是一份 150 行的「文档宪法」(分层 / 命名 / 头部 / 长度预算 / 生命周期 / 敏感信息 / 评审钩子)+ 一条机械门禁 + 一套显式状态词(每篇标题后标「活 / as-of / 历史 / 提案」)。
  • 本仓缺的三处:
    1. 没有「文档规范」的单一权威:今天散在 apps/docs/AGENTS.md 的十条约定、apps/docs/src/zh-CN/content/adr/index.md 的 ADR 形态、check-doc-budgets.ts 的表注释三处;
    2. 归档区没有「归宿」列、也没有收录判据:apps/docs/src/zh-CN/content/archive/index.md 只有「类型 / 说明」两列,读者无法知道哪条结论还成立;
    3. 占位页:apps/docs/src/zh-CN/content/frontend/features/ 下 7 个 1 行的页(7–26 字节)。
  • Z 有一条我们没有的硬判据:「未闭环事项禁止进历史区」 —— 仍开着的待办必须先抽到台账或现行文档,才能归档。本仓的归档约定只写了「新增过程文档时在待办表里登记一行」,没有这条禁令。
  • ⚠️ 实施陷阱:把 apps/docs/AGENTS.md 的十条约定搬进新页之后,那个文件会掉到预算的一半以下 —— 而 lint:doc-budget 规定「预算用不到一半也算失败」,所以搬家必须同时收紧预算数字。

4.4 ESLint / tsconfig / vitest / AST ​

  • Z 的 43.6 KB「本地规则」实为 13 条,每条都有 RuleTester 正反语料 + 两条「结构锁」(名册只增不减、模板分支必须注册)。本仓 packages/tooling/eslint-config/nest-local-rules.ts 只有 1 条规则,全仓 0 个 RuleTester。
  • 13 条里本仓适用且零误报的只有两条(= C5);其余要么被现有机制覆盖(精确版本靠 saveExact + syncpack)、要么无命中点、要么前提不同(中文棘轮)。
  • tsconfig:我们把 Z 的 ts + scripts 两档合成了一档(erasableSyntaxOnly 在 ts.json),属合理取舍;可选补一条 types: ['node'] 显式声明。
  • vitest 预设:本仓 include 不含 .tsx(当前全仓 0 个 .test.tsx,属潜在而非现存问题);Z 还有「include 面偏离 src 时必须写明静默 0 用例风险」这条判据值得借。
  • AST:本仓已有一半(check-doc-ts.ts 用 ts.createSourceFile、check-turbo-cache.ts 用 parseConfigFileTextToJson);缺的是「脚本事实引擎」与「接线解析」——后者正是 B1 要用的。

4.5 产物门禁与安全闸 ​

  • Z 的挂载模式是「声明式清单 + 三维接线」:清单即数据(谁 × 画像 → 跑哪几条,含「是否读产物」以决定要不要透传画像),调度器按清单执行,入口挂在 app 构建链尾与根 postbuild 上。
  • 它的产物级去密断言扫 dist 下全部文本产物,判密钥形态 / 内网网段字面量 / 设备口令字段 / *.map 存在性,规则表与它 CI 侧的密钥扫描读同一份(按 id 取用,改名不同步会当场抛错);豁免按 sha256 记账,不回显原文。
  • 本仓产物侧门禁为 0。已有构建期能力(混淆 / CSP / 压缩 / Sentry / CDN)一个都没有产物侧消费者。
  • B5 落地时对上了它的第 4 / 5 / 9 条教训(这也是这轮唯一"照着教训写"的地方):
    • 第 4 条(每段都要能回答"这次实际扫了多少")→ 输出固定打印 1471 个文件 / 12.68 MB;扫文本 490,跳过压缩/二进制 981 与 拿 49 个机密值比对过。实测全程 424 ms。
    • 第 5 条(不回显原文)→ finding 只报键名与文件、永不回显值,用例里有一条专门钉它(连 JSON.stringify(finding) 都要干净)。
    • 第 9 条(判据要能被负对照证伪)→ 两条负对照都真跑过:① 把 api.conf 的安全头删掉 → lint:nginx-headers 当场红;② 把真 env 值注入一份产物副本(副本在系统临时目录、跑完即删)→ lint:dist 退出码 1 且不回显。
    • 另外它第 2 条的教训在这里反向用了一次:正因为"子串匹配容易被满足",本仓的规则表只收在真产物上零误报的规则 —— 被删掉的 3 条候选规则(裸 PEM 头 / 键名=值 / 超长 base64)各自有 1 / 8 / 5 次实测误报。
  • ⚠️ 2026-09-23 补记:产物侧做完当天就漏了一次,而漏在"源码侧根本没人看"。 我在一个测试夹具里写了腾讯云文档的样本 SecretId(AKID + 32 位),14 段 prepush 门禁 + 300 多个用例全绿, 然后 git push 被 GitHub 服务端的 push protection 拒掉。三条教训已落成新门禁 pnpm lint:secrets: ① 服务端那道闸在 CI 之前 ⇒ 必须在 prepush 拦;② 它只认形状、分不出样本与真货 ⇒ 本仓规矩是「假样本也不许长成真凭据的形状」(夹具运行时拼装,不加白名单 —— 服务端不读我们的白名单); ③ 规则表与产物侧同源(共用 scanText),源码侧另立一套必然漂移。 这也把本节那句「规则表与 Z 的 CI 密钥扫描同源」从意图变成了事实。
  • 从它的安全评审里提炼的门禁设计教训(这些比它的规则本身更有价值):
    1. 单向断言 = 没有断言(删掉调用点全链路仍绿);
    2. 子串匹配的断言可以被一行注释满足 —— 断言必须剥注释后匹配命令位置;
    3. 被检对象与执行对象必须是同一份文件;
    4. 每条规则都要能被问「这次实际扫了多少文件」,答不出就是恒关;
    5. 白名单里永远不许出现明文凭据(它曾把整枚 Key 抄进放行名单);
    6. 豁免必须带机器可读字段(到期条件 / 证据路径),自由文本理由无人核验;
    7. 新门禁忘进名册 = 静默死代码 —— 所以「进名册」或「写明为何不进」应当二选一强制;
    8. 恒红的门禁与没有门禁等价(与本仓「宁可漏报不可误报」同源);
    9. 一条判据要能被负对照证伪(改坏它必须变红),否则只是安慰剂。

4.6 包标准与依赖治理 ​

  • Z 有 22 条机械规则,分 6 类(结构 / exports / scripts / 依赖 / tags 边界 / 文档);规则数据住 JSON、判定住脚本。本仓 0 条,对应物是散文。
  • 最能落地的一条是 turbo-tags(= C2 的首项)。
  • Z 的 check-lockfile 守 5 条不变量,其中 L5(catalog 解析版本 == 声明值)是本仓最该补的(= B2)。
  • 本仓的 catalog 护栏其实比 Z 多(hoist: false / minimumReleaseAge / trustPolicy / blockExoticSubdeps 它都没有);它唯一比我们强的是 L5。
  • B2 落地时踩到的坑(值得单记):Z 的 L5 是「比工作区的 catalog 与锁文件」,照搬过来永远绿。原因是实测发现 pnpm 12 默认会在跑任何脚本之前自动 install(verifyDepsBeforeRun 未设置时的默认行为)—— pnpm lint:lockfile 会先把锁文件改对,门禁看到的已是同步状态。判据:注入一个「存在但不同」的 catalog 版本,pnpm <script> 路径 exit 0(且锁文件被 pnpm 改了),而 node <bin> 直跑 exit 1 并报出是哪一条。 改法:比对对象从工作区换成 git show HEAD: 那两个文件 —— CI 检出的正是这一对,与 pnpm 的 自动 install 无关。这也是本项目里「一个门禁要能被问『它这次到底看了什么』」的又一个实例。
  • 一条可以直接用的判据:「枚举类事实一律不复述」 —— Z 把包清单 / 脚本清单 / 门禁清单全部指向那几份 JSON,正文只写判据。本仓已有半条纪律(「别写会腐烂的计数」),缺的是「指向唯一真源」的落点。
  • 顺带查到本仓三处零消费者的元数据:walnut 自定义字段(14 个包在写,全仓无读取方)、contract 与 utils-core 同时有 main 与 exports、9 个包无 private。Z 的等价信息是从 exports/包名推导的,理由是「手写分类必然与事实漂移」。

4.7 git 钩子 / 提交纪律 / 开发体验 ​

  • lefthook.yml 两边几乎逐字相同;差异在断言圈:Z 主动禁掉 skip / only / glob / exclude_tags 这些「让 job 永不出场」的键,本仓没有这层检查。
  • 命令行公共层是唯一量化得出来的结构差距(= C1)。
  • 子进程层我们不缺能力(心跳 / 预算 / 平台分支 / 输出尾缓冲 / 进程树回收都有),只差 POSIX 的 SIGTERM→SIGKILL 升级。
  • 根脚本:逐组核过,没有重复转发或同 target 双名;缺口是「没有机制拦住未来的漂移」(= C3)。
  • A2 就在这里:hooks:check 是「本机钩子还活着吗」的检查,而 prepush 本身就是钩子调的(循环),所以它只能在不经钩子的入口(pnpm release)里生效 —— lefthook.yml 那句注释描述的正是这个意图,只是没实现。

4.8 提案 / 调研文档、部署、类型 ​

  • Z 的提案区有三态生命周期(提案 → 落地即删 / 放弃即删,不保留已否决档案),调研区定位为 as-of 快照(不随代码维护、冲突以代码为准)+ 文件名日期前缀仅该目录允许。
  • 与本仓的直接冲突:本仓刻意保留执行记录(architecture-todo.md 的执行记录 / 核实记录),Z 是「落地即删」。两者选一,不要混。
  • Z 的调研文档有两个可直接借的写法:头部「定位 / 读者 / 权威边界」三行,与末尾专设「未核实 / 不可引用」节(本页第二节就是这套写法的应用)。
  • 部署:nginx.conf 与我们的差(见 D 表)。原先这里写着「值得补的有两档」,2026-09-23 把两档都核了一遍,结论是只剩一档:
    • ❌ 「运行时配置类资源禁缓存」在本仓不适用(不是"没做"):本仓是构建期注入 env 的纯 SPA,产物里没有任何运行时配置文件 —— 实测 apps/admin/dist 顶层只有 assets/ static/ vendor/ + 两个 favicon + index.html,没有 .json。没有那个东西,就没有那条规则要防的失败。
    • ✅ 安全响应头(B4):不是"学它",而是这块两边都不合格 —— 本仓动手时实测出自家的 api.conf 一个头都没有。
  • 顺手把缓存策略也实测了一遍(因为上面那条要判"适不适用",必须先知道我们到底缓存了什么):
    • location /static/ 的 expires 1y; immutable 是对的 —— 实测该目录下 484 个非压缩文件全部带 hash(换内容必然换文件名,所以"换不掉"这个经典事故不成立);index.html 没有任何缓存头(SPA 最怕的那条坑不存在)。
    • assets/ 与 vendor/ 是稳定文件名(assets/ai_finish.mp3、assets/css/driver-dark.css、vendor/codeMirror/langs/zh-CN.js)且没有任何缓存头 ⇒ 每次重新验证。结论:保持不动 —— 给稳定名加长缓存才是危险方向(改了用户拿不到),真要收益应当先让那些文件 hash 化,那属于构建配置,不在这一档里。理由已就地写进 deploy/nginx/frontend-server.conf 的注释。
  • 类型 / schema:Z 用 ts-json-schema-generator 生成交付契约;本仓的 genJSONSchemas.ts 生成的是编辑器提示用的 schema(无消费者、无门禁)。不建议现在扩成契约 —— 本仓没有「客户配置文件」这类交付物。

五、裁决记录(2026-09-23 你已逐条拍板) ​

#事项裁决口径与理由
Q1hooks:check 怎么收口✅ 真加进发版电池这个检查只能在不经钩子的入口生效(prepush 本身就是钩子调的,是个循环),pnpm release 正是那个入口。lefthook.yml 那句注释描述的意图是对的,补上实现即可
Q2subject-case✅ 关掉类型 / scope 的大小写与白名单由 type-case / type-enum / scope-case / scope-enum 四条独立把守,关它不削弱门禁
Q3参考仓是否匿名✅ 全文匿名文档站是公开的,写实名与路径等于把另一个私有仓的信息发到公网。本页一律用 Z · 路径 前缀
Q4要不要建「提案区」✅ 折中:不建目录,待办表内单独立节 + 侧边栏 anchor建 proposals/ 就要接受「落地即删」,与本仓刻意保留执行记录的做法直接冲突;折中既把「未落地设计」与工程债分开,又不丢历史
Q5先采纳哪一批✅ A + B 都做,B6(后端 i18n key 集合门禁)不做A1–A4 与 B1–B5 逐条展开;B6 的理由见下

B6 不做的口径:后端 apps/server/apps/api/src/i18n/ 的两组语言包当前实测完全对齐(0 缺 / 0 多),而参考仓那条门禁解决的是「多语言文件真源 + 缺 key 静默回退成 key 串」。本仓这一面的风险等级够不上新增一条常驻门禁:它的直接后果是某个语言下显示 key 串,不是数据错误或安全问题;而门禁一旦加上就多一处要维护的常驻面。解冻条件:真的发生了「加了一个 namespace 忘了同步另一种语言」的事故,或者语言数从 2 涨到 3 以上。

六、未核实 / 存疑 ​

以下是这轮没有验证到的,写在这里免得下次被当成结论用:

  1. Z 的 check-scripts.ts(159 KB)没有读完,关于它的教训全部转引自 Z 自己的安全评审;那条评审又明确对某几处已过期(它仍称某脚本「全是旧判据」,而该脚本已大幅改造)。
  2. Z 的 manifest.json / gates.json 全文未读;断言群的逐条实现只读了头部注释与外部概述。
  3. Z 的 Z · docs/README.md 疑似自身已有烂链(两行重复同一句、若干指向不在清单里的文件)—— 未核实它的死链门禁当前状态,不当结论用。
  4. 本仓的产物体积基线(入口闭包 gzip 是多少、modulepreload 几条)未实测;若要加体积预算,阈值必须先跑一次拿实测值。B5 只做去密、不做体积,所以这条没被 B5 顺带解决 —— 顺手量到的只有总量:apps/admin/dist 在 00ff6c4 这次口径下是 1471 个文件 / 12.68 MB(含 489 .br + 489 .gz 预压缩副本、0 个 sourcemap)。
  5. 本仓 apps/admin/dist 里当前有没有密钥形态 —— 已实测(B5 落地时):形态命中 0 条,apps/server/env-local/ 里 49 个机密值一个都没出现;VITE_ 键名在产物里 0 个(Vite 构建时已替换成字面量,所以产物里连这个词都搜不到)。
  6. Z 的 .claude/settings*.json、本仓是否存在同类文件,未核实。
  7. 「Z 的脚本数量约 170」来自它自己的文档自述,未逐个清点。
  8. 各路的「子进程层近乎同源」是人工对照结论,不是工具 diff。

相关文档 ​

基于 MIT 许可发布