@dommaker/harness 0.16.9 → 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 +34 -0
- package/README.md +33 -32
- package/bin/harness.js +33 -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 +5 -1
- package/dist/cli/commands/sync-docs/capabilities-syncer.d.ts.map +1 -1
- package/dist/cli/commands/sync-docs/capabilities-syncer.js +2 -10
- package/dist/cli/commands/sync-docs/capabilities-syncer.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 +7 -0
- package/dist/core/constraints/capabilities-parser.d.ts.map +1 -1
- package/dist/core/constraints/capabilities-parser.js +13 -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 +9 -2
- package/dist/core/constraints/checkers/capability-sync.d.ts.map +1 -1
- package/dist/core/constraints/checkers/capability-sync.js +55 -22
- 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 +29 -572
- 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 +17 -2
- package/dist/core/project-config-loader.d.ts.map +1 -1
- package/dist/core/project-config-loader.js +54 -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 +18 -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 +228 -333
- 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__/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/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 +5 -7
- 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 +13 -0
- package/src/core/constraints/checker.ts +73 -28
- package/src/core/constraints/checkers/capability-sync.ts +72 -29
- 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 +29 -597
- 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 +58 -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 +21 -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
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* harness constraints retire 测试(ADR-0001 P5)
|
|
3
|
+
*
|
|
4
|
+
* 执行逻辑(retireConstraint,纯函数化):config.yml 写入形态、
|
|
5
|
+
* KnowledgeStore 记录、CLAUDE.md 注入段同步、重复 retire / 未知 id 保护。
|
|
6
|
+
* 交互流程(runRetireInteractive):注入 stdin 流测核心分支
|
|
7
|
+
* (候选选择 + iron 二次确认拒绝;无候选手动输入 + 确认执行)。
|
|
8
|
+
*
|
|
9
|
+
* 使用真实临时目录。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import * as os from 'os';
|
|
14
|
+
import * as path from 'path';
|
|
15
|
+
import { PassThrough, Writable } from 'stream';
|
|
16
|
+
import * as yaml from 'js-yaml';
|
|
17
|
+
import type { ExecutionTrace } from '../../../types/trace';
|
|
18
|
+
import { getConstraint } from '../../../core/constraints/definitions';
|
|
19
|
+
import { getEffectiveConstraints } from '../../../core/effective-constraints';
|
|
20
|
+
import { renderConstraintsSection } from '../../../core/constraints/injection-renderer';
|
|
21
|
+
import { FileKnowledgeStore } from '../../../knowledge/store';
|
|
22
|
+
import { retireConstraint, constraintsRetire, runRetireInteractive, printRetireResult } from '../constraints-retire';
|
|
23
|
+
|
|
24
|
+
const FIXED_NOW = new Date('2026-08-08T12:00:00.000Z');
|
|
25
|
+
|
|
26
|
+
function makeTmpProject(): string {
|
|
27
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'harness-retire-test-'));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function writeTraces(root: string, traces: Partial<ExecutionTrace>[]): void {
|
|
31
|
+
const dir = path.join(root, '.harness', 'logs');
|
|
32
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
33
|
+
const lines = traces.map(t =>
|
|
34
|
+
JSON.stringify({ level: 'guideline', timestamp: 1700000000000, result: 'pass', ...t })
|
|
35
|
+
);
|
|
36
|
+
fs.writeFileSync(path.join(dir, 'traces.log'), lines.join('\n') + '\n', 'utf-8');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readConfig(root: string): any {
|
|
40
|
+
return yaml.load(fs.readFileSync(path.join(root, '.harness', 'config.yml'), 'utf-8'));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let logSpy: jest.SpyInstance;
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
logSpy.mockRestore();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('retireConstraint 执行逻辑', () => {
|
|
54
|
+
it('config.yml 写入形态:enabled:false + retired 元数据(at/reason/stats)', () => {
|
|
55
|
+
const root = makeTmpProject();
|
|
56
|
+
writeTraces(root, [
|
|
57
|
+
{ constraintId: 'no_hardcoded_credentials', result: 'pass' },
|
|
58
|
+
{ constraintId: 'no_hardcoded_credentials', result: 'pass', timestamp: 1700000001000 },
|
|
59
|
+
{ constraintId: 'no_hardcoded_credentials', result: 'fail', timestamp: 1700000002000 },
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
const result = retireConstraint(root, 'no_hardcoded_credentials', {
|
|
63
|
+
reason: '由 secret 扫描工具链覆盖',
|
|
64
|
+
now: FIXED_NOW,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(result.status).toBe('retired');
|
|
68
|
+
expect(result.isIronLaw).toBe(false);
|
|
69
|
+
expect(result.stats).toEqual({ total: 3, fail: 1, failRate: 1 / 3 });
|
|
70
|
+
|
|
71
|
+
const config = readConfig(root);
|
|
72
|
+
const entry = config.constraints.no_hardcoded_credentials;
|
|
73
|
+
expect(entry.enabled).toBe(false);
|
|
74
|
+
expect(entry.retired.at).toBe(FIXED_NOW.toISOString());
|
|
75
|
+
expect(entry.retired.reason).toBe('由 secret 扫描工具链覆盖');
|
|
76
|
+
expect(entry.retired.stats.total).toBe(3);
|
|
77
|
+
expect(entry.retired.stats.fail).toBe(1);
|
|
78
|
+
expect(entry.retired.stats.failRate).toBeCloseTo(1 / 3);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('config.yml 不存在时创建;已有其他配置时保留', () => {
|
|
82
|
+
const root = makeTmpProject();
|
|
83
|
+
fs.mkdirSync(path.join(root, '.harness'), { recursive: true });
|
|
84
|
+
fs.writeFileSync(path.join(root, '.harness', 'config.yml'), 'preset: strict\nscenes:\n - llm-app\n', 'utf-8');
|
|
85
|
+
|
|
86
|
+
const result = retireConstraint(root, 'capability_sync', { now: FIXED_NOW });
|
|
87
|
+
expect(result.status).toBe('retired');
|
|
88
|
+
|
|
89
|
+
const config = readConfig(root);
|
|
90
|
+
expect(config.preset).toBe('strict');
|
|
91
|
+
expect(config.scenes).toEqual(['llm-app']);
|
|
92
|
+
expect(config.constraints.capability_sync.enabled).toBe(false);
|
|
93
|
+
expect(config.constraints.capability_sync.retired.reason).toBe('');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('KnowledgeStore 写入退役记录:规则原文 + 原因 + 统计 + signal 模式', () => {
|
|
97
|
+
const root = makeTmpProject();
|
|
98
|
+
writeTraces(root, [{ constraintId: 'no_bypass_checkpoint', result: 'fail' }]);
|
|
99
|
+
|
|
100
|
+
const result = retireConstraint(root, 'no_bypass_checkpoint', { reason: '流程已内置门禁', now: FIXED_NOW });
|
|
101
|
+
expect(result.status).toBe('retired');
|
|
102
|
+
expect(result.knowledgeEntryId).toBe('constraint-retired-no_bypass_checkpoint');
|
|
103
|
+
|
|
104
|
+
const store = new FileKnowledgeStore({ baseDir: path.join(root, '.harness', 'knowledge') });
|
|
105
|
+
const entry = store.get('constraint-retired-no_bypass_checkpoint');
|
|
106
|
+
expect(entry).toBeDefined();
|
|
107
|
+
expect(entry!.consumptionMode).toBe('signal');
|
|
108
|
+
expect(entry!.origin).toBe('human');
|
|
109
|
+
expect(entry!.tags).toContain('constraint-retired');
|
|
110
|
+
expect(entry!.tags).toContain('constraint:no_bypass_checkpoint');
|
|
111
|
+
expect(entry!.title).toContain('no_bypass_checkpoint');
|
|
112
|
+
|
|
113
|
+
const def = getConstraint('no_bypass_checkpoint')!;
|
|
114
|
+
expect(entry!.content).toContain(def.description!);
|
|
115
|
+
expect(entry!.content).toContain('流程已内置门禁');
|
|
116
|
+
expect(entry!.content).toContain('total: 1');
|
|
117
|
+
expect(entry!.content).toContain('failRate: 100%');
|
|
118
|
+
expect(entry!.content).toContain(FIXED_NOW.toISOString());
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('CLAUDE.md 含标记段时同步重渲染(退役条目消失、标记保留)', () => {
|
|
122
|
+
const root = makeTmpProject();
|
|
123
|
+
// 先用当前生效集渲染一个合法注入段
|
|
124
|
+
const before = renderConstraintsSection(getEffectiveConstraints(root), '0.0.0-test');
|
|
125
|
+
expect(before).toContain('no_bypass_checkpoint');
|
|
126
|
+
fs.writeFileSync(path.join(root, 'CLAUDE.md'), `# 项目\n\n## Governance Rules\n${before}\n其他内容\n`, 'utf-8');
|
|
127
|
+
|
|
128
|
+
const result = retireConstraint(root, 'no_bypass_checkpoint', { now: FIXED_NOW });
|
|
129
|
+
expect(result.status).toBe('retired');
|
|
130
|
+
expect(result.claudeMdSynced).toBe(true);
|
|
131
|
+
|
|
132
|
+
const after = fs.readFileSync(path.join(root, 'CLAUDE.md'), 'utf-8');
|
|
133
|
+
expect(after).toContain('HARNESS_CONSTRAINTS_START');
|
|
134
|
+
expect(after).toContain('HARNESS_CONSTRAINTS_END');
|
|
135
|
+
expect(after).not.toContain('**no_bypass_checkpoint**');
|
|
136
|
+
expect(after).toContain('其他内容');
|
|
137
|
+
|
|
138
|
+
printRetireResult(result);
|
|
139
|
+
const output = logSpy.mock.calls.map(c => String(c[0])).join('\n');
|
|
140
|
+
expect(output).toContain('已同步 CLAUDE.md 注入段');
|
|
141
|
+
expect(output).toContain('恢复方法');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('CLAUDE.md 无标记段或不存在时不创建、不同步', () => {
|
|
145
|
+
const root = makeTmpProject();
|
|
146
|
+
fs.writeFileSync(path.join(root, 'CLAUDE.md'), '# 用户自写\n', 'utf-8');
|
|
147
|
+
const result = retireConstraint(root, 'capability_sync', { now: FIXED_NOW });
|
|
148
|
+
expect(result.status).toBe('retired');
|
|
149
|
+
expect(result.claudeMdSynced).toBe(false);
|
|
150
|
+
expect(fs.readFileSync(path.join(root, 'CLAUDE.md'), 'utf-8')).toBe('# 用户自写\n');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('重复 retire:already_retired,不覆盖原 retired 元数据', () => {
|
|
154
|
+
const root = makeTmpProject();
|
|
155
|
+
const first = retireConstraint(root, 'capability_sync', { reason: '第一次', now: FIXED_NOW });
|
|
156
|
+
expect(first.status).toBe('retired');
|
|
157
|
+
|
|
158
|
+
const later = new Date('2026-08-09T00:00:00.000Z');
|
|
159
|
+
const second = retireConstraint(root, 'capability_sync', { reason: '第二次', now: later });
|
|
160
|
+
expect(second.status).toBe('already_retired');
|
|
161
|
+
|
|
162
|
+
const config = readConfig(root);
|
|
163
|
+
expect(config.constraints.capability_sync.retired.at).toBe(FIXED_NOW.toISOString());
|
|
164
|
+
expect(config.constraints.capability_sync.retired.reason).toBe('第一次');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('未知 id:unknown_id,不落盘任何文件', () => {
|
|
168
|
+
const root = makeTmpProject();
|
|
169
|
+
const result = retireConstraint(root, 'not_a_constraint', { now: FIXED_NOW });
|
|
170
|
+
expect(result.status).toBe('unknown_id');
|
|
171
|
+
expect(fs.existsSync(path.join(root, '.harness'))).toBe(false);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('check 层 iron:isIronLaw 标记为 true(供交互模式二次确认)', () => {
|
|
175
|
+
const root = makeTmpProject();
|
|
176
|
+
const result = retireConstraint(root, 'docs_freshness', { now: FIXED_NOW });
|
|
177
|
+
expect(result.status).toBe('retired');
|
|
178
|
+
expect(result.isIronLaw).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('退役后生效集不再包含该约束', () => {
|
|
182
|
+
const root = makeTmpProject();
|
|
183
|
+
expect(getEffectiveConstraints(root).some(c => c.id === 'capability_sync')).toBe(true);
|
|
184
|
+
retireConstraint(root, 'capability_sync', { now: FIXED_NOW });
|
|
185
|
+
expect(getEffectiveConstraints(root).some(c => c.id === 'capability_sync')).toBe(false);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('constraintsRetire 非交互直达', () => {
|
|
190
|
+
it('iron 直达退役时打印额外警示', async () => {
|
|
191
|
+
const root = makeTmpProject();
|
|
192
|
+
await constraintsRetire('docs_freshness', { projectPath: root, reason: '直接退役' });
|
|
193
|
+
|
|
194
|
+
const output = logSpy.mock.calls.map(c => String(c[0])).join('\n');
|
|
195
|
+
expect(output).toContain('Iron Law');
|
|
196
|
+
expect(output).toContain('已退役');
|
|
197
|
+
|
|
198
|
+
const config = readConfig(root);
|
|
199
|
+
expect(config.constraints.docs_freshness.enabled).toBe(false);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('未知 id 直达:明确提示', async () => {
|
|
203
|
+
const root = makeTmpProject();
|
|
204
|
+
await constraintsRetire('ghost', { projectPath: root });
|
|
205
|
+
const output = logSpy.mock.calls.map(c => String(c[0])).join('\n');
|
|
206
|
+
expect(output).toContain('约束不存在');
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('runRetireInteractive 交互流程(注入 IO 流)', () => {
|
|
211
|
+
/** 逐行 drip-feed,等待 readline 消费上一行 */
|
|
212
|
+
function makeIo(lines: string[]) {
|
|
213
|
+
const input = new PassThrough();
|
|
214
|
+
const output = new Writable({ write(_c, _e, cb) { cb(); } });
|
|
215
|
+
let i = 0;
|
|
216
|
+
const timer = setInterval(() => {
|
|
217
|
+
if (i < lines.length) {
|
|
218
|
+
input.write(lines[i] + '\n');
|
|
219
|
+
i++;
|
|
220
|
+
} else {
|
|
221
|
+
clearInterval(timer);
|
|
222
|
+
}
|
|
223
|
+
}, 30);
|
|
224
|
+
return { input, output, done: () => clearInterval(timer) };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
it('候选编号选择 + iron 二次确认拒绝 → 不落盘', async () => {
|
|
228
|
+
const root = makeTmpProject();
|
|
229
|
+
// 无 trace → 候选全部为零触发,1 号候选是第一条 iron(no_completion_without_verification)
|
|
230
|
+
const io = makeIo(['1', 'n']); // 选 1 号 → iron 确认拒绝
|
|
231
|
+
|
|
232
|
+
await runRetireInteractive(root, io);
|
|
233
|
+
io.done();
|
|
234
|
+
|
|
235
|
+
const output = logSpy.mock.calls.map(c => String(c[0])).join('\n');
|
|
236
|
+
expect(output).toContain('退役候选');
|
|
237
|
+
expect(output).toContain('已跳过 no_completion_without_verification');
|
|
238
|
+
expect(output).toContain('无可执行项');
|
|
239
|
+
expect(fs.existsSync(path.join(root, '.harness', 'config.yml'))).toBe(false);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('无候选 → 手动输入 id → 确认执行 → 落盘', async () => {
|
|
243
|
+
const root = makeTmpProject();
|
|
244
|
+
// 所有 check 约束给少量健康数据(低于一切候选阈值)
|
|
245
|
+
const healthy = getEffectiveConstraints(root)
|
|
246
|
+
.filter(c => c.kind === 'check')
|
|
247
|
+
.flatMap(c => [
|
|
248
|
+
{ constraintId: c.id, result: 'pass' as const },
|
|
249
|
+
{ constraintId: c.id, result: 'pass' as const, timestamp: 1700000001000 },
|
|
250
|
+
{ constraintId: c.id, result: 'fail' as const, timestamp: 1700000002000 },
|
|
251
|
+
]);
|
|
252
|
+
writeTraces(root, healthy);
|
|
253
|
+
|
|
254
|
+
const io = makeIo(['no_hardcoded_credentials', '误报太多', 'y']);
|
|
255
|
+
await runRetireInteractive(root, io);
|
|
256
|
+
io.done();
|
|
257
|
+
|
|
258
|
+
const output = logSpy.mock.calls.map(c => String(c[0])).join('\n');
|
|
259
|
+
expect(output).toContain('没有退役候选');
|
|
260
|
+
expect(output).toContain('已退役');
|
|
261
|
+
|
|
262
|
+
const config = readConfig(root);
|
|
263
|
+
expect(config.constraints.no_hardcoded_credentials.enabled).toBe(false);
|
|
264
|
+
expect(config.constraints.no_hardcoded_credentials.retired.reason).toBe('误报太多');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('无候选 → 手动输入未知 id → 提示不存在并取消', async () => {
|
|
268
|
+
const root = makeTmpProject();
|
|
269
|
+
const healthy = getEffectiveConstraints(root)
|
|
270
|
+
.filter(c => c.kind === 'check')
|
|
271
|
+
.flatMap(c => [{ constraintId: c.id, result: 'pass' as const }]);
|
|
272
|
+
writeTraces(root, healthy);
|
|
273
|
+
|
|
274
|
+
const io = makeIo(['ghost_id']);
|
|
275
|
+
await runRetireInteractive(root, io);
|
|
276
|
+
io.done();
|
|
277
|
+
|
|
278
|
+
const output = logSpy.mock.calls.map(c => String(c[0])).join('\n');
|
|
279
|
+
expect(output).toContain('ghost_id');
|
|
280
|
+
expect(output).toContain('约束不存在');
|
|
281
|
+
expect(fs.existsSync(path.join(root, '.harness', 'config.yml'))).toBe(false);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* init 注入重写测试(ADR-0001 P3)
|
|
3
|
+
*
|
|
4
|
+
* - renderConstraintsSection 纯函数
|
|
5
|
+
* - setupClaudeMdConstraints:消费生效集、幂等、尊重 config.yml 禁用与 scenes
|
|
6
|
+
* - setupClaudeMdOutputStyle:标记化三种情形(无段插入 / 旧 harness 段迁移 / 用户自有段跳过)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals';
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import {
|
|
13
|
+
renderConstraintsSection,
|
|
14
|
+
CONSTRAINTS_START_MARKER,
|
|
15
|
+
CONSTRAINTS_END_MARKER,
|
|
16
|
+
} from '../../../core/constraints/injection-renderer';
|
|
17
|
+
import {
|
|
18
|
+
setupClaudeMdConstraints,
|
|
19
|
+
setupClaudeMdOutputStyle,
|
|
20
|
+
} from '../init';
|
|
21
|
+
import { getEffectiveConstraints } from '../../../core/effective-constraints';
|
|
22
|
+
import { getAllConstraints } from '../../../core/constraints/definitions';
|
|
23
|
+
import type { Constraint } from '../../../types/constraint';
|
|
24
|
+
|
|
25
|
+
const OUTPUT_STYLE_START = '<!-- HARNESS_OUTPUT_STYLE_START -->';
|
|
26
|
+
const OUTPUT_STYLE_END = '<!-- HARNESS_OUTPUT_STYLE_END -->';
|
|
27
|
+
|
|
28
|
+
const makeConstraint = (overrides: Partial<Constraint>): Constraint => ({
|
|
29
|
+
id: 'x',
|
|
30
|
+
kind: 'prompt',
|
|
31
|
+
level: 'prompt',
|
|
32
|
+
rule: 'R',
|
|
33
|
+
message: 'M',
|
|
34
|
+
trigger: 'manual',
|
|
35
|
+
enforcement: 'none',
|
|
36
|
+
...overrides,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('renderConstraintsSection(纯函数)', () => {
|
|
40
|
+
it('按 level 分组渲染,含标记与版本行', () => {
|
|
41
|
+
const constraints = [
|
|
42
|
+
makeConstraint({ id: 'iron_a', kind: 'check', level: 'iron_law', promptInjection: 'iron 注入' }),
|
|
43
|
+
makeConstraint({ id: 'guide_a', kind: 'check', level: 'guideline', promptInjection: 'guide 注入' }),
|
|
44
|
+
makeConstraint({ id: 'prompt_a', level: 'prompt', promptInjection: 'prompt 注入' }),
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const section = renderConstraintsSection(constraints, '9.9.9');
|
|
48
|
+
|
|
49
|
+
expect(section.startsWith(`${CONSTRAINTS_START_MARKER}\n<!-- version: 9.9.9 -->\n`)).toBe(true);
|
|
50
|
+
expect(section.endsWith(`${CONSTRAINTS_END_MARKER}\n`)).toBe(true);
|
|
51
|
+
expect(section).toContain('### Iron Laws');
|
|
52
|
+
expect(section).toContain('### Guidelines');
|
|
53
|
+
expect(section).toContain('### Prompts');
|
|
54
|
+
expect(section).toContain('- **iron_a**: iron 注入');
|
|
55
|
+
expect(section).toContain('- **guide_a**: guide 注入');
|
|
56
|
+
expect(section).toContain('- **prompt_a**: prompt 注入');
|
|
57
|
+
// 分组顺序:Iron Laws → Guidelines → Prompts
|
|
58
|
+
expect(section.indexOf('### Iron Laws')).toBeLessThan(section.indexOf('### Guidelines'));
|
|
59
|
+
expect(section.indexOf('### Guidelines')).toBeLessThan(section.indexOf('### Prompts'));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('无 promptInjection 的 check 条目(如 docs_freshness)不出现在注入段', () => {
|
|
63
|
+
const constraints = [
|
|
64
|
+
makeConstraint({ id: 'docs_freshness', kind: 'check', level: 'iron_law' }),
|
|
65
|
+
makeConstraint({ id: 'iron_a', kind: 'check', level: 'iron_law', promptInjection: 'iron 注入' }),
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const section = renderConstraintsSection(constraints, '1.0.0');
|
|
69
|
+
|
|
70
|
+
expect(section).not.toContain('docs_freshness');
|
|
71
|
+
expect(section).toContain('- **iron_a**: iron 注入');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('内置全集渲染:docs_freshness 不出现,场景 prompt 由调用方过滤后传入', () => {
|
|
75
|
+
const section = renderConstraintsSection(getAllConstraints(), '1.0.0');
|
|
76
|
+
expect(section).not.toContain('docs_freshness');
|
|
77
|
+
expect(section).toContain('- **no_fuzzy_completion_claim**');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('setupClaudeMdConstraints(消费生效集)', () => {
|
|
82
|
+
let tempDir: string;
|
|
83
|
+
let claudeMdPath: string;
|
|
84
|
+
|
|
85
|
+
const writeConfig = (yaml: string) => {
|
|
86
|
+
fs.mkdirSync(path.join(tempDir, '.harness'), { recursive: true });
|
|
87
|
+
fs.writeFileSync(path.join(tempDir, '.harness', 'config.yml'), yaml);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
tempDir = fs.mkdtempSync(path.join(process.cwd(), 'temp-test-init-'));
|
|
92
|
+
claudeMdPath = path.join(tempDir, 'CLAUDE.md');
|
|
93
|
+
jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
afterEach(() => {
|
|
97
|
+
jest.restoreAllMocks();
|
|
98
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('CLAUDE.md 不存在时创建并写入约束段', async () => {
|
|
102
|
+
await setupClaudeMdConstraints(tempDir);
|
|
103
|
+
|
|
104
|
+
const content = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
105
|
+
expect(content).toContain('## Governance Rules');
|
|
106
|
+
expect(content).toContain(CONSTRAINTS_START_MARKER);
|
|
107
|
+
expect(content).toContain(CONSTRAINTS_END_MARKER);
|
|
108
|
+
expect(content).toContain('### Iron Laws');
|
|
109
|
+
expect(content).toContain('### Prompts (行为约束)');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('连跑两次输出一致(幂等)', async () => {
|
|
113
|
+
await setupClaudeMdConstraints(tempDir);
|
|
114
|
+
const first = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
115
|
+
await setupClaudeMdConstraints(tempDir);
|
|
116
|
+
const second = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
117
|
+
expect(second).toBe(first);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('标记区间外内容保持不动', async () => {
|
|
121
|
+
fs.writeFileSync(claudeMdPath, '# My Project\n\n用户自定义内容\n');
|
|
122
|
+
await setupClaudeMdConstraints(tempDir);
|
|
123
|
+
const content = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
124
|
+
expect(content).toContain('# My Project');
|
|
125
|
+
expect(content).toContain('用户自定义内容');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('尊重 config.yml 禁用:注入段不含被禁条目', async () => {
|
|
129
|
+
writeConfig('constraints:\n no_fuzzy_completion_claim:\n enabled: false\n');
|
|
130
|
+
await setupClaudeMdConstraints(tempDir);
|
|
131
|
+
|
|
132
|
+
const content = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
133
|
+
expect(content).not.toContain('no_fuzzy_completion_claim');
|
|
134
|
+
expect(content).toContain('- **no_fix_without_root_cause**');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('scenes 缺省不注入场景专属 prompt;配置 scenes 后注入', async () => {
|
|
138
|
+
writeConfig('preset: standard\n');
|
|
139
|
+
await setupClaudeMdConstraints(tempDir);
|
|
140
|
+
let content = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
141
|
+
expect(content).not.toContain('no_skill_without_test');
|
|
142
|
+
|
|
143
|
+
writeConfig('preset: standard\nscenes:\n - agent-skill\n');
|
|
144
|
+
await setupClaudeMdConstraints(tempDir);
|
|
145
|
+
content = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
146
|
+
expect(content).toContain('- **no_skill_without_test**');
|
|
147
|
+
expect(content).not.toContain('no_model_for_deterministic');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('注入段文本与 renderConstraintsSection(getEffectiveConstraints()) 一致', async () => {
|
|
151
|
+
writeConfig('constraints:\n no_bypass_checkpoint:\n enabled: false\n');
|
|
152
|
+
await setupClaudeMdConstraints(tempDir);
|
|
153
|
+
|
|
154
|
+
const content = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
155
|
+
const start = content.indexOf(CONSTRAINTS_START_MARKER);
|
|
156
|
+
const end = content.indexOf(CONSTRAINTS_END_MARKER) + CONSTRAINTS_END_MARKER.length + 1;
|
|
157
|
+
const actual = content.slice(start, end);
|
|
158
|
+
const version = /<!-- version: ([^ ]+) -->/.exec(content)![1];
|
|
159
|
+
// P6 漂移校验同款比对:期望段(生效集渲染) vs 实际段
|
|
160
|
+
expect(actual).toBe(renderConstraintsSection(getEffectiveConstraints(tempDir), version));
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe('setupClaudeMdOutputStyle(标记化)', () => {
|
|
165
|
+
let tempDir: string;
|
|
166
|
+
let claudeMdPath: string;
|
|
167
|
+
|
|
168
|
+
beforeEach(() => {
|
|
169
|
+
tempDir = fs.mkdtempSync(path.join(process.cwd(), 'temp-test-output-style-'));
|
|
170
|
+
claudeMdPath = path.join(tempDir, 'CLAUDE.md');
|
|
171
|
+
jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
afterEach(() => {
|
|
175
|
+
jest.restoreAllMocks();
|
|
176
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('CLAUDE.md 不存在时不创建(由 constraints 注入负责创建)', async () => {
|
|
180
|
+
await setupClaudeMdOutputStyle(tempDir);
|
|
181
|
+
expect(fs.existsSync(claudeMdPath)).toBe(false);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('无 Output Style 段:插入带标记段到文件顶部,二次运行幂等', async () => {
|
|
185
|
+
fs.writeFileSync(claudeMdPath, '# My Project\n\n正文\n');
|
|
186
|
+
await setupClaudeMdOutputStyle(tempDir);
|
|
187
|
+
|
|
188
|
+
const content = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
189
|
+
expect(content.startsWith(OUTPUT_STYLE_START)).toBe(true);
|
|
190
|
+
expect(content).toContain('## Output Style');
|
|
191
|
+
expect(content).toContain(OUTPUT_STYLE_END);
|
|
192
|
+
expect(content).toContain('# My Project');
|
|
193
|
+
|
|
194
|
+
await setupClaudeMdOutputStyle(tempDir);
|
|
195
|
+
expect(fs.readFileSync(claudeMdPath, 'utf-8')).toBe(content);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('旧版 harness 无标记段(特征串匹配):迁移为标记版', async () => {
|
|
199
|
+
const legacy = [
|
|
200
|
+
'## Output Style',
|
|
201
|
+
'',
|
|
202
|
+
'Terse like caveman. Technical substance exact. Only fluff die.',
|
|
203
|
+
'Drop: articles, filler (just/really/basically), pleasantries (sure/certainly/happy to), hedging.',
|
|
204
|
+
'',
|
|
205
|
+
'# My Project',
|
|
206
|
+
'',
|
|
207
|
+
].join('\n');
|
|
208
|
+
fs.writeFileSync(claudeMdPath, legacy);
|
|
209
|
+
|
|
210
|
+
await setupClaudeMdOutputStyle(tempDir);
|
|
211
|
+
const content = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
212
|
+
|
|
213
|
+
expect(content).toContain(OUTPUT_STYLE_START);
|
|
214
|
+
expect(content).toContain(OUTPUT_STYLE_END);
|
|
215
|
+
expect(content).toContain('Terse like caveman');
|
|
216
|
+
expect(content).toContain('# My Project');
|
|
217
|
+
// 迁移后只剩一个 Output Style 标题
|
|
218
|
+
expect(content.match(/^## Output Style$/gm)).toHaveLength(1);
|
|
219
|
+
// 幂等
|
|
220
|
+
await setupClaudeMdOutputStyle(tempDir);
|
|
221
|
+
expect(fs.readFileSync(claudeMdPath, 'utf-8')).toBe(content);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('用户自写 Output Style 段(特征串不匹配):不动、不追加、console 提示', async () => {
|
|
225
|
+
const userContent = '# My Project\n\n## Output Style\n\n用户自己的输出风格要求。\n';
|
|
226
|
+
fs.writeFileSync(claudeMdPath, userContent);
|
|
227
|
+
const logSpy = jest.spyOn(console, 'log');
|
|
228
|
+
|
|
229
|
+
await setupClaudeMdOutputStyle(tempDir);
|
|
230
|
+
|
|
231
|
+
expect(fs.readFileSync(claudeMdPath, 'utf-8')).toBe(userContent);
|
|
232
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('跳过'));
|
|
233
|
+
});
|
|
234
|
+
});
|
|
@@ -75,7 +75,7 @@ describe('status command - 补充覆盖', () => {
|
|
|
75
75
|
threshold: 0.5,
|
|
76
76
|
},
|
|
77
77
|
{
|
|
78
|
-
constraintId: '
|
|
78
|
+
constraintId: 'no_completion_without_verification',
|
|
79
79
|
type: 'frequent_bypass',
|
|
80
80
|
current: 0.3,
|
|
81
81
|
threshold: 0.1,
|
|
@@ -116,13 +116,13 @@ describe('status command - 补充覆盖', () => {
|
|
|
116
116
|
it('应该显示 Iron Laws 统计', async () => {
|
|
117
117
|
const mockTraces = [
|
|
118
118
|
{ constraintId: 'no_bypass_checkpoint', level: 'iron_law', result: 'pass' },
|
|
119
|
-
{ constraintId: '
|
|
119
|
+
{ constraintId: 'no_completion_without_verification', level: 'iron_law', result: 'pass' },
|
|
120
120
|
];
|
|
121
121
|
mockFs.readFileSync.mockReturnValue(mockTraces.map(t => JSON.stringify(t)).join('\n'));
|
|
122
122
|
|
|
123
123
|
const mockSummaries = [
|
|
124
124
|
{ constraintId: 'no_bypass_checkpoint', level: 'iron_law', passRate: 1, totalChecks: 10, bypassRate: 0 },
|
|
125
|
-
{ constraintId: '
|
|
125
|
+
{ constraintId: 'no_completion_without_verification', level: 'iron_law', passRate: 0.8, totalChecks: 5, bypassRate: 0 },
|
|
126
126
|
];
|
|
127
127
|
const mockAnalyze = {
|
|
128
128
|
summarize: jest.fn().mockReturnValue(mockSummaries),
|
|
@@ -138,12 +138,12 @@ describe('status command - 补充覆盖', () => {
|
|
|
138
138
|
|
|
139
139
|
it('应该显示 Guidelines 统计', async () => {
|
|
140
140
|
const mockTraces = [
|
|
141
|
-
{ constraintId: '
|
|
141
|
+
{ constraintId: 'capability_sync', level: 'guideline', result: 'pass' },
|
|
142
142
|
];
|
|
143
143
|
mockFs.readFileSync.mockReturnValue(mockTraces.map(t => JSON.stringify(t)).join('\n'));
|
|
144
144
|
|
|
145
145
|
const mockSummaries = [
|
|
146
|
-
{ constraintId: '
|
|
146
|
+
{ constraintId: 'capability_sync', level: 'guideline', passRate: 0.9, totalChecks: 20, bypassRate: 0.1 },
|
|
147
147
|
];
|
|
148
148
|
const mockAnalyze = {
|
|
149
149
|
summarize: jest.fn().mockReturnValue(mockSummaries),
|
|
@@ -158,12 +158,12 @@ describe('status command - 补充覆盖', () => {
|
|
|
158
158
|
|
|
159
159
|
it('应该显示 Tips 统计', async () => {
|
|
160
160
|
const mockTraces = [
|
|
161
|
-
{ constraintId: '
|
|
161
|
+
{ constraintId: 'context_doc_sync', level: 'tip', result: 'pass' },
|
|
162
162
|
];
|
|
163
163
|
mockFs.readFileSync.mockReturnValue(mockTraces.map(t => JSON.stringify(t)).join('\n'));
|
|
164
164
|
|
|
165
165
|
const mockSummaries = [
|
|
166
|
-
{ constraintId: '
|
|
166
|
+
{ constraintId: 'context_doc_sync', level: 'tip', passRate: 1, totalChecks: 5, bypassRate: 0 },
|
|
167
167
|
];
|
|
168
168
|
const mockAnalyze = {
|
|
169
169
|
summarize: jest.fn().mockReturnValue(mockSummaries),
|
|
@@ -181,15 +181,15 @@ describe('status command - 补充覆盖', () => {
|
|
|
181
181
|
it('详细模式应该显示所有级别详情', async () => {
|
|
182
182
|
const mockTraces = [
|
|
183
183
|
{ constraintId: 'no_bypass_checkpoint', level: 'iron_law', result: 'pass' },
|
|
184
|
-
{ constraintId: '
|
|
185
|
-
{ constraintId: '
|
|
184
|
+
{ constraintId: 'capability_sync', level: 'guideline', result: 'pass' },
|
|
185
|
+
{ constraintId: 'context_doc_sync', level: 'tip', result: 'pass' },
|
|
186
186
|
];
|
|
187
187
|
mockFs.readFileSync.mockReturnValue(mockTraces.map(t => JSON.stringify(t)).join('\n'));
|
|
188
188
|
|
|
189
189
|
const mockSummaries = [
|
|
190
190
|
{ constraintId: 'no_bypass_checkpoint', level: 'iron_law', passRate: 1, totalChecks: 10, bypassRate: 0 },
|
|
191
|
-
{ constraintId: '
|
|
192
|
-
{ constraintId: '
|
|
191
|
+
{ constraintId: 'capability_sync', level: 'guideline', passRate: 0.9, totalChecks: 20, bypassRate: 0.1 },
|
|
192
|
+
{ constraintId: 'context_doc_sync', level: 'tip', passRate: 1, totalChecks: 5, bypassRate: 0 },
|
|
193
193
|
];
|
|
194
194
|
const mockAnalyze = {
|
|
195
195
|
summarize: jest.fn().mockReturnValue(mockSummaries),
|
|
@@ -283,7 +283,7 @@ describe('status command', () => {
|
|
|
283
283
|
(MockTraceAnalyzer as any).mockImplementation(() => mockAnalyze);
|
|
284
284
|
|
|
285
285
|
await status({});
|
|
286
|
-
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('harness
|
|
286
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('harness status --detail'));
|
|
287
287
|
});
|
|
288
288
|
});
|
|
289
289
|
|