自建了一套知识库,用来存放各项目的定稿。发布脚本跑完了,退出码 0,日志干净。过了几天核对才发现,有 16 篇文档从来没进过索引——发布器漏了,索引器不知道,谁也没报错。

后来给推送门禁接审查报告,又遇到同一种错觉:报告格式全对,结论也有,内容却可以是空的。

两次故障都没有错误面:没有异常,没有非零退出码,只有「少做了一件事」的安静。这篇记录两次返工——发布与索引、推送前的审查——以及它们最后为什么都收敛到同一组原则。

两件事各有一个隐式前提。发布线:源仓存放定稿,镜像仓(索引器读取的归档目录)按五类目录归档,索引器读镜像根;旧流程是「跑发布器、再手工提交」,发布漏了不会报错,只是少一批能被检索到的内容——一次核对发现,16 篇文档从未进入镜像仓。门禁线:非文档改动必须先有当前范围的审查报告,难的是目录、字段与结论都要能被机器解析——目录名写错,报告存在也会被判没有。

两次改造围绕三条原则:规则只有一份;失败要在写盘或加载之前暴露;自动化停在需要判断的地方。

速查:两次故障与结论

你看到的现象

跳转

一句话结论

发布脚本退出码 0,日志干净,文档却没进索引

从发布到索引

单文件复制是可靠的,缺的是跨步骤对账;薄封装只编排,不复制规则

审查报告明明存在,门禁却说没有

推送前的审查门禁

目录名按仓名大小写生成,结论必须带覆盖证明

想知道两次返工有没有共同的解法

一条链路,一道门

规则只有一个主人;失败在不可逆操作之前暴露

阅读路线

  • 查故障:上表直达对应章节;两部分各先给现象、约束与处置。

  • 看取舍:两部分都写了被否决的路与理由,失败模式单独列表。

  • 核数字:数字与代价 汇总两次返工的结果与成本。

从发布到索引

先看发布这一端。镜像仓不是冗余副本。源仓按项目分散,目录结构与生命周期各不相同;索引器需要一个稳定根、统一的五类目录和可复现的相对路径。镜像仓把「项目仓的写作节奏」与「索引器的读取契约」解耦:源仓可以随时重构目录,镜像仓的路径保持稳定。代价是多了一次复制,以及一个必须被监控的缺口——副本是否最新。

发布器本身已经处理了单文件层面的可靠性:内容一致时跳过,不一致时写临时文件再原子替换,目标是目录或符号链接时拒绝写入。重复执行不会引起写放大,中途失败也不会留下半个文件。缺的不是单文件可靠性,而是跨步骤的编排与对账。

约束有五条:单一写者,目录枚举、排除规则、类型归属只能有一份实现;增量与幂等,重复执行不能产生重复内容或多余提交;人工提交,提交范围与提交信息需要人判断;离线可诊断,健康检查要在没装索引器、仓库未注册时也能跑;不扩范围,删除同步、自动推送、定时任务都不在这次改动内。

这五条决定了方案空间:能加编排,不能加第二套规则;能调用索引器,不能把索引器变成发布器。

旧流程的失败可以归成四类,每一类都要有可观测的处置,而不是笼统地说「流程自动化了」。

失败模式

旧流程的表现

新流程的处置

漏发

没有对账,少一批文档也看不出来

--dry-run 先给 added / updated / skipped

覆盖

整文件覆盖,手工改动无法从 git 找回

脏目录守卫退出 3,--force 才放行

重复

重复复制、重复索引,产生噪声

内容比较与增量索引,复验 0 / 0

半写

中途失败留下不完整文件

临时文件加原子替换,失败不落目标

16 篇是怎么发现的

发现方式很笨:把源仓五类目录下的 Markdown 列出来,再把镜像仓同路径的文件列出来,做一次集合差。结果有 16 个路径只在左边。

分布是 10 篇 reports、4 篇 retrospectives、1 篇 research、1 篇 handoff。时间集中在两次目录调整之后——源仓把文档从旧的项目结构迁到统一的 docs 结构,发布器的枚举规则也跟着改过一次。迁移时新增的目录被纳入了枚举,但迁移前已经存在、后来只改了内容的文件,没有触发任何一次「新增」事件。

这正是「退出码 0」的盲区。发布器只对「本次枚举到的文件」负责:内容一致就跳过,不一致就复制。它没有跨历史的账本,不知道镜像仓里应该有多少文件、实际有多少。1142 个文件的镜像里少 16 个,占 1.4%,无论看日志还是看目录都很难察觉;只有做集合差,缺口才会变成一个数字。

反过来说,单文件复制的可靠性并没有问题:16 篇里没有一篇是复制到一半损坏的,全部是「从来没被枚举到」。这也决定了修法——不需要重写复制逻辑,只需要在复制之外加一次可重复的对账,以及一条能让人看见缺口的命令。

为什么没做成子命令

四条路:在发布器外再加一层薄封装;给薄封装加定时任务;给索引器加 publish 子命令;用 git hook 在提交时触发。评估只看四个问题:规则是否只有一份、失败是否在写盘前暴露、审查面多大、出问题是否可逆。

薄封装规则只有一份,失败暴露在写盘前,审查面是一个脚本,重跑即可回退;其余三条要么把规则变成两份,要么把失败推到写盘之后。所以采用薄封装,只编排,不重写规则。

「规则份数」是决定性的:它一旦变成 2,审查面、可逆性与失败暴露点都会跟着变差。

「审查面」不是抽象指标。薄封装是 102 行、一个文件、一种语言;把发布逻辑搬进索引器,改动会落在目录枚举、排除规则、配置解析三处,还要同时维护两套测试。评审时要回答的问题,从「这个脚本的四种退出码对不对」变成「两套规则在哪些输入上会分叉」,后者很难穷举。

否决原生子命令,是因为索引器的职责是读镜像、建索引,不是决定「哪些文件该进镜像」。把发布逻辑搬进去,就要复制发布器的目录枚举与排除规则;两份规则一旦分叉,排查成本会落在最不该出问题的地方。薄封装只编排,不重写规则。

否决 git hook,是因为触发点是提交,发布的对象是源仓定稿,两者节奏不同。源仓提交不代表文档定稿;镜像仓提交又可能由发布本身产生,容易递归。更实际的问题是,hook 失败会卡住无关提交,把内容发布的风险扩散到日常开发。

定时任务暂缓,不是否决。无人值守意味着要同时解决自动提交范围、失败告警与并发写冲突。当前需求是「定稿后跑一次」,人工触发足够;等出现明确的无人值守需求,再在薄封装外面加定时器,不需要改脚本本体。

编译也没有并进同一条命令。增量编译是 LLM 调用,单篇成本、失败重试与限流都和发布不同。把它塞进 sync.sh,会让一条日常命令从秒级变成分钟级,也会让发布失败与编译失败混在同一个退出码里。实际做法是发布后单独看队列,再按队列编译。

薄封装

发布器仍然是主体。它按固定的五类目录枚举源文件,跳过依赖目录与缓存目录;目标文件已存在且内容一致时打印 SKIP,不一致时写临时文件再原子替换;目标是目录或符号链接时拒绝写入。

薄封装负责四件事。前置检查:发布器存在、索引器在 PATH、镜像是 git 工作区、镜像已注册为索引根,任何一项不满足就退出 1,且发生在发布之前。脏目录守卫:五类目录有未提交改动时退出 3,列出前 10 条,要求先提交或显式 --force。触碰清单:发布前用 mktemp 打一个时间戳标记,发布后用 find -newer 列出本次变化的文件;它只是诊断输出,索引器仍然按根全量扫描。索引与退出码:调用索引器,把「部分入库」降为告警,其余失败退出 1,未知参数退出 2。

前置检查的顺序是有意的:先确认发布器存在,再确认索引器与 git,最后确认根注册。任何一项失败都发生在写盘之前,避免「文件已经复制、索引却跑不起来」的中间态。根检查用 cut -f1 | grep -qxF 做整行精确匹配,而不是子串匹配,避免把同前缀的旧路径误认成当前镜像根。

核心片段如下,省略了参数解析与错误文案:

 DIRS=(reports retrospectives handoffs research decisions)
 ​
 dirty="$(git -C "$DEST" status --porcelain -- "${DIRS[@]}")"
 if [[ -n "$dirty" && $FORCE -eq 0 ]]; then
   printf '%s\n' "$dirty" | sed -n '1,10p' >&2
   exit 3
 fi
 ​
 marker="$(mktemp)"; touch "$marker"
 bash "$PUBLISH"
 ​
 for d in "${DIRS[@]}"; do
   find "$DEST/$d" -type f -newer "$marker" -printf '%P\0' | sort -z
 done
 ​
 pinax ingest || rc=$?
 [[ $rc -eq 2 ]] && echo "warn: partial ingest" >&2

两个细节值得说明。第一,--dry-run 只跑发布器的对账,不检查索引器与 git,因此健康检查可以在一台没装索引器的机器上执行;第二,脚本不自动提交,只在末尾打印 git add 的下一步提示。把「写文件」和「写历史」分开,是为了让覆盖范围在提交前还能被人看一眼。

退出码因此固定成四个值:0 成功,1 前置或索引失败,2 参数错误,3 脏目录。四个值对应四种处置动作,不需要读日志猜。

退出码

含义

处置

0

发布与索引完成

看新增与更新计数,决定提交范围

1

前置检查或索引失败

前置失败尚未写盘;索引失败时文件已复制,修好后重跑即可补齐

2

参数错误

看用法,不要原样重试

3

五类目录有未提交改动

先提交,或确认覆盖范围后加 --force

脏目录用 3 而不是 1,是有意的:1 表示环境或工具出了问题,重试可能恢复;3 表示需要人做决定——提交还是覆盖,重试不会改变状态。把两者分开,人和 CI 才能对同一个退出码采取不同动作。

前置检查的顺序也有一个反例可讲。如果先复制、再查索引器,索引器缺失时文件已经进镜像,用户看到的是「发布成功但搜不到」;反过来先查索引器,失败发生在写盘之前,镜像保持原样。同样是失败,后者的恢复成本低得多。

第一次跑,暴露了什么

验证分三层:语法与参数、守卫行为、真实首跑。

检查

命令

结果

语法

bash -n tools/sync.sh

通过

对账

tools/sync.sh --dry-run

0 / 0 / 1235

参数

tools/sync.sh --bogus

退出 2

脏目录

五类目录临时追加一行后运行

退出 3,列出 59 条

根注册

pinax root list 比对镜像路径

命中,root_check=0

真实首跑前先做了一次 dry-run,确认新增与更新的规模,再执行。发布器给出 79 新增、14 更新、1142 跳过;索引器给出新增 79、变更 15、删除 0、错误 0。两者新增数一致,变更数相差 1,原因是其中一篇在发布与索引之间被重新读取,属于预期内的时间差。发布提交包含 149 个文件,+33117 / −6526。

首跑前镜像仓并不干净:59 条未提交状态,展开后 60 个文件。逐字节比对后分两类:56 个与源仓完全一致,属于上次发布漏提交;4 个是旧版副本,源仓已经更新。处置顺序是先备份工作区补丁与未跟踪文件,再用 --force 覆盖,然后一次性提交,备份在推送确认后删除。这个顺序不能反:先备份、再覆盖、再提交、最后删备份;覆盖之后再想找回手工改动,代价高得多。

那 4 个旧版副本也值得看一眼:源文件在 10 月 30 日改过,镜像停在 10 月 29 日。直接 --force,旧内容会被新内容覆盖——这正是想要的,但前提是知道覆盖的是什么。备份里保留了覆盖前的完整状态,事后核对才发现,其中两篇只是补了日期,另外两篇改了一段结论。如果当时没有备份,这个判断就只能靠记忆。

首跑之后立刻做幂等复验:再跑一次 --dry-run,结果是 0 新增、0 更新、1235 跳过。发布器的 cmp 跳过与索引器的增量判断都生效,重复执行不会制造噪声。

还有一个不影响计数、但会在检索里显现的细节:两个项目目录下有同名文档,镜像仓按相对路径各存一份,索引器按标题去重时只保留了一份。发布与索引都不报错,后果是另一份在检索里不可见。同名不同源的文档以后还会出现,短期只能靠人工核对统计,长期应该在发布器里保留来源前缀。

索引侧的验证分两步:先看队列,再看完整性。队列决定要编译什么,完整性决定编译后的索引是否健康。

94 篇的构成

发布让待编译从 0 变成 94 篇:79 篇新文档,15 篇内容变更。编译没有并进 sync.sh,原因在上一节说过;实际执行是单独一条命令,一次跑完 94 / 94,prompt 709034、completion 139507、calls 190,合计约 85 万 token。

编译过程留下四条 warn,都是类型字段没有命中词表:refactor、audit、design,以及一条带斜杠的「research / 状态汇总」。索引器的处理是回落到 concept,不失败。查了一遍语料才发现,类型字段本来就是自由文本:research 出现 114 次、progress 57 次、new-owner 33 次、strategy 12 次,而 audit、design、refactor 各只出现 1 次。为这 4 条去扩词表,下一批文档还会出现新的写法;concept 兜底是 profile 的既定设计,不是缺陷。这里的选择是接受回落,把精力放在检索质量上。

编译之后还有一次独立的 link-cards:扫 1570 张卡,改动 989 张,新增 4102 条关联、删除 528 条。关联是全局的,新增一张卡可能改变邻近卡片的关联集合,所以放在编译之后单独跑,而不是每编译一篇就更新一次。

完整性指标:卡片 1570、血缘 1570、related 覆盖 81%、id_sync 100%、dates 64%、supersedes 3%;lint 为 PASS,0 errors、5 warnings;preflight 为 WARN,队列候选 0,但仍有 335 条孤立血缘。

还有一个自指的例子:README 补充健康口径之后,那篇文档自己也进了语料,产生 1 条 stale,补跑一次编译(1450 + 652 token)才归零。文档系统描述自己时,也会成为自己的输入。

缓存与孤立的血缘

健康口径最后固定成三条命令:compile --todo --all 看待编译,preflight 看完整性,sync.sh --dry-run 看发布是否漏。缓存不在其中。

当前有 924 / 1235 的有效缓存,311 篇缺失。这个数字每次检查都会出现,但它不是待办:卡片与血缘已经在,检索可用;索引器对已就绪的文档直接跳过,没有「只补缓存」的入口。要补只能删卡重编,代价是 311 张卡重写,以及 supersedes 与 related 漂移。缓存键还包含 provider 与模型名,换模型即整体失效。

cache explain 能把两种状态分开:已就绪的文档返回 ready (skip),未编译的返回 pending (no_card)。前者是加速器缺失,后者才是真的没进索引。把两者混在一个百分比里,才会产生「缓存覆盖率必须 100%」的错觉。

335 条孤立血缘同理:它们指向已经不存在的卡片,不影响检索,但会在每次 preflight 里出现。清理需要先确认历史来源,属于下一次专门处理。删除同步也仍然手工:发布器只增不删,下架一篇文档要在镜像仓 git rm 再提交。

推送前的审查门禁

发布与索引稳定之后,剩下的风险在推送这一步。仓库的推送门禁有一条硬规则:改动里只要包含非文档文件,就必须先有当前范围的审查报告。规则的目的不是形式主义,而是防止「脚本改了但没人看过」直接进入主分支。文档类改动可以跳过,因为风险面不同。

约束有六条:工具是第三方,审查由 ocr 完成,不自研规则引擎,也不改工具源码;版本固定,结论要能复现,版本号必须写进报告;只审 delta,范围从基线到当前 HEAD,不做全仓扫描;报告有契约,目录名、review.json、gate.txt 的字段都要符合门禁解析器的预期;不自动修复,审查输出只作为证据,修补由人裁定;结论必须带覆盖,可审文件数、完成数、失败数、排除原因,缺一项就不能称为「全绿」。

这里有一个容易忽略的落差:门禁检查的是「有没有报告、结论是否通过」,不是「审查质量好不好」。报告可以形式合规但内容空洞。真正的质量来自审查范围的选择、findings 的处置与 residual 的记录,而不是门禁本身。

围绕这条规则,有五条路。

方案

做法

结论

接第三方 CLI

用 ocr 出报告,按契约落盘

采用

人工清单

自己写检查项,人工勾选

否决

全仓扫描

每次审整个仓库

否决

跳过门禁

用 bypass 标记强推

否决

自动修复

让 agent 直接改 findings

否决

否决人工清单,是因为没有独立执行记录,无法证明「检查过」,也无法给出门禁可解析的结论。人工判断可以保留,但必须建立在机器产出的证据之上。

否决全仓扫描,是因为这次变更 151 个文件,其中 150 个是文档;全仓扫描会把成本花在与本次改动无关的内容上,噪声也会掩盖真正的问题。范围审查更符合「这次改了什么」的语义。

否决跳过门禁,是因为 bypass 是逃生口,不是常规路径。它有明确的适用场景(工具故障、紧急回滚),但需要有书面理由;把 bypass 当默认,等于没有门禁。

否决自动修复,是因为审查发现的问题需要判断「是不是真问题」「修了会不会引入新风险」。自动改代码会把审查变成另一个未经审查的变更源,风险更高。

门禁本身也做过一个取舍:它只校验报告的存在与结论,不校验 findings 的合理性。这样实现简单,但把质量责任留给了人。为了补这个缺口,流程要求每条 residual 都写位置与理由,让「为什么不修」也可审查。

报告契约

接入分三步:确定范围、生成报告、落盘到门禁约定的目录。

范围由 git diff --name-only 给出,共 151 个文件;审查工具再按扩展名过滤,只剩 1 个 shell 脚本可审,其余 150 个标记为 unsupported_ext。这个过滤结果必须写进报告,否则「1 个文件通过」会被误读成「151 个文件通过」。

报告目录的命名是 <Repo>-<base7>-<head7>-<stamp>,其中仓名区分大小写。门禁用仓库目录名做前缀匹配;写成小写会导致「报告明明存在,门禁却说没有」。

第一次推送就这样被拦下:报告写在 knowledge-...,门禁找的是 Knowledge-...,两边的路径只差一个字母。排查时先怀疑字段缺失,再怀疑结论格式,最后才想到目录名——因为「文件存在」这件事看起来不需要验证。后来把目录生成写进流程,并且让生成与 push 分成两条命令:门禁在命令开始时检查,同一条命令里刚创建的报告它看不到。

目录里至少有两个文件:review.json 保存结构化结果,gate.txt 保存人可读的结论。门禁解析的是后者,关键字段包括:

 gate: deltas=2/3 findings=H0/M0/L6 ruled=6 verdict=PASS with residuals
 coverage.failed: none
 selection: total_files=151 reviewable=1 excluded=150 (unsupported_ext)
 findings: High 0 / Medium 0 / Low 6 (all ruled, see residuals)
 tokens: 516824 (cache_read 418304) elapsed 6m36s
 recommendation: proceed (push)

门禁侧只做存在性与结论校验:

 for field in selection coverage findings residuals; do
   grep -qi "^${field}:" "$GATE" || { echo "missing: $field" >&2; exit 1; }
 done
 grep -qiE 'verdict:[[:space:]]*FAIL' "$GATE" && exit 1

这份文本同时回答四个问题:审了什么(selection)、覆盖是否完整(coverage)、问题有多严重(findings)、为什么还能通过(residuals)。缺任何一项,结论都不可核查。

处置循环按 delta 推进:每修一轮,重跑一次审查,范围仍从同一基线起算。这样既能验证修补,也能看到修补是否引入新问题。

三轮收敛与六条残留

轮次

范围

findings

处置

结果

第一轮

基线到首个修补前

7(3 Medium / 4 Low)

修 6,驳回 1

提交修补

第二轮

基线到第二轮 HEAD

7(2 Medium / 5 Low)

修 5,驳回 2

提交修补

第三轮

基线到第三轮 HEAD

6(0 Medium / 6 Low)

全部记 residual

通过

三轮共 20 条,修掉 11 条,驳回 3 条,保留 6 条。修掉的 Medium 都是真实风险:触碰扫描的管道与 NUL 处理、索引器缺失要在发布前暴露、--force 缺少覆盖告警、dry-run 失败丢缓冲、索引退出码区分不完整。保留的 6 条全部落在诊断文案与极端路径。

处置标准只有一条:会不会在真实路径上改变行为。会,就修;只在诊断文案或极端路径上,就记 residual 并写清触发条件。第一轮的三条 Medium 都属于前者——管道与 NUL 处理会让触碰清单漏文件,索引器缺失在发布之后才暴露会让镜像停在半完成状态,--force 没有覆盖告警会让人在不知道覆盖了什么的情况下继续。

驳回的三条则相反:索引器本来就全量扫根,再让薄封装做一次机器对账,成本大于收益;让发布器直接输出文件清单,会改动发布器的职责边界;把部分入库从告警改成失败,会让「大部分已入库」的场景失去可用性。驳回不是忽略,理由要能被下一个人复核。

#

位置

问题

保留理由

1

sync.sh:71

发布失败沿用发布器自身退出码,未统一为 1

发布器本身只返回 1,实际不冲突

2

sync.sh:40

git status 失败无自定义诊断

前置检查已拦住非工作区,剩余仅仓损坏或锁

3

sync.sh:33

根检查吞 stderr,grep -q 可能返回 141

实测 root_check=0,单行输出

4

sync.sh:86

find 失败文案归咎于 find,sort 失败时不准

纯诊断文案

5

sync.sh:48

--force 警告只列前 10 条,却声称列全

超过 10 条属极端情况

6

sync.sh:33

只比路径列,未校验根注册的 project 列

本机根已注册,README 有换机步骤

验证分三层:工具结论、覆盖证明、修补后的自检。第三轮 gate.txt 给出 PASS with residuals,findings 为 High 0 / Medium 0 / Low 6,recommendation: proceed (push)。selection 写明 151 个文件中可审 1 个、排除 150 个;coverage.failed: none 表示选中的文件全部完成审查。两句话合起来才能说明「这次审查对可审范围是完整的」,只看 PASS 不够。

每轮修补后至少跑语法检查与 dry-run:

检查

期望

实际

bash -n tools/sync.sh

通过

通过

sync.sh --dry-run

0 / 0 / 1235

符合

未知参数

退出 2

符合

脏目录守卫

退出 3

符合

根注册检查

命中

root_check=0

验证也有边界:审查工具不执行脚本,只做静态分析;bash -n 只查语法,不查行为;脏目录守卫的测试是临时改一行再还原,不是完整的并发或中断测试。这些边界应当写在报告里,而不是用「已验证」一笔带过。

一条链路,一道门

两次返工最后收敛到同一组原则:规则只能有一个主人,失败要在不可逆操作之前暴露。发布器不重写索引器的目录规则,门禁不重新判断 findings;守卫放在写盘之前,报告校验放在 push 之前。

其余三条同样重要:幂等要当成设计约束,而不是测试项;覆盖证明比结论更重要,PASS 必须和 selection、coverage 一起读;停止条件按严重度定,不按轮次定。自动化只做确定性的部分,提交范围、是否覆盖脏工作区、findings 修还是留,都留给人判断——把判断也自动化,等于把不可逆决策藏进没人看的日志。

「自动化停在判断之前」对应四个具体停点:提交范围——哪些文件进这次提交,脚本不替人决定;覆盖——脏工作区要不要 --force,脚本只负责拦住;findings——修还是留,脚本只提供证据;发布时机——什么时候把长文发出去,工具不碰。把这四个判断也自动化,短期会更快,长期会把不可逆决策藏进没人读的日志。

还有一个反直觉的结论:覆盖率比通过率更能说明问题。PASS 只回答「没发现严重问题」,selection 与 coverage 才回答「看了什么、看全了没有」。151 个文件里只有 1 个可审,这个比例必须和 PASS 一起读,否则「全绿」会被误读成「全部检查过」。

如果重来一次

  • 先做集合差,再改发布器。16 篇的缺口是比对出来的,不是读日志读出来的;诊断命令应该先于自动化存在。

  • 先定退出码,再写实现。四个值对应四种处置,写的时候会自然逼出「哪些失败该在写盘前暴露」。

  • 报告目录交给脚本生成。手写目录名看起来只差一个字母,排查成本却按小时计。

  • 类型字段要么尽早枚举,要么接受 concept 兜底并停止扩词表。自由文本的词表永远追不上下一次写作。

数字与代价

环节

关键结果

索引发布

16 篇漏发 → 0;发布与索引两步 → 一条命令;首跑 79 新增 / 14 更新;重复执行 0 / 0 / 1235

索引侧

待编译 94 → 0;卡片 1491 → 1570;血缘覆盖 100%;related 81%;lint 0 errors

审查门禁

151 个文件 / 可审 1 个;三轮 7 / 7 / 6;修 11、驳回 3、residual 6;PASS with residuals

代价同样明确:

  • 审查成本:三轮 token 分别为 385263、388585、516824;第三轮单独耗时 6 分 36 秒。

  • 编译成本:94 篇增量编译的 prompt 与 completion 合计约 85 万 token。

  • 人工环节:脚本不提交、不推送,提交范围仍需人判断;findings 修还是留也由人裁定。

  • 未处理的遗留:335 条孤立血缘、311 篇无有效缓存、删除同步仍需手工 git rm。

  • 覆盖面:脚本只编排五类目录,新增第六类要同时改发布器、脚本与 README 三处。

缓存与孤立血缘的处理理由见上一节,这里只记代价:924 / 1235 的有效缓存,311 篇缺失不补;335 条孤立血缘暂不清理。缓存是加速器,卡片与血缘才是事实源。

还没做的

索引侧还留着三件事:311 篇文档没有有效缓存,335 条孤立血缘没有清理,删除同步仍然需要手工 git rm。它们都不影响当前检索,但都会在下一次全量检查时重新出现。缓存按上面的理由不补;孤立血缘需要先确认历史来源;删除同步涉及不可逆写操作,暂时留在人工环节。

门禁侧还可以再收紧:写一个小脚本,从仓库名与 HEAD 自动生成报告目录,消除大小写与哈希截断的手工错误;把 gate.txt 的解析做成独立校验,报告缺字段时直接失败;给审查工具加一个预算上限的替代方案,因为当前 provider 不接 --budget-tokens,成本只能靠范围控制。

这两条线都停在「可重复、可诊断」,没有走到「无人值守」。这是有意的:无人值守要求失败告警、并发控制与自动提交范围同时到位,而当前需求还没有到那一步。先让流程在人的判断下稳定运行,再决定哪一段可以交出去。

数据与来源

索引发布的脚本在 Knowledge/tools/sync.sh,102 行;首跑提交为 1e725f6,149 个文件。发布与索引的数字来自 --dry-run 和索引器的 ingest 输出;编译数字来自当天的编译日志;审查结论来自 gate.txt。

审查范围、findings、覆盖与 residual 都来自 gate.txt;token 与耗时来自三轮运行的统计行。工具版本为 open-code-review v1.13.4,换版本后报告格式与分级可能需要重新核对。