@dommaker/harness 1.4.0 → 1.5.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 +12 -0
- package/dist/cli/commands/check.d.ts.map +1 -1
- package/dist/cli/commands/check.js +28 -0
- package/dist/cli/commands/check.js.map +1 -1
- package/dist/cli/commands/definitions.d.ts.map +1 -1
- package/dist/cli/commands/definitions.js +0 -7
- package/dist/cli/commands/definitions.js.map +1 -1
- package/dist/cli/commands/knowledge.d.ts +0 -19
- package/dist/cli/commands/knowledge.d.ts.map +1 -1
- package/dist/cli/commands/knowledge.js +0 -105
- package/dist/cli/commands/knowledge.js.map +1 -1
- package/dist/cli/commands/update-user-model.d.ts +7 -2
- package/dist/cli/commands/update-user-model.d.ts.map +1 -1
- package/dist/cli/commands/update-user-model.js +25 -16
- package/dist/cli/commands/update-user-model.js.map +1 -1
- package/dist/core/constraints/checker.d.ts.map +1 -1
- package/dist/core/constraints/checker.js +5 -3
- package/dist/core/constraints/checker.js.map +1 -1
- package/dist/core/constraints/checkers/capability-sync.d.ts +17 -6
- package/dist/core/constraints/checkers/capability-sync.d.ts.map +1 -1
- package/dist/core/constraints/checkers/capability-sync.js +48 -19
- package/dist/core/constraints/checkers/capability-sync.js.map +1 -1
- package/dist/core/constraints/checkers/docs-freshness.d.ts.map +1 -1
- package/dist/core/constraints/checkers/docs-freshness.js +15 -6
- package/dist/core/constraints/checkers/docs-freshness.js.map +1 -1
- package/dist/core/constraints/checkers/index.d.ts +2 -2
- package/dist/core/constraints/checkers/index.d.ts.map +1 -1
- package/dist/core/constraints/checkers/index.js +2 -1
- package/dist/core/constraints/checkers/index.js.map +1 -1
- package/dist/core/constraints/checkers/types.d.ts +41 -2
- package/dist/core/constraints/checkers/types.d.ts.map +1 -1
- package/dist/core/constraints/checkers/types.js +33 -0
- package/dist/core/constraints/checkers/types.js.map +1 -1
- package/dist/gates/checker-gate.d.ts +2 -1
- package/dist/gates/checker-gate.d.ts.map +1 -1
- package/dist/gates/checker-gate.js +10 -7
- package/dist/gates/checker-gate.js.map +1 -1
- package/dist/types/constraint.d.ts +8 -0
- package/dist/types/constraint.d.ts.map +1 -1
- package/dist/types/constraint.js +6 -1
- package/dist/types/constraint.js.map +1 -1
- package/dist/types/trace.d.ts +9 -0
- package/dist/types/trace.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/CONTEXT.md +1 -0
- package/src/__tests__/passes-gate.test.ts +11 -18
- package/src/cli/commands/CONTEXT.md +2 -2
- package/src/cli/commands/__tests__/bin-exit-mapping.test.ts +6 -0
- package/src/cli/commands/__tests__/check.test.ts +74 -0
- package/src/cli/commands/__tests__/project-path-convention.test.ts +85 -23
- package/src/cli/commands/__tests__/registry.test.ts +6 -0
- package/src/cli/commands/__tests__/update-user-model.test.ts +35 -1
- package/src/cli/commands/check.ts +39 -1
- package/src/cli/commands/definitions.ts +0 -7
- package/src/cli/commands/knowledge.ts +0 -119
- package/src/cli/commands/update-user-model.ts +31 -16
- package/src/core/CONTEXT.md +2 -1
- package/src/core/constraints/checker.ts +8 -4
- package/src/core/constraints/checkers/__tests__/capability-sync.test.ts +90 -31
- package/src/core/constraints/checkers/__tests__/check-outcome.test.ts +176 -0
- package/src/core/constraints/checkers/__tests__/docs-freshness.test.ts +18 -9
- package/src/core/constraints/checkers/__tests__/three-state-consistency.test.ts +12 -4
- package/src/core/constraints/checkers/capability-sync.ts +56 -18
- package/src/core/constraints/checkers/docs-freshness.ts +18 -5
- package/src/core/constraints/checkers/index.ts +9 -2
- package/src/core/constraints/checkers/types.ts +58 -2
- package/src/gates/CONTEXT.md +1 -1
- package/src/gates/__tests__/checker-gate.test.ts +24 -0
- package/src/gates/checker-gate.ts +11 -15
- package/src/types/constraint.ts +16 -2
- package/src/types/trace.ts +10 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 检查结果证据通道测试(harness#119)
|
|
3
|
+
*
|
|
4
|
+
* 背景:checker 只回 `boolean | 'skip'`,fail 的具体原因(哪个文件没登记)在 CLI
|
|
5
|
+
* 与 trace 两头都无处落脚——capability_sync 连红 22 次而无人能说出红在哪。
|
|
6
|
+
* 本套件锁住新接缝:CheckDetail(pass + evidence)经 normalizeCheckOutcome
|
|
7
|
+
* 归一后,落到 ConstraintResult.evidence 与 ExecutionTrace.evidence。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect } from '@jest/globals';
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as os from 'os';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
import { execFileSync } from 'child_process';
|
|
15
|
+
import { ConstraintChecker } from '../../checker';
|
|
16
|
+
import { normalizeCheckOutcome } from '../types';
|
|
17
|
+
import { createProjectFixture } from '../../../../test-setup/project-fixture';
|
|
18
|
+
import type { ExecutionTrace } from '../../../../types/trace';
|
|
19
|
+
import { ConstraintViolationError } from '../../../../types/constraint';
|
|
20
|
+
import type { Constraint, ConstraintResult } from '../../../../types/constraint';
|
|
21
|
+
import type { MergedConstraintsConfig } from '../../../../types/project-config';
|
|
22
|
+
|
|
23
|
+
const CAPABILITY_SYNC: Constraint = {
|
|
24
|
+
id: 'capability_sync',
|
|
25
|
+
kind: 'check',
|
|
26
|
+
level: 'guideline',
|
|
27
|
+
rule: 'CODE CHANGES MUST UPDATE CAPABILITIES.MD',
|
|
28
|
+
message: '核心模块变更必须同步功能清单',
|
|
29
|
+
trigger: ['module_modification'],
|
|
30
|
+
enforcement: 'update-capabilities',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** 声明式夹具(harness#90):CAPABILITIES.md + 源文件,无 git,staged 证据自然为空 */
|
|
34
|
+
function fixture(name: string, files: Record<string, string>): string {
|
|
35
|
+
return createProjectFixture({ name: `cap-sync-${name}`, files });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('normalizeCheckOutcome — 三态 + 证据归一', () => {
|
|
39
|
+
it('boolean 兼容旧形状:true → satisfied / false → violated,evidence 空', () => {
|
|
40
|
+
expect(normalizeCheckOutcome(true)).toEqual({ satisfied: true, skipped: false, evidence: [] });
|
|
41
|
+
expect(normalizeCheckOutcome(false)).toEqual({ satisfied: false, skipped: false, evidence: [] });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("'skip' → skipped 且不记证据", () => {
|
|
45
|
+
expect(normalizeCheckOutcome('skip')).toEqual({ satisfied: true, skipped: true, evidence: [] });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('CheckDetail 透传 pass 与 evidence;缺省 evidence 归一为空数组', () => {
|
|
49
|
+
expect(normalizeCheckOutcome({ pass: false, evidence: ['a.ts'] })).toEqual({
|
|
50
|
+
satisfied: false,
|
|
51
|
+
skipped: false,
|
|
52
|
+
evidence: ['a.ts'],
|
|
53
|
+
});
|
|
54
|
+
expect(normalizeCheckOutcome({ pass: true })).toEqual({
|
|
55
|
+
satisfied: true,
|
|
56
|
+
skipped: false,
|
|
57
|
+
evidence: [],
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/** 只含 capability_sync 的生效集:一次 run 一条 trace,不牵动其他约束 */
|
|
63
|
+
function onlyCapabilitySync(): MergedConstraintsConfig {
|
|
64
|
+
return {
|
|
65
|
+
ironLaws: {},
|
|
66
|
+
guidelines: { capability_sync: CAPABILITY_SYNC },
|
|
67
|
+
prompts: {},
|
|
68
|
+
disabled: [],
|
|
69
|
+
custom: [],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 跑一次 checkConstraints,返回 capability_sync 的结果与它那条 trace */
|
|
74
|
+
async function runGuideline(dir: string): Promise<{
|
|
75
|
+
result: ConstraintResult;
|
|
76
|
+
trace: ExecutionTrace;
|
|
77
|
+
}> {
|
|
78
|
+
const traces: ExecutionTrace[] = [];
|
|
79
|
+
const checker = new ConstraintChecker({ record: (t) => traces.push(t) });
|
|
80
|
+
const run = await checker.checkConstraints(
|
|
81
|
+
{ operation: 'module_modification', projectPath: dir },
|
|
82
|
+
onlyCapabilitySync()
|
|
83
|
+
);
|
|
84
|
+
const result = run.guidelines.find(r => r.id === 'capability_sync')!;
|
|
85
|
+
const trace = traces.find(t => t.constraintId === 'capability_sync')!;
|
|
86
|
+
return { result, trace };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 需要真 git 的用例专用手写根:夹具不建 git,而 staged 分区只能由真仓库产生 */
|
|
90
|
+
function gitProjectRoot(files: Record<string, string>): string {
|
|
91
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-evidence-git-'));
|
|
92
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
93
|
+
const target = path.join(dir, rel);
|
|
94
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
95
|
+
fs.writeFileSync(target, content, 'utf-8');
|
|
96
|
+
}
|
|
97
|
+
execFileSync('git', ['init', '-q'], { cwd: dir });
|
|
98
|
+
execFileSync('git', ['config', 'user.email', 'fixture@example.com'], { cwd: dir });
|
|
99
|
+
execFileSync('git', ['config', 'user.name', 'Fixture'], { cwd: dir });
|
|
100
|
+
execFileSync('git', ['add', '.'], { cwd: dir });
|
|
101
|
+
execFileSync('git', ['commit', '-q', '-m', 'baseline'], { cwd: dir });
|
|
102
|
+
return dir;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
describe('capability_sync 证据经编排层落到 ConstraintResult 与 trace', () => {
|
|
106
|
+
it('满足但存在仓库级未登记文件 → satisfied=true,evidence 带路径,trace 记 pass', async () => {
|
|
107
|
+
const dir = fixture('hint', {
|
|
108
|
+
'CAPABILITIES.md': '| 模块 | 文件 | 说明 |\n|------|------|------|\n| foo | src/foo.ts | foo |\n',
|
|
109
|
+
'src/foo.ts': 'export const a = 1;\n',
|
|
110
|
+
'src/unlisted.ts': 'export const b = 2;\n',
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const { result, trace } = await runGuideline(dir);
|
|
114
|
+
|
|
115
|
+
expect(result.satisfied).toBe(true);
|
|
116
|
+
expect(result.skipped).toBeUndefined();
|
|
117
|
+
expect(result.evidence?.join('\n')).toContain('unlisted.ts');
|
|
118
|
+
expect(trace.result).toBe('pass');
|
|
119
|
+
expect(trace.evidence?.join('\n')).toContain('unlisted.ts');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('违规(staged 新文件未登记)→ satisfied=false,evidence 指出该文件,trace 记 fail', async () => {
|
|
123
|
+
const dir = gitProjectRoot({
|
|
124
|
+
'CAPABILITIES.md': '| 模块 | 文件 | 说明 |\n|------|------|------|\n| foo | src/foo.ts | foo |\n',
|
|
125
|
+
'src/foo.ts': 'export const a = 1;\n',
|
|
126
|
+
});
|
|
127
|
+
fs.writeFileSync(path.join(dir, 'src/brand-new.ts'), 'export const c = 3;\n');
|
|
128
|
+
execFileSync('git', ['add', '--', 'src/brand-new.ts'], { cwd: dir });
|
|
129
|
+
|
|
130
|
+
const { result, trace } = await runGuideline(dir);
|
|
131
|
+
|
|
132
|
+
expect(result.satisfied).toBe(false);
|
|
133
|
+
expect(result.evidence?.join('\n')).toContain('src/brand-new.ts');
|
|
134
|
+
expect(trace.result).toBe('fail');
|
|
135
|
+
expect(trace.evidence?.join('\n')).toContain('src/brand-new.ts');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('skip 不携带证据(未评估不得留下证据行)', async () => {
|
|
139
|
+
const dir = fixture('no-convention', { 'README.md': '# no capabilities convention\n' });
|
|
140
|
+
|
|
141
|
+
const { result, trace } = await runGuideline(dir);
|
|
142
|
+
|
|
143
|
+
expect(result.skipped).toBe(true);
|
|
144
|
+
expect(result.evidence).toBeUndefined();
|
|
145
|
+
expect(trace.result).toBe('skip');
|
|
146
|
+
expect(trace.evidence).toBeUndefined();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('证据随铁律异常外溢(CLI catch 分支只打印 error.message)', () => {
|
|
151
|
+
it('ConstraintViolationError 文案带出证据行', () => {
|
|
152
|
+
const error = new ConstraintViolationError({
|
|
153
|
+
id: 'docs_freshness',
|
|
154
|
+
level: 'iron_law',
|
|
155
|
+
satisfied: false,
|
|
156
|
+
message: '文档新鲜度检查未通过',
|
|
157
|
+
evidence: ['CAPABILITIES.md 登记的条目不存在:', 'src/gone.ts'],
|
|
158
|
+
checkedAt: new Date(),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
expect(error.message).toContain('文档新鲜度检查未通过');
|
|
162
|
+
expect(error.message).toContain('src/gone.ts');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('无证据时文案保持原样(旧消费方断言不受影响)', () => {
|
|
166
|
+
const error = new ConstraintViolationError({
|
|
167
|
+
id: 'no_test_simplification',
|
|
168
|
+
level: 'iron_law',
|
|
169
|
+
satisfied: false,
|
|
170
|
+
message: '禁止简化测试绕过困难',
|
|
171
|
+
checkedAt: new Date(),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
expect(error.message).toBe('禁止简化测试绕过困难');
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -6,17 +6,21 @@
|
|
|
6
6
|
* 项目根由 src/test-setup/project-fixture 声明式构造(回收仍走 mkdtemp-cleanup)。
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { describe, it, expect
|
|
9
|
+
import { describe, it, expect } from '@jest/globals';
|
|
10
10
|
import * as fs from 'fs';
|
|
11
11
|
import * as path from 'path';
|
|
12
12
|
import { docsFreshness } from '../docs-freshness';
|
|
13
|
-
import { buildCheckEnv } from '../types';
|
|
13
|
+
import { buildCheckEnv, normalizeCheckOutcome, type CheckOutcome } from '../types';
|
|
14
14
|
import { collectSourceFiles } from '../../capabilities-reconcile';
|
|
15
15
|
import { createProjectFixture } from '../../../../test-setup/project-fixture';
|
|
16
16
|
import type { ConstraintContext } from '../../../../types/constraint';
|
|
17
17
|
|
|
18
18
|
const TABLE_HEAD = '| 模块 | 文件 | 说明 |\n|------|------|------|\n';
|
|
19
19
|
|
|
20
|
+
/** 证据行拼成单串,便于按路径断言(harness#119) */
|
|
21
|
+
const evidenceText = (outcome: CheckOutcome): string =>
|
|
22
|
+
normalizeCheckOutcome(outcome).evidence.join('\n');
|
|
23
|
+
|
|
20
24
|
function setupProject(name: string, files: string[]): string {
|
|
21
25
|
return createProjectFixture({
|
|
22
26
|
name: `docs-fresh-${name}`,
|
|
@@ -66,10 +70,9 @@ describe('docs_freshness — 幽灵判定(文档→代码方向)', () => {
|
|
|
66
70
|
dir,
|
|
67
71
|
`${TABLE_HEAD}| old | src/old.ts | old |\n| deleted | src/deleted.ts | deleted |`
|
|
68
72
|
);
|
|
69
|
-
const
|
|
70
|
-
expect(
|
|
71
|
-
expect(
|
|
72
|
-
errSpy.mockRestore();
|
|
73
|
+
const outcome = await docsFreshness.evaluate(makeEnv(dir));
|
|
74
|
+
expect(normalizeCheckOutcome(outcome).satisfied).toBe(false);
|
|
75
|
+
expect(evidenceText(outcome)).toContain('src/deleted.ts');
|
|
73
76
|
});
|
|
74
77
|
|
|
75
78
|
it('登记的目录条目已消失 → 失败(ADR-0009 口径从严,文件+目录都查)', async () => {
|
|
@@ -78,7 +81,9 @@ describe('docs_freshness — 幽灵判定(文档→代码方向)', () => {
|
|
|
78
81
|
dir,
|
|
79
82
|
`${TABLE_HEAD}| 核心 | src/core/ | 核心 |\n| 遗魂 | src/gone/ | 已删目录 |`
|
|
80
83
|
);
|
|
81
|
-
|
|
84
|
+
const outcome = await docsFreshness.evaluate(makeEnv(dir));
|
|
85
|
+
expect(normalizeCheckOutcome(outcome).satisfied).toBe(false);
|
|
86
|
+
expect(evidenceText(outcome)).toContain('src/gone/');
|
|
82
87
|
});
|
|
83
88
|
|
|
84
89
|
it('登记的目录条目仍存在 → 通过', async () => {
|
|
@@ -93,7 +98,9 @@ describe('docs_freshness — 幽灵判定(文档→代码方向)', () => {
|
|
|
93
98
|
dir,
|
|
94
99
|
`${TABLE_HEAD}| routes | src/agents/routes.ts | 活 |\n| 旧路由 | src/agent-configs/routes.ts | 已删 |`
|
|
95
100
|
);
|
|
96
|
-
|
|
101
|
+
const outcome = await docsFreshness.evaluate(makeEnv(dir));
|
|
102
|
+
expect(normalizeCheckOutcome(outcome).satisfied).toBe(false);
|
|
103
|
+
expect(evidenceText(outcome)).toContain('src/agent-configs/routes.ts');
|
|
97
104
|
});
|
|
98
105
|
});
|
|
99
106
|
|
|
@@ -112,6 +119,8 @@ describe('docs_freshness — 内置 Runner 检查(changelog/context)', () =>
|
|
|
112
119
|
JSON.stringify({ name: 'x', version: '2.0.0' })
|
|
113
120
|
);
|
|
114
121
|
fs.writeFileSync(path.join(dir, 'CHANGELOG.md'), '## [1.0.0] - 2026-01-01\n\n- old\n');
|
|
115
|
-
|
|
122
|
+
const outcome = await docsFreshness.evaluate(makeEnv(dir));
|
|
123
|
+
expect(normalizeCheckOutcome(outcome).satisfied).toBe(false);
|
|
124
|
+
expect(evidenceText(outcome)).toContain('1.0.0 !== package.json 版本 2.0.0');
|
|
116
125
|
});
|
|
117
126
|
});
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import { describe, it, expect } from '@jest/globals';
|
|
15
15
|
import { contextDocSync } from '../context-doc-sync';
|
|
16
16
|
import { docsFreshness } from '../docs-freshness';
|
|
17
|
-
import { buildCheckEnv } from '../types';
|
|
17
|
+
import { buildCheckEnv, normalizeCheckOutcome, type CheckOutcome } from '../types';
|
|
18
18
|
import { collectSourceFiles } from '../../capabilities-reconcile';
|
|
19
19
|
import { createProjectFixture } from '../../../../test-setup/project-fixture';
|
|
20
20
|
import type { ConstraintContext } from '../../../../types/constraint';
|
|
@@ -62,10 +62,16 @@ function evalDocsFreshness(dir: string) {
|
|
|
62
62
|
);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/** 三态判定(比较判定结论本身,不比较返回形状——harness#119 后 checker 可带回证据) */
|
|
66
|
+
function verdictOf(outcome: CheckOutcome): true | false | 'skip' {
|
|
67
|
+
const normalized = normalizeCheckOutcome(outcome);
|
|
68
|
+
return normalized.skipped ? 'skip' : normalized.satisfied;
|
|
69
|
+
}
|
|
70
|
+
|
|
65
71
|
/** 同一 fixture 上两条约束的判定必须一致(对照断言) */
|
|
66
72
|
async function expectBoth(dir: string, verdict: true | false | 'skip'): Promise<void> {
|
|
67
|
-
expect(await evalContextDocSync(dir)).toBe(verdict);
|
|
68
|
-
expect(await evalDocsFreshness(dir)).toBe(verdict);
|
|
73
|
+
expect(verdictOf(await evalContextDocSync(dir))).toBe(verdict);
|
|
74
|
+
expect(verdictOf(await evalDocsFreshness(dir))).toBe(verdict);
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
describe('context_files 三态对照 — context_doc_sync vs docs_freshness(工单 84)', () => {
|
|
@@ -140,6 +146,8 @@ describe('docs_freshness 评估段 skip 行为变更(工单 84:约定已立
|
|
|
140
146
|
files: ['src/old.ts'],
|
|
141
147
|
capabilitiesBody: `${TABLE_HEAD}| old | src/old.ts | 活 |\n| 遗魂 | src/gone/ | 已删 |`,
|
|
142
148
|
});
|
|
143
|
-
|
|
149
|
+
const outcome = await evalDocsFreshness(dir);
|
|
150
|
+
expect(verdictOf(outcome)).toBe(false);
|
|
151
|
+
expect(normalizeCheckOutcome(outcome).evidence.join('\n')).toContain('src/gone/');
|
|
144
152
|
});
|
|
145
153
|
});
|
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* capability_sync:CAPABILITIES.md 与代码同步检查(工单 21)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* 1. Git diff 增量 —
|
|
6
|
-
* 2.
|
|
4
|
+
* 两步验证,归因不同(harness#119):
|
|
5
|
+
* 1. Git diff 增量 — 本次变更的非测试文件是否在文档中有记录 → **判违规**(有因果)
|
|
6
|
+
* 2. 全量扫描(T-058)— 源码根下所有源文件是否都在 CAPABILITIES.md 中 → **只出提示**
|
|
7
|
+
*
|
|
8
|
+
* Step 2 不判违规的理由:它是仓库级完备性不变量,与正在被评估的那次变更无因果——
|
|
9
|
+
* 一处历史漏登会让此后每次 module_* 评估恒红(studio 实测 22/22 fail,且因 fail
|
|
10
|
+
* 零证据而无人能定位)。本处保留缺口输出是为了让人看得见、能自己修,而不是为了拦。
|
|
11
|
+
*
|
|
12
|
+
* 已知缺口(ADR-0016「影响」①):降级后仓库级漏登**没有自动拦截点**。`sync-docs --check`
|
|
13
|
+
* 单独跑会点名并 exit 1,但 CI/ship 都按「先 sync-docs 写入、后 --check」的顺序跑
|
|
14
|
+
* (2026-08-08 CI 4 连红固化的顺序),而 file 模式的写入正给缺登记自动补行
|
|
15
|
+
* (`capabilities-syncer` 的 `mode !== 'module'` 分支)、那份写入又不回提——于是流水线里
|
|
16
|
+
* 永远查不到漏登。补这个牙齿归流水线侧(sync 后 git diff 非空即失败),不在本约束里做。
|
|
17
|
+
*
|
|
18
|
+
* 例外:「有表格但零条目」= 文档被清空,判违规(历史门,此前靠 Step 2 兜,现显式化)。
|
|
7
19
|
*
|
|
8
20
|
* 覆盖/漏登判定不在本文件实现,统一走 capabilities-reconcile(ADR-0009):
|
|
9
21
|
* 目录条目(以 / 结尾)前缀匹配,文件条目精确匹配或路径边界后缀匹配;
|
|
10
22
|
* module 模式下目录条目参与覆盖,未覆盖文件按源码根下第一级子目录聚合。
|
|
11
23
|
*
|
|
12
24
|
* 清单格式(计数行,mode=listing 或嗅探命中)无文件表可核对,直接放行;
|
|
13
|
-
*
|
|
14
|
-
* 检查异常时 fail-open 但输出 console.warn 保留可观测性。
|
|
25
|
+
* 无表格的散文文档保持历史放行;检查异常时 fail-open 但输出 console.warn 保留可观测性。
|
|
15
26
|
*
|
|
16
27
|
* ADR-0001 存在性探测:项目根无 CAPABILITIES.md(未采用该约定)时返回 'skip',
|
|
17
28
|
* 不计 pass/fail,避免在未采用约定的项目上全量误报。
|
|
@@ -23,7 +34,7 @@ import { isCapabilityListingFormat } from '../capabilities-parser';
|
|
|
23
34
|
import { getCapabilitiesMode } from '../../project-config-loader';
|
|
24
35
|
import { detectSourceRoots } from '../../../utils/detect-source-roots';
|
|
25
36
|
import { reconcileCapabilities, significantCodeChanges } from '../capabilities-reconcile';
|
|
26
|
-
import type
|
|
37
|
+
import { formatEvidence, type ConstraintCheck } from './types';
|
|
27
38
|
|
|
28
39
|
export const capabilitySync: ConstraintCheck = {
|
|
29
40
|
id: 'capability_sync',
|
|
@@ -59,25 +70,52 @@ export const capabilitySync: ConstraintCheck = {
|
|
|
59
70
|
sourceRoots,
|
|
60
71
|
});
|
|
61
72
|
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
if (gateOpen) return true;
|
|
73
|
+
// 无表格的散文文档:历史放行语义
|
|
74
|
+
if (!verdict.hasTable) return true;
|
|
65
75
|
|
|
66
|
-
//
|
|
67
|
-
|
|
76
|
+
// 文档退化门:有表格却什么都没登记(限定「源码根下确有文件」——空仓 + 空表没有可登记
|
|
77
|
+
// 对象,历史行为是放行,本票不顺手扩大 fail 面)
|
|
78
|
+
if (verdict.coverageEntries.length === 0 && population.length > 0) {
|
|
79
|
+
return {
|
|
80
|
+
pass: false,
|
|
81
|
+
evidence: ['CAPABILITIES.md 有表格但零条目登记,运行 harness sync-docs 或直接补登记'],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
68
84
|
|
|
69
|
-
// ── Step
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
85
|
+
// ── Step 1: Git diff 增量检查(与本次变更有因果,每个变更文件都必须被覆盖)──
|
|
86
|
+
if (verdict.uncoveredChanges.length > 0) {
|
|
87
|
+
return {
|
|
88
|
+
pass: false,
|
|
89
|
+
evidence: formatEvidence(
|
|
90
|
+
`本次变更有 ${verdict.uncoveredChanges.length} 个文件未登记进 CAPABILITIES.md`,
|
|
91
|
+
verdict.uncoveredChanges
|
|
92
|
+
),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Step 2: 全量源码根扫描(T-058)——仓库级漂移,只提示不判违规,见文件头 ──
|
|
97
|
+
const gaps = capabilitiesMode === 'module' ? verdict.uncoveredDirs : verdict.uncoveredFiles;
|
|
98
|
+
if (gaps.length > 0) {
|
|
99
|
+
const kind = capabilitiesMode === 'module' ? '未登记模块目录' : '未登记源文件';
|
|
100
|
+
return {
|
|
101
|
+
pass: true,
|
|
102
|
+
evidence: formatEvidence(
|
|
103
|
+
`CAPABILITIES.md ${kind} ${gaps.length} 项(仓库级漂移,与本次变更无关;修复: harness sync-docs)`,
|
|
104
|
+
gaps
|
|
105
|
+
),
|
|
106
|
+
};
|
|
74
107
|
}
|
|
75
108
|
|
|
76
109
|
return true;
|
|
77
110
|
} catch (err) {
|
|
78
|
-
// fail-open
|
|
111
|
+
// fail-open 语义保留,但异常放行也要落地可见:warn 只进本地 stderr,
|
|
112
|
+
// 进不了 trace——静默吞错会让解析 bug 变成「永远通过」(harness#119 同族问题)
|
|
113
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
79
114
|
console.warn('[capability_sync] 检查执行异常,默认放行:', err);
|
|
80
|
-
return
|
|
115
|
+
return {
|
|
116
|
+
pass: true,
|
|
117
|
+
evidence: [`检查异常,按 fail-open 放行: ${reason}(本次结果不代表文档已同步)`],
|
|
118
|
+
};
|
|
81
119
|
}
|
|
82
120
|
},
|
|
83
121
|
};
|
|
@@ -15,6 +15,7 @@ import { detectSourceRoots } from '../../../utils/detect-source-roots';
|
|
|
15
15
|
import { reconcileCapabilities } from '../capabilities-reconcile';
|
|
16
16
|
import type { DocFreshnessCheck } from '../../../types/project-config';
|
|
17
17
|
import type { ConstraintCheck, CheckEnv } from './types';
|
|
18
|
+
import { formatEvidence } from './types';
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* 内置默认文档新鲜度检查配置
|
|
@@ -95,10 +96,13 @@ export const docsFreshness: ConstraintCheck = {
|
|
|
95
96
|
// Step 1: 文件表格式 — 登记的条目(文件+目录)是否仍存在(ADR-0009)
|
|
96
97
|
const deadEntries = findDeadCapabilityEntries(projectPath, env);
|
|
97
98
|
if (deadEntries.length > 0) {
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
return
|
|
99
|
+
// 证据通道点名条目(harness#119):此前是 console.error 侧信道(2026-08-08 studio
|
|
100
|
+
// CI 4 连红时的临时办法),CLI 结构化输出与 trace 两头都拿不到,
|
|
101
|
+
// 铁律拦截时用户只看到一句通用提示,basename 碰撞下无从定位
|
|
102
|
+
return {
|
|
103
|
+
pass: false,
|
|
104
|
+
evidence: formatEvidence('CAPABILITIES.md 登记的条目不存在', deadEntries),
|
|
105
|
+
};
|
|
102
106
|
}
|
|
103
107
|
|
|
104
108
|
// Step 2: 能力清单格式 + CLAUDE.md + CHANGELOG — 通过 FreshnessRunner
|
|
@@ -120,7 +124,16 @@ export const docsFreshness: ConstraintCheck = {
|
|
|
120
124
|
results = runner.runAll({ checks: getBuiltInDocFreshnessConfig() }, projectPath, { requiredDirs });
|
|
121
125
|
}
|
|
122
126
|
|
|
123
|
-
|
|
127
|
+
const failed = results.filter(r => !r.pass);
|
|
128
|
+
if (failed.length > 0) {
|
|
129
|
+
return {
|
|
130
|
+
pass: false,
|
|
131
|
+
evidence: formatEvidence(
|
|
132
|
+
'文档新鲜度检查未通过',
|
|
133
|
+
failed.map(r => `${r.label}: ${r.message ?? '未通过'}`)
|
|
134
|
+
),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
124
137
|
} catch {
|
|
125
138
|
// FreshnessRunner 失败不影响整体
|
|
126
139
|
}
|
|
@@ -80,5 +80,12 @@ export function registeredCheckCount(): number {
|
|
|
80
80
|
return registry.size;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
export { buildCheckEnv } from './types';
|
|
84
|
-
export type {
|
|
83
|
+
export { buildCheckEnv, normalizeCheckOutcome } from './types';
|
|
84
|
+
export type {
|
|
85
|
+
ConstraintCheck,
|
|
86
|
+
CheckEnv,
|
|
87
|
+
CheckOutcome,
|
|
88
|
+
CheckDetail,
|
|
89
|
+
NormalizedOutcome,
|
|
90
|
+
EvidenceProviders,
|
|
91
|
+
} from './types';
|
|
@@ -73,8 +73,64 @@ export function buildCheckEnv(
|
|
|
73
73
|
* - true = 满足(pass)
|
|
74
74
|
* - false = 违反(fail)
|
|
75
75
|
* - 'skip' = 未评估(项目未采用对应约定,或证据 flag 未接线),不计 pass/fail
|
|
76
|
+
* - CheckDetail = 判定 + 证据行(harness#119)
|
|
76
77
|
*/
|
|
77
|
-
export type CheckOutcome = boolean | 'skip';
|
|
78
|
+
export type CheckOutcome = boolean | 'skip' | CheckDetail;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 带证据的检查结果(harness#119)
|
|
82
|
+
*
|
|
83
|
+
* 动机:只回布尔时,fail 的具体原因(哪个文件没登记)在 CLI 与 trace 两头都落脚不了,
|
|
84
|
+
* 一条恒红的约束无法被诊断。证据行由 checker 自行措辞(自描述、可直接给人看),
|
|
85
|
+
* 编排层只负责透传与打印,不参与解释内容。
|
|
86
|
+
*
|
|
87
|
+
* `pass: true` + 非空 evidence = 「满足但有提示」:不进 pass/fail 统计口径,
|
|
88
|
+
* 仅作为可观测输出(如仓库级文档漂移——与本次变更无因果,不该判违规)。
|
|
89
|
+
*/
|
|
90
|
+
export interface CheckDetail {
|
|
91
|
+
/** false = 违反;true = 满足 */
|
|
92
|
+
pass: boolean;
|
|
93
|
+
/** 证据行(每条自描述,如「变更文件未登记: src/foo.ts」) */
|
|
94
|
+
evidence?: string[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 归一后的检查结果:编排层 / gate 唯一消费形状 */
|
|
98
|
+
export interface NormalizedOutcome {
|
|
99
|
+
satisfied: boolean;
|
|
100
|
+
skipped: boolean;
|
|
101
|
+
evidence: string[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 证据条数上限:trace 是 JSONL,缺口上百项时不能整仓落盘 */
|
|
105
|
+
export const MAX_EVIDENCE_ITEMS = 10;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 组装证据行:一行说明 + 逐条依据(超限截断)
|
|
109
|
+
*
|
|
110
|
+
* 每行自描述且不带缩进——缩进由消费端(CLI / gate 理由 / 铁律异常文案)决定,trace 原样存。
|
|
111
|
+
* 修复入口不写在这里:那是各约束自己的事,写进各自的 summary 行(docs_freshness 的
|
|
112
|
+
* 幽灵条目与 sync-docs 无关,共用一个指针会误导)。
|
|
113
|
+
*/
|
|
114
|
+
export function formatEvidence(summary: string, items: string[]): string[] {
|
|
115
|
+
const shown = items.slice(0, MAX_EVIDENCE_ITEMS);
|
|
116
|
+
const hidden = items.length - shown.length;
|
|
117
|
+
return [...(summary ? [summary] : []), ...shown, ...(hidden > 0 ? [`…另 ${hidden} 项`] : [])];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 归一 CheckOutcome(boolean | 'skip' | CheckDetail)为统一形状
|
|
122
|
+
*
|
|
123
|
+
* skip 恒不携带证据:未评估的约束不得留下证据行(否则统计侧会把「没查」读成「查出问题」)。
|
|
124
|
+
*/
|
|
125
|
+
export function normalizeCheckOutcome(outcome: CheckOutcome): NormalizedOutcome {
|
|
126
|
+
if (outcome === 'skip') return { satisfied: true, skipped: true, evidence: [] };
|
|
127
|
+
if (typeof outcome === 'boolean') return { satisfied: outcome, skipped: false, evidence: [] };
|
|
128
|
+
return {
|
|
129
|
+
satisfied: outcome.pass,
|
|
130
|
+
skipped: false,
|
|
131
|
+
evidence: outcome.evidence ?? [],
|
|
132
|
+
};
|
|
133
|
+
}
|
|
78
134
|
|
|
79
135
|
/**
|
|
80
136
|
* 单条约束检查实现
|
|
@@ -82,7 +138,7 @@ export type CheckOutcome = boolean | 'skip';
|
|
|
82
138
|
export interface ConstraintCheck {
|
|
83
139
|
/** 约束 ID(与 definitions 一致) */
|
|
84
140
|
id: string;
|
|
85
|
-
/** 检查主体:true = 满足;false = 违反;'skip' =
|
|
141
|
+
/** 检查主体:true = 满足;false = 违反;'skip' = 未评估;CheckDetail = 判定 + 证据行 */
|
|
86
142
|
evaluate(env: CheckEnv): Promise<CheckOutcome> | CheckOutcome;
|
|
87
143
|
}
|
|
88
144
|
|
package/src/gates/CONTEXT.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
- 定义表:`GATE_DEFINITIONS`(`definitions.ts`)——id/description/默认 order/CLI 元数据;CLI 元数据形状即 `CommandDefinition`(ADR-0007:门禁与非门禁共用同一命令定义形状,bin 单引擎单循环;门禁特有语义 = `subcommandStrict:false` 未知子命令落回默认 action + `bareRunsAction:true` 裸跑执行 action);bin/harness.js 注册表驱动生成 6 门禁命令(纯数据模块,禁 import 实现,保 --help 懒加载;CLI 实现引用为 module+export,per-command 懒加载,H5)
|
|
10
10
|
- 执行器:`runGates`(`runner.ts`)——按 order 升序;deny 单调(下游 abstain 不得改回 allow)+ ask 枚举预留 fail-closed = deny;决策浅冻结
|
|
11
11
|
- 生效集:`getEffectiveGates(projectRoot)`(`effective-gates.ts`)——对齐 getEffectiveConstraints 裁剪模式:config.yml `gates.order` 重排 + `gates.<id>.enabled:false` 移除;enabled 段未注册校验与裁剪走 core/effective-set 共享筛选器(throw 模式),order 重排与重复 id 检测留本侧
|
|
12
|
-
- checker-as-guard 接线点:`createCheckerGate(check)`(`checker-gate.ts`)——ConstraintCheck → Gate(studio #129
|
|
12
|
+
- checker-as-guard 接线点:`createCheckerGate(check)`(`checker-gate.ts`)——ConstraintCheck → Gate(studio #129 随动);判定经唯一归一点 `normalizeCheckOutcome`(harness#119/ADR-0016):`false` 或 `CheckDetail.pass=false` → deny(证据行随理由带出),`true`(含 pass+提示)/'skip' → abstain;env 经 `buildCheckEnv(..., 'none')` 构造(显式不接证据,语义见 core/constraints/checkers/types.ts 工厂 doc)
|
|
13
13
|
- 门禁类:`ReviewGate` / `SecurityGate` / `PerformanceGate` / `ContractGate` / `SpecAcceptanceGate` / `CommandGate`(各自执行细节私有,保留 `check()`/`scan()` 报告方法);SpecAcceptanceGate 的 runner 输出解读与 e2e 判负都从 `core/validators/test-output.ts` 的 `judgeTestRun` 取(解析收口 ADR-0012、判定依据 ADR-0014,本层既不自带正则也不自写判定 `if`)
|
|
14
14
|
- `types.ts` — GateResult(报告结构,保留)/ GateContext / GateDecision 等公共类型 + 报告构造器 `pass` / `fail` / `fromError`(动态 passed 走 `gateResult`):timestamp 与 duration 口径唯一落点,门禁实现禁止手写 GateResult 字面量(duration 是可选字段,漏写无编译期报错)
|
|
15
15
|
- 便捷工厂函数:createReviewGate / createSecurityGate / createPerformanceGate / createContractGate / createSpecAcceptanceGate / createCommandGate
|
|
@@ -37,6 +37,30 @@ describe('createCheckerGate', () => {
|
|
|
37
37
|
expect(decision.result.message).toContain('跳过');
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
+
// harness#119:CheckDetail 形状接入后,映射必须经 normalizeCheckOutcome——
|
|
41
|
+
// 写 `outcome !== false` 会把 { pass: false } 读成「不是 false」= 通过,deny 静默丢失
|
|
42
|
+
it('CheckDetail{pass:false} → deny,理由带出证据行', async () => {
|
|
43
|
+
const gate = createCheckerGate({
|
|
44
|
+
id: 'detail-fail',
|
|
45
|
+
evaluate: () => ({ pass: false, evidence: ['src/gone.ts'] }),
|
|
46
|
+
});
|
|
47
|
+
const decision = await gate.evaluate(ctx);
|
|
48
|
+
expect(decision.status).toBe('deny');
|
|
49
|
+
expect(decision.result.passed).toBe(false);
|
|
50
|
+
expect(decision.result.message).toContain('判定违规');
|
|
51
|
+
expect(decision.result.message).toContain('src/gone.ts');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('CheckDetail{pass:true} + 提示证据 → abstain(提示不得变拦截)', async () => {
|
|
55
|
+
const gate = createCheckerGate({
|
|
56
|
+
id: 'detail-hint',
|
|
57
|
+
evaluate: () => ({ pass: true, evidence: ['仓库级漂移 1 项', 'src/drifted.ts'] }),
|
|
58
|
+
});
|
|
59
|
+
const decision = await gate.evaluate(ctx);
|
|
60
|
+
expect(decision.status).toBe('abstain');
|
|
61
|
+
expect(decision.result.message).toContain('通过');
|
|
62
|
+
});
|
|
63
|
+
|
|
40
64
|
it('决策浅冻结(单调语义契约)', async () => {
|
|
41
65
|
const gate = createCheckerGate(contextFlag('flag-freeze', () => true));
|
|
42
66
|
const decision = await gate.evaluate(ctx);
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { ConstraintCheck } from '../core/constraints/checkers';
|
|
11
|
-
import { buildCheckEnv } from '../core/constraints/checkers';
|
|
11
|
+
import { buildCheckEnv, normalizeCheckOutcome } from '../core/constraints/checkers';
|
|
12
12
|
import type { Gate, GateContext, GateDecision } from './types';
|
|
13
13
|
import { gateResult } from './types';
|
|
14
14
|
import { decisionFromResult } from './decision';
|
|
@@ -16,7 +16,8 @@ import { decisionFromResult } from './decision';
|
|
|
16
16
|
/**
|
|
17
17
|
* 将已注册 checker 适配为统一 Gate(checker-as-guard 接线点)
|
|
18
18
|
*
|
|
19
|
-
* 判定映射:false
|
|
19
|
+
* 判定映射:violated(false 或 CheckDetail.pass=false)→ deny;
|
|
20
|
+
* 满足 / 'skip'(未评估)→ abstain。违规证据随理由带出。
|
|
20
21
|
*
|
|
21
22
|
* @param check 已注册的 ConstraintCheck(经 checkers 注册表取回)
|
|
22
23
|
* @param order 门禁顺序(供 guard 链排序)
|
|
@@ -29,19 +30,14 @@ export function createCheckerGate(check: ConstraintCheck, order = 0): Gate {
|
|
|
29
30
|
const startTime = Date.now();
|
|
30
31
|
const projectPath = ctx.projectPath || process.cwd();
|
|
31
32
|
const env = buildCheckEnv({ operation: 'manual', projectPath }, 'none');
|
|
32
|
-
const outcome = await check.evaluate(env);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
? `checker "${check.id}" 跳过(证据未接线)`
|
|
41
|
-
: `checker "${check.id}" 通过`,
|
|
42
|
-
startTime
|
|
43
|
-
)
|
|
44
|
-
);
|
|
33
|
+
const outcome = normalizeCheckOutcome(await check.evaluate(env));
|
|
34
|
+
const evidence = outcome.evidence.map((e) => `\n - ${e}`).join('');
|
|
35
|
+
const reason = outcome.skipped
|
|
36
|
+
? `checker "${check.id}" 跳过(证据未接线)`
|
|
37
|
+
: outcome.satisfied
|
|
38
|
+
? `checker "${check.id}" 通过`
|
|
39
|
+
: `checker "${check.id}" 判定违规${evidence}`;
|
|
40
|
+
return decisionFromResult(gateResult(check.id, outcome.satisfied, reason, startTime));
|
|
45
41
|
},
|
|
46
42
|
};
|
|
47
43
|
}
|
package/src/types/constraint.ts
CHANGED
|
@@ -108,7 +108,16 @@ export interface ConstraintResult {
|
|
|
108
108
|
|
|
109
109
|
/** 消息 */
|
|
110
110
|
message?: string;
|
|
111
|
-
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 证据行(harness#119)
|
|
114
|
+
*
|
|
115
|
+
* checker 经 CheckDetail 返回的具体判定依据(如未登记的文件路径)。
|
|
116
|
+
* 违规时随 message 一并展示;满足但非空时为「提示」(不进 pass/fail 统计)。
|
|
117
|
+
* skip 恒为空——未评估不得留下证据行。
|
|
118
|
+
*/
|
|
119
|
+
evidence?: string[];
|
|
120
|
+
|
|
112
121
|
/** 建议操作 */
|
|
113
122
|
requiredAction?: string;
|
|
114
123
|
|
|
@@ -194,7 +203,12 @@ export class ConstraintViolationError extends Error {
|
|
|
194
203
|
public readonly result: ConstraintResult;
|
|
195
204
|
|
|
196
205
|
constructor(result: ConstraintResult) {
|
|
197
|
-
|
|
206
|
+
// 铁律违规在 checkConstraints 处直接 throw,CLI 的结构化输出块走不到这里,
|
|
207
|
+
// error.message 是判定证据唯一的外溢面(harness#119)
|
|
208
|
+
const detail = result.evidence?.length
|
|
209
|
+
? `:\n${result.evidence.map((line) => ` - ${line}`).join('\n')}`
|
|
210
|
+
: '';
|
|
211
|
+
super(`${result.message || 'Constraint violation'}${detail}`);
|
|
198
212
|
this.name = 'ConstraintViolationError';
|
|
199
213
|
this.result = result;
|
|
200
214
|
}
|