@dommaker/harness 1.10.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/cli/commands/definitions.d.ts.map +1 -1
  3. package/dist/cli/commands/definitions.js +5 -10
  4. package/dist/cli/commands/definitions.js.map +1 -1
  5. package/dist/cli/commands/scaffold-templates.d.ts +1 -1
  6. package/dist/cli/commands/scaffold-templates.d.ts.map +1 -1
  7. package/dist/cli/commands/scaffold-templates.js +0 -16
  8. package/dist/cli/commands/scaffold-templates.js.map +1 -1
  9. package/dist/core/constraints/usage-report.d.ts +1 -1
  10. package/dist/core/constraints/usage-report.js +1 -1
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +14 -2
  14. package/dist/index.js.map +1 -1
  15. package/dist/knowledge/lint.d.ts +17 -0
  16. package/dist/knowledge/lint.d.ts.map +1 -1
  17. package/dist/knowledge/lint.js +52 -0
  18. package/dist/knowledge/lint.js.map +1 -1
  19. package/dist/knowledge/store.d.ts +7 -0
  20. package/dist/knowledge/store.d.ts.map +1 -1
  21. package/dist/knowledge/store.js +48 -13
  22. package/dist/knowledge/store.js.map +1 -1
  23. package/dist/knowledge/types.d.ts +5 -1
  24. package/dist/knowledge/types.d.ts.map +1 -1
  25. package/dist/knowledge/types.js +5 -1
  26. package/dist/knowledge/types.js.map +1 -1
  27. package/package.json +1 -1
  28. package/src/__tests__/public-exports.test.ts +7 -0
  29. package/src/__tests__/public-type-surface.test.ts +5 -0
  30. package/src/__tests__/usage-report-public-export.test.ts +21 -0
  31. package/src/cli/commands/CONTEXT.md +4 -4
  32. package/src/cli/commands/__tests__/constraints-report-json-flag.test.ts +101 -0
  33. package/src/cli/commands/__tests__/init-ondisk.test.ts +1 -1
  34. package/src/cli/commands/__tests__/init.test.ts +1 -5
  35. package/src/cli/commands/__tests__/numeric-flag-assembly.test.ts +1 -1
  36. package/src/cli/commands/__tests__/passes-gate-dead-options.test.ts +1 -1
  37. package/src/cli/commands/__tests__/registry.test.ts +1 -1
  38. package/src/cli/commands/definitions.ts +5 -10
  39. package/src/cli/commands/scaffold-templates.ts +0 -16
  40. package/src/core/CONTEXT.md +1 -1
  41. package/src/core/constraints/usage-report.ts +1 -1
  42. package/src/index.ts +20 -0
  43. package/src/knowledge/CONTEXT.md +4 -3
  44. package/src/knowledge/__tests__/lint.test.ts +84 -0
  45. package/src/knowledge/__tests__/store.test.ts +70 -0
  46. package/src/knowledge/lint.ts +60 -1
  47. package/src/knowledge/store.ts +48 -11
  48. package/src/knowledge/types.ts +7 -1
  49. package/src/monitoring/CONTEXT.md +1 -1
  50. package/dist/cli/commands/posteval-plan.d.ts +0 -15
  51. package/dist/cli/commands/posteval-plan.d.ts.map +0 -1
  52. package/dist/cli/commands/posteval-plan.js +0 -86
  53. package/dist/cli/commands/posteval-plan.js.map +0 -1
  54. package/src/cli/commands/__tests__/posteval-plan.test.ts +0 -204
  55. package/src/cli/commands/posteval-plan.ts +0 -98
@@ -4,7 +4,7 @@
4
4
  约束引擎核心:severity 显式约束系统(ADR-0029,三层命名与 kind 二元已由 ADR-0001 迁至本模型)、生效集合并(effective-constraints)、检查点验证器(CSO/passes-gate)、Spec 验证器、项目配置加载。
5
5
 
6
6
  ## 核心导出
7
- - `constraints/` — 约束定义(CONSTRAINTS 单桶,severity 在条目上,ADR-0029) + 检查引擎(ConstraintChecker,拦截统一由 checkBeforeExecution 承担,ADR-0004) + 缓存(CheckCache:TTL 缓存 + 计数采样,H6/G5 起公开导出) + 标记区间写入器(injection-writer:「声明→对照→幂等替换」中 marker-range 替换的唯一 writer——replaceStandaloneRange/replaceEnclosedRange/cutMarkerBlock + 半标记守护;治理正本探测 GOVERNANCE_HEADING/hasGovernanceContract/countGovernanceHeadings/hasPreserveBlock 同住此模块,ADR-0011/#83。ADR-0029:注入渲染(injection-renderer)、漂移校验(injection-drift)、Agent prompt 渲染(agent-prompt-renderer) 与注入段落点路由已随文本注入层关停整体删除)/trigger 谓词(triggers.ts——harness#105 定语义,约束域谓词不住 utils 层)/使用统计(usage-report:constraints report·retire 的只读数据层,读 trace 两个入口——`readProjectTracesReport()` 带 `skippedLines`、`readProjectTraces()` 兼容包装丢计数;报告模型 `ConstraintsUsageReport` 的降级位 = `traceFileExists` + `skippedLines`,harness#100)/清单对照(capabilities-reconcile:CAPABILITIES.md 与代码实况的覆盖/幽灵判定唯一实现,capability_sync·docs_freshness·sync-docs 共消费,ADR-0009;一次 check run 内该判定只算一次并由 `RunEnv.capabilities()` 供给两 checker,ADR-0023 决策 4——增量那一维由 capability_sync 用导出的唯一覆盖规则 `isCoveredByEntries` 自算,代码实况清单的唯一取法是 `collectPopulationFiles`);CheckEnv 生产侧唯一构造点 `buildCheckEnv(context, providers|'none', runEnv?)`(checkers/types.ts:CheckEnv `extends RunEnv`,runEnv 注入即与 context-builder 共用同一份上行数据读取、不传则自造一枚;providers = 证据接线,git 两项绑 GitEvidence 实例,'none' = 显式不接 → evidence flag checker 按契约 skip);判定接缝 `CheckOutcome = boolean | 'skip' | CheckDetail{pass, evidence}`(harness#119/ADR-0016)——编排层与 `gates/checker-gate` 一律经唯一归一点 `normalizeCheckOutcome()` 取三态 + 证据,不各自 `=== false`(对新形状会误判);证据行由 checker 自行措辞(自描述、不带缩进),组装与截断策略单点在 `formatEvidence()`/`MAX_EVIDENCE_ITEMS`(同住 checkers/types.ts);证据四出口 = `ConstraintResult.evidence` → CLI 结论块/提示块、`ExecutionTrace.evidence`、error 级 `ConstraintViolationError.message`(error 级违规直接 throw,异常文案是其证据唯一外溢面);检查引擎双出口——`checkConstraints`(block:首个 error 级违规即抛,throw 契约不动)与 `collectConstraints`(collect:共享检查体、不抛、全量收集,report 的不抛出口,架构评审候选1)
7
+ - `constraints/` — 约束定义(CONSTRAINTS 单桶,severity 在条目上,ADR-0029) + 检查引擎(ConstraintChecker,拦截统一由 checkBeforeExecution 承担,ADR-0004) + 缓存(CheckCache:TTL 缓存 + 计数采样,H6/G5 起公开导出) + 标记区间写入器(injection-writer:「声明→对照→幂等替换」中 marker-range 替换的唯一 writer——replaceStandaloneRange/replaceEnclosedRange/cutMarkerBlock + 半标记守护;治理正本探测 GOVERNANCE_HEADING/hasGovernanceContract/countGovernanceHeadings/hasPreserveBlock 同住此模块,ADR-0011/#83。ADR-0029:注入渲染(injection-renderer)、漂移校验(injection-drift)、Agent prompt 渲染(agent-prompt-renderer) 与注入段落点路由已随文本注入层关停整体删除)/trigger 谓词(triggers.ts——harness#105 定语义,约束域谓词不住 utils 层)/使用统计(usage-report:constraints report·retire 的只读数据层,#602 起 buildConstraintsUsageReport/diagnoseRetireCandidates 等经包根公共导出——studio E1 (a) 链路消费,读 trace 两个入口——`readProjectTracesReport()` 带 `skippedLines`、`readProjectTraces()` 兼容包装丢计数;报告模型 `ConstraintsUsageReport` 的降级位 = `traceFileExists` + `skippedLines`,harness#100)/清单对照(capabilities-reconcile:CAPABILITIES.md 与代码实况的覆盖/幽灵判定唯一实现,capability_sync·docs_freshness·sync-docs 共消费,ADR-0009;一次 check run 内该判定只算一次并由 `RunEnv.capabilities()` 供给两 checker,ADR-0023 决策 4——增量那一维由 capability_sync 用导出的唯一覆盖规则 `isCoveredByEntries` 自算,代码实况清单的唯一取法是 `collectPopulationFiles`);CheckEnv 生产侧唯一构造点 `buildCheckEnv(context, providers|'none', runEnv?)`(checkers/types.ts:CheckEnv `extends RunEnv`,runEnv 注入即与 context-builder 共用同一份上行数据读取、不传则自造一枚;providers = 证据接线,git 两项绑 GitEvidence 实例,'none' = 显式不接 → evidence flag checker 按契约 skip);判定接缝 `CheckOutcome = boolean | 'skip' | CheckDetail{pass, evidence}`(harness#119/ADR-0016)——编排层与 `gates/checker-gate` 一律经唯一归一点 `normalizeCheckOutcome()` 取三态 + 证据,不各自 `=== false`(对新形状会误判);证据行由 checker 自行措辞(自描述、不带缩进),组装与截断策略单点在 `formatEvidence()`/`MAX_EVIDENCE_ITEMS`(同住 checkers/types.ts);证据四出口 = `ConstraintResult.evidence` → CLI 结论块/提示块、`ExecutionTrace.evidence`、error 级 `ConstraintViolationError.message`(error 级违规直接 throw,异常文案是其证据唯一外溢面);检查引擎双出口——`checkConstraints`(block:首个 error 级违规即抛,throw 契约不动)与 `collectConstraints`(collect:共享检查体、不抛、全量收集,report 的不抛出口,架构评审候选1)
8
8
  - `constraints/context-reconcile.ts` — CONTEXT.md「核心导出」节与目录导出面的**幽灵/未登记**判定唯一实现(harness#142 / ADR-0025,与 `capabilities-reconcile` 同一套覆盖/幽灵语义,不另造第二套对照逻辑):`reconcileContext({ contextMdContent, exportSurface, barrelExports? })` → `{ declaredSymbols, ghosts, unlistedBarrelExports, hasCoreExportsSection }`;配套两个口径正本也是纯函数——`parseDeclaredExportSymbols`(只认 bullet 行首反引号标识符,`/` 连接视为多声明,形参与 `(type)`注解剥掉,非标识符的模块名 bullet 整条跳过)与 `parseExportStatements`(声明形 / `export { a, b as c }` / 逐项与块级 `type` / `export default` / `export *` 的待解析 spec)。零 fs IO:文件遍历与 `export *` 目标解析在调用方(`cli/commands/sync-docs/context-syncer.ts` 的 `collectContextExportSurface`);覆盖方向只判 barrel 的**值**符号(类型面豁免),mtime 判定就此降级为本地提示。本层 checker 暂无消费方
9
9
  - `git-evidence.ts` — check 链路的 git 取证唯一接缝(#87,范围按 ADR-0021 收窄;`spec/validator` 的 staged 列表同型复用):`createGitEvidence(projectPath, run?)` 返回 `GitEvidence{stagedDiff, changedFileNames(staged), headDirs}`,seam 上两个真 adapter(缺省 `realGitCommandRunner` 走真 git / 注入 runner 供计数与替身)。命令级 memo 落在此实例(含失败结果),实例生命周期 = 一次 check run,故 context-builder 与 ConstraintChecker 传同一实例即共用证据;判定语义与迁移前一致(diff 取证失败 → 空串,`git ls-tree` 失败 → null = 所有目录视为新);`parseHeadDirs`/`splitFileNames` 是配套纯函数
10
10
  - `constraints/context-builder.ts` — 只装配不取证:`buildConstraintContext(options.evidence?, options.runEnv?)` 与 `detectTrigger(…, options.runEnv?)` 经 GitEvidence 取 git 事实(不传 = 自建真 adapter),本模块无 child_process import;上行数据(源码根、trace 记录)经 `RunEnv` 取(ADR-0023),其余证据标志(ROOT_CAUSE/reuse/需求文档标记)仍为本地探测;`detectFailingTest`/`detectVerificationEvidence` 是模块私有的布尔投影(除本模块外零消费者,收私依 ADR-0022 同判据)
@@ -117,7 +117,7 @@ export interface ConstraintsUsageReport {
117
117
  *
118
118
  * 坏行策略:skip(原语义不变——report 只读,不因单行损坏失败);
119
119
  * 计数去向:透传——`skippedLines` 随本方法返回,进 `ConstraintsUsageReport.skippedLines`,
120
- * 消费面为 `constraints report` 的文本行 / `--json` 字段 / `--export` 摘要。
120
+ * 消费面为 `constraints report` 的文本行 / `--json-output` 字段 / `--export` 摘要。
121
121
  */
122
122
  export function readProjectTracesReport(projectRoot: string): { traces: ExecutionTrace[]; skippedLines: number } {
123
123
  const { records, skippedLines } = readJsonl<ExecutionTrace>(
package/src/index.ts CHANGED
@@ -54,6 +54,26 @@ export {
54
54
  } from './core/effective-constraints';
55
55
  export type { EffectiveConfigLint } from './core/effective-constraints';
56
56
 
57
+ // ========================================
58
+ // 约束使用报告(退役候选诊断数据层,studio#602 E1 (a) 链路消费)
59
+ // ========================================
60
+ export {
61
+ buildConstraintsUsageReport,
62
+ diagnoseRetireCandidates,
63
+ collectUsageByConstraint,
64
+ readProjectTraces,
65
+ readProjectTracesReport,
66
+ CANDIDATE_KIND_LABEL,
67
+ DEFAULT_DIAGNOSE_THRESHOLDS,
68
+ } from './core/constraints/usage-report';
69
+ export type {
70
+ ConstraintUsageStats,
71
+ RetireCandidate,
72
+ RetireCandidateKind,
73
+ DiagnoseThresholds,
74
+ ConstraintsUsageReport,
75
+ } from './core/constraints/usage-report';
76
+
57
77
  // ========================================
58
78
  // 约束检查引擎(便捷 API;options 统一签名,ADR-0003)
59
79
  // ========================================
@@ -17,7 +17,7 @@
17
17
  - 所有模式支持 `decayAt` 硬过期
18
18
 
19
19
  ## 核心导出
20
- - `KnowledgeStore`(类型面)/ `FileKnowledgeStore`(`store.ts`,barrel 与包根交出的**值**符号——store 的文件系统实现,构造收 `{ baseDir }`;仓内构造点两处:`cli/commands/knowledge-view.ts` 的 `openKnowledgeStore()`(knowledge 各子命令的取数口)与 `cli/commands/constraints-retire.ts` 的退役沉淀写入)— 知识条目 CRUD + 结构化存储;`saveAll()` 全量批量写入(循环内只更新内存索引、结束一次 writeIndex,harness#107)、`applyAll()` 按 id 部分更新的批量出口(`update(id, partial)` 的批量形,语义含未知 id 逐条跳过与空批零读写,harness#134)、`getConsumptionStats()` 读 `.consumption-stats.json` 的当日消费计数(打分核心唯一的消费统计取数口,harness#134
20
+ - `KnowledgeStore`(类型面)/ `FileKnowledgeStore`(`store.ts`,barrel 与包根交出的**值**符号——store 的文件系统实现,构造收 `{ baseDir }`;仓内构造点两处:`cli/commands/knowledge-view.ts` 的 `openKnowledgeStore()`(knowledge 各子命令的取数口)与 `cli/commands/constraints-retire.ts` 的退役沉淀写入)— 知识条目 CRUD + 结构化存储;`saveAll()` 全量批量写入(循环内只更新内存索引、结束一次 writeIndex,harness#107)、`applyAll()` 按 id 部分更新的批量出口(`update(id, partial)` 的批量形,语义含未知 id 逐条跳过与空批零读写,harness#134)、`getConsumptionStats()` 读 `.consumption-stats.json` 的当日消费计数(打分核心唯一的消费统计取数口,harness#134)。**写入闸(E1 复盘修正 M1)**:save/saveAll/applyAll 落盘前校验 maturity/layer 枚举(update 经 save 继承),未声明值拒写抛错不静默降级;值域正本 = `types.ts` 的 `MATURITY_LEVELS`/`STORAGE_LAYERS` 常量(不进 barrel)。批量路径先整批预校验再落盘(任一脏值全批拒写);applyAll 批内同 id 多条按序累积合并语义不变
21
21
  - `KnowledgeQuery` — 语义搜索 + 类型/标签过滤 + `queryByMode()` + `consume(taskContext)`;`query()` 是 budget 截断管线(仅用于 prompt 注入),`search()` 是全语料文本搜索(先匹配→排序→limit,交互搜索专用,勿用 query() 代替)
22
22
  - `KnowledgeLifecycle` — per-mode 生命周期管理;缺省阈值常量 `DEFAULT_DECAY_CONFIG`(`provenDecayMonths: 12` / `verifiedDecayMonths: 6` / `draftDecayMonths: 3` / `autoPromoteSources: []`,正本 `types.ts`)经构造器 `{...DEFAULT_DECAY_CONFIG, ...config}` 合并,barrel 与包根一并交出
23
23
  - `extractCodeStructure(dir)`(`primitives/code-structure.ts`,配 `CodeStructure`/`DeclarationInfo`/`ImportInfo` 三类型对外)— 目录代码结构提取;仓内零调用方,是留给下游的公开面
@@ -29,7 +29,7 @@
29
29
  - `tree-walker`(包内,不进导出面)— 知识树排除口径单点(harness#134):`isEntryFile()` / `isInfraDir()` / `INDEX_MD_FILE` / `SNAPSHOTS_DIR`,由 store 顶层扫描、migration 顶层扫描、index-generator 递归扫描三处共同消费
30
30
  - `flywheel-metrics`(包内,不进导出面)— 知识飞轮指标唯一实现:`evaluateFlywheel(env)` 出 canonical 比例、`genuineRefs()` 出 synthetic 过滤口径,audit D6 / `knowledge stats` / `knowledge health` 三处共消费(ADR-0013)
31
31
  - `ReferenceTracker` — 知识引用关系图谱
32
- - `KnowledgeLinter` — 知识质量检查(完整性/一致性/时效性)
32
+ - `KnowledgeLinter` — 知识质量检查(完整性/一致性/时效性);maturity/layer 枚举校验两个入口共用判定正本 `invalidEnumIssues`:`run()` 经 `checkInvalidEnums()` 扫存量(缺字段即脏、issue 带 entryId,`invalid_enum` 无 autoFix 分支——映射需人工裁定),`validateEntry()` 管摄入前(字段缺省跳过以兼容只传四字段的 pre-ingest 调用方)
33
33
  - `ColdStartImporter` — 冷启动知识导入
34
34
  - `KnowledgeHealthScorer` — 知识健康评分(doctor.ts)
35
35
 
@@ -43,6 +43,7 @@
43
43
  - 知识条目文件的 frontmatter 语法只由 `src/utils/frontmatter` 定义(harness#89):缺头/空 meta = `absent`(合法输入,按非条目静默处理、不上报——migration 对 `absent` 不计 `errors`、不迁移,只计 `skipped` 保证 `total` 计数闭环,harness#161 对齐 #89 裁决 2);未闭合/YAML 非法 = `malformed`(必须显式上报后按消费方语义恢复——migration 落 `errors`、store 与 index-generator 打一行 stderr 后跳过或走 best-effort),禁止静默丢条目;canonical 字段序是 `store.toFrontmatter` 的私有策略,`join` 只管包裹格式
44
44
  - **知识树的排除口径只有一个正本 `tree-walker`**(harness#134):`_index.md`(索引生成物)、`.snapshots`、`.archive` / `archived`、`resolutions` 是树基建、不是条目人口。store 与 migration 的顶层扫描、index-generator 的递归扫描三处都走它;新增 walker 禁止另立排除清单。统一的是**排除口径**不是遍历深度(store/migration 顶层、index-generator 递归)。例外须原地记名理由:`import.ts` 的 docs 扫描吃的是**项目文档树**,本口径在它那里没有对应物
45
45
  - **循环内禁止逐条 `store.update()`**(harness#134):`update()` 的形状是 get→save、`save()` 每次全量重写 index.json,N 条修复 = N 次全量重写。批量形是 `applyAll(id → partial)`;一次 `audit --fix`、一轮 `runDecayCycle()`、一次 `updateReferencedBy()` 各只重写一次索引,计数闸见 `audit-write-count.test.ts` 与 `lifecycle.test.ts` / `reference-tracker.test.ts` 的 stringify 计数项
46
+ - **写入闸不管读**(E1 复盘修正 M1):枚举校验只在 save/saveAll/applyAll/update 落盘前执行,读取(parseFile)不校验——盘上脏条目可读可列,但任一写入路径触及即抛错(含「只改其他字段」的部分更新),先修脏值再写;绕过本 store 直写文件的外部写入方不受闸约束
46
47
  - **审计判定脱离文件系统可测**(harness#134):规则表与 D1–D7 打分住 `audit-scoring.ts`,零 fs(源形状闸钉在 `audit-scoring.test.ts`);引擎 `audit.ts` 只做 store 装配,环境数据经 `store.getConsumptionStats()` / `getSurvivalRate()` 取好喂入
47
48
  - 知识条目有明确的生命周期状态(按 consumptionMode 分化)
48
49
  - 约束退役(`harness constraints retire`,人确认)时写入 KnowledgeStore:规则原文 + 退役原因 + 历史统计
@@ -56,7 +57,7 @@
56
57
  ## 注意事项
57
58
  - Phase 1+4 实现的知识引擎核心
58
59
  - 约束"退役不删除"——retire 落盘 config.yml `enabled: false` + retired 元数据,保留规则原文 + 退役原因 + 历史统计(可回滚)
59
- - `MaturityLevel` 包含 6 个值: draft/verified/proven/archived/active/deprecated
60
+ - `MaturityLevel` / `StorageLayer` 的值域不抄清单——运行时正本是 `types.ts` 的 `MATURITY_LEVELS` / `STORAGE_LAYERS` 常量(写入闸与 lint 枚举校验共用)
60
61
  - `excludeArchived` 同时排除 archived 和 deprecated
61
62
  - 仍有两处按**原始** `referencedBy.length` 判定,不属飞轮指标、ADR-0013 明确列为范围外:`lifecycle.ts` 的 signal 饱和 / reference 激活(退役阈值调整另票)、`knowledge health` 的 D1「verified 零引用」线索提示(逐条 issue 线索,非聚合分子)
62
63
  - #134 收口后仍在的逐条写点(同型问题,本票裁决点名的循环之外,需要时另票):`lint.ts` 的三处 autoFix `store.update()`、`lifecycle.recordReference()`(逐事件调用,返回更新后条目并触发回调,批量化会改它的契约)、`ingest.mergeEntries()`;`store.list()` 每条一次 `findFile()` 线性扫索引的 O(N²) 按 #134 裁决**未动**,待把知识树实际规模重新量一次再判是否单开票
@@ -57,6 +57,7 @@ describe('KnowledgeLinter', () => {
57
57
  expect(report.fixed).toBe(0);
58
58
  });
59
59
 
60
+
60
61
  it('应该检测所有问题类型', () => {
61
62
  // 孤儿条目
62
63
  saveEntry({ id: 'orphan-1', title: 'Orphan', maturity: 'draft', contributors: [], projects: [] });
@@ -79,6 +80,39 @@ describe('KnowledgeLinter', () => {
79
80
  });
80
81
  });
81
82
 
83
+ // M1 收口:run() 存量扫描必须报出 maturity/layer 脏值(spec:lint 能报出存量脏条目)
84
+ describe('run 存量枚举校验', () => {
85
+ /** 绕过写入闸直写脏条目(模拟实盘直写路径) */
86
+ function writeDirtyFile(name: string, fm: string) {
87
+ const dir = path.join(tmpDir, 'knowledge');
88
+ fs.writeFileSync(path.join(dir, name), `---\n${fm}\n---\n\nbody content long enough.\n`, 'utf-8');
89
+ store.rebuildIndex();
90
+ }
91
+
92
+ it('盘上脏 maturity / 脏 layer 的存量条目 → run() 报 invalid_enum', () => {
93
+ writeDirtyFile('resolution-res_a.md',
94
+ 'id: res_a\ntype: resolution\ntitle: Dirty M\nmaturity: pending\nlayer: project\ncreated: 2026-09-01T00:00:00.000Z');
95
+ writeDirtyFile('resolution-res_b.md',
96
+ 'id: res_b\ntype: resolution\ntitle: Dirty L\nmaturity: draft\nlayer: L3_tool_behavior\ncreated: 2026-09-01T00:00:00.000Z');
97
+ writeDirtyFile('resolution-res_c.md',
98
+ 'id: res_c\ntype: resolution\ntitle: Missing Layer\nmaturity: draft\ncreated: 2026-09-01T00:00:00.000Z');
99
+
100
+ const report = linter.run();
101
+ const enumIssues = report.issues.filter(i => i.type === 'invalid_enum');
102
+ expect(enumIssues).toHaveLength(3);
103
+ expect(enumIssues.map(i => i.entryId).sort()).toEqual(['res_a', 'res_b', 'res_c']);
104
+ expect(report.summary.invalid_enum).toBe(3);
105
+ });
106
+
107
+ it('全合法条目库 → 零 invalid_enum(存量合法条目无新噪音)', () => {
108
+ saveEntry({ id: 'clean-1', maturity: 'verified', layer: 'project' });
109
+ saveEntry({ id: 'clean-2', maturity: 'active', layer: 'tech', title: 'Another' });
110
+
111
+ const report = linter.run();
112
+ expect(report.summary.invalid_enum).toBe(0);
113
+ });
114
+ });
115
+
82
116
  describe('checkIndexConsistency', () => {
83
117
  it('应该检测索引中有但文件不存在的条目', () => {
84
118
  saveEntry({ id: 'entry-1' });
@@ -370,6 +404,56 @@ describe('KnowledgeLinter', () => {
370
404
  });
371
405
  expect(issues.some(i => i.type === 'duplicate')).toBe(true);
372
406
  });
407
+
408
+ // E1 复盘修正 M1:maturity/layer 枚举值域校验(实盘脏值:pending/canonical/L3_tool_behavior)
409
+ it('flags undeclared maturity value', () => {
410
+ const issues = linter.validateEntry({
411
+ title: 'Specific Entry With Valid Title',
412
+ content: 'This is a sufficiently long content for the enum check test case.',
413
+ tags: [],
414
+ type: 'decision',
415
+ maturity: 'pending',
416
+ });
417
+ const issue = issues.find(i => i.type === 'invalid_enum');
418
+ expect(issue).toBeDefined();
419
+ expect(issue!.severity).toBe('high');
420
+ expect(issue!.description).toContain('pending');
421
+ });
422
+
423
+ it('flags undeclared layer value', () => {
424
+ const issues = linter.validateEntry({
425
+ title: 'Specific Entry With Valid Title',
426
+ content: 'This is a sufficiently long content for the enum check test case.',
427
+ tags: [],
428
+ type: 'decision',
429
+ layer: 'L3_tool_behavior',
430
+ });
431
+ const issue = issues.find(i => i.type === 'invalid_enum');
432
+ expect(issue).toBeDefined();
433
+ expect(issue!.description).toContain('L3_tool_behavior');
434
+ });
435
+
436
+ it('accepts declared maturity/layer values', () => {
437
+ const issues = linter.validateEntry({
438
+ title: 'Specific Entry With Valid Title',
439
+ content: 'This is a sufficiently long content for the enum check test case.',
440
+ tags: [],
441
+ type: 'decision',
442
+ maturity: 'verified',
443
+ layer: 'project',
444
+ });
445
+ expect(issues.filter(i => i.type === 'invalid_enum')).toHaveLength(0);
446
+ });
447
+
448
+ it('skips enum check when maturity/layer are not provided (pre-ingest caller compat)', () => {
449
+ const issues = linter.validateEntry({
450
+ title: 'Specific Entry With Valid Title',
451
+ content: 'This is a sufficiently long content for the enum check test case.',
452
+ tags: [],
453
+ type: 'decision',
454
+ });
455
+ expect(issues.filter(i => i.type === 'invalid_enum')).toHaveLength(0);
456
+ });
373
457
  });
374
458
 
375
459
  describe('autoFix', () => {
@@ -174,6 +174,76 @@ describe('KnowledgeStore', () => {
174
174
  });
175
175
  });
176
176
 
177
+ // 写入闸(E1 复盘修正 M1):maturity/layer 是声明枚举,未声明值拒写报错,不静默降级
178
+ describe('schema gate: maturity/layer 枚举校验', () => {
179
+ it('save 拒绝未声明的 maturity 值,不落盘', () => {
180
+ expect(() => store.save(makeEntry({ maturity: 'pending' as KnowledgeEntry['maturity'] })))
181
+ .toThrow(/maturity.*pending/);
182
+ expect(store.get('DEC-001')).toBeUndefined();
183
+ expect(store.readIndex()).toHaveLength(0);
184
+ });
185
+
186
+ it('save 拒绝未声明的 layer 值,不落盘', () => {
187
+ expect(() => store.save(makeEntry({ layer: 'L3_tool_behavior' as KnowledgeEntry['layer'] })))
188
+ .toThrow(/layer.*L3_tool_behavior/);
189
+ expect(store.get('DEC-001')).toBeUndefined();
190
+ });
191
+
192
+ it('save 拒绝缺失 layer 的条目', () => {
193
+ const entry = makeEntry();
194
+ delete (entry as Partial<KnowledgeEntry>).layer;
195
+ expect(() => store.save(entry)).toThrow(/layer/);
196
+ expect(store.get('DEC-001')).toBeUndefined();
197
+ });
198
+
199
+ it('update 传入脏 maturity 值 → 拒写,既有条目不变', () => {
200
+ store.save(makeEntry());
201
+ expect(() => store.update('DEC-001', { maturity: 'canonical' as KnowledgeEntry['maturity'] }))
202
+ .toThrow(/maturity.*canonical/);
203
+ expect(store.get('DEC-001')!.maturity).toBe('draft');
204
+ });
205
+
206
+ it('存量脏条目(绕过闸直写文件)经 update 修补其他字段 → 拒写报错', () => {
207
+ // 模拟实盘脏条目:resolution.service 直写文件的形状(maturity/layer 均未声明)
208
+ fs.writeFileSync(
209
+ path.join(tempDir, 'resolution-res_1.md'),
210
+ '---\nid: res_1\ntype: resolution\ntitle: Dirty\nmaturity: pending\nlayer: L3_tool_behavior\ncreated: 2026-09-01T00:00:00.000Z\n---\n\nbody\n',
211
+ 'utf-8'
212
+ );
213
+ store.rebuildIndex();
214
+ expect(() => store.update('res_1', { tags: ['touched'] })).toThrow(/maturity.*pending/);
215
+ });
216
+
217
+ it('存量脏条目经 update 把脏字段修成合法值 → 放行(清洗路径)', () => {
218
+ fs.writeFileSync(
219
+ path.join(tempDir, 'resolution-res_2.md'),
220
+ '---\nid: res_2\ntype: resolution\ntitle: Dirty\nmaturity: pending\nlayer: L3_tool_behavior\ncreated: 2026-09-01T00:00:00.000Z\n---\n\nbody\n',
221
+ 'utf-8'
222
+ );
223
+ store.rebuildIndex();
224
+ const updated = store.update('res_2', { maturity: 'draft', layer: 'project' });
225
+ expect(updated!.maturity).toBe('draft');
226
+ expect(store.get('res_2')!.layer).toBe('project');
227
+ });
228
+
229
+ it('saveAll 含一条脏值 → 整批拒写(合法条目也不落盘)', () => {
230
+ expect(() => store.saveAll([
231
+ makeEntry({ id: 'DEC-001' }),
232
+ makeEntry({ id: 'DEC-002', layer: 'process' as KnowledgeEntry['layer'] }),
233
+ ])).toThrow(/layer.*process/);
234
+ expect(store.get('DEC-001')).toBeUndefined();
235
+ expect(store.readIndex()).toHaveLength(0);
236
+ });
237
+
238
+ it('applyAll 部分更新把 maturity 改成脏值 → 拒写', () => {
239
+ store.save(makeEntry());
240
+ expect(() => store.applyAll([
241
+ { id: 'DEC-001', partial: { maturity: 'pending' as KnowledgeEntry['maturity'] } },
242
+ ])).toThrow(/maturity.*pending/);
243
+ expect(store.get('DEC-001')!.maturity).toBe('draft');
244
+ });
245
+ });
246
+
177
247
  describe('rebuildIndex', () => {
178
248
  it('should rebuild index from files', () => {
179
249
  store.save(makeEntry({ id: 'DEC-001' }));
@@ -13,6 +13,7 @@
13
13
  import type { KnowledgeStore } from './store';
14
14
  import { ReferenceTracker } from './reference-tracker';
15
15
  import type { KnowledgeEntry, LintIssue, LintIssueType, MaturityLevel } from './types';
16
+ import { MATURITY_LEVELS, STORAGE_LAYERS } from './types';
16
17
 
17
18
  const MAX_SOURCE_REFS = 20;
18
19
 
@@ -46,6 +47,7 @@ export class KnowledgeLinter {
46
47
  issues.push(...this.checkOutdated(entries));
47
48
  issues.push(...this.checkDuplicates(entries));
48
49
  issues.push(...this.checkContradictions(entries));
50
+ issues.push(...this.checkInvalidEnums(entries));
49
51
 
50
52
  // 自动修复
51
53
  let fixed = 0;
@@ -60,6 +62,7 @@ export class KnowledgeLinter {
60
62
  outdated: 0,
61
63
  duplicate: 0,
62
64
  index_inconsistent: 0,
65
+ invalid_enum: 0,
63
66
  };
64
67
  for (const issue of issues) {
65
68
  summary[issue.type]++;
@@ -204,6 +207,55 @@ export class KnowledgeLinter {
204
207
  return issues;
205
208
  }
206
209
 
210
+ /**
211
+ * 检查存量条目的 maturity/layer 枚举值域(E1 复盘修正 M1 收口:
212
+ * 写入闸只挡新写,存量脏值由本检查进 lint 报告;缺字段在存量侧算脏值)。
213
+ * 与 validateEntry 共用同一判定正本(invalidEnumIssues),autoFix 不动此类
214
+ * issue——枚举映射需人工裁定,不自动改。
215
+ */
216
+ checkInvalidEnums(entries: KnowledgeEntry[]): LintIssue[] {
217
+ const issues: LintIssue[] = [];
218
+ for (const entry of entries) {
219
+ issues.push(...this.invalidEnumIssues(entry, true, entry.id));
220
+ }
221
+ return issues;
222
+ }
223
+
224
+ /**
225
+ * maturity/layer 枚举判定正本。missingIsDirty=false 时字段缺省跳过
226
+ * (pre-ingest 调用方只传四字段的兼容面);存量扫描传 true(缺字段即脏)。
227
+ */
228
+ private invalidEnumIssues(
229
+ fields: { maturity?: string; layer?: string },
230
+ missingIsDirty: boolean,
231
+ entryId?: string
232
+ ): LintIssue[] {
233
+ const issues: LintIssue[] = [];
234
+ if ((missingIsDirty || fields.maturity !== undefined) && !(MATURITY_LEVELS as readonly string[]).includes(fields.maturity as string)) {
235
+ issues.push({
236
+ type: 'invalid_enum',
237
+ entryId,
238
+ severity: 'high',
239
+ description: fields.maturity === undefined
240
+ ? `maturity is missing (declared values: ${MATURITY_LEVELS.join(' | ')}).`
241
+ : `maturity "${fields.maturity}" is not a declared value (${MATURITY_LEVELS.join(' | ')}).`,
242
+ suggestion: 'Map to a declared maturity (e.g. pending → draft) before writing; the store write gate rejects undeclared values.',
243
+ });
244
+ }
245
+ if ((missingIsDirty || fields.layer !== undefined) && !(STORAGE_LAYERS as readonly string[]).includes(fields.layer as string)) {
246
+ issues.push({
247
+ type: 'invalid_enum',
248
+ entryId,
249
+ severity: 'high',
250
+ description: fields.layer === undefined
251
+ ? `layer is missing (declared values: ${STORAGE_LAYERS.join(' | ')}).`
252
+ : `layer "${fields.layer}" is not a declared value (${STORAGE_LAYERS.join(' | ')}).`,
253
+ suggestion: 'Map to a declared layer or drop the field; the store write gate rejects undeclared values.',
254
+ });
255
+ }
256
+ return issues;
257
+ }
258
+
207
259
  /**
208
260
  * 检查矛盾(同主题相反结论)
209
261
  *
@@ -260,10 +312,17 @@ export class KnowledgeLinter {
260
312
  * - Title too vague (common stop words only) → flag
261
313
  * - Contradicts a proven entry with same tags → reject
262
314
  * - Near-duplicate of existing entry (title similarity > 80%) → flag merge
315
+ * - maturity/layer 未声明枚举值 → reject(E1 复盘修正 M1;字段缺省则跳过,
316
+ * 兼容只传 title/content/tags/type 的 pre-ingest 调用方;存量报告由调用方
317
+ * 把条目字段显式传入触发)
263
318
  */
264
- validateEntry(entry: { title: string; content: string; tags: string[]; type: string }): LintIssue[] {
319
+ validateEntry(entry: { title: string; content: string; tags: string[]; type: string; maturity?: string; layer?: string }): LintIssue[] {
265
320
  const issues: LintIssue[] = [];
266
321
 
322
+ // Undeclared maturity/layer enum values(判定正本 = invalidEnumIssues;字段缺省跳过,
323
+ // 兼容只传四字段的 pre-ingest 调用方;存量扫描走 run() → checkInvalidEnums)
324
+ issues.push(...this.invalidEnumIssues(entry, false));
325
+
267
326
  // Content too short
268
327
  if ((entry.content || '').length < 20) {
269
328
  issues.push({
@@ -18,6 +18,7 @@ import type {
18
18
  SnapshotSurvival,
19
19
  StoreUpdate,
20
20
  } from './types';
21
+ import { MATURITY_LEVELS, STORAGE_LAYERS } from './types';
21
22
 
22
23
  const DEFAULT_DIR = '.harness/knowledge';
23
24
  const INDEX_FILE = 'index.json';
@@ -109,12 +110,36 @@ export class FileKnowledgeStore implements KnowledgeStore {
109
110
  }
110
111
 
111
112
  save(entry: KnowledgeEntry): void {
113
+ this.assertEnumFields(entry);
112
114
  this.writeEntryFile(entry);
113
115
  this.updateIndexEntry(entry);
114
116
  }
115
117
 
118
+ /**
119
+ * 写入闸(E1 复盘修正 M1):maturity/layer 是声明枚举(值域正本在 types.ts),
120
+ * 未声明值拒写报错,不静默降级。save/saveAll/applyAll 三条落盘路径共用;
121
+ * update 经 save 继承同一闸。存量脏条目(绕过本 store 直写文件的)在被
122
+ * 任一写入路径触及时抛错——先修脏值再写。
123
+ */
124
+ private assertEnumFields(entry: KnowledgeEntry): void {
125
+ if (!MATURITY_LEVELS.includes(entry.maturity)) {
126
+ throw new Error(
127
+ `[harness] 知识条目 ${entry.id} 的 maturity 值 "${entry.maturity}" 未声明` +
128
+ `(允许: ${MATURITY_LEVELS.join(' | ')}),拒写`
129
+ );
130
+ }
131
+ if (!STORAGE_LAYERS.includes(entry.layer)) {
132
+ throw new Error(
133
+ `[harness] 知识条目 ${entry.id} 的 layer 值 "${entry.layer}" 未声明` +
134
+ `(允许: ${STORAGE_LAYERS.join(' | ')}),拒写`
135
+ );
136
+ }
137
+ }
138
+
116
139
  saveAll(entries: KnowledgeEntry[]): void {
117
140
  if (entries.length === 0) return;
141
+ // 整批预校验(写入闸):任一脏值全批拒写,不落半个批次
142
+ for (const entry of entries) this.assertEnumFields(entry);
118
143
  const index = this.readIndex();
119
144
  const position = new Map(index.map((e, i) => [e.id, i]));
120
145
  for (const entry of entries) {
@@ -133,25 +158,37 @@ export class FileKnowledgeStore implements KnowledgeStore {
133
158
 
134
159
  applyAll(updates: StoreUpdate[]): void {
135
160
  if (updates.length === 0) return;
161
+ // 先合并再整批预校验(写入闸):任一合并结果脏值全批拒写,不落半个批次。
162
+ // 批内同 id 多条按序累积合并(后者基于前者的合并结果,与逐条 update 同语义)。
163
+ const merged: KnowledgeEntry[] = [];
164
+ const pending = new Map<string, number>(); // id → merged 下标
165
+ for (const { id, partial } of updates) {
166
+ const idx = pending.get(id);
167
+ const base = idx !== undefined ? merged[idx] : this.get(id);
168
+ if (!base) continue;
169
+ const next: KnowledgeEntry = { ...base, ...partial, id };
170
+ if (idx !== undefined) {
171
+ merged[idx] = next;
172
+ } else {
173
+ pending.set(id, merged.length);
174
+ merged.push(next);
175
+ }
176
+ }
177
+ if (merged.length === 0) return;
178
+ for (const entry of merged) this.assertEnumFields(entry);
136
179
  const index = this.readIndex();
137
180
  const position = new Map(index.map((e, i) => [e.id, i]));
138
- let applied = 0;
139
- for (const { id, partial } of updates) {
140
- const existing = this.get(id);
141
- if (!existing) continue;
142
- const updated: KnowledgeEntry = { ...existing, ...partial, id };
143
- this.writeEntryFile(updated);
144
- const indexEntry = this.toIndexEntry(updated);
145
- const idx = position.get(id);
181
+ for (const entry of merged) {
182
+ this.writeEntryFile(entry);
183
+ const indexEntry = this.toIndexEntry(entry);
184
+ const idx = position.get(entry.id);
146
185
  if (idx !== undefined) {
147
186
  index[idx] = indexEntry;
148
187
  } else {
149
- position.set(id, index.length);
188
+ position.set(entry.id, index.length);
150
189
  index.push(indexEntry);
151
190
  }
152
- applied++;
153
191
  }
154
- if (applied === 0) return;
155
192
  this.writeIndex(index);
156
193
  }
157
194
 
@@ -15,6 +15,12 @@ export type MaturityLevel = 'draft' | 'verified' | 'proven' | 'archived' | 'acti
15
15
 
16
16
  export type StorageLayer = 'personal' | 'team' | 'tech' | 'domain' | 'project' | 'system';
17
17
 
18
+ /** MaturityLevel 的运行时值域正本(store 写入闸与 lint 枚举校验共用) */
19
+ export const MATURITY_LEVELS: readonly MaturityLevel[] = ['draft', 'verified', 'proven', 'archived', 'active', 'deprecated'];
20
+
21
+ /** StorageLayer 的运行时值域正本(store 写入闸与 lint 枚举校验共用) */
22
+ export const STORAGE_LAYERS: readonly StorageLayer[] = ['personal', 'team', 'tech', 'domain', 'project', 'system'];
23
+
18
24
  /** How agent consumes this knowledge — drives injection strategy and lifecycle */
19
25
  export type ConsumptionMode = 'rule' | 'reference' | 'context' | 'signal';
20
26
 
@@ -101,7 +107,7 @@ export interface QueryFilter {
101
107
 
102
108
  // ── Lint ────────────────────────────────────────────────────
103
109
 
104
- export type LintIssueType = 'orphan' | 'contradiction' | 'outdated' | 'duplicate' | 'index_inconsistent';
110
+ export type LintIssueType = 'orphan' | 'contradiction' | 'outdated' | 'duplicate' | 'index_inconsistent' | 'invalid_enum';
105
111
 
106
112
  export interface LintIssue {
107
113
  type: LintIssueType;
@@ -22,7 +22,7 @@
22
22
  - 零 Token 成本:不调用 LLM
23
23
  - **凡以 `skip` 策略读 JSONL,坏行计数必须到达该消费面的用户可见输出,或在该调用点显式记名豁免并写明理由(harness#100)**——静默吞掉「数据不全」不是一种可选项。承载方式纯增量:新报告入口带计数,旧入口退化为丢计数的薄包装(#82 裁决 4 的兼容约束继续成立),不另立第二份计数概念、不把 `JsonlReadResult` 提升进包根(ADR-0003)
24
24
  - 机器可检:`src/utils/__tests__/jsonl-skip-disposition.test.ts` 三道闸——每个 `readJsonl` 调用点必须判得出策略(判不出即形状逃逸,失败)、skip 读点集合冻结(新增未声明即失败,站点消失却不删条目也失败)、每个读点上方 6 行内有 `计数去向:` 声明且邻居声明不得顶替
25
- - 三类去向(逐条对得上代码):**报告体内部的降级位**随报告体走 stdout——`constraints report` 的坏行提示紧挨既有的 `traceFileExists` 说明;**不挤动既有输出的诊断告知**走 stderr——`status`(`logError`,stdout 的 `记录数` 口径与字节不变)、`failure list`(#96 定稿的 `console.error`)、`constraints retire`(交互与 `--yes` 直达两条路径各自告知);**结构化面用字段**——`constraints report --json` 的 `skippedLines`、`--export` markdown 的警示行、studio 端点响应体(配套票 studio#451)
25
+ - 三类去向(逐条对得上代码):**报告体内部的降级位**随报告体走 stdout——`constraints report` 的坏行提示紧挨既有的 `traceFileExists` 说明;**不挤动既有输出的诊断告知**走 stderr——`status`(`logError`,stdout 的 `记录数` 口径与字节不变)、`failure list`(#96 定稿的 `console.error`)、`constraints retire`(交互与 `--yes` 直达两条路径各自告知);**结构化面用字段**——`constraints report --json-output` 的 `skippedLines`、`--export` markdown 的警示行、studio 端点响应体(配套票 studio#451)
26
26
 
27
27
  ## 注意事项
28
28
  - 追踪数据供 `harness constraints report` 统计与退役候选诊断消费(观测用途,不做自动降级)
@@ -1,15 +0,0 @@
1
- /**
2
- * harness posteval-plan 命令
3
- *
4
- * 调用 Studio PostEval API,验证 plan 文件的 checklist items 是否都有对应的 staged diff。
5
- * pre-commit hook 使用此命令防止"假装完成"。
6
- *
7
- * 判定经返回值外溢(架构评审候选7):API 不可用/5xx 属"未判定"→ skip(退出码面 0 不变);
8
- * 覆盖率不足与 4xx → fail,reason 指明是哪一项否决。
9
- */
10
- import { type CommandIO, type CommandResult } from '../command-contract';
11
- export interface PostEvalPlanOptions {
12
- planPath: string;
13
- }
14
- export declare function postevalPlan(options: PostEvalPlanOptions, io?: CommandIO): Promise<CommandResult>;
15
- //# sourceMappingURL=posteval-plan.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"posteval-plan.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/posteval-plan.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAA4B,KAAK,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEnG,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;CAClB;AA6BD,wBAAsB,YAAY,CAChC,OAAO,EAAE,mBAAmB,EAC5B,EAAE,GAAE,SAAqB,GACxB,OAAO,CAAC,aAAa,CAAC,CAkDxB"}
@@ -1,86 +0,0 @@
1
- "use strict";
2
- /**
3
- * harness posteval-plan 命令
4
- *
5
- * 调用 Studio PostEval API,验证 plan 文件的 checklist items 是否都有对应的 staged diff。
6
- * pre-commit hook 使用此命令防止"假装完成"。
7
- *
8
- * 判定经返回值外溢(架构评审候选7):API 不可用/5xx 属"未判定"→ skip(退出码面 0 不变);
9
- * 覆盖率不足与 4xx → fail,reason 指明是哪一项否决。
10
- */
11
- var __importDefault = (this && this.__importDefault) || function (mod) {
12
- return (mod && mod.__esModule) ? mod : { "default": mod };
13
- };
14
- Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.postevalPlan = postevalPlan;
16
- const chalk_1 = __importDefault(require("chalk"));
17
- const command_contract_1 = require("../command-contract");
18
- const MAX_RETRIES = 3;
19
- const RETRY_DELAY_MS = 2000;
20
- async function fetchWithRetry(url, body, retries) {
21
- let lastError;
22
- for (let i = 0; i < retries; i++) {
23
- try {
24
- const controller = new AbortController();
25
- const timeout = setTimeout(() => controller.abort(), 5000);
26
- const res = await fetch(url, {
27
- method: 'POST',
28
- headers: { 'Content-Type': 'application/json' },
29
- body,
30
- signal: controller.signal,
31
- });
32
- clearTimeout(timeout);
33
- return res;
34
- }
35
- catch (error) {
36
- lastError = error;
37
- if (i < retries - 1) {
38
- await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS * (i + 1))); // exponential backoff
39
- }
40
- }
41
- }
42
- throw lastError;
43
- }
44
- async function postevalPlan(options, io = command_contract_1.processIO) {
45
- const apiPort = process.env.API_PORT || '3001';
46
- const url = `http://localhost:${apiPort}/api/v1/agents/post-eval/plan-coverage`;
47
- try {
48
- const res = await fetchWithRetry(url, JSON.stringify({ planPath: options.planPath }), MAX_RETRIES);
49
- if (!res.ok) {
50
- if (res.status >= 500) {
51
- // Server error — allow commit with warning (don't block on infrastructure failure)
52
- (0, command_contract_1.logError)(io, chalk_1.default.yellow(`⚠️ PostEval API unavailable (${res.status}), allowing commit with warning`));
53
- (0, command_contract_1.logError)(io, chalk_1.default.yellow(` Plan: ${options.planPath}`));
54
- return { kind: 'skip', reason: `PostEval API 服务端错误 ${res.status},未判定` };
55
- }
56
- (0, command_contract_1.logError)(io, chalk_1.default.red(`❌ PostEval API error: ${res.status} ${res.statusText}`));
57
- return { kind: 'fail', reason: `PostEval API error: ${res.status} ${res.statusText}` };
58
- }
59
- const report = await res.json();
60
- const pct = Math.round(report.completeness * 100);
61
- if (report.completeness < 1) {
62
- (0, command_contract_1.logError)(io, chalk_1.default.red(`❌ Plan coverage: ${pct}% (${report.matchedAcs.length}/${report.matchedAcs.length + report.missedAcs.length})`));
63
- if (report.missedAcs.length > 0) {
64
- (0, command_contract_1.logError)(io, chalk_1.default.red('Missed items:'));
65
- report.missedAcs.forEach((item) => (0, command_contract_1.logError)(io, chalk_1.default.red(` - ${item}`)));
66
- }
67
- return {
68
- kind: 'fail',
69
- reason: `plan coverage ${pct}% < 100%,缺 ${report.missedAcs.length} 项: ${report.missedAcs.join('; ')}`,
70
- };
71
- }
72
- (0, command_contract_1.log)(io, chalk_1.default.green(`✅ Plan coverage: ${pct}%`));
73
- return { kind: 'ok' };
74
- }
75
- catch (error) {
76
- // API unreachable after all retries — allow with warning (don't block commits on infra)
77
- (0, command_contract_1.logError)(io, chalk_1.default.yellow(`⚠️ Studio API unreachable after ${MAX_RETRIES} retries, allowing commit with warning`));
78
- (0, command_contract_1.logError)(io, chalk_1.default.yellow(` Plan: ${options.planPath}`));
79
- (0, command_contract_1.logError)(io, chalk_1.default.yellow(` Error: ${error?.code || error?.message || String(error)}`));
80
- return {
81
- kind: 'skip',
82
- reason: `Studio API 不可达(${error?.code || error?.message || String(error)}),未判定`,
83
- };
84
- }
85
- }
86
- //# sourceMappingURL=posteval-plan.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"posteval-plan.js","sourceRoot":"","sources":["../../../src/cli/commands/posteval-plan.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;;;;AAoCH,oCAqDC;AAvFD,kDAA0B;AAC1B,0DAAmG;AAMnG,MAAM,WAAW,GAAG,CAAC,CAAC;AACtB,MAAM,cAAc,GAAG,IAAI,CAAC;AAE5B,KAAK,UAAU,cAAc,CAAC,GAAW,EAAE,IAAY,EAAE,OAAe;IACtE,IAAI,SAAc,CAAC;IACnB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,EAAE,CAAC,EAAE,EAAE,CAAC;QACjC,IAAI,CAAC;YACH,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;YACzC,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,CAAC;YAC3D,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;gBAC3B,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;gBAC/C,IAAI;gBACJ,MAAM,EAAE,UAAU,CAAC,MAAM;aAC1B,CAAC,CAAC;YACH,YAAY,CAAC,OAAO,CAAC,CAAC;YACtB,OAAO,GAAG,CAAC;QACb,CAAC;QAAC,OAAO,KAAU,EAAE,CAAC;YACpB,SAAS,GAAG,KAAK,CAAC;YAClB,IAAI,CAAC,GAAG,OAAO,GAAG,CAAC,EAAE,CAAC;gBACpB,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,cAAc,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,sBAAsB;YACrG,CAAC;QACH,CAAC;IACH,CAAC;IACD,MAAM,SAAS,CAAC;AAClB,CAAC;AAEM,KAAK,UAAU,YAAY,CAChC,OAA4B,EAC5B,KAAgB,4BAAS;IAEzB,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,MAAM,CAAC;IAC/C,MAAM,GAAG,GAAG,oBAAoB,OAAO,wCAAwC,CAAC;IAEhF,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,cAAc,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,EAAE,WAAW,CAAC,CAAC;QAEnG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,IAAI,GAAG,CAAC,MAAM,IAAI,GAAG,EAAE,CAAC;gBACtB,mFAAmF;gBACnF,IAAA,2BAAQ,EAAC,EAAE,EAAE,eAAK,CAAC,MAAM,CAAC,iCAAiC,GAAG,CAAC,MAAM,iCAAiC,CAAC,CAAC,CAAC;gBACzG,IAAA,2BAAQ,EAAC,EAAE,EAAE,eAAK,CAAC,MAAM,CAAC,YAAY,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;gBAC3D,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,sBAAsB,GAAG,CAAC,MAAM,MAAM,EAAE,CAAC;YAC1E,CAAC;YACD,IAAA,2BAAQ,EAAC,EAAE,EAAE,eAAK,CAAC,GAAG,CAAC,yBAAyB,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC;YACjF,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,uBAAuB,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,UAAU,EAAE,EAAE,CAAC;QACzF,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,IAAI,EAI5B,CAAC;QAEF,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,YAAY,GAAG,GAAG,CAAC,CAAC;QAElD,IAAI,MAAM,CAAC,YAAY,GAAG,CAAC,EAAE,CAAC;YAC5B,IAAA,2BAAQ,EAAC,EAAE,EAAE,eAAK,CAAC,GAAG,CAAC,oBAAoB,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,MAAM,IAAI,MAAM,CAAC,UAAU,CAAC,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YACxI,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAChC,IAAA,2BAAQ,EAAC,EAAE,EAAE,eAAK,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC;gBACzC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,IAAA,2BAAQ,EAAC,EAAE,EAAE,eAAK,CAAC,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;YACrF,CAAC;YACD,OAAO;gBACL,IAAI,EAAE,MAAM;gBACZ,MAAM,EAAE,iBAAiB,GAAG,cAAc,MAAM,CAAC,SAAS,CAAC,MAAM,OAAO,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;aACtG,CAAC;QACJ,CAAC;QAED,IAAA,sBAAG,EAAC,EAAE,EAAE,eAAK,CAAC,KAAK,CAAC,oBAAoB,GAAG,GAAG,CAAC,CAAC,CAAC;QACjD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IACxB,CAAC;IAAC,OAAO,KAAU,EAAE,CAAC;QACpB,wFAAwF;QACxF,IAAA,2BAAQ,EAAC,EAAE,EAAE,eAAK,CAAC,MAAM,CAAC,oCAAoC,WAAW,wCAAwC,CAAC,CAAC,CAAC;QACpH,IAAA,2BAAQ,EAAC,EAAE,EAAE,eAAK,CAAC,MAAM,CAAC,YAAY,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;QAC3D,IAAA,2BAAQ,EAAC,EAAE,EAAE,eAAK,CAAC,MAAM,CAAC,aAAa,KAAK,EAAE,IAAI,IAAI,KAAK,EAAE,OAAO,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC;QAC1F,OAAO;YACL,IAAI,EAAE,MAAM;YACZ,MAAM,EAAE,kBAAkB,KAAK,EAAE,IAAI,IAAI,KAAK,EAAE,OAAO,IAAI,MAAM,CAAC,KAAK,CAAC,OAAO;SAChF,CAAC;IACJ,CAAC;AACH,CAAC"}