@dommaker/harness 0.16.8 → 0.17.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 +41 -0
- package/README.md +33 -32
- package/bin/harness.js +34 -20
- package/dist/cli/commands/check.d.ts +3 -2
- package/dist/cli/commands/check.d.ts.map +1 -1
- package/dist/cli/commands/check.js +69 -36
- package/dist/cli/commands/check.js.map +1 -1
- package/dist/cli/commands/constraints-report.d.ts +42 -0
- package/dist/cli/commands/constraints-report.d.ts.map +1 -0
- package/dist/cli/commands/constraints-report.js +233 -0
- package/dist/cli/commands/constraints-report.js.map +1 -0
- package/dist/cli/commands/constraints-retire.d.ts +71 -0
- package/dist/cli/commands/constraints-retire.d.ts.map +1 -0
- package/dist/cli/commands/constraints-retire.js +384 -0
- package/dist/cli/commands/constraints-retire.js.map +1 -0
- package/dist/cli/commands/constraints.d.ts +1 -1
- package/dist/cli/commands/constraints.d.ts.map +1 -1
- package/dist/cli/commands/constraints.js +4 -4
- package/dist/cli/commands/constraints.js.map +1 -1
- package/dist/cli/commands/index.d.ts +2 -1
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +10 -4
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/init.d.ts +20 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +81 -59
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/report.js +4 -4
- package/dist/cli/commands/report.js.map +1 -1
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +3 -4
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/commands/sync-docs/capabilities-syncer.d.ts +19 -2
- package/dist/cli/commands/sync-docs/capabilities-syncer.d.ts.map +1 -1
- package/dist/cli/commands/sync-docs/capabilities-syncer.js +79 -19
- package/dist/cli/commands/sync-docs/capabilities-syncer.js.map +1 -1
- package/dist/cli/commands/sync-docs/index.d.ts +2 -0
- package/dist/cli/commands/sync-docs/index.d.ts.map +1 -1
- package/dist/cli/commands/sync-docs/index.js +83 -20
- package/dist/cli/commands/sync-docs/index.js.map +1 -1
- package/dist/cli/commands/validate.d.ts.map +1 -1
- package/dist/cli/commands/validate.js +1 -5
- package/dist/cli/commands/validate.js.map +1 -1
- package/dist/core/constraints/capabilities-parser.d.ts +14 -0
- package/dist/core/constraints/capabilities-parser.d.ts.map +1 -1
- package/dist/core/constraints/capabilities-parser.js +30 -0
- package/dist/core/constraints/capabilities-parser.js.map +1 -1
- package/dist/core/constraints/checker.d.ts +13 -5
- package/dist/core/constraints/checker.d.ts.map +1 -1
- package/dist/core/constraints/checker.js +62 -25
- package/dist/core/constraints/checker.js.map +1 -1
- package/dist/core/constraints/checkers/capability-sync.d.ts +10 -0
- package/dist/core/constraints/checkers/capability-sync.d.ts.map +1 -1
- package/dist/core/constraints/checkers/capability-sync.js +73 -11
- package/dist/core/constraints/checkers/capability-sync.js.map +1 -1
- package/dist/core/constraints/checkers/context-doc-sync.d.ts +3 -0
- package/dist/core/constraints/checkers/context-doc-sync.d.ts.map +1 -1
- package/dist/core/constraints/checkers/context-doc-sync.js +8 -2
- package/dist/core/constraints/checkers/context-doc-sync.js.map +1 -1
- package/dist/core/constraints/checkers/docs-freshness.d.ts +1 -0
- package/dist/core/constraints/checkers/docs-freshness.d.ts.map +1 -1
- package/dist/core/constraints/checkers/docs-freshness.js +39 -0
- package/dist/core/constraints/checkers/docs-freshness.js.map +1 -1
- package/dist/core/constraints/checkers/index.d.ts +8 -4
- package/dist/core/constraints/checkers/index.d.ts.map +1 -1
- package/dist/core/constraints/checkers/index.js +29 -46
- package/dist/core/constraints/checkers/index.js.map +1 -1
- package/dist/core/constraints/checkers/iron-flags.d.ts +6 -7
- package/dist/core/constraints/checkers/iron-flags.d.ts.map +1 -1
- package/dist/core/constraints/checkers/iron-flags.js +10 -17
- package/dist/core/constraints/checkers/iron-flags.js.map +1 -1
- package/dist/core/constraints/checkers/no-hardcoded-credentials.d.ts +13 -0
- package/dist/core/constraints/checkers/no-hardcoded-credentials.d.ts.map +1 -0
- package/dist/core/constraints/checkers/no-hardcoded-credentials.js +91 -0
- package/dist/core/constraints/checkers/no-hardcoded-credentials.js.map +1 -0
- package/dist/core/constraints/checkers/types.d.ts +15 -4
- package/dist/core/constraints/checkers/types.d.ts.map +1 -1
- package/dist/core/constraints/checkers/types.js +16 -4
- package/dist/core/constraints/checkers/types.js.map +1 -1
- package/dist/core/constraints/context-builder.d.ts +6 -3
- package/dist/core/constraints/context-builder.d.ts.map +1 -1
- package/dist/core/constraints/context-builder.js +29 -20
- package/dist/core/constraints/context-builder.js.map +1 -1
- package/dist/core/constraints/definitions/guidelines.d.ts +6 -2
- package/dist/core/constraints/definitions/guidelines.d.ts.map +1 -1
- package/dist/core/constraints/definitions/guidelines.js +33 -574
- package/dist/core/constraints/definitions/guidelines.js.map +1 -1
- package/dist/core/constraints/definitions/iron-laws.d.ts +6 -2
- package/dist/core/constraints/definitions/iron-laws.d.ts.map +1 -1
- package/dist/core/constraints/definitions/iron-laws.js +48 -231
- package/dist/core/constraints/definitions/iron-laws.js.map +1 -1
- package/dist/core/constraints/definitions/prompts.d.ts +12 -0
- package/dist/core/constraints/definitions/prompts.d.ts.map +1 -0
- package/dist/core/constraints/definitions/prompts.js +405 -0
- package/dist/core/constraints/definitions/prompts.js.map +1 -0
- package/dist/core/constraints/definitions.d.ts +15 -9
- package/dist/core/constraints/definitions.d.ts.map +1 -1
- package/dist/core/constraints/definitions.js +18 -12
- package/dist/core/constraints/definitions.js.map +1 -1
- package/dist/core/constraints/index.d.ts +3 -2
- package/dist/core/constraints/index.d.ts.map +1 -1
- package/dist/core/constraints/index.js +8 -2
- package/dist/core/constraints/index.js.map +1 -1
- package/dist/core/constraints/injection-drift.d.ts +48 -0
- package/dist/core/constraints/injection-drift.d.ts.map +1 -0
- package/dist/core/constraints/injection-drift.js +140 -0
- package/dist/core/constraints/injection-drift.js.map +1 -0
- package/dist/core/constraints/injection-renderer.d.ts +23 -0
- package/dist/core/constraints/injection-renderer.d.ts.map +1 -0
- package/dist/core/constraints/injection-renderer.js +44 -0
- package/dist/core/constraints/injection-renderer.js.map +1 -0
- package/dist/core/constraints/interceptor.d.ts.map +1 -1
- package/dist/core/constraints/interceptor.js +3 -2
- package/dist/core/constraints/interceptor.js.map +1 -1
- package/dist/core/constraints/usage-report.d.ts +95 -0
- package/dist/core/constraints/usage-report.d.ts.map +1 -0
- package/dist/core/constraints/usage-report.js +206 -0
- package/dist/core/constraints/usage-report.js.map +1 -0
- package/dist/core/effective-constraints.d.ts +47 -0
- package/dist/core/effective-constraints.d.ts.map +1 -0
- package/dist/core/effective-constraints.js +60 -0
- package/dist/core/effective-constraints.js.map +1 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/project-config-loader.d.ts +28 -3
- package/dist/core/project-config-loader.d.ts.map +1 -1
- package/dist/core/project-config-loader.js +74 -8
- package/dist/core/project-config-loader.js.map +1 -1
- package/dist/dashboard/data.d.ts +1 -9
- package/dist/dashboard/data.d.ts.map +1 -1
- package/dist/dashboard/data.js +7 -15
- package/dist/dashboard/data.js.map +1 -1
- package/dist/dashboard/types.d.ts +13 -1
- package/dist/dashboard/types.d.ts.map +1 -1
- package/dist/index.d.ts +0 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -13
- package/dist/index.js.map +1 -1
- package/dist/monitoring/index.d.ts +0 -2
- package/dist/monitoring/index.d.ts.map +1 -1
- package/dist/monitoring/index.js +0 -2
- package/dist/monitoring/index.js.map +1 -1
- package/dist/monitoring/knowledge-evolver.d.ts +1 -1
- package/dist/monitoring/knowledge-evolver.js +1 -1
- package/dist/monitoring/trace-analyzer.d.ts +1 -1
- package/dist/monitoring/trace-analyzer.d.ts.map +1 -1
- package/dist/monitoring/trace-analyzer.js +16 -10
- package/dist/monitoring/trace-analyzer.js.map +1 -1
- package/dist/monitoring/traces.d.ts +2 -1
- package/dist/monitoring/traces.d.ts.map +1 -1
- package/dist/monitoring/traces.js +5 -3
- package/dist/monitoring/traces.js.map +1 -1
- package/dist/presets/standard.d.ts +3 -1
- package/dist/presets/standard.d.ts.map +1 -1
- package/dist/presets/standard.js +18 -8
- package/dist/presets/standard.js.map +1 -1
- package/dist/types/constraint.d.ts +51 -11
- package/dist/types/constraint.d.ts.map +1 -1
- package/dist/types/constraint.js +8 -4
- package/dist/types/constraint.js.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/monitoring-types.d.ts +2 -63
- package/dist/types/monitoring-types.d.ts.map +1 -1
- package/dist/types/monitoring-types.js +2 -2
- package/dist/types/project-config.d.ts +32 -1
- package/dist/types/project-config.d.ts.map +1 -1
- package/dist/types/trace.d.ts +16 -7
- package/dist/types/trace.d.ts.map +1 -1
- package/dist/types/trace.js +8 -0
- package/dist/types/trace.js.map +1 -1
- package/package.json +1 -1
- package/src/CONTEXT.md +2 -2
- package/src/__tests__/checker-extra.test.ts +356 -331
- package/src/__tests__/checker.test.ts +87 -724
- package/src/__tests__/constraint-doctor.test.ts +5 -5
- package/src/__tests__/doctor-extra.test.ts +164 -0
- package/src/__tests__/effective-constraints.test.ts +147 -0
- package/src/__tests__/extend-exceptions.test.ts +3 -3
- package/src/__tests__/iron-laws.test.ts +115 -91
- package/src/__tests__/no-hardcoded-credentials.test.ts +90 -0
- package/src/__tests__/presets.test.ts +7 -7
- package/src/__tests__/project-config-loader-extra.test.ts +4 -4
- package/src/__tests__/project-config-loader.test.ts +40 -1
- package/src/__tests__/skip-semantics.test.ts +322 -0
- package/src/cli/commands/CONTEXT.md +5 -3
- package/src/cli/commands/__tests__/check-drift.test.ts +127 -0
- package/src/cli/commands/__tests__/check-skip-output.test.ts +128 -0
- package/src/cli/commands/__tests__/check.test.ts +14 -25
- package/src/cli/commands/__tests__/constraints-report.test.ts +327 -0
- package/src/cli/commands/__tests__/constraints-retire.test.ts +283 -0
- package/src/cli/commands/__tests__/init-injection.test.ts +234 -0
- package/src/cli/commands/__tests__/status-extra.test.ts +11 -11
- package/src/cli/commands/__tests__/status.test.ts +1 -1
- package/src/cli/commands/__tests__/sync-docs.test.ts +163 -0
- package/src/cli/commands/check.ts +72 -38
- package/src/cli/commands/constraints-report.ts +234 -0
- package/src/cli/commands/constraints-retire.ts +443 -0
- package/src/cli/commands/constraints.ts +5 -5
- package/src/cli/commands/index.ts +2 -1
- package/src/cli/commands/init.ts +94 -66
- package/src/cli/commands/report.ts +6 -6
- package/src/cli/commands/status.ts +3 -4
- package/src/cli/commands/sync-docs/capabilities-syncer.ts +90 -15
- package/src/cli/commands/sync-docs/index.ts +87 -24
- package/src/cli/commands/validate.ts +1 -5
- package/src/core/CONTEXT.md +7 -4
- package/src/core/constraints/__tests__/injection-drift.test.ts +164 -0
- package/src/core/constraints/__tests__/interceptor.test.ts +4 -0
- package/src/core/constraints/capabilities-parser.ts +28 -0
- package/src/core/constraints/checker.ts +73 -28
- package/src/core/constraints/checkers/capability-sync.ts +89 -18
- package/src/core/constraints/checkers/context-doc-sync.ts +9 -2
- package/src/core/constraints/checkers/docs-freshness.ts +43 -0
- package/src/core/constraints/checkers/index.ts +38 -71
- package/src/core/constraints/checkers/iron-flags.ts +13 -49
- package/src/core/constraints/checkers/no-hardcoded-credentials.ts +93 -0
- package/src/core/constraints/checkers/types.ts +27 -5
- package/src/core/constraints/context-builder.ts +28 -19
- package/src/core/constraints/definitions/guidelines.ts +33 -599
- package/src/core/constraints/definitions/iron-laws.ts +48 -239
- package/src/core/constraints/definitions/prompts.ts +419 -0
- package/src/core/constraints/definitions.ts +18 -11
- package/src/core/constraints/index.ts +10 -1
- package/src/core/constraints/injection-drift.ts +139 -0
- package/src/core/constraints/injection-renderer.ts +44 -0
- package/src/core/constraints/interceptor.ts +3 -2
- package/src/core/constraints/usage-report.ts +254 -0
- package/src/core/effective-constraints.ts +85 -0
- package/src/core/index.ts +4 -1
- package/src/core/project-config-loader.ts +82 -9
- package/src/dashboard/__tests__/data.test.ts +5 -12
- package/src/dashboard/data.ts +9 -19
- package/src/dashboard/types.ts +18 -1
- package/src/index.ts +0 -12
- package/src/knowledge/CONTEXT.md +2 -2
- package/src/monitoring/CONTEXT.md +3 -4
- package/src/monitoring/index.ts +0 -2
- package/src/monitoring/knowledge-evolver.ts +1 -1
- package/src/monitoring/trace-analyzer.ts +15 -10
- package/src/monitoring/traces.ts +10 -6
- package/src/presets/__tests__/standard.test.ts +11 -9
- package/src/presets/standard.ts +24 -15
- package/src/types/constraint.ts +59 -14
- package/src/types/index.ts +1 -3
- package/src/types/monitoring-types.ts +2 -84
- package/src/types/project-config.ts +37 -1
- package/src/types/trace.ts +18 -7
- package/src/verification/CONTEXT.md +1 -1
- package/dist/cli/commands/flow.d.ts +0 -22
- package/dist/cli/commands/flow.d.ts.map +0 -1
- package/dist/cli/commands/flow.js +0 -275
- package/dist/cli/commands/flow.js.map +0 -1
- package/dist/constraints/lifecycle-runner.d.ts +0 -52
- package/dist/constraints/lifecycle-runner.d.ts.map +0 -1
- package/dist/constraints/lifecycle-runner.js +0 -151
- package/dist/constraints/lifecycle-runner.js.map +0 -1
- package/dist/constraints/registry.d.ts +0 -27
- package/dist/constraints/registry.d.ts.map +0 -1
- package/dist/constraints/registry.js +0 -128
- package/dist/constraints/registry.js.map +0 -1
- package/dist/constraints/types.d.ts +0 -53
- package/dist/constraints/types.d.ts.map +0 -1
- package/dist/constraints/types.js +0 -8
- package/dist/constraints/types.js.map +0 -1
- package/dist/core/constraints/checkers/always-pass.d.ts +0 -17
- package/dist/core/constraints/checkers/always-pass.d.ts.map +0 -1
- package/dist/core/constraints/checkers/always-pass.js +0 -23
- package/dist/core/constraints/checkers/always-pass.js.map +0 -1
- package/dist/core/constraints/checkers/guideline-flags.d.ts +0 -9
- package/dist/core/constraints/checkers/guideline-flags.d.ts.map +0 -1
- package/dist/core/constraints/checkers/guideline-flags.js +0 -13
- package/dist/core/constraints/checkers/guideline-flags.js.map +0 -1
- package/dist/core/constraints/checkers/no-any-type.d.ts +0 -6
- package/dist/core/constraints/checkers/no-any-type.d.ts.map +0 -1
- package/dist/core/constraints/checkers/no-any-type.js +0 -42
- package/dist/core/constraints/checkers/no-any-type.js.map +0 -1
- package/dist/core/constraints/checkers/no-excuse-patterns.d.ts +0 -6
- package/dist/core/constraints/checkers/no-excuse-patterns.d.ts.map +0 -1
- package/dist/core/constraints/checkers/no-excuse-patterns.js +0 -25
- package/dist/core/constraints/checkers/no-excuse-patterns.js.map +0 -1
- package/dist/core/constraints/checkers/no-fuzzy-completion-claim.d.ts +0 -6
- package/dist/core/constraints/checkers/no-fuzzy-completion-claim.d.ts.map +0 -1
- package/dist/core/constraints/checkers/no-fuzzy-completion-claim.js +0 -34
- package/dist/core/constraints/checkers/no-fuzzy-completion-claim.js.map +0 -1
- package/dist/core/constraints/checkers/no-performative-agreement.d.ts +0 -8
- package/dist/core/constraints/checkers/no-performative-agreement.d.ts.map +0 -1
- package/dist/core/constraints/checkers/no-performative-agreement.js +0 -23
- package/dist/core/constraints/checkers/no-performative-agreement.js.map +0 -1
- package/dist/core/constraints/checkers/no-simplification-without-approval.d.ts +0 -6
- package/dist/core/constraints/checkers/no-simplification-without-approval.d.ts.map +0 -1
- package/dist/core/constraints/checkers/no-simplification-without-approval.js +0 -30
- package/dist/core/constraints/checkers/no-simplification-without-approval.js.map +0 -1
- package/dist/core/constraints/checkers/test-coverage.d.ts +0 -6
- package/dist/core/constraints/checkers/test-coverage.d.ts.map +0 -1
- package/dist/core/constraints/checkers/test-coverage.js +0 -33
- package/dist/core/constraints/checkers/test-coverage.js.map +0 -1
- package/dist/core/constraints/checkers/yagni.d.ts +0 -8
- package/dist/core/constraints/checkers/yagni.d.ts.map +0 -1
- package/dist/core/constraints/checkers/yagni.js +0 -96
- package/dist/core/constraints/checkers/yagni.js.map +0 -1
- package/dist/core/constraints/definitions/tips.d.ts +0 -7
- package/dist/core/constraints/definitions/tips.d.ts.map +0 -1
- package/dist/core/constraints/definitions/tips.js +0 -62
- package/dist/core/constraints/definitions/tips.js.map +0 -1
- package/dist/evolution/auto-evolve.d.ts +0 -35
- package/dist/evolution/auto-evolve.d.ts.map +0 -1
- package/dist/evolution/auto-evolve.js +0 -64
- package/dist/evolution/auto-evolve.js.map +0 -1
- package/dist/evolution/index.d.ts +0 -2
- package/dist/evolution/index.d.ts.map +0 -1
- package/dist/evolution/index.js +0 -18
- package/dist/evolution/index.js.map +0 -1
- package/dist/monitoring/constraint-evolver.d.ts +0 -120
- package/dist/monitoring/constraint-evolver.d.ts.map +0 -1
- package/dist/monitoring/constraint-evolver.js +0 -410
- package/dist/monitoring/constraint-evolver.js.map +0 -1
- package/src/__tests__/constraint-evolver.test.ts +0 -405
- package/src/__tests__/doctor-evolver.test.ts +0 -381
- package/src/cli/commands/__tests__/flow.test.ts +0 -343
- package/src/cli/commands/flow.ts +0 -277
- package/src/constraints/__tests__/lifecycle-runner.test.ts +0 -246
- package/src/constraints/__tests__/registry.test.ts +0 -196
- package/src/constraints/lifecycle-runner.ts +0 -184
- package/src/constraints/registry.ts +0 -134
- package/src/constraints/types.ts +0 -59
- package/src/core/constraints/checkers/always-pass.ts +0 -22
- package/src/core/constraints/checkers/guideline-flags.ts +0 -30
- package/src/core/constraints/checkers/no-any-type.ts +0 -42
- package/src/core/constraints/checkers/no-excuse-patterns.ts +0 -24
- package/src/core/constraints/checkers/no-fuzzy-completion-claim.ts +0 -33
- package/src/core/constraints/checkers/no-performative-agreement.ts +0 -22
- package/src/core/constraints/checkers/no-simplification-without-approval.ts +0 -29
- package/src/core/constraints/checkers/test-coverage.ts +0 -35
- package/src/core/constraints/checkers/yagni.ts +0 -65
- package/src/core/constraints/definitions/tips.ts +0 -63
- package/src/evolution/CONTEXT.md +0 -22
- package/src/evolution/__tests__/auto-evolve.test.ts +0 -319
- package/src/evolution/auto-evolve.ts +0 -98
- package/src/evolution/index.ts +0 -1
- package/src/monitoring/constraint-evolver.ts +0 -501
package/src/core/CONTEXT.md
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# core/
|
|
2
2
|
|
|
3
3
|
## 职责
|
|
4
|
-
|
|
4
|
+
约束引擎核心:check/prompt 二元约束系统(ADR-0001)、生效集合并(effective-constraints)、检查点验证器(CSO/passes-gate)、会话管理、Spec 验证器、项目配置加载。
|
|
5
5
|
|
|
6
6
|
## 核心导出
|
|
7
|
-
- `constraints/` — 约束定义(IRON_LAWS/GUIDELINES/TIPS) + 检查引擎(ConstraintChecker) + 拦截器(ConstraintInterceptor) + 缓存(CheckCache)
|
|
7
|
+
- `constraints/` — 约束定义(IRON_LAWS/GUIDELINES/PROMPTS;TIPS 已退役为空表) + 检查引擎(ConstraintChecker) + 拦截器(ConstraintInterceptor) + 缓存(CheckCache) + 注入渲染(injection-renderer)/漂移校验(injection-drift)/使用统计(usage-report)
|
|
8
|
+
- `effective-constraints.ts` — `getEffectiveConstraints(projectRoot)`:全仓唯一生效集来源(内置 → preset → config.yml 禁用 → custom 追加 → scenes 过滤);`lintEffectiveConfig` 配置诊断
|
|
8
9
|
- `validators/` — checkpoints、passes-gate、CSO 验证器
|
|
9
10
|
- `session/` — 会话管理
|
|
10
11
|
- `spec/validator` — SpecValidator
|
|
@@ -17,10 +18,12 @@
|
|
|
17
18
|
- 依赖 `src/monitoring/traces` TraceCollector
|
|
18
19
|
|
|
19
20
|
## 约定
|
|
20
|
-
- 约束定义在 `constraints/definitions.ts`,不在运行时代码中定义
|
|
21
|
+
- 约束定义在 `constraints/definitions/{iron-laws,guidelines,prompts}.ts`,不在运行时代码中定义
|
|
22
|
+
- check 层必须带真实 checker(注册表闭环,引用未注册 checker 构建报错)
|
|
21
23
|
- Iron Law 违规必须 throw ConstraintViolationError
|
|
22
24
|
- 检查器使用单例模式(ConstraintChecker.getInstance())
|
|
23
25
|
|
|
24
26
|
## 注意事项
|
|
25
27
|
- 零 Token 成本:所有分析纯文件操作,无 LLM 调用
|
|
26
|
-
- 约束配置支持 .harness/config.yml
|
|
28
|
+
- 约束配置支持 .harness/config.yml 自定义合并(preset 真实生效)
|
|
29
|
+
- 存在性探测约束(capability_sync/docs_freshness/context_doc_sync)在约定文件缺失时 skip,不计 pass/fail
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 注入漂移校验测试(ADR-0001 决策 7,P6)
|
|
3
|
+
*
|
|
4
|
+
* - 三类漂移各自检出:版本 / 内容(条目级 missing/extra)/ 重复章节
|
|
5
|
+
* - 无漂移静默;无标记段 = 未注入不算漂移;CLAUDE.md 不存在不算漂移
|
|
6
|
+
* - config.yml 变更后未重跑 init → 内容漂移(extra)
|
|
7
|
+
*
|
|
8
|
+
* 使用真实临时目录(getEffectiveConstraints 读真实 fs)。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as os from 'os';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
import { detectInjectionDrift, INJECTION_DRIFT_FIX_HINT } from '../injection-drift';
|
|
15
|
+
import {
|
|
16
|
+
renderConstraintsSection,
|
|
17
|
+
CONSTRAINTS_START_MARKER,
|
|
18
|
+
} from '../injection-renderer';
|
|
19
|
+
import { getEffectiveConstraints } from '../../effective-constraints';
|
|
20
|
+
|
|
21
|
+
const TEST_VERSION = '9.9.9-test';
|
|
22
|
+
|
|
23
|
+
function makeTmpProject(): string {
|
|
24
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'harness-drift-test-'));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 写入与当前生效集完全一致的注入段(版本 TEST_VERSION) */
|
|
28
|
+
function writeSyncedClaudeMd(root: string, extra = ''): string {
|
|
29
|
+
const section =
|
|
30
|
+
'## Governance Rules\n' + renderConstraintsSection(getEffectiveConstraints(root), TEST_VERSION);
|
|
31
|
+
const content = `# Test Project\n\n${section}${extra}`;
|
|
32
|
+
fs.writeFileSync(path.join(root, 'CLAUDE.md'), content, 'utf-8');
|
|
33
|
+
return content;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('detectInjectionDrift', () => {
|
|
37
|
+
it('无漂移:注入段与期望渲染一致 → hasDrift=false', () => {
|
|
38
|
+
const root = makeTmpProject();
|
|
39
|
+
writeSyncedClaudeMd(root);
|
|
40
|
+
|
|
41
|
+
const drift = detectInjectionDrift(root, TEST_VERSION);
|
|
42
|
+
|
|
43
|
+
expect(drift.notInjected).toBe(false);
|
|
44
|
+
expect(drift.hasDrift).toBe(false);
|
|
45
|
+
expect(drift.versionDrift).toBeUndefined();
|
|
46
|
+
expect(drift.contentDrift).toBeUndefined();
|
|
47
|
+
expect(drift.duplicateHeading).toBe(false);
|
|
48
|
+
expect(drift.fixHint).toContain('npx harness init');
|
|
49
|
+
expect(drift.fixHint).toBe(INJECTION_DRIFT_FIX_HINT);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('版本漂移:标记段版本号改旧 → versionDrift', () => {
|
|
53
|
+
const root = makeTmpProject();
|
|
54
|
+
const synced = writeSyncedClaudeMd(root);
|
|
55
|
+
fs.writeFileSync(
|
|
56
|
+
path.join(root, 'CLAUDE.md'),
|
|
57
|
+
synced.replace(`<!-- version: ${TEST_VERSION} -->`, '<!-- version: 0.0.1-old -->'),
|
|
58
|
+
'utf-8'
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const drift = detectInjectionDrift(root, TEST_VERSION);
|
|
62
|
+
|
|
63
|
+
expect(drift.hasDrift).toBe(true);
|
|
64
|
+
expect(drift.versionDrift).toEqual({ expected: TEST_VERSION, actual: '0.0.1-old' });
|
|
65
|
+
expect(drift.contentDrift).toBeUndefined();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('内容漂移:手改标记段内一条 → 原条目 missing、改后条目 extra', () => {
|
|
69
|
+
const root = makeTmpProject();
|
|
70
|
+
const synced = writeSyncedClaudeMd(root);
|
|
71
|
+
const originalLine = synced.split('\n').find(l => l.startsWith('- **'))!;
|
|
72
|
+
const editedLine = originalLine.replace(/: .+$/, ': 手工篡改的注入文本');
|
|
73
|
+
fs.writeFileSync(
|
|
74
|
+
path.join(root, 'CLAUDE.md'),
|
|
75
|
+
synced.replace(originalLine, editedLine),
|
|
76
|
+
'utf-8'
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const drift = detectInjectionDrift(root, TEST_VERSION);
|
|
80
|
+
|
|
81
|
+
expect(drift.hasDrift).toBe(true);
|
|
82
|
+
expect(drift.versionDrift).toBeUndefined();
|
|
83
|
+
expect(drift.contentDrift).toBeDefined();
|
|
84
|
+
expect(drift.contentDrift!.missing).toEqual([originalLine]);
|
|
85
|
+
expect(drift.contentDrift!.extra).toEqual([editedLine]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('内容漂移:config.yml 变更后未重跑 init → 被禁条目成为 extra', () => {
|
|
89
|
+
const root = makeTmpProject();
|
|
90
|
+
writeSyncedClaudeMd(root);
|
|
91
|
+
// 注入后禁用一条 prompt,但不重跑 init
|
|
92
|
+
fs.mkdirSync(path.join(root, '.harness'), { recursive: true });
|
|
93
|
+
fs.writeFileSync(
|
|
94
|
+
path.join(root, '.harness', 'config.yml'),
|
|
95
|
+
'constraints:\n no_fuzzy_completion_claim:\n enabled: false\n',
|
|
96
|
+
'utf-8'
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const drift = detectInjectionDrift(root, TEST_VERSION);
|
|
100
|
+
|
|
101
|
+
expect(drift.hasDrift).toBe(true);
|
|
102
|
+
expect(drift.contentDrift).toBeDefined();
|
|
103
|
+
expect(drift.contentDrift!.missing).toEqual([]);
|
|
104
|
+
expect(drift.contentDrift!.extra).toHaveLength(1);
|
|
105
|
+
expect(drift.contentDrift!.extra[0]).toContain('no_fuzzy_completion_claim');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('重复章节:标记段之外还有一个 ## Governance Rules 标题 → duplicateHeading', () => {
|
|
109
|
+
const root = makeTmpProject();
|
|
110
|
+
writeSyncedClaudeMd(root, '\n## Governance Rules\n\n旧版遗留的同名章节\n');
|
|
111
|
+
|
|
112
|
+
const drift = detectInjectionDrift(root, TEST_VERSION);
|
|
113
|
+
|
|
114
|
+
expect(drift.duplicateHeading).toBe(true);
|
|
115
|
+
expect(drift.hasDrift).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('无标记段 = 未注入,不算漂移', () => {
|
|
119
|
+
const root = makeTmpProject();
|
|
120
|
+
fs.writeFileSync(path.join(root, 'CLAUDE.md'), '# Test Project\n\n## Governance Rules\n\n用户自写内容\n', 'utf-8');
|
|
121
|
+
|
|
122
|
+
const drift = detectInjectionDrift(root, TEST_VERSION);
|
|
123
|
+
|
|
124
|
+
expect(drift.notInjected).toBe(true);
|
|
125
|
+
expect(drift.hasDrift).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('CLAUDE.md 不存在 = 未注入,不算漂移', () => {
|
|
129
|
+
const root = makeTmpProject();
|
|
130
|
+
|
|
131
|
+
const drift = detectInjectionDrift(root, TEST_VERSION);
|
|
132
|
+
|
|
133
|
+
expect(drift.notInjected).toBe(true);
|
|
134
|
+
expect(drift.hasDrift).toBe(false);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('默认版本参数读 package.json:与真实版本一致的注入段无版本漂移', () => {
|
|
138
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
139
|
+
const realVersion = require('../../../../package.json').version as string;
|
|
140
|
+
const root = makeTmpProject();
|
|
141
|
+
const section =
|
|
142
|
+
'## Governance Rules\n' + renderConstraintsSection(getEffectiveConstraints(root), realVersion);
|
|
143
|
+
fs.writeFileSync(path.join(root, 'CLAUDE.md'), section, 'utf-8');
|
|
144
|
+
|
|
145
|
+
const drift = detectInjectionDrift(root);
|
|
146
|
+
|
|
147
|
+
expect(drift.versionDrift).toBeUndefined();
|
|
148
|
+
expect(drift.hasDrift).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('标记段含 ' + CONSTRAINTS_START_MARKER + ' 但版本行缺失 → versionDrift actual=(缺失)', () => {
|
|
152
|
+
const root = makeTmpProject();
|
|
153
|
+
const synced = writeSyncedClaudeMd(root);
|
|
154
|
+
fs.writeFileSync(
|
|
155
|
+
path.join(root, 'CLAUDE.md'),
|
|
156
|
+
synced.replace(`<!-- version: ${TEST_VERSION} -->\n`, ''),
|
|
157
|
+
'utf-8'
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const drift = detectInjectionDrift(root, TEST_VERSION);
|
|
161
|
+
|
|
162
|
+
expect(drift.versionDrift).toEqual({ expected: TEST_VERSION, actual: '(缺失)' });
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -24,6 +24,7 @@ describe('ConstraintInterceptor', () => {
|
|
|
24
24
|
|
|
25
25
|
const mockIronLaw = {
|
|
26
26
|
id: 'test_iron_law',
|
|
27
|
+
kind: 'check' as const,
|
|
27
28
|
rule: 'Test Iron Law',
|
|
28
29
|
message: 'Test iron law message',
|
|
29
30
|
level: 'iron_law' as const,
|
|
@@ -34,6 +35,7 @@ describe('ConstraintInterceptor', () => {
|
|
|
34
35
|
|
|
35
36
|
const mockGuideline = {
|
|
36
37
|
id: 'test_guideline',
|
|
38
|
+
kind: 'check' as const,
|
|
37
39
|
rule: 'Test Guideline',
|
|
38
40
|
message: 'Test guideline message',
|
|
39
41
|
level: 'guideline' as const,
|
|
@@ -63,6 +65,7 @@ describe('ConstraintInterceptor', () => {
|
|
|
63
65
|
ironLaws: { test_iron_law: mockIronLaw },
|
|
64
66
|
guidelines: { test_guideline: mockGuideline },
|
|
65
67
|
tips: {},
|
|
68
|
+
prompts: {},
|
|
66
69
|
});
|
|
67
70
|
});
|
|
68
71
|
|
|
@@ -200,6 +203,7 @@ describe('ConstraintInterceptor', () => {
|
|
|
200
203
|
ironLaws: {},
|
|
201
204
|
guidelines: {},
|
|
202
205
|
tips: {},
|
|
206
|
+
prompts: {},
|
|
203
207
|
});
|
|
204
208
|
|
|
205
209
|
const result = await interceptor.intercept('code_implementation', mockContext);
|
|
@@ -68,3 +68,31 @@ export function readCapabilitiesEntries(capabilitiesPath: string, options: Capab
|
|
|
68
68
|
return [];
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 检测 CAPABILITIES.md 是否使用能力清单格式(计数行),而非文件表格格式
|
|
74
|
+
*
|
|
75
|
+
* 清单格式没有文件表可核对,计数由 sync-docs 自动维护,
|
|
76
|
+
* checker 与 sync-docs 都应按此跳过条目级比对。
|
|
77
|
+
*/
|
|
78
|
+
export function isCapabilityListingFormat(content: string): boolean {
|
|
79
|
+
// 能力清单格式特征:包含 "CLI Commands (N)" / "Iron Laws (N)" 等计数行
|
|
80
|
+
return /CLI Commands\s*\(\d+\)/.test(content) ||
|
|
81
|
+
/Iron Laws?\s*\(\d+\)/.test(content) ||
|
|
82
|
+
/Guidelines?\s*\(\d+\)/.test(content);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 未覆盖文件的聚合目录(module 模式)
|
|
87
|
+
*
|
|
88
|
+
* 规则:取源码根下的第一级子目录(root=src、file=src/core/foo.ts → src/core/);
|
|
89
|
+
* 源码根直接下的文件聚到根(file=src/foo.ts → src/)。
|
|
90
|
+
*/
|
|
91
|
+
export function aggregateToSourceSubdir(root: string, file: string): string {
|
|
92
|
+
if (!root) return file;
|
|
93
|
+
const prefix = root.endsWith('/') ? root : root + '/';
|
|
94
|
+
const rest = file.startsWith(prefix) ? file.slice(prefix.length) : file;
|
|
95
|
+
const idx = rest.indexOf('/');
|
|
96
|
+
if (idx === -1) return prefix;
|
|
97
|
+
return prefix + rest.slice(0, idx) + '/';
|
|
98
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 约束检查引擎
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* - Iron Laws:检查失败立即抛出异常
|
|
6
|
-
* - Guidelines:检查失败记录警告
|
|
7
|
-
* -
|
|
3
|
+
*
|
|
4
|
+
* kind 二元模型(ADR-0001):
|
|
5
|
+
* - check · Iron Laws:检查失败立即抛出异常
|
|
6
|
+
* - check · Guidelines:检查失败记录警告
|
|
7
|
+
* - prompt:不执行 checker,仅参与注入
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import {
|
|
@@ -17,14 +17,14 @@ import {
|
|
|
17
17
|
ConstraintViolationError,
|
|
18
18
|
} from '../../types/constraint';
|
|
19
19
|
import type { ExecutionTrace } from '../../types/trace';
|
|
20
|
-
import { IRON_LAWS, GUIDELINES, TIPS } from './definitions';
|
|
20
|
+
import { IRON_LAWS, GUIDELINES, TIPS, PROMPTS } from './definitions';
|
|
21
21
|
import type { MergedConstraintsConfig } from '../../types/project-config';
|
|
22
22
|
import { normalizeTriggers } from '../../utils/exec';
|
|
23
23
|
import { runCommand } from '../../utils/exec';
|
|
24
24
|
import { join, relative } from 'path';
|
|
25
25
|
import { CheckCache } from './check-cache';
|
|
26
26
|
import { findTsSourceFiles } from '../../utils/file-walk';
|
|
27
|
-
import { getConstraintCheck, type CheckEnv } from './checkers';
|
|
27
|
+
import { getConstraintCheck, type CheckEnv, type CheckOutcome } from './checkers';
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* 例外名称 → ConstraintContext 中布尔字段的映射
|
|
@@ -158,11 +158,12 @@ export class ConstraintChecker {
|
|
|
158
158
|
ironLaws: Record<string, Constraint & { check: (ctx: ConstraintContext) => Promise<ConstraintResult> }>;
|
|
159
159
|
guidelines: Record<string, Constraint & { check: (ctx: ConstraintContext) => Promise<ConstraintResult> }>;
|
|
160
160
|
tips: Record<string, Constraint & { check: (ctx: ConstraintContext) => Promise<ConstraintResult> }>;
|
|
161
|
+
prompts: Record<string, Constraint & { check: (ctx: ConstraintContext) => Promise<ConstraintResult> }>;
|
|
161
162
|
} {
|
|
162
163
|
const config = customConfig ?? this.customConfig;
|
|
163
164
|
const source = config
|
|
164
|
-
? { ironLaws: config.ironLaws, guidelines: config.guidelines, tips: config.tips }
|
|
165
|
-
: { ironLaws: IRON_LAWS, guidelines: GUIDELINES, tips: TIPS };
|
|
165
|
+
? { ironLaws: config.ironLaws, guidelines: config.guidelines, tips: config.tips, prompts: config.prompts ?? PROMPTS }
|
|
166
|
+
: { ironLaws: IRON_LAWS, guidelines: GUIDELINES, tips: TIPS, prompts: PROMPTS };
|
|
166
167
|
|
|
167
168
|
// Wire unified check() method on every constraint
|
|
168
169
|
const wire = (constraints: Record<string, Constraint>) => {
|
|
@@ -171,7 +172,12 @@ export class ConstraintChecker {
|
|
|
171
172
|
}
|
|
172
173
|
return constraints as Record<string, Constraint & { check: (ctx: ConstraintContext) => Promise<ConstraintResult> }>;
|
|
173
174
|
};
|
|
174
|
-
return {
|
|
175
|
+
return {
|
|
176
|
+
ironLaws: wire(source.ironLaws),
|
|
177
|
+
guidelines: wire(source.guidelines),
|
|
178
|
+
tips: wire(source.tips),
|
|
179
|
+
prompts: wire(source.prompts),
|
|
180
|
+
};
|
|
175
181
|
}
|
|
176
182
|
|
|
177
183
|
/**
|
|
@@ -181,6 +187,17 @@ export class ConstraintChecker {
|
|
|
181
187
|
constraint: Constraint,
|
|
182
188
|
context: ConstraintContext
|
|
183
189
|
): Promise<ConstraintResult> {
|
|
190
|
+
// prompt 类约束不参与 checker 执行(ADR-0001:仅参与注入)
|
|
191
|
+
if (constraint.kind === 'prompt') {
|
|
192
|
+
return {
|
|
193
|
+
id: constraint.id,
|
|
194
|
+
level: constraint.level,
|
|
195
|
+
satisfied: true,
|
|
196
|
+
constraint,
|
|
197
|
+
checkedAt: new Date(),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
184
201
|
// 检查例外条件(仅 Guidelines 有效)
|
|
185
202
|
if (constraint.level === 'guideline' && constraint.exceptions) {
|
|
186
203
|
if (this.checkException(constraint, context)) {
|
|
@@ -195,8 +212,22 @@ export class ConstraintChecker {
|
|
|
195
212
|
}
|
|
196
213
|
}
|
|
197
214
|
|
|
198
|
-
//
|
|
199
|
-
const
|
|
215
|
+
// 检查前置条件('skip' = 约定未采用/证据未接线:satisfied 置 true 但不计 pass/fail)
|
|
216
|
+
const outcome = await this.checkPrecondition(constraint, context);
|
|
217
|
+
|
|
218
|
+
if (outcome === 'skip') {
|
|
219
|
+
return {
|
|
220
|
+
id: constraint.id,
|
|
221
|
+
level: constraint.level,
|
|
222
|
+
satisfied: true,
|
|
223
|
+
skipped: true,
|
|
224
|
+
constraint,
|
|
225
|
+
message: `约束 ${constraint.id} 跳过评估(约定未采用或证据未接线)`,
|
|
226
|
+
checkedAt: new Date(),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const satisfied = outcome;
|
|
200
231
|
|
|
201
232
|
return {
|
|
202
233
|
id: constraint.id,
|
|
@@ -228,6 +259,7 @@ export class ConstraintChecker {
|
|
|
228
259
|
return 'error';
|
|
229
260
|
case 'guideline':
|
|
230
261
|
return 'warning';
|
|
262
|
+
case 'prompt':
|
|
231
263
|
case 'tip':
|
|
232
264
|
return 'info';
|
|
233
265
|
default:
|
|
@@ -237,10 +269,14 @@ export class ConstraintChecker {
|
|
|
237
269
|
|
|
238
270
|
/**
|
|
239
271
|
* 判断约束是否匹配当前触发条件
|
|
272
|
+
*
|
|
273
|
+
* context.operation 为主触发条件,context.extraTriggers 为次级推断
|
|
274
|
+
* (ADR-0001:pre-commit 代码变更附加 code_implementation),任一命中即匹配。
|
|
240
275
|
*/
|
|
241
|
-
private matchesTrigger(constraint: Constraint,
|
|
276
|
+
private matchesTrigger(constraint: Constraint, context: ConstraintContext): boolean {
|
|
242
277
|
const triggers = normalizeTriggers<ConstraintTrigger>(constraint.trigger);
|
|
243
|
-
|
|
278
|
+
const operations = [context.operation, ...(context.extraTriggers ?? [])];
|
|
279
|
+
return operations.some(op => triggers.includes(op));
|
|
244
280
|
}
|
|
245
281
|
|
|
246
282
|
/**
|
|
@@ -256,7 +292,7 @@ export class ConstraintChecker {
|
|
|
256
292
|
constraintId: constraint.id,
|
|
257
293
|
level: constraint.level,
|
|
258
294
|
timestamp: Date.now(),
|
|
259
|
-
result: checkResult.satisfied ? 'pass' : 'fail',
|
|
295
|
+
result: checkResult.skipped ? 'skip' : checkResult.satisfied ? 'pass' : 'fail',
|
|
260
296
|
operation: context.operation,
|
|
261
297
|
severity: this.getSeverity(constraint.level),
|
|
262
298
|
exceptionApplied: checkResult.message?.includes('豁免')
|
|
@@ -270,14 +306,20 @@ export class ConstraintChecker {
|
|
|
270
306
|
/**
|
|
271
307
|
* 检查约束前置条件(工单 21:分发至 checkers/ 注册表)
|
|
272
308
|
*
|
|
273
|
-
*
|
|
309
|
+
* 注册表闭环(ADR-0001):kind='check' 未注册 checker 直接抛错,
|
|
310
|
+
* 不再有"未注册默认通过"路径;kind='prompt' 在 check() 入口已短路。
|
|
274
311
|
*/
|
|
275
312
|
private async checkPrecondition(
|
|
276
313
|
constraint: Constraint,
|
|
277
314
|
context: ConstraintContext
|
|
278
|
-
): Promise<
|
|
315
|
+
): Promise<CheckOutcome> {
|
|
279
316
|
const impl = getConstraintCheck(constraint.id);
|
|
280
|
-
if (!impl)
|
|
317
|
+
if (!impl) {
|
|
318
|
+
throw new Error(
|
|
319
|
+
`[harness] 约束 "${constraint.id}" (kind='check') 未注册 checker,拒绝静默通过。` +
|
|
320
|
+
`请在 checkers/ 注册实现,或将约束改为 kind='prompt'。`
|
|
321
|
+
);
|
|
322
|
+
}
|
|
281
323
|
|
|
282
324
|
const projectPath = context.projectPath || process.cwd();
|
|
283
325
|
const env: CheckEnv = {
|
|
@@ -309,8 +351,9 @@ export class ConstraintChecker {
|
|
|
309
351
|
ironLaws: Constraint[];
|
|
310
352
|
guidelines: Constraint[];
|
|
311
353
|
tips: Constraint[];
|
|
354
|
+
prompts: Constraint[];
|
|
312
355
|
} {
|
|
313
|
-
const
|
|
356
|
+
const operations = [context.operation, ...(context.extraTriggers ?? [])];
|
|
314
357
|
const constraints = this.getConstraints(customConfig);
|
|
315
358
|
|
|
316
359
|
const filterByTrigger = (constraintSet: Record<string, Constraint>): Constraint[] => {
|
|
@@ -318,7 +361,7 @@ export class ConstraintChecker {
|
|
|
318
361
|
const triggers = Array.isArray(constraint.trigger)
|
|
319
362
|
? constraint.trigger
|
|
320
363
|
: [constraint.trigger];
|
|
321
|
-
return triggers.includes(
|
|
364
|
+
return operations.some(op => triggers.includes(op));
|
|
322
365
|
});
|
|
323
366
|
};
|
|
324
367
|
|
|
@@ -326,6 +369,7 @@ export class ConstraintChecker {
|
|
|
326
369
|
ironLaws: filterByTrigger(constraints.ironLaws),
|
|
327
370
|
guidelines: filterByTrigger(constraints.guidelines),
|
|
328
371
|
tips: filterByTrigger(constraints.tips),
|
|
372
|
+
prompts: filterByTrigger(constraints.prompts),
|
|
329
373
|
};
|
|
330
374
|
}
|
|
331
375
|
|
|
@@ -362,7 +406,7 @@ export class ConstraintChecker {
|
|
|
362
406
|
|
|
363
407
|
// 1. Iron Laws: 必须全部通过
|
|
364
408
|
for (const constraint of Object.values(constraints.ironLaws)) {
|
|
365
|
-
if (!this.matchesTrigger(constraint, context
|
|
409
|
+
if (!this.matchesTrigger(constraint, context)) continue;
|
|
366
410
|
|
|
367
411
|
const checkResult = await this.check(constraint, context);
|
|
368
412
|
result.ironLaws.push(checkResult);
|
|
@@ -376,7 +420,7 @@ export class ConstraintChecker {
|
|
|
376
420
|
|
|
377
421
|
// 2. Guidelines: 记录警告
|
|
378
422
|
for (const constraint of Object.values(constraints.guidelines)) {
|
|
379
|
-
if (!this.matchesTrigger(constraint, context
|
|
423
|
+
if (!this.matchesTrigger(constraint, context)) continue;
|
|
380
424
|
|
|
381
425
|
const checkResult = await this.check(constraint, context);
|
|
382
426
|
result.guidelines.push(checkResult);
|
|
@@ -389,7 +433,7 @@ export class ConstraintChecker {
|
|
|
389
433
|
|
|
390
434
|
// 3. Tips: 仅记录
|
|
391
435
|
for (const constraint of Object.values(constraints.tips)) {
|
|
392
|
-
if (!this.matchesTrigger(constraint, context
|
|
436
|
+
if (!this.matchesTrigger(constraint, context)) continue;
|
|
393
437
|
|
|
394
438
|
const checkResult = await this.check(constraint, context);
|
|
395
439
|
result.tips.push(checkResult);
|
|
@@ -434,7 +478,7 @@ export class ConstraintChecker {
|
|
|
434
478
|
const constraints = this.getConstraints(customConfig);
|
|
435
479
|
|
|
436
480
|
for (const constraint of Object.values(constraints.ironLaws)) {
|
|
437
|
-
if (!this.matchesTrigger(constraint, context
|
|
481
|
+
if (!this.matchesTrigger(constraint, context)) continue;
|
|
438
482
|
const checkResult = await this.check(constraint, context);
|
|
439
483
|
result.ironLaws.push(checkResult);
|
|
440
484
|
this.recordTrace(traceCollector, constraint, checkResult, context);
|
|
@@ -442,7 +486,7 @@ export class ConstraintChecker {
|
|
|
442
486
|
}
|
|
443
487
|
|
|
444
488
|
for (const constraint of Object.values(constraints.guidelines)) {
|
|
445
|
-
if (!this.matchesTrigger(constraint, context
|
|
489
|
+
if (!this.matchesTrigger(constraint, context)) continue;
|
|
446
490
|
const checkResult = await this.check(constraint, context);
|
|
447
491
|
result.guidelines.push(checkResult);
|
|
448
492
|
this.recordTrace(traceCollector, constraint, checkResult, context);
|
|
@@ -450,7 +494,7 @@ export class ConstraintChecker {
|
|
|
450
494
|
}
|
|
451
495
|
|
|
452
496
|
for (const constraint of Object.values(constraints.tips)) {
|
|
453
|
-
if (!this.matchesTrigger(constraint, context
|
|
497
|
+
if (!this.matchesTrigger(constraint, context)) continue;
|
|
454
498
|
const checkResult = await this.check(constraint, context);
|
|
455
499
|
result.tips.push(checkResult);
|
|
456
500
|
this.recordTrace(traceCollector, constraint, checkResult, context);
|
|
@@ -475,7 +519,7 @@ export class ConstraintChecker {
|
|
|
475
519
|
customConfig?: MergedConstraintsConfig | null
|
|
476
520
|
): Promise<void> {
|
|
477
521
|
const constraints = this.getConstraints(customConfig);
|
|
478
|
-
const operations =
|
|
522
|
+
const operations = [context.operation, ...(context.extraTriggers ?? [])];
|
|
479
523
|
|
|
480
524
|
for (const constraint of Object.values(constraints.ironLaws)) {
|
|
481
525
|
if (!normalizeTriggers(constraint.trigger).some((t) => operations.includes(t))) continue;
|
|
@@ -505,7 +549,8 @@ export async function checkConstraint(
|
|
|
505
549
|
const constraint =
|
|
506
550
|
constraints.ironLaws[constraintId] ||
|
|
507
551
|
constraints.guidelines[constraintId] ||
|
|
508
|
-
constraints.tips[constraintId]
|
|
552
|
+
constraints.tips[constraintId] ||
|
|
553
|
+
constraints.prompts[constraintId];
|
|
509
554
|
|
|
510
555
|
if (!constraint) {
|
|
511
556
|
return {
|
|
@@ -4,18 +4,55 @@
|
|
|
4
4
|
* 两步验证:
|
|
5
5
|
* 1. Git diff 增量 — 当前变更的非测试文件是否在文档中有记录
|
|
6
6
|
* 2. 全量扫描 — 源码根下所有源文件是否都在 CAPABILITIES.md 中(T-058)
|
|
7
|
+
*
|
|
8
|
+
* 覆盖判定两步统一(isCoveredBy):目录条目(以 / 结尾)前缀匹配,
|
|
9
|
+
* 文件条目精确匹配或路径边界后缀匹配。module 模式下目录条目参与覆盖,
|
|
10
|
+
* 未覆盖文件按源码根下第一级子目录聚合。
|
|
11
|
+
*
|
|
12
|
+
* 清单格式(计数行,mode=listing 或嗅探命中)无文件表可核对,直接放行;
|
|
13
|
+
* 检查异常时 fail-open 但输出 console.warn 保留可观测性。
|
|
14
|
+
*
|
|
15
|
+
* ADR-0001 存在性探测:项目根无 CAPABILITIES.md(未采用该约定)时返回 'skip',
|
|
16
|
+
* 不计 pass/fail,避免在未采用约定的项目上全量误报。
|
|
7
17
|
*/
|
|
8
18
|
|
|
9
|
-
import { existsSync } from 'fs';
|
|
19
|
+
import { existsSync, readFileSync } from 'fs';
|
|
10
20
|
import { join } from 'path';
|
|
11
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
aggregateToSourceSubdir,
|
|
23
|
+
isCapabilityListingFormat,
|
|
24
|
+
parseCapabilitiesEntries,
|
|
25
|
+
} from '../capabilities-parser';
|
|
26
|
+
import { getCapabilitiesMode } from '../../project-config-loader';
|
|
12
27
|
import { detectSourceRoots } from '../../../utils/detect-source-roots';
|
|
13
28
|
import type { ConstraintCheck } from './types';
|
|
14
29
|
|
|
30
|
+
/** markdown 表格分隔行(|------|------|):区分「无表格的散文文档」与「有表格但零条目」 */
|
|
31
|
+
const TABLE_SEPARATOR_REGEX = /^\s*\|[\s:|-]+\|\s*$/m;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 变更文件是否被条目覆盖
|
|
35
|
+
*
|
|
36
|
+
* 目录条目(以 / 结尾)前缀匹配;文件条目精确匹配或路径边界后缀匹配
|
|
37
|
+
* (兼容 basename 条目,但拒绝 xfoo.ts 命中 foo.ts、docs/src/foo.tsx 命中 src/foo.ts
|
|
38
|
+
* 这类 endsWith/includes 模糊匹配造成的碰撞)
|
|
39
|
+
*/
|
|
40
|
+
function isCoveredBy(entries: string[], changed: string): boolean {
|
|
41
|
+
return entries.some((listed) =>
|
|
42
|
+
listed.endsWith('/')
|
|
43
|
+
? changed.startsWith(listed)
|
|
44
|
+
: changed === listed || changed.endsWith('/' + listed)
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
15
48
|
export const capabilitySync: ConstraintCheck = {
|
|
16
49
|
id: 'capability_sync',
|
|
17
50
|
async evaluate(env) {
|
|
18
51
|
const projectPath = env.projectPath;
|
|
52
|
+
// ADR-0001 存在性探测:项目未采用 CAPABILITIES.md 约定 → skip(不计 pass/fail)
|
|
53
|
+
if (!existsSync(join(projectPath, 'CAPABILITIES.md'))) {
|
|
54
|
+
return 'skip';
|
|
55
|
+
}
|
|
19
56
|
try {
|
|
20
57
|
const capabilitiesPath = join(projectPath, 'CAPABILITIES.md');
|
|
21
58
|
if (!existsSync(capabilitiesPath)) {
|
|
@@ -27,35 +64,67 @@ export const capabilitySync: ConstraintCheck = {
|
|
|
27
64
|
return changedCodeFiles.length === 0;
|
|
28
65
|
}
|
|
29
66
|
|
|
30
|
-
const
|
|
67
|
+
const content = readFileSync(capabilitiesPath, 'utf-8');
|
|
68
|
+
const capabilitiesMode = getCapabilitiesMode(projectPath);
|
|
69
|
+
|
|
70
|
+
// 清单格式(计数行)没有文件表可核对,计数由 sync-docs 维护,直接放行
|
|
71
|
+
if (capabilitiesMode === 'listing' || isCapabilityListingFormat(content)) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const listedFiles = parseCapabilitiesEntries(content, { secondColumnOnly: true });
|
|
76
|
+
// module 模式下目录条目同样构成覆盖依据,与 step 2 判定对齐
|
|
77
|
+
const entries =
|
|
78
|
+
capabilitiesMode === 'module'
|
|
79
|
+
? parseCapabilitiesEntries(content, { includeDirs: true })
|
|
80
|
+
: listedFiles;
|
|
81
|
+
// 有表格但零条目 = 文档什么都没登记,不得直接放行;无表格的散文文档保持历史放行
|
|
82
|
+
const hasTable = TABLE_SEPARATOR_REGEX.test(content);
|
|
31
83
|
|
|
32
84
|
// ── Step 1: Git diff 增量检查 ──
|
|
33
85
|
const diff = await env.stagedDiffNames();
|
|
34
86
|
const changedCodeFiles = diff.split('\n').filter(
|
|
35
87
|
(f: string) => f.endsWith('.ts') || f.endsWith('.tsx') || f.endsWith('.js')
|
|
36
88
|
);
|
|
89
|
+
const significantChanges = changedCodeFiles.filter(
|
|
90
|
+
(f: string) => !f.includes('__tests__') && !f.includes('.test.') && !f.includes('.spec.')
|
|
91
|
+
);
|
|
37
92
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
listedFiles.some((listed: string) => changed.endsWith(listed) || changed.includes(listed))
|
|
46
|
-
);
|
|
47
|
-
if (!covered) return false;
|
|
48
|
-
}
|
|
93
|
+
// 每个变更文件都必须被覆盖(every;some 会让一个命中掩盖其余漏网)
|
|
94
|
+
if (
|
|
95
|
+
significantChanges.length > 0 &&
|
|
96
|
+
(entries.length > 0 || hasTable) &&
|
|
97
|
+
!significantChanges.every((changed: string) => isCoveredBy(entries, changed))
|
|
98
|
+
) {
|
|
99
|
+
return false;
|
|
49
100
|
}
|
|
50
101
|
|
|
51
102
|
// ── Step 2: 全量源码根扫描(T-058 修复核心)──
|
|
52
|
-
if (
|
|
103
|
+
if (capabilitiesMode === 'module') {
|
|
104
|
+
// module 模式:目录条目参与覆盖(isCoveredBy 统一判定);
|
|
105
|
+
// 未覆盖文件按源码根下第一级子目录聚合,任一组非空即失败
|
|
106
|
+
if (entries.length > 0 || hasTable) {
|
|
107
|
+
const uncoveredDirs = new Set<string>();
|
|
108
|
+
const sourceRoots = detectSourceRoots(projectPath);
|
|
109
|
+
for (const root of sourceRoots) {
|
|
110
|
+
const srcDir = join(projectPath, root);
|
|
111
|
+
if (existsSync(srcDir)) {
|
|
112
|
+
for (const file of env.srcScan(root)) {
|
|
113
|
+
if (!isCoveredBy(entries, file)) {
|
|
114
|
+
uncoveredDirs.add(aggregateToSourceSubdir(root, file));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (uncoveredDirs.size > 0) return false;
|
|
120
|
+
}
|
|
121
|
+
} else if (listedFiles.length > 0 || hasTable) {
|
|
53
122
|
const sourceRoots = detectSourceRoots(projectPath);
|
|
54
123
|
for (const root of sourceRoots) {
|
|
55
124
|
const srcDir = join(projectPath, root);
|
|
56
125
|
if (existsSync(srcDir)) {
|
|
57
126
|
for (const file of env.srcScan(root)) {
|
|
58
|
-
if (!listedFiles
|
|
127
|
+
if (!isCoveredBy(listedFiles, file)) {
|
|
59
128
|
return false;
|
|
60
129
|
}
|
|
61
130
|
}
|
|
@@ -64,8 +133,10 @@ export const capabilitySync: ConstraintCheck = {
|
|
|
64
133
|
}
|
|
65
134
|
|
|
66
135
|
return true;
|
|
67
|
-
} catch {
|
|
68
|
-
|
|
136
|
+
} catch (err) {
|
|
137
|
+
// fail-open 语义保留,但必须可观测:静默吞错会让解析 bug 变成「永远通过」
|
|
138
|
+
console.warn('[capability_sync] 检查执行异常,默认放行:', err);
|
|
139
|
+
return true;
|
|
69
140
|
}
|
|
70
141
|
},
|
|
71
142
|
};
|