@dommaker/harness 1.10.0 → 1.12.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.
- package/CHANGELOG.md +31 -0
- package/dist/cli/commands/constraints-pack-proposal.d.ts +48 -0
- package/dist/cli/commands/constraints-pack-proposal.d.ts.map +1 -0
- package/dist/cli/commands/constraints-pack-proposal.js +169 -0
- package/dist/cli/commands/constraints-pack-proposal.js.map +1 -0
- package/dist/cli/commands/constraints-reactivate.d.ts +55 -0
- package/dist/cli/commands/constraints-reactivate.d.ts.map +1 -0
- package/dist/cli/commands/constraints-reactivate.js +175 -0
- package/dist/cli/commands/constraints-reactivate.js.map +1 -0
- package/dist/cli/commands/constraints-report.d.ts +12 -1
- package/dist/cli/commands/constraints-report.d.ts.map +1 -1
- package/dist/cli/commands/constraints-report.js +35 -2
- package/dist/cli/commands/constraints-report.js.map +1 -1
- package/dist/cli/commands/constraints-retire.d.ts +49 -4
- package/dist/cli/commands/constraints-retire.d.ts.map +1 -1
- package/dist/cli/commands/constraints-retire.js +116 -34
- package/dist/cli/commands/constraints-retire.js.map +1 -1
- package/dist/cli/commands/definitions.d.ts.map +1 -1
- package/dist/cli/commands/definitions.js +36 -11
- package/dist/cli/commands/definitions.js.map +1 -1
- package/dist/cli/commands/knowledge-view.d.ts +6 -3
- package/dist/cli/commands/knowledge-view.d.ts.map +1 -1
- package/dist/cli/commands/knowledge-view.js +8 -26
- package/dist/cli/commands/knowledge-view.js.map +1 -1
- package/dist/cli/commands/report.d.ts.map +1 -1
- package/dist/cli/commands/report.js +9 -5
- package/dist/cli/commands/report.js.map +1 -1
- package/dist/cli/commands/scaffold-templates.d.ts +1 -1
- package/dist/cli/commands/scaffold-templates.d.ts.map +1 -1
- package/dist/cli/commands/scaffold-templates.js +0 -16
- package/dist/cli/commands/scaffold-templates.js.map +1 -1
- package/dist/cli/state-io.d.ts +8 -1
- package/dist/cli/state-io.d.ts.map +1 -1
- package/dist/cli/state-io.js +2 -1
- package/dist/cli/state-io.js.map +1 -1
- package/dist/core/app-constraints-loader.d.ts +28 -0
- package/dist/core/app-constraints-loader.d.ts.map +1 -0
- package/dist/core/app-constraints-loader.js +187 -0
- package/dist/core/app-constraints-loader.js.map +1 -0
- package/dist/core/constraints/checker.js +1 -1
- package/dist/core/constraints/checker.js.map +1 -1
- package/dist/core/constraints/checkers/index.d.ts +20 -9
- package/dist/core/constraints/checkers/index.d.ts.map +1 -1
- package/dist/core/constraints/checkers/index.js +40 -11
- package/dist/core/constraints/checkers/index.js.map +1 -1
- package/dist/core/constraints/checkers/templated/file-exists.d.ts +16 -0
- package/dist/core/constraints/checkers/templated/file-exists.d.ts.map +1 -0
- package/dist/core/constraints/checkers/templated/file-exists.js +108 -0
- package/dist/core/constraints/checkers/templated/file-exists.js.map +1 -0
- package/dist/core/constraints/checkers/templated/glob.d.ts +10 -0
- package/dist/core/constraints/checkers/templated/glob.d.ts.map +1 -0
- package/dist/core/constraints/checkers/templated/glob.js +51 -0
- package/dist/core/constraints/checkers/templated/glob.js.map +1 -0
- package/dist/core/constraints/checkers/templated/index.d.ts +12 -0
- package/dist/core/constraints/checkers/templated/index.d.ts.map +1 -0
- package/dist/core/constraints/checkers/templated/index.js +17 -0
- package/dist/core/constraints/checkers/templated/index.js.map +1 -0
- package/dist/core/constraints/checkers/templated/regex-scan.d.ts +15 -0
- package/dist/core/constraints/checkers/templated/regex-scan.d.ts.map +1 -0
- package/dist/core/constraints/checkers/templated/regex-scan.js +129 -0
- package/dist/core/constraints/checkers/templated/regex-scan.js.map +1 -0
- package/dist/core/constraints/checkers/templates-registry.d.ts +17 -0
- package/dist/core/constraints/checkers/templates-registry.d.ts.map +1 -0
- package/dist/core/constraints/checkers/templates-registry.js +23 -0
- package/dist/core/constraints/checkers/templates-registry.js.map +1 -0
- package/dist/core/constraints/checkers/types.d.ts +14 -0
- package/dist/core/constraints/checkers/types.d.ts.map +1 -1
- package/dist/core/constraints/checkers/types.js.map +1 -1
- package/dist/core/constraints/definitions/guidelines.d.ts.map +1 -1
- package/dist/core/constraints/definitions/guidelines.js +0 -14
- package/dist/core/constraints/definitions/guidelines.js.map +1 -1
- package/dist/core/constraints/definitions/iron-laws.d.ts.map +1 -1
- package/dist/core/constraints/definitions/iron-laws.js +15 -0
- package/dist/core/constraints/definitions/iron-laws.js.map +1 -1
- package/dist/core/constraints/usage-report.d.ts +59 -7
- package/dist/core/constraints/usage-report.d.ts.map +1 -1
- package/dist/core/constraints/usage-report.js +62 -13
- package/dist/core/constraints/usage-report.js.map +1 -1
- package/dist/core/project-config-loader.d.ts +9 -6
- package/dist/core/project-config-loader.d.ts.map +1 -1
- package/dist/core/project-config-loader.js +18 -8
- package/dist/core/project-config-loader.js.map +1 -1
- package/dist/gates/command.d.ts.map +1 -1
- package/dist/gates/command.js +50 -0
- package/dist/gates/command.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -2
- package/dist/index.js.map +1 -1
- package/dist/knowledge/lifecycle.d.ts +14 -0
- package/dist/knowledge/lifecycle.d.ts.map +1 -1
- package/dist/knowledge/lifecycle.js +50 -0
- package/dist/knowledge/lifecycle.js.map +1 -1
- package/dist/knowledge/lint.d.ts +17 -0
- package/dist/knowledge/lint.d.ts.map +1 -1
- package/dist/knowledge/lint.js +52 -0
- package/dist/knowledge/lint.js.map +1 -1
- package/dist/knowledge/store.d.ts +7 -0
- package/dist/knowledge/store.d.ts.map +1 -1
- package/dist/knowledge/store.js +48 -13
- package/dist/knowledge/store.js.map +1 -1
- package/dist/knowledge/types.d.ts +5 -1
- package/dist/knowledge/types.d.ts.map +1 -1
- package/dist/knowledge/types.js +5 -1
- package/dist/knowledge/types.js.map +1 -1
- package/dist/presets/standard.d.ts +1 -1
- package/dist/presets/standard.d.ts.map +1 -1
- package/dist/presets/standard.js +2 -3
- package/dist/presets/standard.js.map +1 -1
- package/dist/types/constraint.d.ts +24 -0
- package/dist/types/constraint.d.ts.map +1 -1
- package/dist/types/constraint.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/app-constraints-loader.test.ts +220 -0
- package/src/__tests__/iron-laws.test.ts +5 -5
- package/src/__tests__/public-exports.test.ts +7 -0
- package/src/__tests__/public-type-surface.test.ts +5 -0
- package/src/__tests__/usage-report-public-export.test.ts +21 -0
- package/src/cli/commands/CONTEXT.md +5 -4
- package/src/cli/commands/__tests__/check.test.ts +22 -4
- package/src/cli/commands/__tests__/constraints-pack-proposal.test.ts +142 -0
- package/src/cli/commands/__tests__/constraints-reactivate.test.ts +210 -0
- package/src/cli/commands/__tests__/constraints-report-json-flag.test.ts +101 -0
- package/src/cli/commands/__tests__/constraints-report.test.ts +175 -6
- package/src/cli/commands/__tests__/constraints-retire.test.ts +135 -10
- package/src/cli/commands/__tests__/init-ondisk.test.ts +1 -1
- package/src/cli/commands/__tests__/init.test.ts +1 -5
- package/src/cli/commands/__tests__/knowledge.test.ts +6 -16
- package/src/cli/commands/__tests__/numeric-flag-assembly.test.ts +1 -1
- package/src/cli/commands/__tests__/passes-gate-dead-options.test.ts +1 -1
- package/src/cli/commands/__tests__/registry.test.ts +1 -1
- package/src/cli/commands/__tests__/report-app-constraints.test.ts +67 -0
- package/src/cli/commands/constraints-pack-proposal.ts +172 -0
- package/src/cli/commands/constraints-reactivate.ts +197 -0
- package/src/cli/commands/constraints-report.ts +44 -2
- package/src/cli/commands/constraints-retire.ts +129 -36
- package/src/cli/commands/definitions.ts +36 -11
- package/src/cli/commands/knowledge-view.ts +9 -28
- package/src/cli/commands/report.ts +9 -5
- package/src/cli/commands/scaffold-templates.ts +0 -16
- package/src/cli/state-io.ts +8 -1
- package/src/core/CONTEXT.md +5 -4
- package/src/core/app-constraints-loader.ts +165 -0
- package/src/core/constraints/__tests__/collect-constraints.test.ts +3 -1
- package/src/core/constraints/__tests__/usage-report.test.ts +63 -0
- package/src/core/constraints/checker.ts +1 -1
- package/src/core/constraints/checkers/__tests__/templated-checkers.test.ts +180 -0
- package/src/core/constraints/checkers/__tests__/templates-registry.test.ts +81 -0
- package/src/core/constraints/checkers/index.ts +44 -10
- package/src/core/constraints/checkers/templated/file-exists.ts +75 -0
- package/src/core/constraints/checkers/templated/glob.ts +46 -0
- package/src/core/constraints/checkers/templated/index.ts +12 -0
- package/src/core/constraints/checkers/templated/regex-scan.ts +132 -0
- package/src/core/constraints/checkers/templates-registry.ts +22 -0
- package/src/core/constraints/checkers/types.ts +15 -0
- package/src/core/constraints/definitions/guidelines.ts +0 -15
- package/src/core/constraints/definitions/iron-laws.ts +16 -0
- package/src/core/constraints/usage-report.ts +129 -14
- package/src/core/project-config-loader.ts +19 -8
- package/src/gates/__tests__/command.test.ts +54 -0
- package/src/gates/command.ts +51 -0
- package/src/index.ts +20 -0
- package/src/knowledge/CONTEXT.md +7 -4
- package/src/knowledge/__tests__/lifecycle.test.ts +81 -0
- package/src/knowledge/__tests__/lint.test.ts +84 -0
- package/src/knowledge/__tests__/store.test.ts +70 -0
- package/src/knowledge/lifecycle.ts +49 -0
- package/src/knowledge/lint.ts +60 -1
- package/src/knowledge/store.ts +48 -11
- package/src/knowledge/types.ts +7 -1
- package/src/monitoring/CONTEXT.md +1 -1
- package/src/presets/__tests__/standard.test.ts +5 -4
- package/src/presets/standard.ts +2 -3
- package/src/types/constraint.ts +28 -0
- package/dist/cli/commands/posteval-plan.d.ts +0 -15
- package/dist/cli/commands/posteval-plan.d.ts.map +0 -1
- package/dist/cli/commands/posteval-plan.js +0 -86
- package/dist/cli/commands/posteval-plan.js.map +0 -1
- package/src/cli/commands/__tests__/posteval-plan.test.ts +0 -204
- package/src/cli/commands/posteval-plan.ts +0 -98
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# cli/commands/
|
|
2
2
|
|
|
3
3
|
## 职责
|
|
4
|
-
CLI 子命令实现:
|
|
4
|
+
CLI 子命令实现:20 个顶层命令 + constraints 治理子命令 report/retire/reactivate/pack-proposal,覆盖约束检查/门禁验证/文档同步/知识管理/失败诊断/发布等。
|
|
5
5
|
|
|
6
6
|
H5(#44)起:
|
|
7
7
|
- **命令定义即注册**:`definitions.ts` 的 `COMMAND_DEFINITIONS` 是命令形状(名称/别名/位置参数/选项/子命令/实现引用)的单一来源;bin/harness.js 遍历本表注册表驱动生成,不再手写 commander 命令块,不再有 index.ts barrel 两处同步(R6)
|
|
@@ -14,11 +14,12 @@ H5(#44)起:
|
|
|
14
14
|
- 门禁命令共享面(`src/cli/gate-command.ts`,上层目录)— `GateDecision → CommandResult` 的唯一映射 `gateCommandResult`,加 ✓/✗ 输出骨架 `reportGateDecision` 与出错横幅 `reportGateError`。同一句失败措辞此前抄在 6 个 handler 里(「`<id>` gate denied」×6 / 「`<id>` gate error」×4),门禁特有的指标行经 `onPass`/`onFail` 闭包传入(架构评审候选1)
|
|
15
15
|
- 脚手架落盘面(`src/cli/commands/scaffold.ts`)— 受管文件(managed file)三态判定 `writeManagedFile` / 一组落盘 `runPlan` + 8 个站点工厂(含对外文案;ADR-0029:custom-constraints.yml 示例站随 custom 约束面退役移除);模板正文住 `scaffold-templates.ts`,落盘注入面 `ScaffoldFileSystem` 可换内存替身(harness#132);CI 站点工厂 `harnessCheckCiFile` 带平台维度(github / gitlab,harness#143);两道 Git hook 工厂 `preCommitHookFile` / `prePushHookFile`(后者 harness#144,带可执行位)
|
|
16
16
|
- knowledge 投影面(`src/cli/commands/knowledge-view.ts`)— 11 个 knowledge 子操作的**display model** 渲染与出口:`emitKnowledgeView`(json/人读唯一分派 + 退出码)、`announce`(取数期进度行,`--json` 下静默)、`TONE_STYLES` 角色→样式单表、维度/规则 label 与 `toneForMaturity`/`toneForScore`/`toneForSeverity` 三张映射、`resolveKnowledgeBaseDir` + `openKnowledgeStore`(路径兜底与 store 构造单点)(harness#133)
|
|
17
|
-
- 各命令文件:check / validate / passes-gate / init / report / status / spec / sync-docs / knowledge / sdd / failure /
|
|
17
|
+
- 各命令文件:check / validate / passes-gate / init / report / status / spec / sync-docs / knowledge / sdd / failure / release / constraints / spec-baseline-check
|
|
18
|
+
- `report` 的约束面走生效集(ADR-0033 修缺口):collectConstraints 传 getMergedConstraintsConfig 的 merged config(与 check.ts 同一入口),总数/分级计数按生效集——此前跑内置全集,config.yml 禁用与应用层条目(`.harness/constraints.yml`)都进不了报告面
|
|
18
19
|
- 6 门禁命令实现在 `acceptance` / `command` / `contract` / `performance` / `review` / `security`(其 CLI 元数据在 `src/gates/definitions.ts`,形状同为 `CommandDefinition`,ADR-0007);**判定一律穿过统一接口 `evaluate()`**,成败与措辞由共享面映射。`command` 只复用映射、保留自己的单行 ✓/✗ 输出(已对外的机器友好形状);`security audit` / `acceptance list` / `contract validate-schema` / `review status` / `command --list/--level` 是只展示不判断的子命令,直读报告面(`scan()` 等),不套骨架
|
|
19
20
|
- CLI 子命令 `command` 的两个分支共用同一台 gate 实例(#135/ADR-0024):默认分支经 `evaluate()` 取裁决、`--level` 经同一实例的 `getRiskLevel()` 取等级,命令侧不再引用模块级单例出口——「配置传进去却到不了判定」的那条路已封。`--strict` 旗帜随 `CommandGateConfig.strict`(零消费者、从不生效)一并删除
|
|
20
21
|
|
|
21
|
-
`constraints` 下挂治理子命令:`constraints report`(使用统计 + 退役候选诊断 + 配置健康,`--export`
|
|
22
|
+
`constraints` 下挂治理子命令:`constraints report`(使用统计 + 退役候选诊断 + 配置健康,`--export` 脱敏;JSON 输出走 `--json-output`——原名 `--json` 与父命令同名会被 commander 消费在父侧、JSON 分支不可达,E1 复盘修正 M3.1 换名,闸见 `__tests__/constraints-report-json-flag.test.ts`)、`constraints retire`(交互选择 + 人确认退役;带 id 直达需显式 `--yes`(#24 人确认闸门),无 `--yes` 报错 + 非零退出码且不落盘;落 config.yml retired 墓碑 + KnowledgeStore 沉淀——沉淀写口的 baseDir 走 knowledge-view `openKnowledgeStore` 同一解析点,与 knowledge 读口同根,不再硬编码 projectRoot 拼接(harness#177)。ADR-0029:注入漂移小节、prompt 注入清单、custom 落点与治理注入段同步已随文本注入层关停移除。ADR-0032:already_retired 只认 retired 墓碑,裸 enabled:false 由退休流程接管补墓碑+沉淀(票 02 断点 6);落盘后 git 仓内附 commit 提示,不替用户动 git(断点 4)。ADR-0033:findRetireTarget 扩到内置 + 应用层(`.harness/constraints.yml`)——应用层退休墓碑同写 config.yml(enabled:false + retired 同形),constraints.yml 条文保留不删,沉淀条目 tags 加 `source:app`)、`constraints reactivate`(点对点复活:删 retired 墓碑段 + 写 `constraint-reactivated-<id>` 新沉淀,不改历史(ADR-0032 决策 6.5,票 02 断点 5);直达同需 `--yes`,无交互模式;ADR-0033 起对应用层约束同路径可用)、`constraints pack-proposal`(升级提案材料打包,ADR-0033 决策 4,块 3 子项 5:条文 + checker 模板与参数 + traces 使用统计 + 升级理由留白的脱敏 markdown,缺省落 `.harness/reports/proposal-<id>-<YYYYMMDD>.md`,`--stdout` 打印不落盘;脱敏只做项目根路径→`<repoRoot>` 替换,params 原文带出并标注人工确认——打包是质量前置不是保密闸门,人拿材料去 harness 仓开 issue)。
|
|
22
23
|
|
|
23
24
|
## 依赖关系
|
|
24
25
|
- 依赖 `src/core/constraints/` 约束引擎
|
|
@@ -41,7 +42,7 @@ H5(#44)起:
|
|
|
41
42
|
- 行为可检:`__tests__/project-path-anchoring.test.ts` 前提统一 **cwd ≠ projectPath**、不 mock 任何 IO,断言测试真在 B 执行、证据落 `B/.harness/evidence/`、acceptance 读 `B/tasks.yml`、`check --project-path` 的 trace 落 `B/.harness/logs/traces.log` 且 `status` 读得到它(站点 3,#139)。「根已传入却又取 cwd」这类静态看不出的漂移由它守,冻结表守的是不再新增站点
|
|
42
43
|
- **读到部分结果必须告知(harness#100)**:命令面以 `skip` 策略读 JSONL(traces.log / failures.log)时,坏行计数要到用户可见输出,**不允许默默丢弃**。共同约束:无损坏零噪声(计数为 0 不出行)、不改任何既有计数口径(`status` 的 `记录数` 仍是原始非空行数,逐字节不变)、计数一律取自 `src/utils/jsonl` 读链返回的 `skippedLines`(经报告入口透传或在读点就地取用),**禁止**用「原始行数 − 统计条数」反推(窗过滤掉的合法行与坏行分不开)。三处落点各自对得上一行代码:
|
|
43
44
|
- `status`:告知**走 stderr**(`logError(io, …)`),因为 stdout 是报告体、其字节已被 `记录数` 那一行定死,挤进去等于改输出
|
|
44
|
-
- `constraints report`:提示属于**报告体内的降级位**,与既有的 `traceFileExists` 说明同在 stdout;结构化面另两份——`--json` 的 `skippedLines` 字段、`--export` markdown 的警示行(只报条数,不报路径与坏行内容)
|
|
45
|
+
- `constraints report`:提示属于**报告体内的降级位**,与既有的 `traceFileExists` 说明同在 stdout;结构化面另两份——`--json-output` 的 `skippedLines` 字段、`--export` markdown 的警示行(只报条数,不报路径与坏行内容)
|
|
45
46
|
- `constraints retire`:两条路径都**走 stderr**——交互模式在候选列表前一行(沿用本函数既有的 console 例外,`console.error`),`--yes` 直达分支在落盘前 `logError(io, …)`(`retireConstraint` 本身按契约「纯执行无交互」不打印)
|
|
46
47
|
- 原则正本与逐点去向见 `src/monitoring/CONTEXT.md`「约定」段;机器可检面是 `src/utils/__tests__/jsonl-skip-disposition.test.ts`(冻结全仓 skip 读点集合 + 要求每个读点记名声明 `计数去向:`)
|
|
47
48
|
- **一次命令运行内,同一份数据至多取一次(harness#146,#140 A 票;与 ADR-0023 同族,但不扩 `RunEnv` 公共面)**:`spec-baseline-check` 的三条验证路径(文件存在性 / 依赖 / 代码模式)一律从命令级共享索引 `createBaselineIndex(projectPath)` 取数,不再各自裸调 `fs`——全仓 `.ts`/`.js` 内容扫描一遍建成 `路径 → 内容` 表、`package.json` 读解析一次(**读取失败态同样入库**,不逐条重试)、同一落点的存在性探测只 stat 一次;逐条前置与每个关键词只在索引上重放判定。索引**懒建**(没有前置需要某类数据时该类读取一次都不发生)、**运行结束即弃**(不跨命令复用:本命令是独立命令而非 `harness check` 的子步骤,「同一口径取一次」命令内自足即够,#140 triage 裁决 5)。改前代价 = 前置条件数 × 关键词数 × 源文件数,改后与条数无关,判定/证据文案/stdout/退出码逐字不变。**驻留上界(harness#162)**:索引容量有双上限 `SOURCE_INDEX_MAX_ENTRIES = 5000` / `SOURCE_INDEX_MAX_BYTES = 64MB`——超任一上限即放弃驻留、**回落逐文件流式读**(每个关键词重扫一遍,读完即弃),峰值内存由 O(全仓源码) 回到 O(单文件),正常规模仓保留「一次遍历」收益;取数面相应由 `sourceContents(): Map` 改为 `countFilesContaining(keyword)`,容量策略全部收在索引内部。机器可检:`__tests__/spec-baseline-read-count.test.ts`(形状照 `check-read-count.test.ts`:`readFileSync`·只读 `openSync`·`existsSync`·`readdirSync` 记件 + 整张读取表与目录扫描表逐条冻结 + 「同一份数据的同一口径至多取一次」+ N×K 缩放不变式 + 懒建不扫 + 失败态入库)+ `__tests__/spec-baseline-index-cap.test.ts`(驻留上界闸:上限内每文件至多读一遍、超条目/字节上限回落流式且判定不变)
|
|
@@ -165,8 +165,8 @@ describe('check command(真 git fixture)', () => {
|
|
|
165
165
|
io
|
|
166
166
|
);
|
|
167
167
|
|
|
168
|
-
|
|
169
|
-
expect(io.outText()).toContain('✅
|
|
168
|
+
// ADR-0032 起凭证扫描为 error 级:code_implementation 下 error 评 2 条,warning 无命中不输出
|
|
169
|
+
expect(io.outText()).toContain('✅ error 级约束: 全部通过 (2 条)');
|
|
170
170
|
expect(io.outText()).toContain('✅ 约束检查通过');
|
|
171
171
|
expect(result).toEqual({ kind: 'ok' });
|
|
172
172
|
});
|
|
@@ -204,7 +204,7 @@ describe('check command(真 git fixture)', () => {
|
|
|
204
204
|
expect(result.kind).toBe('fail');
|
|
205
205
|
});
|
|
206
206
|
|
|
207
|
-
it('staged 硬编码凭证 →
|
|
207
|
+
it('staged 硬编码凭证 → error 级违规即阻断(ADR-0032 升级)', async () => {
|
|
208
208
|
const dir = gitRepo();
|
|
209
209
|
stageChange(
|
|
210
210
|
dir,
|
|
@@ -220,8 +220,26 @@ describe('check command(真 git fixture)', () => {
|
|
|
220
220
|
io
|
|
221
221
|
);
|
|
222
222
|
|
|
223
|
+
// error 级违规走 throw 契约(#119):异常外溢为 fail,消息即约束 message
|
|
224
|
+
expect(io.outText()).toContain('❌ 约束检查异常: 禁止在代码/文档/配置文件中硬编码密码');
|
|
225
|
+
expect(result.kind).toBe('fail');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('warning 级违规(治理契约缺失)→ 警告但不阻断', async () => {
|
|
229
|
+
const dir = gitRepo();
|
|
230
|
+
// 采用 harness 治理(config.yml 在场)但无 PRESERVE:governance 段 → governance_presence 判 warning
|
|
231
|
+
write(dir, '.harness/config.yml', 'preset: standard\n');
|
|
232
|
+
stageChange(dir, 'src/existing.ts', 'export const a = 2;\n');
|
|
233
|
+
passTraces(dir);
|
|
234
|
+
|
|
235
|
+
// 不显式传 trigger:governance_presence 的触发词是 file_modification,由 diff 推断
|
|
236
|
+
const result = await check(
|
|
237
|
+
{ preset: 'standard', staged: true, projectPath: dir },
|
|
238
|
+
io
|
|
239
|
+
);
|
|
240
|
+
|
|
223
241
|
expect(io.outText()).toContain('⚠️ warning 级约束警告: 1 条');
|
|
224
|
-
expect(io.outText()).toContain('-
|
|
242
|
+
expect(io.outText()).toContain('- governance_presence');
|
|
225
243
|
expect(io.outText()).toContain('✅ 约束检查通过');
|
|
226
244
|
expect(result).toEqual({ kind: 'ok' });
|
|
227
245
|
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* harness constraints pack-proposal 测试(ADR-0033 决策 4,块 3 子项 5)
|
|
3
|
+
*
|
|
4
|
+
* 覆盖:材料内容形状(条文/severity/checker/统计/升级理由留白)、路径脱敏(<repoRoot>)、
|
|
5
|
+
* 应用层约束带模板 id 与 params 原文、未知 id → usage-error、--stdout、缺省落盘路径。
|
|
6
|
+
*
|
|
7
|
+
* 使用真实临时目录(loadAppConstraints / traces 读真实 fs)。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import { captureIO, type CapturingIO } from '../../command-contract';
|
|
13
|
+
import { constraintsPackProposal, renderProposalMarkdown } from '../constraints-pack-proposal';
|
|
14
|
+
import { createProjectFixture, writeProjectTraces } from '../../../test-setup/project-fixture';
|
|
15
|
+
import type { Constraint } from '../../../types/constraint';
|
|
16
|
+
|
|
17
|
+
const APP_YML = [
|
|
18
|
+
'constraints:',
|
|
19
|
+
' - id: app_no_internal_url',
|
|
20
|
+
' rule: Web code must not contain internal URLs',
|
|
21
|
+
' checker: regex-scan',
|
|
22
|
+
' params:',
|
|
23
|
+
" pattern: 'https?://10\\.'",
|
|
24
|
+
" glob: 'apps/web/src/**'",
|
|
25
|
+
' severity: warning',
|
|
26
|
+
' message: 检测到内网地址,请改用环境变量配置',
|
|
27
|
+
'',
|
|
28
|
+
].join('\n');
|
|
29
|
+
|
|
30
|
+
const NOW = new Date('2026-09-22T00:00:00.000Z');
|
|
31
|
+
|
|
32
|
+
let io: CapturingIO;
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
io = captureIO();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('constraintsPackProposal', () => {
|
|
38
|
+
it('内置约束:材料含条文/severity/统计/升级理由留白,缺省落 .harness/reports/proposal-<id>-<YYYYMMDD>.md', async () => {
|
|
39
|
+
const root = createProjectFixture({ name: 'pack-proposal-test' });
|
|
40
|
+
writeProjectTraces(root, [
|
|
41
|
+
{ constraintId: 'no_hardcoded_credentials', result: 'pass', timestamp: 1700000000000 },
|
|
42
|
+
{ constraintId: 'no_hardcoded_credentials', result: 'fail', timestamp: 1700000001000 },
|
|
43
|
+
{ constraintId: 'no_hardcoded_credentials', result: 'skip', timestamp: 1700000002000 },
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
const result = await constraintsPackProposal('no_hardcoded_credentials', { projectPath: root, now: NOW }, io);
|
|
47
|
+
|
|
48
|
+
expect(result.kind).toBe('ok');
|
|
49
|
+
const outPath = path.join(root, '.harness', 'reports', 'proposal-no_hardcoded_credentials-20260922.md');
|
|
50
|
+
expect(fs.existsSync(outPath)).toBe(true);
|
|
51
|
+
|
|
52
|
+
const md = fs.readFileSync(outPath, 'utf-8');
|
|
53
|
+
expect(md).toContain('# 约束升级提案材料:no_hardcoded_credentials');
|
|
54
|
+
expect(md).toContain('- 生成日期: 2026-09-22');
|
|
55
|
+
expect(md).toContain('- 来源: 内置');
|
|
56
|
+
expect(md).toContain('- severity: error');
|
|
57
|
+
expect(md).toContain('## 条文');
|
|
58
|
+
expect(md).toContain('## checker');
|
|
59
|
+
expect(md).toContain('内置定制 checker');
|
|
60
|
+
// 统计:total=3,evaluated=2(skip 不计入),只有计数
|
|
61
|
+
expect(md).toContain('- total: 3');
|
|
62
|
+
expect(md).toContain('- evaluated: 2(pass 1 / fail 1 / skip 1)');
|
|
63
|
+
expect(md).toContain('- 首次触发: 2023-11-14');
|
|
64
|
+
expect(md).toContain('## 升级理由');
|
|
65
|
+
expect(md).toContain('请在此填写');
|
|
66
|
+
// 脱敏:材料不含项目根绝对路径
|
|
67
|
+
expect(md).not.toContain(root);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('应用层约束:checker 段带模板 id 与 params 原文 + 人工确认标注', async () => {
|
|
71
|
+
const root = createProjectFixture({
|
|
72
|
+
name: 'pack-proposal-test',
|
|
73
|
+
files: { [path.join('.harness', 'constraints.yml')]: APP_YML },
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const result = await constraintsPackProposal('app_no_internal_url', { projectPath: root, stdout: true, now: NOW }, io);
|
|
77
|
+
|
|
78
|
+
expect(result.kind).toBe('ok');
|
|
79
|
+
const out = io.outText();
|
|
80
|
+
expect(out).toContain('- 来源: 应用层(.harness/constraints.yml)');
|
|
81
|
+
expect(out).toContain('模板: `regex-scan`');
|
|
82
|
+
expect(out).toContain('pattern:');
|
|
83
|
+
expect(out).toContain('https?://10\\.');
|
|
84
|
+
expect(out).toContain("glob: apps/web/src/**");
|
|
85
|
+
expect(out).toContain('请人工确认无应用内部信息');
|
|
86
|
+
expect(out).toContain('message: 检测到内网地址,请改用环境变量配置');
|
|
87
|
+
// --stdout:不落盘
|
|
88
|
+
expect(fs.existsSync(path.join(root, '.harness', 'reports'))).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('路径脱敏:params/条文中出现的项目根绝对路径替换为 <repoRoot>', () => {
|
|
92
|
+
const root = '/home/someone/my-app';
|
|
93
|
+
const constraint: Constraint = {
|
|
94
|
+
id: 'app_x',
|
|
95
|
+
kind: 'check',
|
|
96
|
+
rule: '不得引用 /home/someone/my-app/secrets 下的文件',
|
|
97
|
+
message: 'm',
|
|
98
|
+
severity: 'warning',
|
|
99
|
+
trigger: [],
|
|
100
|
+
enforcement: '',
|
|
101
|
+
source: 'app',
|
|
102
|
+
checker: 'file-exists',
|
|
103
|
+
params: { path: '/home/someone/my-app/.harness/config.yml' },
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const md = renderProposalMarkdown(
|
|
107
|
+
{ constraint, stats: { total: 0, pass: 0, fail: 0, skip: 0, evaluated: 0 } },
|
|
108
|
+
root,
|
|
109
|
+
NOW
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
expect(md).not.toContain(root);
|
|
113
|
+
expect(md).toContain('<repoRoot>/secrets');
|
|
114
|
+
expect(md).toContain('<repoRoot>/.harness/config.yml');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('未知 id → usage-error,不落盘', async () => {
|
|
118
|
+
const root = createProjectFixture({ name: 'pack-proposal-test' });
|
|
119
|
+
|
|
120
|
+
const result = await constraintsPackProposal('ghost_constraint', { projectPath: root }, io);
|
|
121
|
+
|
|
122
|
+
expect(result.kind).toBe('usage-error');
|
|
123
|
+
expect(result.kind === 'usage-error' && result.reason).toContain('ghost_constraint');
|
|
124
|
+
expect(io.errText()).toContain('未知约束 id');
|
|
125
|
+
expect(fs.existsSync(path.join(root, '.harness', 'reports'))).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('缺 id → usage-error', async () => {
|
|
129
|
+
const result = await constraintsPackProposal(undefined, { projectPath: '/nonexistent' }, io);
|
|
130
|
+
expect(result.kind).toBe('usage-error');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('无 trace 时统计为零计数且不报错', async () => {
|
|
134
|
+
const root = createProjectFixture({ name: 'pack-proposal-test' });
|
|
135
|
+
|
|
136
|
+
await constraintsPackProposal('docs_freshness', { projectPath: root, stdout: true, now: NOW }, io);
|
|
137
|
+
|
|
138
|
+
const out = io.outText();
|
|
139
|
+
expect(out).toContain('- total: 0');
|
|
140
|
+
expect(out).toContain('(无记录)');
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* harness constraints reactivate 测试(ADR-0032 决策 6.5,票 02 断点 5)
|
|
3
|
+
*
|
|
4
|
+
* 执行逻辑(reactivateConstraint,纯函数化):墓碑段删除、复活沉淀条目、
|
|
5
|
+
* not_retired / unknown_id 保护、旧 retired 沉淀不改历史。
|
|
6
|
+
* CLI 直达(constraintsReactivate):--yes 人确认闸门。
|
|
7
|
+
*
|
|
8
|
+
* 使用真实临时目录。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import { captureIO, type CapturingIO } from '../../command-contract';
|
|
13
|
+
import * as os from 'os';
|
|
14
|
+
import * as path from 'path';
|
|
15
|
+
import * as yaml from 'js-yaml';
|
|
16
|
+
import { getEffectiveConstraints } from '../../../core/effective-constraints';
|
|
17
|
+
import { FileKnowledgeStore } from '../../../knowledge/store';
|
|
18
|
+
import { retireConstraint } from '../constraints-retire';
|
|
19
|
+
import { reactivateConstraint, constraintsReactivate, printReactivateResult } from '../constraints-reactivate';
|
|
20
|
+
import { createProjectFixture, writeProjectConfig } from '../../../test-setup/project-fixture';
|
|
21
|
+
|
|
22
|
+
const FIXED_NOW = new Date('2026-08-08T12:00:00.000Z');
|
|
23
|
+
const REACTIVATE_NOW = new Date('2026-09-01T08:00:00.000Z');
|
|
24
|
+
|
|
25
|
+
function readConfig(root: string): any {
|
|
26
|
+
return yaml.load(fs.readFileSync(path.join(root, '.harness', 'config.yml'), 'utf-8'));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let io: CapturingIO;
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
io = captureIO();
|
|
32
|
+
// 复活沉淀写口与 retire 同一解析点:不隔离会写进真实用户主目录
|
|
33
|
+
process.env.KNOWLEDGE_BASE_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-reactivate-kb-'));
|
|
34
|
+
});
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
fs.rmSync(process.env.KNOWLEDGE_BASE_DIR!, { recursive: true, force: true });
|
|
37
|
+
delete process.env.KNOWLEDGE_BASE_DIR;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('reactivateConstraint 执行逻辑', () => {
|
|
41
|
+
it('复活落盘:config.yml 墓碑段删除,约束回到生效集', () => {
|
|
42
|
+
const root = createProjectFixture({ name: 'harness-reactivate-test' });
|
|
43
|
+
retireConstraint(root, 'capability_sync', { reason: '先退', now: FIXED_NOW });
|
|
44
|
+
expect(getEffectiveConstraints(root).some(c => c.id === 'capability_sync')).toBe(false);
|
|
45
|
+
|
|
46
|
+
const result = reactivateConstraint(root, 'capability_sync', { reason: '误退', now: REACTIVATE_NOW });
|
|
47
|
+
|
|
48
|
+
expect(result.status).toBe('reactivated');
|
|
49
|
+
const config = readConfig(root);
|
|
50
|
+
expect(config.constraints?.capability_sync).toBeUndefined();
|
|
51
|
+
expect(getEffectiveConstraints(root).some(c => c.id === 'capability_sync')).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('KnowledgeStore 写复活条目:引用原 retired 条目 + 复活原因 + signal 模式;旧沉淀不改历史', () => {
|
|
55
|
+
const root = createProjectFixture({ name: 'harness-reactivate-test' });
|
|
56
|
+
retireConstraint(root, 'capability_sync', { reason: '先退', now: FIXED_NOW });
|
|
57
|
+
|
|
58
|
+
const result = reactivateConstraint(root, 'capability_sync', { reason: '模型能力回退,仍需兜底', now: REACTIVATE_NOW });
|
|
59
|
+
|
|
60
|
+
expect(result.status).toBe('reactivated');
|
|
61
|
+
expect(result.knowledgeEntryId).toBe('constraint-reactivated-capability_sync');
|
|
62
|
+
expect(result.knowledgeBaseDir).toBe(process.env.KNOWLEDGE_BASE_DIR);
|
|
63
|
+
|
|
64
|
+
const store = new FileKnowledgeStore({ baseDir: process.env.KNOWLEDGE_BASE_DIR! });
|
|
65
|
+
const entry = store.get('constraint-reactivated-capability_sync');
|
|
66
|
+
expect(entry).toBeDefined();
|
|
67
|
+
expect(entry!.consumptionMode).toBe('signal');
|
|
68
|
+
expect(entry!.origin).toBe('human');
|
|
69
|
+
expect(entry!.tags).toContain('constraint-reactivated');
|
|
70
|
+
expect(entry!.tags).toContain('constraint:capability_sync');
|
|
71
|
+
expect(entry!.content).toContain('constraint-retired-capability_sync');
|
|
72
|
+
expect(entry!.content).toContain('模型能力回退,仍需兜底');
|
|
73
|
+
expect(entry!.content).toContain(REACTIVATE_NOW.toISOString());
|
|
74
|
+
|
|
75
|
+
// 不改历史:旧 retired 沉淀原样保留(原因仍是"先退")
|
|
76
|
+
const retired = store.get('constraint-retired-capability_sync');
|
|
77
|
+
expect(retired).toBeDefined();
|
|
78
|
+
expect(retired!.content).toContain('先退');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('裸 disable(enabled:false 无 retired 墓碑):not_retired,不落盘不吞配置', () => {
|
|
82
|
+
const root = createProjectFixture({
|
|
83
|
+
name: 'harness-reactivate-test',
|
|
84
|
+
config: 'constraints:\n capability_sync:\n enabled: false\n',
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const result = reactivateConstraint(root, 'capability_sync', { now: REACTIVATE_NOW });
|
|
88
|
+
|
|
89
|
+
expect(result.status).toBe('not_retired');
|
|
90
|
+
// 配置原样保留(裸 disable 的处置权在用户,reactivate 不代为"清理")
|
|
91
|
+
expect(readConfig(root).constraints.capability_sync.enabled).toBe(false);
|
|
92
|
+
const store = new FileKnowledgeStore({ baseDir: process.env.KNOWLEDGE_BASE_DIR! });
|
|
93
|
+
expect(store.get('constraint-reactivated-capability_sync')).toBeUndefined();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('从未配置的约束:not_retired,不落盘任何文件', () => {
|
|
97
|
+
const root = createProjectFixture({ name: 'harness-reactivate-test' });
|
|
98
|
+
|
|
99
|
+
const result = reactivateConstraint(root, 'capability_sync', { now: REACTIVATE_NOW });
|
|
100
|
+
|
|
101
|
+
expect(result.status).toBe('not_retired');
|
|
102
|
+
expect(fs.existsSync(path.join(root, '.harness'))).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('未知 id:unknown_id,不落盘任何文件', () => {
|
|
106
|
+
const root = createProjectFixture({ name: 'harness-reactivate-test' });
|
|
107
|
+
const result = reactivateConstraint(root, 'not_a_constraint', { now: REACTIVATE_NOW });
|
|
108
|
+
expect(result.status).toBe('unknown_id');
|
|
109
|
+
expect(fs.existsSync(path.join(root, '.harness'))).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('复活结果打印:git 仓内附 commit 提示,非 git 目录不提示(票 02 断点 4)', () => {
|
|
113
|
+
const gitRoot = createProjectFixture({ name: 'harness-reactivate-test', files: { '.git/HEAD': 'ref: refs/heads/master\n' } });
|
|
114
|
+
retireConstraint(gitRoot, 'capability_sync', { now: FIXED_NOW });
|
|
115
|
+
const inGit = reactivateConstraint(gitRoot, 'capability_sync', { now: REACTIVATE_NOW });
|
|
116
|
+
printReactivateResult(inGit, io, gitRoot);
|
|
117
|
+
expect(io.outText()).toContain('git add .harness/config.yml');
|
|
118
|
+
|
|
119
|
+
const plainRoot = createProjectFixture({ name: 'harness-reactivate-test' });
|
|
120
|
+
retireConstraint(plainRoot, 'capability_sync', { now: FIXED_NOW });
|
|
121
|
+
const plainIo = captureIO();
|
|
122
|
+
const notInGit = reactivateConstraint(plainRoot, 'capability_sync', { now: REACTIVATE_NOW });
|
|
123
|
+
printReactivateResult(notInGit, plainIo, plainRoot);
|
|
124
|
+
expect(plainIo.outText()).not.toContain('git add');
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('constraintsReactivate 非交互直达', () => {
|
|
129
|
+
it('无 --yes 直达:报错 + 非零退出码 + 不落盘任何文件(人确认闸门)', async () => {
|
|
130
|
+
const root = createProjectFixture({ name: 'harness-reactivate-test' });
|
|
131
|
+
retireConstraint(root, 'capability_sync', { now: FIXED_NOW });
|
|
132
|
+
|
|
133
|
+
const result = await constraintsReactivate('capability_sync', { projectPath: root, reason: '误退' }, io);
|
|
134
|
+
|
|
135
|
+
expect(io.errText()).toContain('--yes');
|
|
136
|
+
expect(result.kind).toBe('usage-error');
|
|
137
|
+
// 墓碑原样在,复活条目未写
|
|
138
|
+
expect(readConfig(root).constraints.capability_sync.retired).toBeDefined();
|
|
139
|
+
const store = new FileKnowledgeStore({ baseDir: process.env.KNOWLEDGE_BASE_DIR! });
|
|
140
|
+
expect(store.get('constraint-reactivated-capability_sync')).toBeUndefined();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('--yes 直达复活:落盘并打印结果', async () => {
|
|
144
|
+
const root = createProjectFixture({ name: 'harness-reactivate-test' });
|
|
145
|
+
retireConstraint(root, 'capability_sync', { reason: '先退', now: FIXED_NOW });
|
|
146
|
+
|
|
147
|
+
const result = await constraintsReactivate('capability_sync', { projectPath: root, reason: '误退', yes: true }, io);
|
|
148
|
+
|
|
149
|
+
expect(result.kind).toBe('ok');
|
|
150
|
+
expect(io.outText()).toContain('已复活');
|
|
151
|
+
expect(readConfig(root).constraints?.capability_sync).toBeUndefined();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('--yes 直达无墓碑:明确提示 not_retired 语义', async () => {
|
|
155
|
+
const root = createProjectFixture({ name: 'harness-reactivate-test' });
|
|
156
|
+
writeProjectConfig(root, 'constraints:\n capability_sync:\n enabled: false\n');
|
|
157
|
+
|
|
158
|
+
const result = await constraintsReactivate('capability_sync', { projectPath: root, yes: true }, io);
|
|
159
|
+
|
|
160
|
+
expect(result.kind).toBe('skip');
|
|
161
|
+
expect(io.outText()).toContain('无 retired 墓碑');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('缺 id:usage-error 提示用法', async () => {
|
|
165
|
+
const result = await constraintsReactivate(undefined, { yes: true }, io);
|
|
166
|
+
expect(result.kind).toBe('usage-error');
|
|
167
|
+
expect(io.errText()).toContain('用法');
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('应用层约束复活(ADR-0033 块 3 子项 3)', () => {
|
|
172
|
+
const APP_YML = [
|
|
173
|
+
'constraints:',
|
|
174
|
+
' - id: app_no_internal_url',
|
|
175
|
+
' rule: Web code must not contain internal URLs',
|
|
176
|
+
' checker: regex-scan',
|
|
177
|
+
' params:',
|
|
178
|
+
" pattern: 'https?://10\\.'",
|
|
179
|
+
' severity: warning',
|
|
180
|
+
'',
|
|
181
|
+
].join('\n');
|
|
182
|
+
|
|
183
|
+
function appFixture(): string {
|
|
184
|
+
return createProjectFixture({
|
|
185
|
+
name: 'harness-reactivate-test',
|
|
186
|
+
files: { [path.join('.harness', 'constraints.yml')]: APP_YML },
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
it('退休后可复活:删 config.yml 墓碑,约束回生效集(constraints.yml 条文一直在)', () => {
|
|
191
|
+
const root = appFixture();
|
|
192
|
+
retireConstraint(root, 'app_no_internal_url', { reason: '先退', now: FIXED_NOW });
|
|
193
|
+
expect(getEffectiveConstraints(root).some(c => c.id === 'app_no_internal_url')).toBe(false);
|
|
194
|
+
|
|
195
|
+
const result = reactivateConstraint(root, 'app_no_internal_url', { reason: '误退', now: REACTIVATE_NOW });
|
|
196
|
+
|
|
197
|
+
expect(result.status).toBe('reactivated');
|
|
198
|
+
expect(readConfig(root).constraints?.app_no_internal_url).toBeUndefined();
|
|
199
|
+
expect(getEffectiveConstraints(root).some(c => c.id === 'app_no_internal_url')).toBe(true);
|
|
200
|
+
// 复活沉淀照写
|
|
201
|
+
const store = new FileKnowledgeStore({ baseDir: process.env.KNOWLEDGE_BASE_DIR! });
|
|
202
|
+
expect(store.get('constraint-reactivated-app_no_internal_url')).toBeDefined();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('应用层未退休(无墓碑)→ not_retired', () => {
|
|
206
|
+
const root = appFixture();
|
|
207
|
+
const result = reactivateConstraint(root, 'app_no_internal_url', { now: REACTIVATE_NOW });
|
|
208
|
+
expect(result.status).toBe('not_retired');
|
|
209
|
+
});
|
|
210
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* constraints report --json-output 测试(E1 复盘修正 M3.1)
|
|
3
|
+
*
|
|
4
|
+
* 复现的 bug:父命令 constraints 与子命令 report 同名 --json 相撞
|
|
5
|
+
* (definitions.ts 两处注册),commander 把 flag 消费在父命令上,
|
|
6
|
+
* report 的 JSON 分支(constraints-report.ts)不可达——
|
|
7
|
+
* `constraints report --json` 输出的仍是文本格式。
|
|
8
|
+
*
|
|
9
|
+
* 修法(计划允许二选一):子命令换名 --json-output。
|
|
10
|
+
* 理由:父命令 --json 有活跃消费者(studio-agent 取约束 hash),
|
|
11
|
+
* 删除会静默降级其取数;子命令 --json 因本 bug 从未可达、零消费者,
|
|
12
|
+
* 换名零破坏。
|
|
13
|
+
*
|
|
14
|
+
* 两层证据:
|
|
15
|
+
* 1. 定义表不变式(无 dist 依赖):父子命令不得注册同名 option flag
|
|
16
|
+
* (commander 会把子命令位置的同名 flag 消费在父命令上);
|
|
17
|
+
* 2. 端到端 spawn bin/harness.js(dist 存在时):--json-output 真实到达
|
|
18
|
+
* report 的 JSON 分支,父命令 --json 行为不变。
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import * as fs from 'fs';
|
|
22
|
+
import * as os from 'os';
|
|
23
|
+
import * as path from 'path';
|
|
24
|
+
import { spawnSync } from 'child_process';
|
|
25
|
+
import { COMMAND_DEFINITIONS } from '../definitions';
|
|
26
|
+
import { GATE_DEFINITIONS } from '../../../gates/definitions';
|
|
27
|
+
|
|
28
|
+
/** 从 flags 串提取长选项名:'-p, --project-path <path>' → '--project-path' */
|
|
29
|
+
function longFlags(flags: string): string[] {
|
|
30
|
+
return [...flags.matchAll(/--[a-z][a-z-]*/g)].map(m => m[0]);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('定义表不变式:父子命令不得注册同名 option flag', () => {
|
|
34
|
+
const defs = [
|
|
35
|
+
...COMMAND_DEFINITIONS,
|
|
36
|
+
...GATE_DEFINITIONS.filter(g => g.cli).map(g => g.cli!),
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
it('constraints report 与父命令无同名 flag(M3.1 复现场景)', () => {
|
|
40
|
+
const constraints = defs.find(d => d.command === 'constraints')!;
|
|
41
|
+
const parentFlags = new Set((constraints.options ?? []).flatMap(o => longFlags(o.flags)));
|
|
42
|
+
for (const child of constraints.children ?? []) {
|
|
43
|
+
const childFlags = (child.options ?? []).flatMap(o => longFlags(o.flags));
|
|
44
|
+
const collisions = childFlags.filter(f => parentFlags.has(f));
|
|
45
|
+
expect(collisions).toEqual([]);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('全表泛化:任何带 children 的命令都与子命令 flag 不相交', () => {
|
|
50
|
+
for (const def of defs) {
|
|
51
|
+
if (!def.children || def.children.length === 0) continue;
|
|
52
|
+
const parentFlags = new Set((def.options ?? []).flatMap(o => longFlags(o.flags)));
|
|
53
|
+
for (const child of def.children) {
|
|
54
|
+
const childFlags = (child.options ?? []).flatMap(o => longFlags(o.flags));
|
|
55
|
+
const collisions = childFlags.filter(f => parentFlags.has(f));
|
|
56
|
+
expect(collisions).toEqual([]);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// dist 缺失时显式失败而非静默 skip(同 bin-exit-mapping.test.ts 口径,harness#99)
|
|
63
|
+
const repoRoot = path.join(__dirname, '..', '..', '..', '..');
|
|
64
|
+
const distBin = path.join(repoRoot, 'bin', 'harness.js');
|
|
65
|
+
const hasDist = fs.existsSync(path.join(repoRoot, 'dist', 'cli', 'commands'));
|
|
66
|
+
|
|
67
|
+
if (!hasDist && process.env.CI) {
|
|
68
|
+
throw new Error('dist 未构建:bin 端到端 smoke 不得静默 skip —— CI 必须先 npm run build 再 npm test(harness#99)');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const smoke = hasDist ? describe : describe.skip;
|
|
72
|
+
|
|
73
|
+
smoke('端到端:constraints report --json-output 到达 JSON 分支', () => {
|
|
74
|
+
const run = (argv: string[], cwd: string) => spawnSync(process.execPath, [distBin, ...argv], {
|
|
75
|
+
cwd,
|
|
76
|
+
encoding: 'utf-8',
|
|
77
|
+
timeout: 60000,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('report --json-output → stdout 是报告 JSON(含 stats),退出码 0', () => {
|
|
81
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-m31-'));
|
|
82
|
+
try {
|
|
83
|
+
const r = run(['constraints', 'report', '--json-output'], dir);
|
|
84
|
+
expect(r.status).toBe(0);
|
|
85
|
+
const report = JSON.parse(r.stdout);
|
|
86
|
+
expect(Array.isArray(report.stats)).toBe(true);
|
|
87
|
+
expect(report.stats.length).toBeGreaterThan(0);
|
|
88
|
+
expect(Array.isArray(report.candidates)).toBe(true);
|
|
89
|
+
} finally {
|
|
90
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('父命令 constraints --json 行为不变(活跃消费者面)', () => {
|
|
95
|
+
const r = run(['constraints', '--json'], repoRoot);
|
|
96
|
+
expect(r.status).toBe(0);
|
|
97
|
+
const meta = JSON.parse(r.stdout);
|
|
98
|
+
expect(typeof meta.hash).toBe('string');
|
|
99
|
+
expect(meta.counts).toBeDefined();
|
|
100
|
+
});
|
|
101
|
+
});
|