chore: release 1.8.0 - #166
Merged
Merged
Conversation
grilling 决策树已走完,当人面确认。基线来自只插桩不改文件的 fs 计数脚本(monkeypatch fs 后 require dist/cli/commands/check.js,目标 studio 与 harness):一次 check 在 studio 干净树 595 次 fs 操作、pre-commit 带源码改动 ≈650-700;traces.log 全量读 3 次 51-63ms(占 110-180ms 墙钟的三分之一到二分之一)、源根探测 2-3 次、reconcileCapabilities 相同输入算 2 次。 六项决策:① CheckEnv 提到最外层成为一次 check 的统一环境对象(否:外层另立第二个同类概念、 否:挂进 ConstraintContext——后者是「本次为何触发」的数据描述且由 context-builder 构造,会形成 构造顺序死结);② 一切「一次运行一份、跑完即弃」,撤销工单 16 的进程级 rawConfigCache 与 mtime+size 指纹(毫秒级时间戳判新鲜,同一运行内先改后读仍读到老内容);③ readJsonl 的 tail 改从文件末尾倒读分块;④ 环境对象上加 traceLog()/sourceRoots()/capabilities();⑤ 验收两道硬 门槛——照 check.test.ts recordingEvidence() 形状做计数假读盘件断言「每文件至多读一次」+ 固定 项目跑 check 逐字节对比 stdout 与 trace 证明判定未变;⑥ 收益归因写清:③ 单独落地即让 traces.log 落到一次全文读的下限,④ 的 traceLog() 与之不叠加,避免后续按双倍收益期待。 范围外并各自记名:traces.log 轮转(属数据保留取舍,仓内有 context-tracker 按大小 rename 先例)、 治理正本落点三处口径不一致(governance-presence AGENTS 优先 / injection-writer+drift CLAUDE 优先 / context-builder 只看 CLAUDE,是正确性缺陷非性能)、DEFAULT_TRACE_FILE 相对路径致 trace 落到 CWD 而非 --project-path(违反 harness#95,缺陷票)。
…62ms(ADR-0023 决策 3)
本文件头原断言「tail 仍全量读取,parse 才是大头」「10MB 级文本读廉价」,被量数推翻:完全不
parse 的 countJsonlLines 在同一份 6.45MB 日志上单次 23.2ms,成本在 split('\n') + 空白行过滤
三万多行,不在 JSON.parse。一次 harness check 走这条全量读 3 遍(check.ts:232 数行数 +
context-builder.ts:97/:137 取尾部),合计 51-63ms,占该命令 180ms 墙钟的三分之一以上。
改法:tail 改为 openSync/fstatSync/readSync 按 64KB 从末尾倒着捞块,够数即停;左侧残段按字节
留到下一轮再解码(UTF-8 多字节序列不含 0x0a,故按换行字节切块再解码不破坏字符)。语义与「整读
后 slice(-want)」逐字一致:行以 \n 界定、末行允许无换行、\r 随行进文本、纯空白行不计数、尾部
窗口内坏行照旧计入 skippedLines。head 与需要全文行数的消费(countJsonlLines/readJsonlEnds)
保持整读路径不动。
测试:jsonl.test.ts 补等价/CJK 跨块边界/末行无换行/CRLF/尾部空白行/tail:0 与缺文件六组;
新增 jsonl-tail-seek.test.ts 作「不整读全文」的反证闸——Node 的 fs 导出属性不可 redefine
(jest.spyOn 抛 Cannot redefine property),只能在模块层把 readFileSync 换成必炸实现,并配
一条反向断言(全文读确实必炸)挡「mock 没生效也算通过」。context-tracker 的 fs 假件原先只
覆盖 readFileSync,补上倒读三件并与 readFileSync 同源后,其 5 条 tail 用例恢复且断言一字未改。
验证:npx jest --maxWorkers=2 → 152 suites / 2100 passed / 6 skipped(存量);tsc --noEmit
exit 0;harness check 在 studio 副本中位 180ms → 130ms(5 次)。
…删除(ADR-0023 决策 3 推广) 承 8458e31(tail 倒读)。第三处全量读在 getSmartHint:它拿 countJsonlLines 数完 6.45MB 的 traces.log,只为比两个较——== 0 与 >= 50,总数从不打印。为消费方的真实需求改形而非为口径 将就:改经有界 head 读(读够 50 行即停),以 records.length + skippedLines 作与原纯计数 逐字等价的判定输入(坏行照旧占位),countJsonlLines 随之失去唯一生产消费者,按 ADR-0022 同判据删除、不留兼容壳;其文档说明与 jsonl.test.ts 的专用 describe 一并清掉,appendJsonl 用例里借它做的断言改经存活的 readJsonl 表达同一事实(未弱化)。readJsonlEnds 要全文行数, 整读路径不动。 阈值 50 提为 TRACE_HINT_THRESHOLD,与 head 读取行数同源,防改常量忘改条件的静默失真。 夹具层新增 test-setup/jsonl-fake-fs(两个消费方才立接缝:context-tracker 与 session-manager 的 fs 假件原先只有全文读入口,有界读一到就抛,再被被测代码自己的 catch 吞成「Checkpoint 不 存在」「空快照」——即假件缺件伪装成业务缺陷)。夹具按 openSync 的路径把内容喂给 fstatSync/readSync,两条读入口同源;两处测试各自内联的八行副本收编于此。 两道闸的记账(属 ADR-0023 决策 5 预告的当人面过账,未偷改期望值): - jsonl-skip-disposition.test.ts:冻结表新增第 9 个 skip 站点 src/cli/commands/check.ts:1 (提示改 skip 策略的 head 读即成为契约内站点),去向豁免写在调用点注释;文件头关于 countJsonlLines 不在表内的说明随之改写为删除事由。 - jsonl-tail-seek.test.ts → jsonl-bounded-read.test.ts(git mv):闸的范围从 tail 扩到两端 有界读,补 head 的不整读、CJK 跨块边界、超过总行数、head:0/缺文件、坏行占位、末行无换行。 夹具自身被自己的用例抓到两处:Buffer.copy 在 position 越过 EOF 时抛 RangeError(真 readSync 返回 0,已补边界归零);「close 后读到空」这条断言原用不看路径的内容源,任何路径都返回同一 份内容,断言与夹具契约无关——改成按路径分派后「路径丢失即读不到」才真被测到。 验证:npx jest --maxWorkers=2 → 153 suites / 2113 passed / 6 skipped(存量); tsc --noEmit rc=0(须带 --max-old-space-size=1536,本机可用内存不足会 OOM 杀 tsc—— 上一提交里我那句「tsc exit 0」取的是管道尾端 tail 的状态,是假凭据,此条更正)。 量数据:head:50 0.224ms、tail:20 0.330ms(同文件整读 21.2ms);harness check 在 studio 副本 5 次 0.10-0.11s(改前基线中位 0.18s)。提示路径三条端到端各跑一遍:够数出提示、 二次被 state 抑制、日志仅 2 条不出提示不报错。
承 8458e31 / 3fbf8a3。context-builder 的两个证据探测各自整读一次 traces.log(最近 20 条判 有无失败、最近 10 条判有无通过),detectTrigger 与 checker 层各探一次源码根——同一次运行 重复读同一批上行数据。git 事实早在 #87 就收成「一次 run 一份实例、沿调用链显式传」,本票把 同一形状复制到上行数据面。 形状更正(决策 1 原写「只把 CheckEnv 提到最外层、不留第二个概念」,动手时死于构造顺序): CheckEnv 含 context,而 context 正是通过该观察面读文件算出来的产物,同一对象不能既是构造者 的输入又是其产物。落成两个类型、一份实现、一条读路径——RunEnv{projectPath, traceTail(limit), sourceRoots()}(不需要 context 就能造的那半,CLI 入口构造)+ CheckEnv 后续从它派生。checker 侧名字不动,12 个 checker 与 12 处 buildCheckEnv 测试零改;本票有意不把 RunEnv 接进 CheckEnv(暂无 checker 消费它,先接就是为将来预先改签名,该接线属步骤 4)。 jsonl 新增 readJsonlWindow:一次分块读尾部行文本、按不同 limit 反复截窗。必须**先按行文本 截窗再 parse**——坏行占尾部槽位不占 records 名额,若改拿大窗口的 records 再切,窗口内有坏行 时小窗口会多收更老的有效记录,判定就变了(run-env.test.ts 用逐字等价断言钉住这一点)。窗口 上限取本 run 最大消费方 20 条,请求超出即抛,不静默少给。缺文件返回空窗口;懒建——没有消费 方就不碰文件。 context-builder 的 detectFailingTest/detectVerificationEvidence 除本模块外双仓零消费者(含 studio,其 node_modules 里的命中是已发布包源码不是引用),按 ADR-0022 同判据收为私有。 两道闸与两处文档的记账:jsonl-skip-disposition 识别正则纳入 readJsonlWindow(否则「改用窗口 读」成为 skip 契约的逃逸通道,harness#114 同判据),冻结表把 context-builder.ts: 2 迁为 run-env.ts: 1(合并读点导致的站点迁移,豁免理由随读点搬走,非删条目留空);新增术语「运行级 观察面(run env)」入 src/CONTEXT.md 术语表,src/core/CONTEXT.md 补 run-env.ts 模块说明并更正 context-builder 那条。ADR-0023 决策 1/4、理由首条、影响段按实面改写(含被证伪的旧结论就地 更正,不留旧名义给人照抄)。 验证:npx jest --maxWorkers=2 → 154 suites / 2123 passed / 6 skipped(存量);新增 run-env.test.ts 10 例;tsc --noEmit rc=0(带 --max-old-space-size=1536,本机可用内存不足)。 量数据(studio 副本 6.45MB traces.log,插桩计数):全文读 3 次 19.4MB → 0 次,分块读 3 次 192KB → 2 次 128KB;readdirSync 51 次(源根探测由 2 次降为 1 次,checker 侧那 1-2 次 待步骤 4)。check 墙钟 180ms → 100-110ms。
承 b8fa0e8。撤销工单 16 的进程级 rawConfigCache + mtime/size 指纹:它靠时间戳判新鲜, 同一次运行内「先改后读」照样读到老内容,是在运行级快照旁边再养一套口径。 - RunEnv 增两个观察口:rawConfig()(懒建 + run 内 memo,缺失 undefined、空文件 {}、 解析失败照抛)与 customConstraints(fileName)(同名文件 run 内至多读一次); project-config-loader 自此不碰文件系统,只做解读与合并 - 访问器族(loadRawProjectConfig / getGovernanceConfig / resolveContextFiles / getCapabilitiesMode)、ProjectConfigLoader 构造器、getMergedConstraintsConfig / getEffectiveConstraints、detectInjectionDrift 的根入参放宽为 RunTarget: 传路径 = 一次性读取语义,既有消费方(含 studio)零改动 - CheckEnv 改为 extends RunEnv(步骤 4 的派生形状提前落地,配置共用需要它); 四个读配置的 checker 经 env 取数;ConstraintChecker 三个 run 入口逐层透传, 调用方未传则自造一枚供全 run 共用,不再逐条 checker 各读一遍 - 生效集的合并计算仍跑两遍(check 侧与漂移侧),但第二遍零 I/O:两侧 --preset 口径 本就不同(记进 ADR 决策 6 第四项,属判定正确性、不在本票顺手统一) - 公共面 ./core 新增类型导出 RunEnv / RunTarget(纯增加,非 breaking) 量得(/tmp/sx studio 副本,同一插桩):config.yml 1 读 + 2 次判指纹 stat → 1 读 0 stat; custom-constraints.yml 2 读 12520 字节 → 1 读 6260 字节;墙钟中位 0.11s → 0.10s; stdout 与改前逐字节一致。全量 154 套件 2132 passed / 6 skipped(基线 2123,新增 9 例)。
承 0d3f520。 取数合一次: - RunEnv 增 capabilities(populationFiles) 观察口:CAPABILITIES.md 原文 + 源码根 + 代码实况清单 + 对照判定一次算好,capability_sync 与 docs_freshness 共用(决策 4) - 两处逐字节相同的 reconcileCapabilities 输入合成一次;增量那一维由 capability_sync 经导出的唯一覆盖规则 isCoveredByEntries 自算(规则本身仍单点在对照模块,不重写) - 代码实况清单的唯一取法收成 collectPopulationFiles —— 共享判定要求两边输入同形 - 源码根探测改走 env.sourceRoots();doc-freshness/runner 的兜底那支改接调用方注入的 provider,选择接线不删除:它承接「文档里一个目录条目都没登记」的形状,删掉等于 对空文档静默放行(在两个真实配置里 0 次命中属配置形状,不是死码) - gates/checker-gate 随形状接 GateContext.runEnv(可选字段):一条守卫链共用一枚观察面 证据面(ADR-0016 补迁两条漏改): - governance_presence 的 console.error 侧信道 → 随判定返回证据行(判 fail 不变) - context_doc_sync 的裸 false → CheckDetail,列全缺失的 CONTEXT.md(只加证据粒度,不动 pass/fail) 量得(/tmp/sx studio 副本,触发条件 module_modification;同一树状态、trace 与 state 回卷后 A/B): - 对照判定调用 2 → 1;源码根探测 3 → 1;CAPABILITIES.md 读 2 次 59900 字节 → 1 次 29950 字节 - 一次运行 fs 操作 954 → 807;check() 本体中位耗时 56.8 / 60.1 / 49.6ms → 42.4 / 42.2 / 42.9ms - stdout 与 step 3 逐字节一致;干净树(test_creation 形状)两 checker 不触发,计数与输出不变 - 全量 154 套件 2137 passed / 6 skipped(+5 例:能力表共享 4 例 + runner 兜底注入 1 例)
承 63d9f3e。步骤 3 接线时读出的口径不一致,根因不在生效集规则(工单 23 的 「--preset 仅在无自定义配置时覆盖」仍然成立),而在 CLI 给 -p 塞了 defaultValue 'standard' —— 「没传」与「传了 standard」不可区分,覆盖规则恒被触发: - 项目 .harness/config.yml 的 preset 键对无自定义配置的项目静默失效(假执法) - 漂移检测侧不带 preset、按 config.yml 生成期望段 → 两边稳定对不上: 写了非 standard preset 的项目稳定报假内容漂移,真漂移反而不报 修两处: - definitions.ts 去掉该缺省值,CheckOptions.preset 收为可选;未显式传时首行如实 标注「(按 config.yml,缺省 standard)」 - effective-constraints 新增 constraintsFromMerged(生效集清单推导单点), detectInjectionDrift 增可选第三参 expectedConstraints(不传仍自取,report 侧行为 逐字不变);check 把本 run 那份交给它 → 生效集链路一次运行只算一遍 对外可见变化(定级待人裁):无自定义配置且 config.yml 写了非 standard preset 的项目, 执法集回归 config.yml;未传 -p 时「预设:」一行文案变。两仓(harness / studio)stdout 除该行外零差异,studio 的 config.yml 本就写 standard 且带自定义约束。 新增 3 例:check-drift 2 例(不带 -p 出现「已禁用约束」行且零漂移;显式 -p standard 时该行消失并报「缺失 N 条 / 多余 0 条」——两条都能证伪改前行为)、check 1 例(预设行 两种标注)。全量 154 套件 2140 passed / 6 skipped,tsc + lint 干净。
两道验收都过,零生产代码改动: ① 新增 src/cli/commands/__tests__/check-read-count.test.ts —— 形状照 check.test.ts 的 recordingEvidence():真执行、只在 readFileSync 与只读 openSync 上记一笔。夹具让两个吃 能力表的 checker 都真评估(staged 源码改动 + CAPABILITIES.md + 自定义约束文件),并带 正对照防闸空跑。断言两层:整张读取表逐条冻结 + 「除点名例外外任何项目文件至多读一次」。 反证:把 rawConfig() 的 memo 拆成每次真读,两条用例同时转红(config.yml 读到 6 次)后还原。 例外与理由写在表旁:traces.log = 2(head 50 阈值判定与 tail 20 证据窗口是两个窗口的有界读, 并成一个就回到整读)、AGENTS.md = 3 / CLAUDE.md = 2(决策表明示不处理的治理文档重复读)。 → 决策 5 的口径精确化为「同一份数据的同一个窗口口径至多取一次」。 ② 等值对比:用改动前的 1.7.0 构建(7a544b7)与当前构建分别跑 check,比 stdout 与本 run 追加的 trace。三组形状(studio 副本 test_creation 12 条、studio 副本 module_modification 15 条、 harness 副本 6 条):trace 抹掉 timestamp 后逐字节一致;stdout 只差「预设:」那一行(步骤 4.5 刻意的标注变化),判定输出零差异。 量得(同一插桩、两个构建自比):studio 副本 module 路径 总 fs 操作 959 → 807、traces.log 全文读 20,459,767 → 0 字节、CAPABILITIES.md 读 2 → 1 次、readdirSync 243 → 141、5 次总耗时 2536 → 1955 ms; 干净树形状 398 → 397 操作、2181 → 1803 ms。 ADR-0023 状态改为步骤 1–5 全落,并按实面重写「影响版本」:原定 breaking 触发条件(ConstraintContext / CheckCache 形状)未命中,级别由 ./core 两个新增类型导出(纯增加)与 -p 缺省语义/一行文案决定, 建议 minor 并随附说明,定级待人裁;studio 侧无需适配(传路径的公开签名照旧可用)。 全量 155 套件 2142 passed / 6 skipped,tsc + lint 干净。
测量目标改为泛化措辞,不再写出开发机目录布局。其余文本不动:该处语义是量过 的性能数据(非漏洞确认),临时测量副本路径(/tmp/*)不指向真实布局。
GateContext 混了两类东西:门禁真读取的运行信息,和一批与构造器配置平行的字段。 逐个查过读取点—— - projectId:必填,但全仓零读取点。后果是 3 个 CLI 调用点只好塞假值 'default', 另有 2 处为躲开它写 as any(步骤 2 消除)。删。 - minReviewers / reviewers / ignoreWarnings / openApiSpec / timeout:零读取。 构造器那条配置通道一直在用,这条平行通道填了不生效。删。 - prNumber / securityScanCommand / performanceThresholds / oldContractPath / newContractPath / tasksPath / command:实现里真读,留。 四个门禁的 enabled 配置字段与各自「禁用即放行」分支同批删:没有任何代码路径给 实例设过 enabled——CLI 不传,而 config.yml 的 gates.<id>.enabled 只被 getEffectiveGates 用来从数组里移除门禁、从不写进实例配置,两半从未接上。 连带删 6 条钉这条分支的用例。 外溢面(全部机械):33 处 projectId 字面量(11 个测试文件逐字填过这个假值, 正是类型失真的证据)+ 26 处 enabled 填充。1079 测试通过,84 套件全绿。 公共面:GateContext 与 4 个 *GateConfig 属包根导出,删必填字段对下游是编译级 变更。按 ADR-0022 口径走 minor(2026-09-09 人类裁决:breaking 内容按 minor 发布)。
新增 src/cli/gate-command.ts:GateDecision → CommandResult 的唯一映射 (gateCommandResult),加 ✓/✗ 输出骨架 reportGateDecision 与出错横幅 reportGateError。六个 handler 原先各写一遍同样的翻译与同样的话—— 「<id> gate denied」6 份、「<id> gate error」4 份,改一句措辞要开 6 个文件, 且「六条命令失败说法必须一致」这条断言此前写不出来。 - 判定入口从 check()/scan() 换成 evaluate():统一接口第一次有生产消费者。 三处 as any / as GateContext 强转随步骤 1 的 GateContext 瘦身一并消失。 - command 命令只复用映射,保留自己的单行 ✓/✗ 与 --json 输出(已对外的形状); security audit / acceptance list / contract validate-schema / review status / command --list 是只展示不判断的子命令,仍直读报告面,不套骨架。 - 按裁决改动一处对外文案:security 失败 reason 去掉六者中唯一的例外 「(threshold x)」,阈值改到失败详情首行。其余五条 reason 逐字不变。 - 替身换成「产决策的假门禁」(__tests__/gate-decision.ts):六个命令的测试因此 不需要 git、gh 或真扫描;contract/acceptance 补上此前没钉的详情行与 reason。 #95 守护闸拦到一处新增的 CLI cwd 站点(GateContext 要求项目根,而 CommandGate 零文件读写、该命令本无 -p),按其既有机制登记豁免,并同步 CONTEXT 的豁免清单 (三处 → 四处)。 真命令核过:command 命中黑名单 ✗ / exit 1、--json 结构不变;review exit 1、 acceptance 无 tasks.yml 通过 exit 0、contract 缺文件前置 exit 1、performance 通过。 1079 测试通过,84 套件全绿;tsc 与 eslint 零告警。
…除(架构评审候选1 步骤 3)
三条事实叠在一起才动手,逐条核过:
- 无调用方:getEffectiveGates / runGates 在两仓生产路径均无消费者(步骤 2 之后
evaluate() 有六个消费者,链级仍只有测试在用)。
- 无配置数据:harness 自身没有 .harness/config.yml;studio 与 studio-prod 两份配置文件
各 43 字节(只有 preset 与 harness.version);harness init 生成的模板也不写 gates: 段。
- 无对外承诺、且不纠错:README 无该段示例;加载器只做 {...DEFAULT_CONFIG, ...raw}
不校验顶层键,ProjectConfig 类型里也没有 gates 字段(getEffectiveGates 自己钻
raw.gates)——把 gates: 拼成 gate: 静默通过。
生效集与四个门禁配置接口的 enabled 字段是一对从未接上的两半:getEffectiveGates 的裁剪
方式是「从数组里移除门禁」,从不往实例写 enabled:false。字段与「禁用即放行」分支已随
步骤 1 删,本步删掉另一半,这条对外配置承诺整体消失(净删 176 行含其测试)。
保留部分:order 字段与 runGates(链内默认序,兼 #115 写明的复开点)、注册表闭环
(ADR-0002 真正值钱的那半)。core/effective-set.ts 的 throw 模式因此暂无生产调用方——
按已确认范围只改注释、不删选项,去向记在 effective-set 与 core/CONTEXT.md,
留给下一轮零消费者清点。
记档:ADR-0002 状态行按 ADR-0010 惯例标注部分失效,文末「后续变更」记判据与影响面;
gates/CONTEXT.md、core/CONTEXT.md、CAPABILITIES.md 同步;#95 守护表里 effective-gates
的豁免条目随站点消失一并摘掉(该闸要求每条豁免仍然成立)。
1432 测试通过,109 套件全绿;tsc 与 eslint 零告警。sync-docs --check 另报 hooks/
knowledge/monitoring/release 四处 CONTEXT 过时,经核其源码 mtime 早于本票(1.7.0 那批
改动未回灌文档),不在本票范围。
…view 收口模型 票体「方案」段的待裁问题已裁完,本条为唯一正本,实现会话按此开工。
仅记录已定方向与只读契约分工,未开工、不关票;ADR-0024 待出后由实现票接续。
类型面自 1.7.0 起按 package.json 的 exports 逐入口冻结,值面却只有包根一处被
public-exports.test.ts 钉住(Object.keys 只看得到 `.`)。本票补另一半:四个子路径
入口的运行时导出键集合逐入口冻结,入口清单同样从 exports 派生,新增子路径未登记即红。
包根不重复钉——同一入口两份清单必然漂移,且那条判定按票面要求逐字保留;「两边都不漏」
由登记闸与委派闸各钉一条。
冻结清单不手抄:src 侧 ts-jest 运行时键与 tsc 产物(各 exports[*].require 指向的 .js)
逐入口对撞,106/29/4/4/24 全部逐字一致。
落闸前实测校正了票面的前提——票上举的例子(删 ./gates 的一个 create*Gate 当前无测试会红)
不成立:src/index.ts 经 './gates' barrel 再导出这批符号,删它会让包根那条闸红,只是红名
报 `.`、分不清入口归属。真实的零覆盖面是包根未转发的符号,共 11 个:./core 的
ConstraintChecker / ProjectConfigLoader / constraintChecker / getCapabilitiesMode /
getGovernanceConfig / loadRawProjectConfig / resolveContextFiles 七个,./presets 全部四个
(实测删 STRICT_PRESET 时既有三道公共面闸 18 项全绿)。反方向四个子路径都漏:向 ./gates
注入一个运行时导出,既有闸同样全绿。getEffectiveGates 在本票开工前已随门禁声明式配置面
收缩删除,不在清单内。
反向验闸四例各红一次、diff 指名到入口与符号:删 ./gates 的 createReviewGate、向 ./gates
注入 createScratchGate、删 ./presets 的 STRICT_PRESET、package.json 新增 ./monitoring
子路径(落登记闸);另测类型面注入(export type { DynamicTask } 令 public-exports.test.ts
TS2578「failed to run」)不连带抹掉本闸。每次注入后按 sha1 还原现场。
非 breaking:零生产代码改动、零公共面变化。文档:ADR-0022 追记第 5 条改判已收口、
src/gates/CONTEXT.md 记账、CHANGELOG 落 Unreleased。
验收:npx jest --runInBand 156 套件 / 2146 passed / 6 skipped(含本闸 1 套件 7 项);
tsc --noEmit 与 eslint src 零错;行覆盖 92.62%(纯增测试、生产代码行数不变,覆盖只增
不减);node bin/harness.js check 通过。
init/validate 的 8 处「目标在不在场→写或告知」判定收敛为 writeManagedFile + 8 个 ManagedFile 工厂;模板正文落 scaffold-templates.ts(不落 templates/)。 落盘面可注入内存替身:scaffold.test.ts 零真实文件系统测全三态并逐字冻结 8 站点 文案,init-ondisk.test.ts 不 mock IO 真跑 init 断言落盘字节与「打印即落盘」同源 (#103 判据)。GH Actions 冲突分支改打印完整 workflow 全文;死选项 -t/--type 随票删除;init.ts 922 → 620 行,反向 import validate 消失。
新增 knowledge-view.ts 收口四件事:json/人读唯一分派与退出码(emitKnowledgeView)、
取数期进度行(announce,--json 下静默)、角色→样式单表 + 维度/规则 label 与成熟度/
分数/严重度三张映射、路径兜底与 store 构造单点。命令模块只产 { data, human() }:
data 是 --json 正文唯一正本,human() 返回 display model(sections/rows/cells,一格
三选一 label|field|derived),排版不再靠 \n 前后缀藏、命令侧零 chalk。
json 面按决策现状冻结(11 个顶层形状逐字不动)。三处有意偏离均不改字段集合:
snapshot/index 的 --json 由单行紧凑改为缩进;sync-rag 两个空态此前把人类文案打到
--json 的 stdout(吐出的不是 JSON),现恒为 {directory, files};stats 人读面 verified
的颜色随成熟度单表归一(此前 list 青 / stats 黄两处手抄不一致,文字逐字不变)。
测试:knowledge-view.test.ts 66 项四道闸(源形状 / 11·11 json 字段清单冻结 / 两投影
一致性 + derived 双向对撞防化石 / 满态与空态人读逐行冻结,两份人读基线取自改造前实现
实测捕获)。反向验闸四例各红一次并还原现场。knowledge.test.ts 的 MOCK_REPORT 标注成
AuditReport,7 维度 14 规则键改由编译期穷尽性管住,缺键补偿写不出来;既有 20 条用例
逐条保留、断言不降。registry 懒加载探针按新增行扩登记 knowledge-view.js(随命令模块
一并懒加载,per-command 不变式仍成立)。
验收:npx jest --runInBand 160 套件 / 2249 passed / 6 skipped;tsc --noEmit 与
eslint src 零错;行覆盖 94.98%;node bin/harness.js check --staged 铁律全通过;
npm run build 后真跑 knowledge 11 子命令两投影冒烟(-p 锚临时工程根)。
非 breaking:命令语法、退出码、json 字段集合、公共导出面零变化。
…落定待实施 仅记录已裁方向(--ci 显式选择、解析链 flag > config > github、.gitlab-ci.yml 三态、 --no-github-actions 降级为别名),未开工、不关票;实现由 #143 接续。
…yml(#143) 服务端 CI 是约束门禁唯一不可绕过的执行时机,而 init 的 CI 面此前只懂 GitHub Actions。落地 #143 决议 ①–⑥:CI 站点工厂收口成 harnessCheckCiFile(projectPath, platform, existingCiWorkflows, governanceLevel?),gitlab 形落 .gitlab-ci.yml(同三条 命令、image: node:20、stage: test、rules 触发 MR + main/master、无缓存,与 GH 版对仗); 治理 CI 渲染函数按平台参数化,gitlab 的 -g 档任务并入同一份正文(一个平台一份 CI 文件),docs 新鲜度的非阻断对应 allow_failure: true;模板正文住 scaffold-templates.ts, GITLAB_CI_SNIPPET 同时是 --print-snippets 与冲突分支打印的 job 片段(#103 判据:打印即 落盘正文的一部分)。 平台解析在命令层:flag > config.yml 的 ci.platform > github(不做 remote 自动检测), 只有非默认平台才写键(github 不写即等于清除,旧配置零迁移、validate 不报错); --no-github-actions 降为 --ci none 的别名并打 deprecation 警告,与显式 --ci <非 none> 同时给出判 usage-error 退出 1 且完全不落盘;--ci 非法值同样报错。scaffold 仍不持有 跳过策略——none 就是 plan 里没有 CI 站点。GH 侧模板、冲突判定与文案逐字节不动。 三处对外行为变化(是否记 breaking 随本车 ship 时裁决,判据同 1.6.0/1.7.0): --no-github-actions 新增警告行且现在会把 ci.platform: none 写进 config.yml(此前无痕); --ci 的非法值与冲突组合从「无从属关系」变为非零退出;--print-snippets 走同一解析链。 测试:scaffold.test.ts 新增「CI 站点的平台维度」7 项 + 同源闸扩到两平台四形(另 2 项), init-ondisk.test.ts 新增「init --ci 平台维度」8 项(.gitlab-ci.yml 逐字节两档、第二趟 manual 且用户正文不动、持久化回读、none/别名/冲突/非法值/脏 ci 段回落), init-dead-options.test.ts 的旗帜面冻结随 --ci 更新。反向验闸七处各红一次并还原现场。 验收:npx jest --runInBand 160 套件 / 2266 passed / 6 skipped;tsc --noEmit 与 eslint src 零错;覆盖率 94.22% stmts,scaffold.ts 与 scaffold-templates.ts 行覆盖 100%; node bin/harness.js check 通过;build 后真跑 CLI 冒烟(gitlab 落盘与重跑 manual、 非法值与冲突退 1、print-snippets 按 config 出面、--ci github 回到 GH 形状、全新目录 裸 init 输出逐字节不变)。文档:commands/CONTEXT.md 的 CI 平台维度条目改为已落地。
手抄的第二份 YAML 比 harness init 落盘的 harness-check.yml 少 validate 与 passes-gate 两道门禁,且两份文本间无同源闸——照抄的用户拿到弱一半的 CI。 单一正本收口成 HARNESS_CHECK_WORKFLOW.slice(jobs: 段之后),形状与 #143 的 GitLab 侧「打印的是落盘全文的尾巴」对仗;引导语说清这是完整 workflow 的 job 段。 三道闸先红后绿:同源闸(对 harness-check.yml 字节级冻结基线 + 真落盘文件回读)、 steps 集合闸(按 YAML 解析枚举 job 的 run: 命令)、片段视图的包含闸 + 引导语冻结。 反向验闸:删正本 passes-gate → 同源闸与 steps 集合闸各红一次;改回手抄 → 包含闸红。 PRE_COMMIT_SNIPPET 同族自查已是单份常量组合(落盘 = 头 + 片段),本票零改动。 验收:jest --runInBand 160 套件 / 2270 passed、tsc 与 eslint 零错、harness check 通过、 覆盖率 94.22% stmts 持平、真 CLI 打印段与落盘文件逐字同源。
…150) #132 口径外的第 9 处同形写手——sync-docs/context-syncer.ts 的 createContextMd 自带一份 CONTEXT.md 正文,与正本 renderContextDoc 已漂移(引导行/占位形两套文本)。 - 副本独有的两行引导并入正本 renderContextDoc,四节占位统一取 HTML 注释形 (sync-docs 落盘正文随之变化,属授权收口) - createContextMd 改走 contextDocFile() + runPlan()(含 io 与注入 fs), 与 init 的 CONTEXT.md 落点共用单一正本;只在 contextMissing 时落 created - 移除 sync-docs 调用点重复绿字(runPlan 已打印 created 文案) - 新增 __tests__/context-syncer.test.ts:对撞闸(sync-docs 落点 == init 站点骨架) + 正本文案钉住 + present→exists 不覆盖 - src/cli/commands/CONTEXT.md 补记 9 处已归位与模板正本落点 Co-authored-by: qoder-agent <agents@qoder.com>
writer 层的 `io: CommandIO = processIO` 缺省删除、改必传(编译期防再漏), 落点路由 setupGovernanceConstraints 加 io 形参透传给两个下游 writer; 缺省只留在对外命令入口 init(),真机输出字节不变。 新增 __tests__/init-governance-io.test.ts:临时目录真跑 init 断言治理段提示 (✅ 已追加治理契约 /⚠️ 标记残缺 / Output Style 写入)落在注入面 + 源形状闸。 init-ondisk 的整屏冻结基线因此多出 AGENTS.md 一行——那行一直在真机上, 此前 io 被丢才逃在捕获面外(#132 遗留漂移,scaffold 各站行逐字节不变)。
`--threshold <n>` 经 commander 恒给字符串,声明却写 number,形状相反靠 `parseInt(options.threshold as any)` 躲编译期;脏值 parseInt 出 NaN 后引擎侧 `??` 只兜 null/undefined,`len < NaN` 恒 false 就把短内容判定静默关掉而退出码 仍是 0(实测:同一知识库缺省 short-content 1 条、--threshold abc 报 0)。 收口取「编组归命令模块」那一形(knowledge 族是纯投影条目):声明改 string、 窄化在 knowledgeAudit 装配点做一次,只认 trim 后全数字,转不出来 logError + usage-error;未传落引擎缺省 50、显式 0 不被当未传,审计与索引重建均不再发生。 引擎侧不重复兜(上游已挡)。
identity chalk 之下的文本基线钉不住颜色:toneForMaturity('verified') 改回黄、
TONE_STYLES.accent 换成 chalk.yellow,全仓零红。新增 knowledge-tone-mapping.test.ts
把「输入值→tone」与「tone→chalk 样式键」两张表逐格冻住(渲染链真实走一遍,非只读表),
另加表键集合穷尽双钉。生产侧零改动,TONE_STYLES 保持不导出。
闸 4b 的 emptyRoot() 没有 knowledge-docs/ 子目录,syncRagRows 第二空态走不到,
「No knowledge docs found」改字全绿。新增闸 4c 按 case 平行表建第二形(自带 makeRoot,
人读逐行 + --json 双钉),不改既有 Record<SubName, string[]>。
判据两条写进 src/cli/commands/CONTEXT.md:fixture 走不到的分支须逐个建形;
渲染类映射必须有纯函数级断言。
…单点化(#134) 按 #134 票下 triage 三项裁决落地。构造形状:KnowledgeAudit 由「收 baseDir 并自建 FileKnowledgeStore」改为收 KnowledgeStore + 可选阈值形参,与 lint/doctor/query/ lifecycle/ingest 同形;D1–D7 判定随之拆进零 IO 的 audit-scoring.ts(规则表、 scanEntries/summarizeIssues/computeDimensions/calculateHealthScore),打分核心的两处 IO 按裁决归 store 供给(新增 getConsumptionStats(),不走 RunEnv),audit.test.ts 的 makeTmpDir() 47 → 5 且既有断言逐条保留。写放大:新增 applyAll(id → partial) 与既有 saveAll 并存(saveAll 有 4 处生产消费、签名不动),audit 修复循环、lifecycle runDecayCycle、reference-tracker updateReferencedBy 三个循环改走它,一次批量 = 一次 index.json 重写(fs 写计数闸 audit-write-count.test.ts 钉住,红基线实测 3)。 排除口径:新增 tree-walker.ts 作唯一正本,store/migration/index-generator 三处共消费, migrate 不再把生成物 _index.md 判成「no frontmatter」计入 errors(实跑复核过的假阳性, total 亦随之从 4 变 3);import 的 docs 扫描吃项目文档树,按记名豁免不改。 对外:KnowledgeAudit 构造签名属包根公开面,breaking 定级随本车 ship 时裁决(判据同 [1.7.0],唯一消费者 studio 需同步跟进),AuditOptions 去掉 baseDir 与从未被读取的 autoFix;migrate 的两个计数属判定变化。CHANGELOG 的 Unreleased 条目已记名。 验收:jest --runInBand --coverage 167 套件 / 2396 passed / 6 skipped、94.29% stmts (不降)、tsc --noEmit 与 eslint src 零错、harness check 通过、真 CLI 冒烟 (audit --fix + migrate + stats)。
按 #135 票下 triage 三项裁决落地(判据 ADR-0018/0022)。匹配单点化:新增私有谓词 matchCommand(command) → 命中规则集合(规则表遍历、类别忽略、模式测试只在此处)与 折裁点 judge(command)(级别语义只在此处解释一次),check() / isAllowed() / getRiskLevel() 退化为同一裁决的三个投影;等级改取命中集合最高档,不变式「存在阻断级 命中 ⟺ check 不通过 ⟺ isAllowed false ⟺ 等级 high」——旧形状下 getRiskLevel 取首条 命中,可在同集合另有更高级命中时报出与裁决相反的等级。两个配置位删(不接进判定): CommandGateConfig.strict 只在构造时读、判定路径从不使用,连 CLI --strict 旗帜一并 removal;customBlacklist 零生产注入者,运行时扩展点是 addRule()。CLI 的 --level 与 默认分支改共用它创建的那台实例,不再引用模块级单例出口;hook 侧按裁决保持裸构造, 「hook 只用出厂规则」写进 gates CONTEXT。 对外:CommandGateConfig 收缩两字段 + harness command 删 --strict 属 breaking(定级随 本车 ship 时裁决,判据同 ADR-0018/0022 的 minor 先例,studio 侧对删除面零引用); --level 在多重命中输入上报出的等级可能变高。规则表内容零改动(本票只动结构);三个 模块级便捷出口自此在本仓生产路径零消费者,属公共面收缩另票评估。措辞纪律 PIT-021: 只写泛化表述,不复述规则表内容、不写生效范围结论。 测试:新增 command-single-predicate.test.ts(源形状闸:模式测试 / 类别忽略 / 遍历规则 表的循环体各恰好一处、级别 switch 归零——红基线实测 3/3/3;三投影一致性闸含次序无关的 正对照与忽略类别对三投影同样生效)+ command-gate-dead-options.test.ts(定义表旗帜面逐字 冻结、@ts-expect-error 编译期钉三个已删字段、配置读取面形状钉、CLI 源面禁再引单例出 口);既有 customBlacklist 用例改走 addRule();CLI --level 替身补 getRiskLevel 并新增 「等级取自本命令创建的那台实例」一条。三道闸先红后绿。 验收:jest --runInBand --coverage 169 套件 / 2410 passed / 6 skipped、94.42% stmts (#134 的 94.29% 不降)、tsc --noEmit 与 eslint src 零错、harness check 通过、 sync-docs --check 回到既有四处目录漂移(本票触碰的 cli/commands 已随条目补齐)、 真 CLI 冒烟(--help 无 --strict / 默认裁决 / --level 两档 / --json / --strict 退 1)。
按 #136 票下 triage 裁决落地方案 A 的修正形:注入车辆是 CheckpointContext,不是 handler 形参(验证器固定的 (check, context) 分发塞不进新形参)。CheckpointContext 加可选 sleep?: (ms: number) => Promise<void>,与同类型既有的调用方注入位 customHandlers 同形; checkHttpStatus / checkHttpBody 由「收 context 而从不读」改为把 context.sleep 透传给 fetchWithRetry() 的新形参,形参缺省 = 模块私有的真定时器实现。退避公式 2000 × (n+1)、 retries = 3、两个调用点逐字不动。不做 retries 与退避基数的可配置化(票面 A 的后半句按 裁决移出:验收正是对着这两个固定缺省写的),不取 jest fake timers(要为待决 promise 重排测试并手动推进计时器,且与处理器用的原生 AbortSignal.timeout 相冲)。 基线实测 checkpoint.test.ts --runInBand 27.19s,其中 24.01s 是两个 mock-fetch-reject 用例真烧的 2+4+6s(12002ms + 12012ms)。落地后同一套件墙钟 1.87s,46 条用例体合计 82ms(那两条 3ms + 2ms)。 测试:新增 http-retry-sleep.test.ts 10 项——8 项以注入替身钉「尝试次数 = retries + 1 (fetch 恰 4 次)」与「退避序列 = 2000/4000/6000」(另含 5xx/429 后恢复只退避一次、5xx 用尽后返回最后响应、404 与首试成功不重试),1 项经 CheckpointValidator.validate() 钉 分发接线(context 真抵达 handler),1 项以同步触发的 setTimeout 替身钉「未注入时缺省仍 走真定时器且序列不变」且自身零真等待;既有两条重试用例改注入记录型 sleep,断言只增 不减(原 passed + message 保留,新增尝试次数与退避序列),随真等待消失移除 20_000 超时 兜底。反向验闸三处各改坏一次并还原现场:handler 不读 context.sleep → 注入侧 4 项 + 接线项红(各退化回 5s 超时);缺省 realSleep 改 no-op → 仅缺省路径项红;retries 3→2 → 尝试次数与序列类断言 7 项红(含既有套件那 2 项)。 对外:发布类型 CheckpointContext 追加一个可选字段,非 breaking——未注入的缺省路径逐字 不变,命令语法/旗帜集合/退出码/五入口公共面名字清单零改动,三道类型面闸零清单改动, 下游构造对象的代码不受影响。本仓生产与 CLI 行为不变。 验收:npx jest --runInBand --coverage 170 套件 / 2420 passed / 6 skipped、覆盖率 94.46% stmts(#134 记的 94.29% 不降)、npm test 并行 4 worker 同绿(票面提到的并行 SIGTERM 本会话未复现)、tsc --noEmit 与 eslint src 零错、node bin/harness.js check 通过(铁律 3/3、指导原则 3/3)、sync-docs --check 仍只剩既有四处目录漂移。文档: CHANGELOG Unreleased 记名,src/core/CONTEXT.md 的 validators 条目补记「等待类副作用 经 context 注入、不再内联」。范围外按票面裁决不动:posteval-plan.ts 的另一份同名 fetchWithRetry、约 30 个测试文件共用的固定 temp-test-* 工作目录模式。
…137) 按 #137 票面(架构评审 2026-09-14 候选 8,判据同 ADR-0022 的删除测试)落地,逐项已核 消费者、无开放设计决策。 删(零生产消费者):utils/exec.ts 的 isCommandAvailable() / delay()(runCommand() 按票面 保留);ProjectConfigLoader.isConstraintEnabled() / getConstraintSource()——二者是 mergeConstraints → getEffectiveConstraints(ADR-0001 唯一生效集来源)的平行复印,属 ADR-0022 型漏收;failure/types.ts 兼容再导出 shim(类型正本早在工单 14 归位 types/failure.ts,转发链三跳里两跳是星号),classifier/recorder 与三个测试文件改直连正本。 归位:normalizeTriggers() / matchesTrigger() 迁到新增 core/constraints/triggers.ts—— harness#105 定的 trigger 匹配语义单点住在 utils 层,等于给 utils 挂一枚只有约束域会调的 假接缝;checker 与 agent-prompt-renderer 的拉用点转为同目录 import,utils/exec.ts 不再含 约束域谓词。 子 barrel 星号:票面 8 处目录级 export * 全部收口(agents 2 处、completion-checkers、 failure/index、hooks、knowledge、tools 改显式清单;第 8 处即 failure/types.ts 那枚 shim, 按票面「与 shim 一起裁决避免一处删一处留」取删,故零豁免条目)。新增 __tests__/sub-barrels-explicit.test.ts 把这条例子落成闸:扫 src 下每个 barrel 的 export * / export type *,豁免须逐文件登记理由且不留死账;ADR-0003 的禁令从此不只钉五个 exports 入口。 分层闸参数化:layering.test.ts 由「只守 core ↛ cli/gates/monitoring 一条边」改成「每个 顶层单元声明允许的下行集合、逐目录冻结」——20 个单元各一条 DOWNSTREAM,登记闸双向对撞 (未登记的新目录红、已消失的单元留账也红);测试文件走单列的 TEST_ONLY_EDGES 放行,免得 把「failure 可值导入 cli」宣布进生产面;同层债(context → knowledge/monitoring、hooks → monitoring 组合根)逐条带注,集合只多不减。扫描器改成 (路径, 代码) 纯函数,反证因此进测试 (11 条越界边各红一次并指名单元/文件/目标/源串,11 条合法边与噪声不误报);文件级真反证 另做:src/types 注入一枚指 cli 的探针文件 → 「types 零越界值 import」单项转红并打出该边。 retire 内部重构:两段只差键路径的 YAML 读-改-写合成 setYamlEntry();同一次 retireConstraint 的 ProjectConfigLoader 由 2–3 枚收成一份观察面(ADR-0023 决策 2 的形状, 写盘后同步注入段仍另起一枚以读新状态);手拼的 already_retired 判定改为复用 project-config-loader 新增导出的 isConstraintRetired(customDef, disabled),与 mergeConstraints 的 custom 追加跳过共读同一规则。落盘逐字一致由两枚新增的**逐字节**冻结 用例钉住(重构前先跑绿取基线、重构后逐字仍绿),装载次数用例钉 custom 路径 load() 3 → 1。 公共面定级(ADR-0022 追记 5 双证据:五入口源文件 + published @1.7.0 的 .d.ts):exec 两 函数与两个 trigger 谓词、failure/types shim 均非 breaking——五入口的 src 与 dist .d.ts 全部零命中,1.7.0 的 exports 只有五个入口、深路径不可解析;删 ProjectConfigLoader 两方法 是类型面 breaking on ./core(ProjectConfigLoader 经 dist/core/index.d.ts:13 再导出,值面 冻结闸看不见类成员,故按人工双证据定级);barrel 显式化与 isConstraintRetired 零公共面 变化(三道公共面闸清单零改动且全绿)。含 breaking 一项,按 2026-09-09 人类裁决走 minor。 记名:票面称 runCommand 已有生产消费者,本仓复核那两处命中是文档注释而非 import, runCommand 当下生产消费者为 0——按票面「不得删」保留,删不删由下一轮同判据裁。子路径入口 删除按票面属 #131 阻塞项、票面「明确不再提」清单,一律未碰。 验收:npx jest --runInBand --coverage 172 套件 / 2471 passed / 6 skipped、94.47% stmts (#136 的 94.46% 不降,triggers.ts 四项 100%)、npm test 并行同绿、tsc --noEmit 与 eslint src 零错、npm run build 绿且产物内零 export *、node bin/harness.js check 通过 (铁律 3/3、指导原则 3/3)。文档:src/CONTEXT.md 改写禁 export * 的管辖面、 src/core/CONTEXT.md 补 triggers 归位与退役判定单点并改参数化口径、 src/failure/CONTEXT.md 的 types.ts 条目随 shim 消失改字。
…138) SecurityGate 构造器曾把缺省 scanCommand 兜成空串(`?? ''`),而 scan() 的 三级优先级用 `??`(空串非 nullish),链恒停在第二级、detectScanCommand() 全仓不可达;runScan('') 拿去 exec 直接抛「The argument 'file' cannot be empty」,harness security 不带 --scan-command 必失败而非给出判定。 - 构造器 `config.scanCommand || undefined` 把空串归一为 undefined,判定链 保持 `??`;this.config/getConfig() 类型由 Required<SecurityGateConfig> 改回 SecurityGateConfig(仅 scanCommand 可选,其余三字段本已必填) - detectScanCommand() 恢复可达,内容不变(仍常量返回 npm audit --json) - --scan-command "" 视为未提供 → 落到探测;显式命令与 context 两条路径逐字不变 测试:新增 gates 级与 cli 级两组缺省路径回归(先红后绿、与修复同 commit)。 替身不落整个 child_process(那正是缺省路径一直亮绿灯的机制),改在 exec 唯一 出口 src/utils/exec 的 execAsync 上打缝,断言实际下发执行的命令 = detectScanCommand() 返回值。src/gates/CONTEXT.md 补 SecurityGate 三级优先级条目。 定级 fix/patch 非 breaking:两仓 CI 无缺省 harness security 调用点,security 门禁只经其 CLI 命令进入,harness check 不受影响(见票 #138)。 验收:npx jest 174 套件全绿(全局覆盖率 94.49% stmts 不降)、tsc --noEmit 与 eslint src 零错、node bin/harness.js check 通过、真 CLI 复现三形正确。 Co-Authored-By: Claude <noreply@anthropic.com>
DEFAULT_TRACE_FILE 是相对片段而 TraceCollector 全程按 cwd 解析(建目录/追加/读/stat), 生产构造点又是 cwd 锚定的模块级单例 getTraceCollector()(configureTraceCollector 生产 调用点为零)。于是从别处跑 `check --project-path B`:约束评估真跑了 B,trace 落进调用方 cwd,按 projectPath 直读 jsonl 正本的消费方(status、constraints report)读不到刚写的 记录——#95「收到的根要传到自己每个 IO/执行点」的假绿族,且表现为静默写错位置而非报错。 按票下 Agent Brief 裁决取 A(构造收根),不做 B(CLI 入口记得配置单例): - TraceCollectorConfig 新增可选 projectPath:给了则缺省/相对的 traceFile 一律 path.resolve(projectPath, …),没给保持 cwd 解析(studio 两处直调依赖的兼容面) - DEFAULT_TRACE_FILE 常量名不变,JSDoc 明确其为「项目相对片段、不是可直接打开的路径」 - 四个组合根(CLI check、CLI report、bootstrapHarness、bootstrapHarnessSync)改为每调用 一次锚根构造 TraceCollector,形状照先例 failure.ts 的 getRecorder(projectPath); 本仓生产代码对单例的消费归零,公共面按裁决保留不摘除 - 闸 3 扩两臂:键名后缀 Path → Path|File|Log,新增 xxxFile: SOME_CONSTANT 常量引用臂 (traceFile 病灶正是「键名不带 Path + 值引用常量」两头都不认的形状);只扫使用点, 不冻结常量定义本体。豁免 +2 带理由(traces.ts 锚定后、trace-analyzer summaryFile 同型 病灶登记不修)。扩形后下游层新增命中恰好这 2 处,零假阳性 - 新增闸 4:src/cli、src/hooks 生产代码对 getTraceCollector() 零消费 不带 --project-path 的项目内使用逐字不变(入口本就 || process.cwd(),锚定后解析出同一 绝对路径)。存量杂散 trace 已于 2026-09-15 判为垃圾并本机清理,无迁移。 测试:新增 src/monitoring/__tests__/trace-file-anchoring.test.ts(真 fs,两态落点 + 读侧 同锚)+ project-path-anchoring.test.ts 站点 3(红基线实测:修复前 B 侧新增 0 条、A 侧长出 traces.log;含 status 读-写闭环与「不带 -p 落点仍是 cwd」回归)+ 闸 3 反证用例(未锚三形 各顶闸、锚定/绝对/小写值/maxFileSize 不误报)。随迁 check.test.ts(改断言项目正本)、 check-read-count.test.ts(删除已失效的重定向)、check-drift / check-skip-output(traces 替身换构造形)。 验收:npx jest --runInBand --coverage 175 套件 / 2491 passed / 6 skipped、覆盖率 94.56% stmts(#137 的 94.47% 不降)、npm test 并行同绿、tsc --noEmit 与 eslint src 零错、 node bin/harness.js check 通过、真 CLI 复现(cwd=/tmp 跑 check --project-path 本仓 → cwd 侧零文件、目标项目 traces.log 追加且 projectPath 字段正确)。文档:monitoring/CONTEXT.md 新增落点锚根约定条、cli/commands/CONTEXT.md 三道闸改四道闸、hooks 与 gates 的 #95 条目同步。
filterEnabledEntries 的两个未知 id 模式按语义分:'collect' 归约束侧 mergeConstraints、'throw' 归门禁侧 getEffectiveGates。后者连同 config.yml 的 gates: 声明式面已随架构评审候选 1 收缩删除(209511e,理由记在 ADR-0002 文末 「后续变更」),于是 'throw' 分支与 unknownIdError 选项只剩自己的单元测试在供养 ——一处为两个消费方共用而抽出的共享件退化成单消费方 + 一个无人使用的分支, 是 ADR-0022 定义的那类漏收面,只是这次由候选 1 的删除动作新造出来。 - FilterEnabledEntriesOptions 整个接口删(唯一必填成员是 onUnknownId, 单成员联合是空转开关)、unknownIdError 删、函数体内 throw 分支删 - 签名退化为 (knownIds, entries) 单一语义;unknownIds 保留 (mergeConstraints 用它做诊断,其诊断行为逐字不变) - 对外行为变化 0 处、零公共面变化:effective-set 是包内模块,不在五个 exports 入口的名字清单里,三道公共面闸清单零改动且全绿 测试:throw 那组 3 条用例删,collect 侧 4 条断言逐字保留(调用点随签名去掉第三 参数);按 ADR-0022 决策 2 的先例补三枚新形状钉——length === 2 运行期钉 + 两枚 @ts-expect-error 编译期钉。先红后绿,实现前实测 TS2554 ×4 + TS2578 ×2 令套件红。 反证两跑:① 加一条真·无主分支(签名不变)→ 7 项全绿,说明清掉的是无主分支而非 把断言挪地方;② 把选项按可选形参复原 → 两枚编译期钉各顶一次 TS2578、套件红, 说明形状冻结挡得住「悄悄改回来」(ADR-0003 本意)。 同型面登记不改(票面第 4 条):utils/jsonl.ts 的 JsonlBadLinePolicy = 'skip' | 'throw' 与本病灶同形(11 个生产调用点一律 'skip'),但不属漏收——工单 #82 裁决 1 明文保留该选项,且 jsonl-skip-disposition.test.ts 有活闸钉「throw 调用点目前为零、 出现即逐个裁决」;本病灶两者皆无,故一删一留。另记 knowledge/lifecycle.ts 的 recordReference(…, source?)——本仓唯一生产调用点只传 1 个实参,但经包根导出、 studio 侧消费未核实,删不删须先双仓查。 验收:npm test 175 套件 / 2491 passed / 6 skipped 全绿;npm test -- --coverage 94.56% stmts 与 #139 基线逐位相同不降(effective-set.ts 四项 100%);tsc --noEmit 与 eslint src 零错;npm run build 绿;node bin/harness.js check 通过(铁律 3/3、 指导原则 3/3)。文档:effective-set.ts 头部与 core/CONTEXT.md 两处历史叙述压成 一行去向指引。
#142) 旧判定是「目录内最新 .ts 的 mtime 比 CONTEXT.md 新即过时」,三种失真都实测过: 改一行注释算漂移;什么都不改、只碰时间戳也算(git checkout / stash / cherry-pick 都刷新源文件 mtime);接口真变了反而漏报。同一棵树本会话两次跑出不同集合(票面 4 处 vs 实测 6 处且不重合),CI 全新 clone 里 mtime 由 clone 顺序决定 → 恒假阳性。 另一半病灶是无人报警:governance 工作流只对 CAPABILITIES.md 的 diff 报错,CONTEXT 漂多远都不红,票面那 4 处就是这么攒出来的。 - 新增纯判定 core/constraints/context-reconcile.ts(对位 ADR-0009 的形状): reconcileContext({contextMdContent, exportSurface, barrelExports?}) 双向出 幽灵 / barrel 未登记清单;parseDeclaredExportSymbols 与 parseExportStatements 两个口径正本同为纯函数,零 fs IO - fs 采集在 cli 侧 sync-docs/context-syncer.ts:collectContextExportSurface, 含 export * 与具名再导出的仓内目标解析、类型-only 剔除、按正本核对本名 (反证撞出:虚再导出会洗白幽灵,改名恰是最该抓的情形;环上核不到则从宽) - 口径不对称:幽灵方向值与类型一视同仁;覆盖方向只判 barrel 的值符号(类型面 已有 public-*-surface 两道闸冻结清单),无 index.ts 的目录跳过该方向 - 级别内建、不加开关:内容漂移 --check 判 fail 且 reason 点名文件与符号; mtime 降为本地提示、CI 置位即抑制;写入模式不改写散文、退出码面不变; --json 增 contentDrift 与 summary.contextContentDrift - governance 工作流在 CAPABILITIES diff 门后加一步 sync-docs --check, 按 #120 方案 A 先例清完存量直接变红 存量清理按闸的全部报出执行(7 目录 32 项):core 12 / failure 7 / gates 5 / knowledge 3 / monitoring 3 / context 1 是公开值符号真漏登记,逐条核过消费者与 签名后补进文档;cli/commands 那 1 项幽灵是 bullet 行首写的是 CLI 子命令名, 按「行首 = 声明」约定加散文前缀,句子一字未删。 逐条核对另出五处不实陈述已改文档(hooks 类型正本与两条失效消费边、monitoring 读入口漏 getStats 与 ContextTracker 消费边、knowledge src/types 依赖与 audit-scoring 类型面、release 闸门归属),五处「文档正确、代码待改」不改文档、 另开缺陷票 #159 / #160 / #161 并就地标注现状偏差。 对外行为变化 2 处同向(minor,CHANGELOG 标 !):内容漂移起判非零退出(此前 永不报警),mtime 差异不再判失败。消费方影响:升级后 --check 对所有消费方变严 (studio 49 个 CONTEXT.md 将获得新判定);init 生成的工作流里该步骤带 continue-on-error / allow_failure,故不会立刻红。 测试:新增 3 文件 36 项(判定 19 + 采集 5 + 接线 12),既有 sync-docs-agents 一处断言随措辞改字。反证两跑:改声明侧类名 → rc=1 指名 monitoring/CONTEXT.md 与 ContextTracker;往 release barrel 加公开值再导出 → rc=1 指名 HARNESS_PACKAGE_NAME;还原后 rc=0。 验收:npm test 178 套件 / 2527 passed / 6 skipped 全绿,npx jest --coverage 全局 94.58% stmts(#141 基线 94.56% 不降),tsc --noEmit 与 eslint src 零错, npm run build 绿,harness check 通过,sync-docs --check 在 CI 与非 CI 两态均干净。 决策记 ADR-0025(含绕闸限度登记)。
pre-commit 可被 git commit --no-verify 绕过,绕过后的内容此前无任何本地拦截直接 离机(全仓无 pre-push)。定位不变:本地 hook = 自检与提醒,门禁真正的落点仍是 服务端 CI;这道只是把「commit 时绕过」的东西在离机前拦一次。 - scaffold.ts 新增 prePushHookFile 工厂(.git/hooks/pre-push、mode 0o755、merge 三态),正文住 scaffold-templates.ts(PRE_PUSH_SNIPPET / renderPrePushHook, 与 pre-commit 同形状:落盘正文 = shebang 头 + 打印片段,#103 判据) - init 的 setupGitHooks 退化为「一条 plan 装两道 hook」;跳过策略仍在命令层, --no-git-hooks 一并覆盖本站点,不新增任何跳过旗帜 - 三道决定按 grilling 定论钉进 hook 正文:① 不做增量(不解析 stdin 的 <local_ref> <local_oid> <remote_ref> <remote_oid> 清单,两条命令一律 < /dev/null,不让子进程吃掉 ref 清单)② 不内建逃生机制(无 env 开关、无超时、 无降级,唯一口子是 git 原生 push --no-verify)③ 全量 check(不带 --staged) + validate,任一失败 exit 1 - 分工叙事落到用户看得见的三处:hook 注释、--print-snippets 新增 pre-push 段、 init 收尾「下一步」第 3 行(commit 查暂存的、push 查整仓的,重复是设计使然) 测试 +1 套件 / +14 项(179 套件 2541 passed):scaffold 站点表 8→9(路径、文案、 0o755、同源闸与反证探针扩到两道 hook)+ 两枚静态闸(恰好 check+validate 两道、 无 ref 解析无逃生);init-ondisk 整屏基线与 pre-push 落盘字节级冻结、第三态与 「打印即磁盘正文」扩到 9 站点;新增 pre-push-hook.test.ts 走真 git 真 push (本地 bare 远端 + PATH 记账替身 npx,装的是 renderPrePushHook 正本):全过→放行、 check 失败→挡住且输出点名 Iron law、validate 失败→同样挡住、--no-verify→钩子整个 人没被调用、删远端分支(stdin 空清单)→仍整仓跑一遍。同源反证实测:只追加落盘 正文一行而不动片段 → 5 项转红。真机跑过 init → push 被挡 → --no-verify 全链。 范围外未碰:pre-commit 既有内容一字未改、GitLab CI 接线与服务端门禁另开票。 文档:src/cli/commands/CONTEXT.md 站点口径 8→9,「pre-push 为已知缺口」改为 已落地并新增「本地防线两道」条。
固定名夹具让同一 checkout 的并发 jest 进程共享路径(一方 afterAll rmSync 删掉 另一方在用的夹具,表现为像产品 bug 的 flaky),被 kill 的运行还会把目录留在工作树 里让下一轮从脏夹具起步;temp-test-* 不在 .gitignore,残留可被误提交。 夹具根改经 fs.mkdtempSync 取唯一目录,缺省父目录 = tmpdir(对齐 test-setup/ project-fixture 既有口径,顺带把残留移出工作树);被测语义本身是 cwd 相对解析的 (governance-presence / effective-constraints 的 parentDir 显式 opt-out)保持不动。 describe 作用域派生路径常量随之改钩内赋值,否则采集期求值拿到 undefined。 新增卫生闸 src/__tests__/test-fixture-hygiene.test.ts 冻结该不变量(先红后绿)。 断言零改动、无删测试:2549 total = 基线 2547 + 闸的 2 条;覆盖率四档逐位不变。
…#146) 工单 19 只把文件遍历收敛成一次 walkFiles,读取仍留在逐关键词的循环里:每个关键词 把全部源文件 readFileSync 一遍,checkDependency 又逐条前置重新 readFileSync + JSON.parse 一次 package.json。三条验证路径(文件存在性 / 依赖 / 代码模式)自此一律 从命令级共享索引 createBaselineIndex(projectPath) 取数——全仓 .ts/.js 内容扫描一遍 建成 路径→内容 表(读失败的文件不入表 = 原逐文件 try/catch 跳过同义)、package.json 读解析一次(失败态同样入库,不逐条重试)、同一落点的存在性探测只 stat 一次;逐条前置 与每个关键词只在索引上重放,匹配语义逐字等价。索引懒建(没有前置要某类数据时该类读取 一次都不发生)且运行结束即弃;按 #140 triage 裁决 5 不扩 RunEnv 公共面,index 形参 全为模块私有面。 对外行为变化 0 处:改动前后两版真构建对同一夹具跑普通模式与 --json,stdout 与退出码 逐字节一致。实测数字对照(同一插桩、同一夹具:7 条前置含 2 条依赖 + 3 条代码模式各 2 关键词,6 份源文件)——内容读总数 39 → 8、package.json 解析 2 → 1、每个源文件 6 → 1 遍、全仓目录列举 9 → 3 次、存在性探测 3 → 2 次;关键词扩到 15 个时改前每文件 15 遍、改后仍是 1 遍。 计数闸先红后绿:新增 spec-baseline-read-count.test.ts(形状照 check-read-count.test.ts ——readFileSync / 只读 openSync / existsSync / readdirSync 记件 + 整张读取表与目录扫描 表逐条冻结 + 「同一口径至多取一次」+ N×K 缩放不变式 + 懒建不扫 + 失败态入库,每条带 正对照防闸空跑);三枚 memo 逐枚拆开的反证都实测过(拆源码索引 3 项红、拆 package.json 4 项红、拆存在性 1 项红),还原后全绿。验收:全量 181 套件 2548 passed(改前同树同命令 2543)、npx jest --coverage 四档全升(全局 lines 95.37% → 96.18%、branches 83.36% → 84.23%;本文件 lines 91/159 → 168/176)、tsc --noEmit 与 eslint src 零错、 harness check 与 sync-docs --check 通过。文档:src/cli/commands/CONTEXT.md「约定」新增 一条、CHANGELOG 记一条。范围外未碰:判定与提取正则、证据文案、#147/#148 的读取收口。
两态对撞首测带缓存跑,jest 的 transform 缓存跨源文件切换端出旧插桩表,同一棵树上 读到过 159/165 与 176/182 两组互相矛盾的本文件分母;改完全部 --no-cache 重量。 更正三处:全局 branches 改前 83.36% → 83.39%;本文件余下未覆盖数由「17 条语句」 改为实测的 9 条(并逐条列出:三处「无法从描述中提取 X」+ 两处「部分存在/依赖缺失」 证据行 + implMatch 追加关键词一支 + formatTable 空表早退与两条未满足排版行,全是 本票未动的既有分支);记下「两态数字一律 --no-cache 量得」这条方法学约束,防后续 读盘面收口票(#147/#148)照抄带缓存的对照数。
入口此前无条件对所有源码目录全树扫描:逐文件 await readFile 只为首行注释作 描述,而 capability-listing 格式(本仓即是)的判定面是计数,条目级比对与表格 写回都住 file-table 分支——那份描述根本不进任何输出,currentModules 在 listing 分支是自扫自用的死数据;串行 await 又在大仓上线性放大。 形状 = 先判定「本次要什么」再决定「读什么」:index.ts 把 CAPABILITIES.md 的格式 判定移到扫描之前(只搬读取路径,判定/漂移检测/写回内容一字未动),listing 分支 自此整棵树一份 .ts 内容都不读;要描述的 file-table 分支照旧取描述,但 scanSourceModules 拆两步——先按目录遍历序定清单,再经新增的 extractFileDescriptions() 并发取数并按原索引回填,上限固定 16(按票下裁决不做成 配置面)。输出顺序逐字保持现状是票面裁决(对外经 --check 行级比对可见;改字典序 要付一次全仓漂移,已裁不值),故「按完成顺序 append」按行为漂移对待。 对外行为变化 0 处。实测(同一插桩、同一棵树):本仓 listing 模式 .ts 内容读 290 → 144、命中文件 161 → 144,消掉的 146 份正是扫描分支全部读取(= 源码树内排 barrel/.d.ts/__tests__ 的 .ts 数,与生成的模块表 146 行同源),余下 144 份是 CONTEXT 导出面采集(#142 范围外);同树改判 file-table 后读取数同为 290(本票不 削减该分支读取量,只并行)、在途并发 1 → 16、--check 三轮 430/453/420ms → 408/398/407ms(上限是 libuv 线程池缺省 4,承诺是「不再串行放大」而非倍增)。 等值两版真构建对撞:同一棵树 --check 的 stdout 与退出码逐字节一致;写回面删掉 CAPABILITIES.md 让两版各自生成 146 行模块表,产物逐字节一致。 计数闸先红后绿:新增 3 套件 12 项——sync-docs-read-count.test.ts(形状照 check-read-count.test.ts:listing 零内容读,改前实测红 4 份 + file-table 反证每份 恰读一次且描述进对外 JSON + 整表冻结,表上 CAPABILITIES.md=5 是本票范围外的既有 重复读,如实登记)、sync-docs/module-descriptions.test.ts(替身把起始序与完成序 刻意错开:串位或重排即红,另钉在途 ≤16 与读失败回退文件名)、 sync-docs-module-order.test.ts(生成的表格行序 = 目录遍历序、两次运行逐字复现; 不写字面顺序,readdir 序由文件系统决定)。四态反证各自实测:拆扫描闸 → 计数闸 2 红、file-table 两项照绿;按完成序 append → 3 红;去上限 → 上限那条红;退回串行 → 2 红;还原后 12 项复绿。验收:npx jest 184 套件 2560 passed(改前同树同命令 181 套件 2548)、--no-cache 覆盖率四档全升(lines 96.18% → 96.24%、stmts 95.35% → 95.4%、branches 84.23% → 84.32%、functions 82.06% → 82.12%;project-reader.ts stmts/lines 79/81、functions 14/14)、tsc --noEmit 与 eslint src 零错、npm run build 绿、harness check 通过(铁律 3/3、指导原则 3/3)、sync-docs --check 零漂移。 文档:src/cli/commands/CONTEXT.md「约定」新增「先判定要什么再决定读什么,且清单 顺序是对外面」条、CHANGELOG 记一条。范围外按票面未碰:顺序改字典序、 agents-syncer.ts、spec-baseline-check(#146 已落)与 getSmartHint(#148)的读取 收口、并发上限配置化。
triage 四条裁决成文:不扩 RunEnv(只读契约 ADR-0023)、不扩 CommandIO(输出面)、 两个写者同票收编、落 src/cli/ 层。核代码时补两条票面未记的事实并写入本篇: status 侧不读就整文件重写,会把 check 写的 shownHints 抹掉(提示去重实际失效); lastDiagnoseRun 全仓零生产者零消费者,按 ADR-0022 判删。 同时更正 src/CONTEXT.md 的 StateIO 条目:票面原写 ADR-0024,该号已被同批 #135 占用、0025 被 #142 占用,本篇取 0026。本 commit 只冻结决策,无代码改动、无对外 行为变化,CHANGELOG 条目留给实现票 #148 随码补。
统一复审 9 次变异验闸中两次全绿 + 两条覆盖尾巴,零生产代码改动: - CONTEXT.md 骨架:context-syncer 冻正本字面量、init-ondisk 落盘从 toContain 升为 CONTEXT_MD_BYTES 字节基线(M3「正本加一行全绿」补钉) - knowledge stats:display model 结构断言成熟度格必携 tone 且 = toneForMaturity(maturity)(M7「摘掉 tone 全绿」补钉) - init 标记残缺提示四处分支各补一条捕获面断言(#149 只钉了一条) - init --ci github 迁移方向:gitlab 已持久化后再显式 github → config.yml ci 键消失、回 GitHub 形状(#143 全套件零用例的方向) 变异逐个实测变红后 git checkout 还原,sha1 对拍一致;全量 184 套件 2566 绿,tsc/eslint 零输出,init.ts 行覆盖 97.22% → 97.6%。
#161) - migration: frontmatter 'absent' 不再写 result.errors,按非条目语义计 skipped(#89 裁决 2 正本不动) - injector: origin==='external' 条目注入带 EXTERNAL_SOURCE_MARKER 前缀(正本收在 knowledge/query.ts,formatForPrompt 与注入路径共用),source metadata 带 origin - lifecycle: rule 降级条件改 成功率 <= 0.5(失败率 >=50%),与方法注释/CONTEXT.md 一致 - 三处各补边界反证用例;knowledge/context 两份 CONTEXT.md 的现状偏差标注删除、与实现逐字对齐
…nceLevel、打 renderGitLabCiJobs 同一正本 (#157)
HookDefinition 删除 enabled/errorStrategy 字段,注册环节由 HookConfig 填充有效值(EffectiveHook);register/registerAll 签名变为配置配对, bootstrapHarness* 加 hookConfigs 参数;HookErrorStrategy 收窄为 'block' | 'warn'('ignore' 退出有效面)。声明一处、读取一处, 两侧值矛盾在构造上不可能出现。breaking,发版级别随本车 ship 裁决。
…、definitions 规则回写、-g 原型链穿透修复、collectDiff JSDoc 修正 (#164) - CHANGELOG: 补 #161 (c9e42cf) Unreleased 条目——migrateKnowledgeEntries 的 absent 改计 skipped、KnowledgeInjector 外部来源标记接线、rule 降级边界取 <=;按 ADR-0022 判非 breaking/patch(判据同 #138 恢复声明语义先例),BREAKING 汇总节不动 - CLAUDE.md: definitions 纯数据规则回写已落地例外——utils/numeric-flag 纯函数解析器(harness#154 指名、零 IO);机器闸口径为「加载 definitions 不引入任何命令实现」,以闸为准 - init: -g 值域校验 in 改 Object.hasOwn——in 走原型链,-g constructor/toString 曾穿透校验把 Object 构造函数写进 configData 并在 yaml.dump 抛 YAMLException;init-ondisk 补两例反证(先红后绿) - check-cache: JSDoc 示例引用不存在的 collectDiff 改为同目录真实 API createGitEvidence().stagedDiff() - src/cli/commands/CONTEXT.md 同步 hasOwn 口径
本车 50 个 commit 首次进 CI 时 coverage / governance 两个 job 各红一处,根因都在 测试的环境依赖,不在门禁: - sync-docs-agents.test.ts「CONTEXT.md 缺失/过时」:#142 把 mtime 过时判定改为 CI 下 不启用(全新 checkout 的 mtime 由 clone 顺序决定,比出来是假灯),但该用例断言的正是 本地 mtime 行为却没声明环境——跟着跑测试的机器走,本地绿、CI 红。按同仓 sync-docs-context-drift.test.ts 的既有写法显式清空 process.env.CI 并在 afterEach 复原。 - report.test.ts「应该使用自定义项目路径」:传的是假绝对路径,#139 让 report 按 projectPath 锚根创建 trace 目录后,这个路径变成真实写请求;以 root 身份跑恰好成功 (并在文件系统根下留下目录),CI runner 非 root 直接 EACCES。换成现场创建的临时目录, 并把 #139 的锚根行为写成断言(原用例只验「有输出」)。 验证:CI=true 全量 npm test 187 suite / 2661 passed 全绿(先复现红、修后复跑), 清空 CI 的同批文件亦全绿;npm run build 与 tsc --noEmit 通过。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🤖 harness-ship