@dommaker/harness 1.0.0 → 1.1.1
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 +19 -13
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +0 -9
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/commands/sync-docs/agents-syncer.d.ts.map +1 -1
- package/dist/cli/commands/sync-docs/agents-syncer.js +25 -4
- package/dist/cli/commands/sync-docs/agents-syncer.js.map +1 -1
- package/dist/completion-checkers/classify.d.ts +23 -0
- package/dist/completion-checkers/classify.d.ts.map +1 -0
- package/dist/completion-checkers/classify.js +37 -0
- package/dist/completion-checkers/classify.js.map +1 -0
- package/dist/completion-checkers/contract-presence.d.ts +12 -0
- package/dist/completion-checkers/contract-presence.d.ts.map +1 -0
- package/dist/completion-checkers/contract-presence.js +36 -0
- package/dist/completion-checkers/contract-presence.js.map +1 -0
- package/dist/completion-checkers/glob-match.d.ts +13 -0
- package/dist/completion-checkers/glob-match.d.ts.map +1 -0
- package/dist/completion-checkers/glob-match.js +58 -0
- package/dist/completion-checkers/glob-match.js.map +1 -0
- package/dist/completion-checkers/index.d.ts +19 -0
- package/dist/completion-checkers/index.d.ts.map +1 -0
- package/dist/completion-checkers/index.js +47 -0
- package/dist/completion-checkers/index.js.map +1 -0
- package/dist/completion-checkers/phase-format.d.ts +15 -0
- package/dist/completion-checkers/phase-format.d.ts.map +1 -0
- package/dist/completion-checkers/phase-format.js +42 -0
- package/dist/completion-checkers/phase-format.js.map +1 -0
- package/dist/completion-checkers/tdd-chain.d.ts +16 -0
- package/dist/completion-checkers/tdd-chain.d.ts.map +1 -0
- package/dist/completion-checkers/tdd-chain.js +57 -0
- package/dist/completion-checkers/tdd-chain.js.map +1 -0
- package/dist/completion-checkers/types.d.ts +77 -0
- package/dist/completion-checkers/types.d.ts.map +1 -0
- package/dist/completion-checkers/types.js +10 -0
- package/dist/completion-checkers/types.js.map +1 -0
- package/dist/core/constraints/checker.d.ts +3 -1
- package/dist/core/constraints/checker.d.ts.map +1 -1
- package/dist/core/constraints/checker.js +4 -2
- package/dist/core/constraints/checker.js.map +1 -1
- package/dist/core/constraints/interceptor.d.ts +3 -2
- package/dist/core/constraints/interceptor.d.ts.map +1 -1
- package/dist/core/constraints/interceptor.js +4 -4
- package/dist/core/constraints/interceptor.js.map +1 -1
- package/dist/index.d.ts +10 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -5
- package/dist/index.js.map +1 -1
- package/dist/monitoring/constraint-doctor.d.ts +2 -8
- package/dist/monitoring/constraint-doctor.d.ts.map +1 -1
- package/dist/monitoring/constraint-doctor.js +2 -4
- package/dist/monitoring/constraint-doctor.js.map +1 -1
- package/dist/monitoring/traces.d.ts +3 -3
- package/dist/monitoring/traces.d.ts.map +1 -1
- package/dist/monitoring/traces.js.map +1 -1
- package/dist/pretool-use-hook.d.ts +16 -0
- package/dist/pretool-use-hook.d.ts.map +1 -0
- package/dist/pretool-use-hook.js +68 -0
- package/dist/pretool-use-hook.js.map +1 -0
- package/dist/types/trace.d.ts +4 -4
- package/dist/types/trace.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/CONTEXT.md +2 -2
- package/src/__tests__/checker-extra.test.ts +47 -0
- package/src/__tests__/pretool-use-hook.test.ts +82 -0
- package/src/cli/commands/__tests__/status-extra.test.ts +0 -22
- package/src/cli/commands/__tests__/status.test.ts +0 -15
- package/src/cli/commands/__tests__/sync-docs-agents.test.ts +55 -0
- package/src/cli/commands/status.ts +0 -10
- package/src/cli/commands/sync-docs/agents-syncer.ts +26 -6
- package/src/completion-checkers/CONTEXT.md +25 -0
- package/src/completion-checkers/__tests__/contract-presence.test.ts +44 -0
- package/src/completion-checkers/__tests__/phase-format.test.ts +60 -0
- package/src/completion-checkers/__tests__/tdd-chain.test.ts +99 -0
- package/src/completion-checkers/classify.ts +47 -0
- package/src/completion-checkers/contract-presence.ts +40 -0
- package/src/completion-checkers/glob-match.ts +54 -0
- package/src/completion-checkers/index.ts +19 -0
- package/src/completion-checkers/phase-format.ts +48 -0
- package/src/completion-checkers/tdd-chain.ts +63 -0
- package/src/completion-checkers/types.ts +84 -0
- package/src/core/CONTEXT.md +1 -1
- package/src/core/constraints/__tests__/interceptor.test.ts +14 -0
- package/src/core/constraints/checker.ts +5 -2
- package/src/core/constraints/interceptor.ts +7 -4
- package/src/index.ts +24 -6
- package/src/monitoring/constraint-doctor.ts +4 -15
- package/src/monitoring/traces.ts +3 -3
- package/src/pretool-use-hook.ts +68 -0
- package/src/types/trace.ts +4 -4
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* verifyPhaseFormat 测试(studio#160 验收:subject 不合规 / merge commit 违规)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { verifyPhaseFormat } from '../phase-format';
|
|
6
|
+
import type { CommitInput } from '../types';
|
|
7
|
+
|
|
8
|
+
const sha = (n: number) => n.toString(16).padStart(40, '0');
|
|
9
|
+
|
|
10
|
+
function commit(partial: Partial<CommitInput> & { sha: string }): CommitInput {
|
|
11
|
+
return { subject: 'phase(impl): x', body: '', files: ['src/a.ts'], ...partial };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('verifyPhaseFormat', () => {
|
|
15
|
+
it('合规 subject → pass', () => {
|
|
16
|
+
const commits = [
|
|
17
|
+
commit({ sha: sha(1), subject: 'phase(test): add chain tests' }),
|
|
18
|
+
commit({ sha: sha(2), subject: 'phase(impl-2): wire it' }),
|
|
19
|
+
];
|
|
20
|
+
const result = verifyPhaseFormat(commits);
|
|
21
|
+
expect(result.verdict).toBe('pass');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it.each([
|
|
25
|
+
'feat: not a phase subject',
|
|
26
|
+
'phase: missing scope',
|
|
27
|
+
'phase(Impl): uppercase scope',
|
|
28
|
+
'phase(impl):missing space after colon',
|
|
29
|
+
'phase(impl): ',
|
|
30
|
+
])('subject 不合规 → violation:%s', (subject) => {
|
|
31
|
+
const result = verifyPhaseFormat([commit({ sha: sha(1), subject })]);
|
|
32
|
+
expect(result.verdict).toBe('violation');
|
|
33
|
+
expect(result.commits[0].reason).toContain('subject 不合规');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('merge commit(显式 isMerge)→ violation', () => {
|
|
37
|
+
const result = verifyPhaseFormat([commit({ sha: sha(1), isMerge: true })]);
|
|
38
|
+
expect(result.verdict).toBe('violation');
|
|
39
|
+
expect(result.commits[0].reason).toContain('merge commit');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('merge commit(isMerge 缺省时 subject 启发式)→ violation', () => {
|
|
43
|
+
const result = verifyPhaseFormat([
|
|
44
|
+
commit({ sha: sha(1), subject: "Merge branch 'w/x'" }),
|
|
45
|
+
]);
|
|
46
|
+
expect(result.verdict).toBe('violation');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('显式 isMerge=false 覆盖 subject 启发式', () => {
|
|
50
|
+
const result = verifyPhaseFormat([
|
|
51
|
+
commit({ sha: sha(1), subject: 'Merge 字样但非 merge', isMerge: false }),
|
|
52
|
+
]);
|
|
53
|
+
// subject 不含 phase 结构,仍因 subject 违规——但不应记 merge 违规
|
|
54
|
+
expect(result.commits[0].reason).not.toContain('merge commit');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('checker 关闭 → skip', () => {
|
|
58
|
+
expect(verifyPhaseFormat([commit({ sha: sha(1) })], { checkers: { phaseFormat: false } }).verdict).toBe('skip');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* verifyTddChain 测试(studio#160 验收:伪造引用三形态 / Tests: none waiver / 免检分类)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { verifyTddChain } from '../tdd-chain';
|
|
6
|
+
import type { CommitInput } from '../types';
|
|
7
|
+
|
|
8
|
+
const sha = (n: number) => n.toString(16).padStart(40, '0');
|
|
9
|
+
|
|
10
|
+
function commit(partial: Partial<CommitInput> & { sha: string }): CommitInput {
|
|
11
|
+
return { subject: 'phase(impl): x', body: '', files: ['src/a.ts'], ...partial };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('verifyTddChain', () => {
|
|
15
|
+
it('合法引用链 → pass(测试 commit 在前,实现 commit 引用它)', () => {
|
|
16
|
+
const commits = [
|
|
17
|
+
commit({ sha: sha(1), files: ['src/a.test.ts'], subject: 'phase(test): add test' }),
|
|
18
|
+
commit({ sha: sha(2), files: ['src/a.ts'], body: 'impl\n\nTested-By: ' + sha(1) }),
|
|
19
|
+
];
|
|
20
|
+
const result = verifyTddChain(commits);
|
|
21
|
+
expect(result.verdict).toBe('pass');
|
|
22
|
+
expect(result.commits.map((c) => c.verdict)).toEqual(['skip', 'pass']);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('短 sha(7 位前缀)引用可命中', () => {
|
|
26
|
+
const commits = [
|
|
27
|
+
commit({ sha: sha(1), files: ['__tests__/a.ts'] }),
|
|
28
|
+
commit({ sha: sha(2), body: 'Tested-By: ' + sha(1).slice(0, 7) }),
|
|
29
|
+
];
|
|
30
|
+
expect(verifyTddChain(commits).verdict).toBe('pass');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('伪造引用:sha 不在提交集内 → violation', () => {
|
|
34
|
+
const commits = [commit({ sha: sha(2), body: 'Tested-By: ' + sha(9) })];
|
|
35
|
+
const result = verifyTddChain(commits);
|
|
36
|
+
expect(result.verdict).toBe('violation');
|
|
37
|
+
expect(result.commits[0].reason).toContain('不在本 WU 提交集内');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('伪造引用:被引 commit 位置在本 commit 之后 → violation', () => {
|
|
41
|
+
const commits = [
|
|
42
|
+
commit({ sha: sha(2), body: 'Tested-By: ' + sha(1) }),
|
|
43
|
+
commit({ sha: sha(1), files: ['src/a.test.ts'] }),
|
|
44
|
+
];
|
|
45
|
+
const result = verifyTddChain(commits);
|
|
46
|
+
expect(result.verdict).toBe('violation');
|
|
47
|
+
expect(result.commits[0].reason).toContain('位置不在本 commit 之前');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('伪造引用:被引 commit 文件清单不含测试文件 → violation', () => {
|
|
51
|
+
const commits = [
|
|
52
|
+
commit({ sha: sha(1), files: ['src/b.ts'] }),
|
|
53
|
+
commit({ sha: sha(2), body: 'Tested-By: ' + sha(1) }),
|
|
54
|
+
];
|
|
55
|
+
const result = verifyTddChain(commits);
|
|
56
|
+
expect(result.verdict).toBe('violation');
|
|
57
|
+
expect(result.commits[1].reason).toContain('未命中 test_globs');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('缺 Tested-By trailer → violation', () => {
|
|
61
|
+
const result = verifyTddChain([commit({ sha: sha(1) })]);
|
|
62
|
+
expect(result.verdict).toBe('violation');
|
|
63
|
+
expect(result.commits[0].reason).toContain('缺 Tested-By trailer');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('Tests: none trailer → waiver(放行,不算 violation)', () => {
|
|
67
|
+
const result = verifyTddChain([commit({ sha: sha(1), body: 'chore\n\nTests: none' })]);
|
|
68
|
+
expect(result.verdict).toBe('pass');
|
|
69
|
+
expect(result.commits[0].verdict).toBe('waiver');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('纯测试 commit 与纯非代码 commit 天然免检', () => {
|
|
73
|
+
const commits = [
|
|
74
|
+
commit({ sha: sha(1), files: ['src/a.test.ts'] }),
|
|
75
|
+
commit({ sha: sha(2), files: ['README.md', 'docs/guide.md'] }),
|
|
76
|
+
];
|
|
77
|
+
const result = verifyTddChain(commits);
|
|
78
|
+
expect(result.verdict).toBe('pass');
|
|
79
|
+
expect(result.commits.every((c) => c.verdict === 'skip')).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('混合 commit(代码+测试同 commit)仍需引用链', () => {
|
|
83
|
+
const commits = [commit({ sha: sha(1), files: ['src/a.ts', 'src/a.test.ts'] })];
|
|
84
|
+
expect(verifyTddChain(commits).verdict).toBe('violation');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('自定义 test_globs / noncode_globs 生效', () => {
|
|
88
|
+
const commits = [commit({ sha: sha(1), files: ['assets/logo.png'] })];
|
|
89
|
+
const withGlob = verifyTddChain(commits, { noncodeGlobs: ['**/*.png'] });
|
|
90
|
+
expect(withGlob.commits[0].verdict).toBe('skip');
|
|
91
|
+
expect(verifyTddChain(commits).commits[0].verdict).toBe('violation');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('checker 关闭 / 总开关关闭 → skip', () => {
|
|
95
|
+
const commits = [commit({ sha: sha(1) })];
|
|
96
|
+
expect(verifyTddChain(commits, { checkers: { tddChain: false } }).verdict).toBe('skip');
|
|
97
|
+
expect(verifyTddChain(commits, { enabled: false }).verdict).toBe('skip');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commit 文件分类(tdd-chain 与 phase-format 共享同一份分类结果,Q4 定稿)
|
|
3
|
+
*
|
|
4
|
+
* - 纯非代码(全部命中 noncode_globs)与纯测试(全部命中 test_globs)commit 天然免检
|
|
5
|
+
* - 混合 commit 只要触到代码文件即需走引用链
|
|
6
|
+
* - 空文件清单按免检处理
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { CommitInput, CompletionCheckersConfig } from './types';
|
|
10
|
+
import { DEFAULT_NONCODE_GLOBS, DEFAULT_TEST_GLOBS, matchAnyGlob } from './glob-match';
|
|
11
|
+
|
|
12
|
+
/** 单 commit 文件分类结果 */
|
|
13
|
+
export interface CommitFileClassification {
|
|
14
|
+
/** 无代码文件(纯测试 / 纯非代码 / 空清单)→ 天然免检 */
|
|
15
|
+
exempt: boolean;
|
|
16
|
+
/** 文件清单命中 test_globs(被引 commit 须满足) */
|
|
17
|
+
hasTests: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** 从配置取 glob(缺省落默认) */
|
|
21
|
+
export function resolveGlobs(config: CompletionCheckersConfig): { testGlobs: string[]; noncodeGlobs: string[] } {
|
|
22
|
+
return {
|
|
23
|
+
testGlobs: config.testGlobs ?? DEFAULT_TEST_GLOBS,
|
|
24
|
+
noncodeGlobs: config.noncodeGlobs ?? DEFAULT_NONCODE_GLOBS,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 分类单 commit 的文件清单 */
|
|
29
|
+
export function classifyCommitFiles(
|
|
30
|
+
commit: CommitInput,
|
|
31
|
+
testGlobs: string[],
|
|
32
|
+
noncodeGlobs: string[],
|
|
33
|
+
): CommitFileClassification {
|
|
34
|
+
if (commit.files.length === 0) {
|
|
35
|
+
return { exempt: true, hasTests: false };
|
|
36
|
+
}
|
|
37
|
+
let hasTests = false;
|
|
38
|
+
let hasCode = false;
|
|
39
|
+
for (const file of commit.files) {
|
|
40
|
+
if (matchAnyGlob(file, testGlobs)) {
|
|
41
|
+
hasTests = true;
|
|
42
|
+
} else if (!matchAnyGlob(file, noncodeGlobs)) {
|
|
43
|
+
hasCode = true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { exempt: !hasCode, hasTests };
|
|
47
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* contract-presence:通用契约在场引擎(Q5 定稿)
|
|
3
|
+
*
|
|
4
|
+
* - 按 yml contracts 类型清单查表:类型不在清单内 = skip(不算违规)
|
|
5
|
+
* - 类型 → 判定方法的映射是代码不是配置(CONTRACT_JUDGMENTS);
|
|
6
|
+
* 首个活跃条目 review → context.reviewReport 在场(studio agent-loop 已解析字段,不重复解析)
|
|
7
|
+
* - 类型在清单内但无判定方法注册 = violation(暴露配置与代码失配)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { CompletionCheckersConfig, ContractPresenceContext, ContractPresenceResult } from './types';
|
|
11
|
+
|
|
12
|
+
/** 类型 → 判定方法映射(代码,不是配置)。新增契约类型 = 在此加一条 + 测试 */
|
|
13
|
+
const CONTRACT_JUDGMENTS: Record<string, (context: ContractPresenceContext) => boolean> = {
|
|
14
|
+
review: (context) => context.reviewReport != null,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** 契约在场判定 */
|
|
18
|
+
export function verifyContractPresence(
|
|
19
|
+
type: string,
|
|
20
|
+
context: ContractPresenceContext,
|
|
21
|
+
config: CompletionCheckersConfig = {},
|
|
22
|
+
): ContractPresenceResult {
|
|
23
|
+
if (config.enabled === false || config.checkers?.contractPresence === false) {
|
|
24
|
+
return { checker: 'contract-presence', verdict: 'skip', detail: 'checker 已禁用' };
|
|
25
|
+
}
|
|
26
|
+
if (!(config.contracts ?? []).includes(type)) {
|
|
27
|
+
return { checker: 'contract-presence', verdict: 'skip', detail: `类型 ${type} 无 contracts 表项` };
|
|
28
|
+
}
|
|
29
|
+
const judge = CONTRACT_JUDGMENTS[type];
|
|
30
|
+
if (!judge) {
|
|
31
|
+
return {
|
|
32
|
+
checker: 'contract-presence',
|
|
33
|
+
verdict: 'violation',
|
|
34
|
+
detail: `类型 ${type} 已在 contracts 声明但无判定方法注册(配置与代码失配)`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return judge(context)
|
|
38
|
+
? { checker: 'contract-presence', verdict: 'pass' }
|
|
39
|
+
: { checker: 'contract-presence', verdict: 'violation', detail: `类型 ${type} 契约标记缺失` };
|
|
40
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 轻量 glob 匹配(支持 `**`、`*`、`?`),用于 commits 文件清单分类。
|
|
3
|
+
* 不匹配文件系统,纯字符串判定。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** 默认测试文件 glob(Q2 定稿) */
|
|
7
|
+
export const DEFAULT_TEST_GLOBS = ['**/*.test.ts', '**/*.spec.ts', '**/__tests__/**'];
|
|
8
|
+
|
|
9
|
+
/** 默认非代码文件 glob(兜底;权威清单由 yml noncode_globs 供给) */
|
|
10
|
+
export const DEFAULT_NONCODE_GLOBS = ['**/*.md', '**/*.mdx', '**/*.txt', 'docs/**'];
|
|
11
|
+
|
|
12
|
+
function escapeRegExp(s: string): string {
|
|
13
|
+
return s.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** glob → 锚定正则。前缀 `**` + `/` 匹配零或多段路径;`*` 不跨 `/`;`?` 匹配单个非 `/` 字符 */
|
|
17
|
+
function globToRegExp(glob: string): RegExp {
|
|
18
|
+
let re = '';
|
|
19
|
+
let i = 0;
|
|
20
|
+
while (i < glob.length) {
|
|
21
|
+
const c = glob[i];
|
|
22
|
+
if (c === '*') {
|
|
23
|
+
if (glob[i + 1] === '*') {
|
|
24
|
+
if (glob[i + 2] === '/') {
|
|
25
|
+
re += '(?:[^/]+/)*';
|
|
26
|
+
i += 3;
|
|
27
|
+
} else {
|
|
28
|
+
re += '.*';
|
|
29
|
+
i += 2;
|
|
30
|
+
}
|
|
31
|
+
} else {
|
|
32
|
+
re += '[^/]*';
|
|
33
|
+
i += 1;
|
|
34
|
+
}
|
|
35
|
+
} else if (c === '?') {
|
|
36
|
+
re += '[^/]';
|
|
37
|
+
i += 1;
|
|
38
|
+
} else {
|
|
39
|
+
re += escapeRegExp(c);
|
|
40
|
+
i += 1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return new RegExp('^' + re + '$');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 单 glob 匹配 */
|
|
47
|
+
export function matchGlob(file: string, glob: string): boolean {
|
|
48
|
+
return globToRegExp(glob).test(file);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 命中任一 glob */
|
|
52
|
+
export function matchAnyGlob(file: string, globs: string[]): boolean {
|
|
53
|
+
return globs.some((g) => matchGlob(file, g));
|
|
54
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Completion Checkers(T7-E1,studio#160)
|
|
3
|
+
*
|
|
4
|
+
* WU 收尾软观测三件套——纯判定函数库:
|
|
5
|
+
* - verifyTddChain:实现 commit 的 Tested-By 引用链验证
|
|
6
|
+
* - verifyPhaseFormat:phase(...) subject 结构验证
|
|
7
|
+
* - verifyContractPresence:通用契约在场引擎
|
|
8
|
+
*
|
|
9
|
+
* 与 ConstraintCheck 闭环注册表无关:直接 export,不注册、不碰 checkers/index.ts。
|
|
10
|
+
* 函数不碰文件系统与 git;commits 由调用方(studio 第四段守卫,T7-E2)供给。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export * from './types';
|
|
14
|
+
export { DEFAULT_TEST_GLOBS, DEFAULT_NONCODE_GLOBS, matchGlob, matchAnyGlob } from './glob-match';
|
|
15
|
+
export { classifyCommitFiles, resolveGlobs } from './classify';
|
|
16
|
+
export type { CommitFileClassification } from './classify';
|
|
17
|
+
export { verifyTddChain, TESTED_BY_RE, TESTS_NONE_RE } from './tdd-chain';
|
|
18
|
+
export { verifyPhaseFormat, PHASE_SUBJECT_RE } from './phase-format';
|
|
19
|
+
export { verifyContractPresence } from './contract-presence';
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* phase-format:WU 提交 subject 的 phase 结构验证(Q4 定稿)
|
|
3
|
+
*
|
|
4
|
+
* 协议(正则写死在函数里,不进配置):
|
|
5
|
+
* - 全部非 merge commit 的 subject 命中 `^phase\([a-z0-9-]+\):\s+\S`
|
|
6
|
+
* - merge commit 出现即记违规
|
|
7
|
+
* - 阶段名不维护词表不查
|
|
8
|
+
* - 文件分类口径与 tdd-chain 共享(classify.ts),本 checker 不做免检——subject 结构要求覆盖全部非 merge commit
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { CommitInput, CommitVerdict, CompletionCheckersConfig, PhaseFormatResult } from './types';
|
|
12
|
+
|
|
13
|
+
/** phase subject 结构(写死) */
|
|
14
|
+
export const PHASE_SUBJECT_RE = /^phase\([a-z0-9-]+\):\s+\S/;
|
|
15
|
+
|
|
16
|
+
/** merge commit 的 subject 启发式(调用方未显式给 isMerge 时兜底) */
|
|
17
|
+
const MERGE_SUBJECT_RE = /^Merge\s/;
|
|
18
|
+
|
|
19
|
+
/** 判定 merge commit:优先 isMerge 显式字段,缺省按 subject 启发式 */
|
|
20
|
+
function isMergeCommit(commit: CommitInput): boolean {
|
|
21
|
+
return commit.isMerge ?? MERGE_SUBJECT_RE.test(commit.subject);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** phase 格式验证 */
|
|
25
|
+
export function verifyPhaseFormat(
|
|
26
|
+
commits: CommitInput[],
|
|
27
|
+
config: CompletionCheckersConfig = {},
|
|
28
|
+
): PhaseFormatResult {
|
|
29
|
+
if (config.enabled === false || config.checkers?.phaseFormat === false) {
|
|
30
|
+
return { checker: 'phase-format', verdict: 'skip', commits: [] };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const verdicts: CommitVerdict[] = commits.map((commit) => {
|
|
34
|
+
if (isMergeCommit(commit)) {
|
|
35
|
+
return { sha: commit.sha, verdict: 'violation', reason: 'WU 提交集内出现 merge commit' };
|
|
36
|
+
}
|
|
37
|
+
if (!PHASE_SUBJECT_RE.test(commit.subject)) {
|
|
38
|
+
return { sha: commit.sha, verdict: 'violation', reason: `subject 不合规:${commit.subject}` };
|
|
39
|
+
}
|
|
40
|
+
return { sha: commit.sha, verdict: 'pass' };
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
checker: 'phase-format',
|
|
45
|
+
verdict: verdicts.some((v) => v.verdict === 'violation') ? 'violation' : 'pass',
|
|
46
|
+
commits: verdicts,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tdd-chain:实现 commit 的测试引用链验证(Q2/Q3 定稿)
|
|
3
|
+
*
|
|
4
|
+
* 协议(写死为机制本体,不进配置):
|
|
5
|
+
* - 实现 commit 必须带 trailer `Tested-By: <sha>`
|
|
6
|
+
* - 被引 sha 须 a) 在本 WU 提交集内 b) 序列位置在本 commit 之前(比位置不比时间戳)c) 其文件清单命中 test_globs
|
|
7
|
+
* - 豁免:trailer `Tests: none` → waiver(放行记台账);纯非代码/纯测试 commit 天然免检
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { CommitInput, CommitVerdict, CompletionCheckersConfig, TddChainResult } from './types';
|
|
11
|
+
import { classifyCommitFiles, resolveGlobs } from './classify';
|
|
12
|
+
|
|
13
|
+
/** Tested-By 引用协议(写死) */
|
|
14
|
+
export const TESTED_BY_RE = /^Tested-By:\s*([0-9a-f]{7,40})\s*$/gim;
|
|
15
|
+
|
|
16
|
+
/** Tests: none 豁免协议(写死) */
|
|
17
|
+
export const TESTS_NONE_RE = /^Tests:\s*none\s*$/im;
|
|
18
|
+
|
|
19
|
+
/** 引用链验证:伪造引用(不存在 / 位置在后 / 不含测试文件)一律记 violation */
|
|
20
|
+
export function verifyTddChain(
|
|
21
|
+
commits: CommitInput[],
|
|
22
|
+
config: CompletionCheckersConfig = {},
|
|
23
|
+
): TddChainResult {
|
|
24
|
+
if (config.enabled === false || config.checkers?.tddChain === false) {
|
|
25
|
+
return { checker: 'tdd-chain', verdict: 'skip', commits: [] };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const { testGlobs, noncodeGlobs } = resolveGlobs(config);
|
|
29
|
+
// 文件分类一次性算好,与同批 commit 的其他 checker 共享口径
|
|
30
|
+
const classifications = commits.map((c) => classifyCommitFiles(c, testGlobs, noncodeGlobs));
|
|
31
|
+
|
|
32
|
+
const verdicts: CommitVerdict[] = commits.map((commit, i) => {
|
|
33
|
+
if (classifications[i].exempt) {
|
|
34
|
+
return { sha: commit.sha, verdict: 'skip', reason: '纯测试/纯非代码 commit,天然免检' };
|
|
35
|
+
}
|
|
36
|
+
if (TESTS_NONE_RE.test(commit.body)) {
|
|
37
|
+
return { sha: commit.sha, verdict: 'waiver', reason: 'Tests: none 显式豁免' };
|
|
38
|
+
}
|
|
39
|
+
TESTED_BY_RE.lastIndex = 0;
|
|
40
|
+
const m = TESTED_BY_RE.exec(commit.body);
|
|
41
|
+
if (!m) {
|
|
42
|
+
return { sha: commit.sha, verdict: 'violation', reason: '缺 Tested-By trailer 且未声明 Tests: none' };
|
|
43
|
+
}
|
|
44
|
+
const ref = m[1].toLowerCase();
|
|
45
|
+
const j = commits.findIndex((c) => c.sha.toLowerCase().startsWith(ref));
|
|
46
|
+
if (j === -1) {
|
|
47
|
+
return { sha: commit.sha, verdict: 'violation', reason: `Tested-By 引用 ${ref} 不在本 WU 提交集内` };
|
|
48
|
+
}
|
|
49
|
+
if (j >= i) {
|
|
50
|
+
return { sha: commit.sha, verdict: 'violation', reason: `Tested-By 引用 ${ref} 位置不在本 commit 之前` };
|
|
51
|
+
}
|
|
52
|
+
if (!classifications[j].hasTests) {
|
|
53
|
+
return { sha: commit.sha, verdict: 'violation', reason: `被引 commit ${ref} 文件清单未命中 test_globs` };
|
|
54
|
+
}
|
|
55
|
+
return { sha: commit.sha, verdict: 'pass' };
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
checker: 'tdd-chain',
|
|
60
|
+
verdict: verdicts.some((v) => v.verdict === 'violation') ? 'violation' : 'pass',
|
|
61
|
+
commits: verdicts,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Completion Checkers 类型定义(T7-E1,studio#160)
|
|
3
|
+
*
|
|
4
|
+
* 三个纯判定函数共享的输入/输出/配置类型。
|
|
5
|
+
* 与 ConstraintCheck 闭环注册表无关:直接 export,不注册、不碰 checkers/index.ts。
|
|
6
|
+
* 函数不碰文件系统与 git,commits 由调用方(studio 侧 git log)供给。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** WU 提交输入(有序,base..HEAD 升序) */
|
|
10
|
+
export interface CommitInput {
|
|
11
|
+
/** 完整 sha */
|
|
12
|
+
sha: string;
|
|
13
|
+
/** commit subject(第一行) */
|
|
14
|
+
subject: string;
|
|
15
|
+
/** commit body(trailer 所在) */
|
|
16
|
+
body: string;
|
|
17
|
+
/** 本 commit 触碰的文件清单(相对路径) */
|
|
18
|
+
files: string[];
|
|
19
|
+
/**
|
|
20
|
+
* 是否 merge commit。调用方有 git 数据时应显式供给(如 %P 父数 > 1);
|
|
21
|
+
* 缺省时按 subject 启发式判定(`/^Merge\s/`)。
|
|
22
|
+
*/
|
|
23
|
+
isMerge?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 判定结论:pass / violation / waiver(豁免放行,记台账)/ skip(不适用,不记台账) */
|
|
27
|
+
export type CheckerVerdict = 'pass' | 'violation' | 'waiver' | 'skip';
|
|
28
|
+
|
|
29
|
+
/** 单 commit 判定明细 */
|
|
30
|
+
export interface CommitVerdict {
|
|
31
|
+
sha: string;
|
|
32
|
+
verdict: CheckerVerdict;
|
|
33
|
+
reason?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** verifyTddChain 结果。整体 verdict 只取 pass/violation/skip;waiver 是 commit 级结论 */
|
|
37
|
+
export interface TddChainResult {
|
|
38
|
+
checker: 'tdd-chain';
|
|
39
|
+
verdict: 'pass' | 'violation' | 'skip';
|
|
40
|
+
commits: CommitVerdict[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** verifyPhaseFormat 结果 */
|
|
44
|
+
export interface PhaseFormatResult {
|
|
45
|
+
checker: 'phase-format';
|
|
46
|
+
verdict: 'pass' | 'violation' | 'skip';
|
|
47
|
+
commits: CommitVerdict[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** verifyContractPresence 上下文(类型→字段的判定方法映射是代码不是配置) */
|
|
51
|
+
export interface ContractPresenceContext {
|
|
52
|
+
/** review 类型契约:studio agent-loop 已解析的 metadata.reviewReport */
|
|
53
|
+
reviewReport?: unknown;
|
|
54
|
+
[key: string]: unknown;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** verifyContractPresence 结果 */
|
|
58
|
+
export interface ContractPresenceResult {
|
|
59
|
+
checker: 'contract-presence';
|
|
60
|
+
verdict: 'pass' | 'violation' | 'skip';
|
|
61
|
+
detail?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* completion_checkers 配置(对应 yml 顶层键 `completion_checkers:`,studio 侧自解)。
|
|
66
|
+
* 只配开关/glob/契约类型清单;协议格式(Tested-By、Tests: none、phase 结构)写死为机制本体。
|
|
67
|
+
* 缺省 = 全开 + 默认 glob。
|
|
68
|
+
*/
|
|
69
|
+
export interface CompletionCheckersConfig {
|
|
70
|
+
/** 总开关,缺省 true */
|
|
71
|
+
enabled?: boolean;
|
|
72
|
+
/** 各 checker 开关,缺省全开 */
|
|
73
|
+
checkers?: {
|
|
74
|
+
tddChain?: boolean;
|
|
75
|
+
phaseFormat?: boolean;
|
|
76
|
+
contractPresence?: boolean;
|
|
77
|
+
};
|
|
78
|
+
/** 测试文件 glob,缺省 DEFAULT_TEST_GLOBS */
|
|
79
|
+
testGlobs?: string[];
|
|
80
|
+
/** 非代码文件 glob,缺省 DEFAULT_NONCODE_GLOBS */
|
|
81
|
+
noncodeGlobs?: string[];
|
|
82
|
+
/** 契约类型清单:类型在清单内才判定,无表项 = skip */
|
|
83
|
+
contracts?: string[];
|
|
84
|
+
}
|
package/src/core/CONTEXT.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
约束引擎核心:check/prompt 二元约束系统(ADR-0001)、生效集合并(effective-constraints)、检查点验证器(CSO/passes-gate)、会话管理、Spec 验证器、项目配置加载。
|
|
5
5
|
|
|
6
6
|
## 核心导出
|
|
7
|
-
- `constraints/` — 约束定义(IRON_LAWS/GUIDELINES/PROMPTS
|
|
7
|
+
- `constraints/` — 约束定义(IRON_LAWS/GUIDELINES/PROMPTS) + 检查引擎(ConstraintChecker) + 拦截器(ConstraintInterceptor) + 缓存(CheckCache:TTL 缓存 + 计数采样,H6/G5 起公开导出) + 注入渲染(injection-renderer) + Agent prompt 渲染(agent-prompt-renderer:trigger 参数化分组渲染,role 路由留 studio,H6/G6)/漂移校验(injection-drift)/使用统计(usage-report)
|
|
8
8
|
- `effective-constraints.ts` — `getEffectiveConstraints(projectRoot)`:全仓唯一生效集来源(内置 → preset → config.yml 禁用(内置与 custom 同效)→ custom 追加(禁用/已退役的不追加)→ scenes 过滤);`lintEffectiveConfig` 配置诊断
|
|
9
9
|
- `validators/` — checkpoints、passes-gate、CSO 验证器
|
|
10
10
|
- `session/` — 会话管理
|
|
@@ -215,6 +215,20 @@ describe('ConstraintInterceptor', () => {
|
|
|
215
215
|
|
|
216
216
|
await expect(interceptor.intercept('code_implementation', mockContext)).rejects.toThrow();
|
|
217
217
|
});
|
|
218
|
+
|
|
219
|
+
it('should forward customConfig to checker.getConstraints', async () => {
|
|
220
|
+
interceptor.register('test-enforcement', mockExecutor);
|
|
221
|
+
(mockExecutor.execute as jest.Mock).mockResolvedValue({ passed: true });
|
|
222
|
+
|
|
223
|
+
const customConfig = {
|
|
224
|
+
ironLaws: { test_iron_law: mockIronLaw },
|
|
225
|
+
guidelines: {},
|
|
226
|
+
prompts: {},
|
|
227
|
+
} as any;
|
|
228
|
+
|
|
229
|
+
await interceptor.intercept('code_implementation', mockContext, customConfig);
|
|
230
|
+
expect(mockChecker.getConstraints).toHaveBeenCalledWith(customConfig);
|
|
231
|
+
});
|
|
218
232
|
});
|
|
219
233
|
|
|
220
234
|
describe('claim', () => {
|
|
@@ -494,13 +494,16 @@ export class ConstraintChecker {
|
|
|
494
494
|
|
|
495
495
|
/**
|
|
496
496
|
* 快捷函数:检查约束
|
|
497
|
+
*
|
|
498
|
+
* @param customConfig 可选,per-request 自定义配置;不传则返回内置约束集
|
|
497
499
|
*/
|
|
498
500
|
export async function checkConstraint(
|
|
499
501
|
constraintId: string,
|
|
500
|
-
context: ConstraintContext
|
|
502
|
+
context: ConstraintContext,
|
|
503
|
+
customConfig?: MergedConstraintsConfig | null
|
|
501
504
|
): Promise<ConstraintResult> {
|
|
502
505
|
const checker = ConstraintChecker.getInstance();
|
|
503
|
-
const constraints = checker.getConstraints();
|
|
506
|
+
const constraints = checker.getConstraints(customConfig);
|
|
504
507
|
|
|
505
508
|
const constraint =
|
|
506
509
|
constraints.ironLaws[constraintId] ||
|
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
EnforcementContext,
|
|
17
17
|
InterceptionResult,
|
|
18
18
|
} from '../../types/enforcement';
|
|
19
|
+
import type { MergedConstraintsConfig } from '../../types/project-config';
|
|
19
20
|
import { constraintChecker } from './checker';
|
|
20
21
|
|
|
21
22
|
export class ConstraintInterceptor {
|
|
@@ -91,7 +92,8 @@ export class ConstraintInterceptor {
|
|
|
91
92
|
|
|
92
93
|
async intercept(
|
|
93
94
|
trigger: ConstraintTrigger,
|
|
94
|
-
context: ConstraintContext
|
|
95
|
+
context: ConstraintContext,
|
|
96
|
+
customConfig?: MergedConstraintsConfig | null
|
|
95
97
|
): Promise<InterceptionResult> {
|
|
96
98
|
if (!this.enabled) {
|
|
97
99
|
return {
|
|
@@ -110,7 +112,7 @@ export class ConstraintInterceptor {
|
|
|
110
112
|
interceptedAt: new Date(),
|
|
111
113
|
};
|
|
112
114
|
|
|
113
|
-
const constraints = constraintChecker.getConstraints();
|
|
115
|
+
const constraints = constraintChecker.getConstraints(customConfig);
|
|
114
116
|
// prompt 类约束不参与拦截执行(ADR-0001:仅参与注入)
|
|
115
117
|
const allConstraints = [
|
|
116
118
|
...Object.values(constraints.ironLaws),
|
|
@@ -201,9 +203,10 @@ export const constraintInterceptor = ConstraintInterceptor.getInstance();
|
|
|
201
203
|
|
|
202
204
|
export async function interceptOperation(
|
|
203
205
|
trigger: ConstraintTrigger,
|
|
204
|
-
context: ConstraintContext
|
|
206
|
+
context: ConstraintContext,
|
|
207
|
+
customConfig?: MergedConstraintsConfig | null
|
|
205
208
|
): Promise<InterceptionResult> {
|
|
206
|
-
return constraintInterceptor.intercept(trigger, context);
|
|
209
|
+
return constraintInterceptor.intercept(trigger, context, customConfig);
|
|
207
210
|
}
|
|
208
211
|
|
|
209
212
|
export async function claimOperation(
|