@dommaker/harness 1.10.0 → 1.11.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/cli/commands/definitions.d.ts.map +1 -1
  3. package/dist/cli/commands/definitions.js +5 -10
  4. package/dist/cli/commands/definitions.js.map +1 -1
  5. package/dist/cli/commands/scaffold-templates.d.ts +1 -1
  6. package/dist/cli/commands/scaffold-templates.d.ts.map +1 -1
  7. package/dist/cli/commands/scaffold-templates.js +0 -16
  8. package/dist/cli/commands/scaffold-templates.js.map +1 -1
  9. package/dist/core/constraints/usage-report.d.ts +1 -1
  10. package/dist/core/constraints/usage-report.js +1 -1
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +14 -2
  14. package/dist/index.js.map +1 -1
  15. package/dist/knowledge/lint.d.ts +17 -0
  16. package/dist/knowledge/lint.d.ts.map +1 -1
  17. package/dist/knowledge/lint.js +52 -0
  18. package/dist/knowledge/lint.js.map +1 -1
  19. package/dist/knowledge/store.d.ts +7 -0
  20. package/dist/knowledge/store.d.ts.map +1 -1
  21. package/dist/knowledge/store.js +48 -13
  22. package/dist/knowledge/store.js.map +1 -1
  23. package/dist/knowledge/types.d.ts +5 -1
  24. package/dist/knowledge/types.d.ts.map +1 -1
  25. package/dist/knowledge/types.js +5 -1
  26. package/dist/knowledge/types.js.map +1 -1
  27. package/package.json +1 -1
  28. package/src/__tests__/public-exports.test.ts +7 -0
  29. package/src/__tests__/public-type-surface.test.ts +5 -0
  30. package/src/__tests__/usage-report-public-export.test.ts +21 -0
  31. package/src/cli/commands/CONTEXT.md +4 -4
  32. package/src/cli/commands/__tests__/constraints-report-json-flag.test.ts +101 -0
  33. package/src/cli/commands/__tests__/init-ondisk.test.ts +1 -1
  34. package/src/cli/commands/__tests__/init.test.ts +1 -5
  35. package/src/cli/commands/__tests__/numeric-flag-assembly.test.ts +1 -1
  36. package/src/cli/commands/__tests__/passes-gate-dead-options.test.ts +1 -1
  37. package/src/cli/commands/__tests__/registry.test.ts +1 -1
  38. package/src/cli/commands/definitions.ts +5 -10
  39. package/src/cli/commands/scaffold-templates.ts +0 -16
  40. package/src/core/CONTEXT.md +1 -1
  41. package/src/core/constraints/usage-report.ts +1 -1
  42. package/src/index.ts +20 -0
  43. package/src/knowledge/CONTEXT.md +4 -3
  44. package/src/knowledge/__tests__/lint.test.ts +84 -0
  45. package/src/knowledge/__tests__/store.test.ts +70 -0
  46. package/src/knowledge/lint.ts +60 -1
  47. package/src/knowledge/store.ts +48 -11
  48. package/src/knowledge/types.ts +7 -1
  49. package/src/monitoring/CONTEXT.md +1 -1
  50. package/dist/cli/commands/posteval-plan.d.ts +0 -15
  51. package/dist/cli/commands/posteval-plan.d.ts.map +0 -1
  52. package/dist/cli/commands/posteval-plan.js +0 -86
  53. package/dist/cli/commands/posteval-plan.js.map +0 -1
  54. package/src/cli/commands/__tests__/posteval-plan.test.ts +0 -204
  55. package/src/cli/commands/posteval-plan.ts +0 -98
@@ -1,204 +0,0 @@
1
- /**
2
- * posteval-plan 命令测试
3
- */
4
-
5
- import * as fs from 'fs';
6
- import { captureIO, type CapturingIO } from '../../command-contract';
7
- import * as path from 'path';
8
- import * as os from 'os';
9
-
10
- let io: CapturingIO;
11
- beforeEach(() => {
12
- io = captureIO();
13
- });
14
-
15
- describe('postevalPlan', () => {
16
- jest.setTimeout(30000); // Retry-based tests with exponential backoff need longer timeout
17
- let tempDir: string;
18
- let planPath: string;
19
- let originalFetch: typeof fetch;
20
- let originalEnv: NodeJS.ProcessEnv;
21
-
22
- beforeAll(() => {
23
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'temp-test-posteval-'));
24
- planPath = path.join(tempDir, 'plan.md');
25
- fs.writeFileSync(planPath, '# Test plan\n- [AC-001] Task 1\n- [AC-002] Task 2');
26
- });
27
-
28
- afterAll(() => {
29
- try {
30
- fs.rmSync(tempDir, { recursive: true, force: true });
31
- } catch {
32
- // ignore
33
- }
34
- });
35
-
36
- beforeEach(() => {
37
- jest.clearAllMocks();
38
- originalFetch = global.fetch;
39
- originalEnv = { ...process.env };
40
- delete process.env.API_PORT;
41
- delete process.env.POSTEVAL_GRACE_MS;
42
- });
43
-
44
- afterEach(() => {
45
- global.fetch = originalFetch;
46
- process.env = originalEnv;
47
- jest.useRealTimers();
48
- });
49
-
50
- it('应该成功验证 plan 覆盖率并打印绿色信息', async () => {
51
- global.fetch = jest.fn().mockResolvedValue({
52
- ok: true,
53
- json: () => Promise.resolve({
54
- completeness: 1,
55
- matchedAcs: ['AC-001', 'AC-002'],
56
- missedAcs: [],
57
- }),
58
- }) as any;
59
-
60
- const { postevalPlan } = await import('../posteval-plan');
61
- await postevalPlan({ planPath }, io);
62
-
63
- expect(io.outText()).toContain('100%');
64
- });
65
-
66
- it('completeness < 1:fail(覆盖率不足,含缺失项)', async () => {
67
- global.fetch = jest.fn().mockResolvedValue({
68
- ok: true,
69
- json: () => Promise.resolve({
70
- completeness: 0.5,
71
- matchedAcs: ['AC-001'],
72
- missedAcs: ['AC-002'],
73
- }),
74
- }) as any;
75
-
76
- const { postevalPlan } = await import('../posteval-plan');
77
- const result = await postevalPlan({ planPath }, io);
78
-
79
- expect(result).toEqual({ kind: 'fail', reason: expect.stringContaining('plan coverage 50% < 100%') });
80
- expect(io.errText()).toContain('50%');
81
- });
82
-
83
- it('completeness < 1 时应该列出缺失项', async () => {
84
- global.fetch = jest.fn().mockResolvedValue({
85
- ok: true,
86
- json: () => Promise.resolve({
87
- completeness: 0.33,
88
- matchedAcs: ['AC-001'],
89
- missedAcs: ['AC-002', 'AC-003'],
90
- }),
91
- }) as any;
92
-
93
- const { postevalPlan } = await import('../posteval-plan');
94
- const result = await postevalPlan({ planPath }, io);
95
-
96
- expect(result).toEqual({
97
- kind: 'fail',
98
- reason: 'plan coverage 33% < 100%,缺 2 项: AC-002; AC-003',
99
- });
100
- expect(io.errText()).toContain('AC-002');
101
- });
102
-
103
- it('4xx 错误应该 exit(1)', async () => {
104
- global.fetch = jest.fn().mockResolvedValue({
105
- ok: false,
106
- status: 400,
107
- statusText: 'Bad Request',
108
- }) as any;
109
-
110
- const { postevalPlan } = await import('../posteval-plan');
111
- const result = await postevalPlan({ planPath }, io);
112
-
113
- expect(result).toEqual({ kind: 'fail', reason: 'PostEval API error: 400 Bad Request' });
114
- expect(io.errText()).toContain('400');
115
- });
116
-
117
- it('5xx 错误:skip(未判定,允许提交;退出码面 0 不变)', async () => {
118
- global.fetch = jest.fn().mockResolvedValue({
119
- ok: false,
120
- status: 503,
121
- statusText: 'Service Unavailable',
122
- }) as any;
123
-
124
- const { postevalPlan } = await import('../posteval-plan');
125
- const result = await postevalPlan({ planPath }, io);
126
-
127
- expect(result).toEqual({ kind: 'skip', reason: expect.stringContaining('服务端错误 503') });
128
- expect(io.errText()).toContain('unavailable');
129
- });
130
-
131
- it('网络错误重试耗尽:skip(未判定,允许提交)', async () => {
132
- global.fetch = jest.fn().mockRejectedValue(new Error('ECONNREFUSED'));
133
-
134
- const { postevalPlan } = await import('../posteval-plan');
135
- const result = await postevalPlan({ planPath }, io);
136
-
137
- expect(result).toEqual({ kind: 'skip', reason: expect.stringContaining('Studio API 不可达') });
138
- expect(io.errText()).toContain('unreachable');
139
- });
140
-
141
- it('应该支持自定义 API_PORT', async () => {
142
- process.env.API_PORT = '3999';
143
- global.fetch = jest.fn().mockResolvedValue({
144
- ok: true,
145
- json: () => Promise.resolve({
146
- completeness: 1,
147
- matchedAcs: ['AC-001'],
148
- missedAcs: [],
149
- }),
150
- }) as any;
151
-
152
- const { postevalPlan } = await import('../posteval-plan');
153
- await postevalPlan({ planPath }, io);
154
-
155
- // Verify the fetch was called with port 3999
156
- const fetchCall = (global.fetch as jest.Mock).mock.calls[0][0];
157
- expect(fetchCall).toContain(':3999');
158
- });
159
-
160
- it('网络错误重试后成功应该不退出', async () => {
161
- // First 2 calls fail, 3rd succeeds
162
- const mockFetch = jest.fn()
163
- .mockRejectedValueOnce(new Error('ECONNREFUSED'))
164
- .mockRejectedValueOnce(new Error('ECONNREFUSED'))
165
- .mockResolvedValueOnce({
166
- ok: true,
167
- json: () => Promise.resolve({
168
- completeness: 1,
169
- matchedAcs: ['AC-001'],
170
- missedAcs: [],
171
- }),
172
- });
173
-
174
- global.fetch = mockFetch as any;
175
-
176
- const { postevalPlan } = await import('../posteval-plan');
177
- const result = await postevalPlan({ planPath }, io);
178
-
179
- // Should have been called 3 times
180
- expect(mockFetch).toHaveBeenCalledTimes(3);
181
- expect(io.outText()).toContain('100%');
182
- expect(result.kind).toBe('ok');
183
- });
184
-
185
- it('completeness 为 0 时应该正确显示 0%', async () => {
186
- global.fetch = jest.fn().mockResolvedValue({
187
- ok: true,
188
- json: () => Promise.resolve({
189
- completeness: 0,
190
- matchedAcs: [],
191
- missedAcs: ['AC-001'],
192
- }),
193
- }) as any;
194
-
195
- const { postevalPlan } = await import('../posteval-plan');
196
- const result = await postevalPlan({ planPath }, io);
197
-
198
- expect(result).toEqual({
199
- kind: 'fail',
200
- reason: 'plan coverage 0% < 100%,缺 1 项: AC-001',
201
- });
202
- expect(io.errText()).toContain('0%');
203
- });
204
- });
@@ -1,98 +0,0 @@
1
- /**
2
- * harness posteval-plan 命令
3
- *
4
- * 调用 Studio PostEval API,验证 plan 文件的 checklist items 是否都有对应的 staged diff。
5
- * pre-commit hook 使用此命令防止"假装完成"。
6
- *
7
- * 判定经返回值外溢(架构评审候选7):API 不可用/5xx 属"未判定"→ skip(退出码面 0 不变);
8
- * 覆盖率不足与 4xx → fail,reason 指明是哪一项否决。
9
- */
10
-
11
- import chalk from 'chalk';
12
- import { log, logError, processIO, type CommandIO, type CommandResult } from '../command-contract';
13
-
14
- export interface PostEvalPlanOptions {
15
- planPath: string;
16
- }
17
-
18
- const MAX_RETRIES = 3;
19
- const RETRY_DELAY_MS = 2000;
20
-
21
- async function fetchWithRetry(url: string, body: string, retries: number): Promise<Response> {
22
- let lastError: any;
23
- for (let i = 0; i < retries; i++) {
24
- try {
25
- const controller = new AbortController();
26
- const timeout = setTimeout(() => controller.abort(), 5000);
27
- const res = await fetch(url, {
28
- method: 'POST',
29
- headers: { 'Content-Type': 'application/json' },
30
- body,
31
- signal: controller.signal,
32
- });
33
- clearTimeout(timeout);
34
- return res;
35
- } catch (error: any) {
36
- lastError = error;
37
- if (i < retries - 1) {
38
- await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS * (i + 1))); // exponential backoff
39
- }
40
- }
41
- }
42
- throw lastError;
43
- }
44
-
45
- export async function postevalPlan(
46
- options: PostEvalPlanOptions,
47
- io: CommandIO = processIO,
48
- ): Promise<CommandResult> {
49
- const apiPort = process.env.API_PORT || '3001';
50
- const url = `http://localhost:${apiPort}/api/v1/agents/post-eval/plan-coverage`;
51
-
52
- try {
53
- const res = await fetchWithRetry(url, JSON.stringify({ planPath: options.planPath }), MAX_RETRIES);
54
-
55
- if (!res.ok) {
56
- if (res.status >= 500) {
57
- // Server error — allow commit with warning (don't block on infrastructure failure)
58
- logError(io, chalk.yellow(`⚠️ PostEval API unavailable (${res.status}), allowing commit with warning`));
59
- logError(io, chalk.yellow(` Plan: ${options.planPath}`));
60
- return { kind: 'skip', reason: `PostEval API 服务端错误 ${res.status},未判定` };
61
- }
62
- logError(io, chalk.red(`❌ PostEval API error: ${res.status} ${res.statusText}`));
63
- return { kind: 'fail', reason: `PostEval API error: ${res.status} ${res.statusText}` };
64
- }
65
-
66
- const report = await res.json() as {
67
- completeness: number;
68
- matchedAcs: string[];
69
- missedAcs: string[];
70
- };
71
-
72
- const pct = Math.round(report.completeness * 100);
73
-
74
- if (report.completeness < 1) {
75
- logError(io, chalk.red(`❌ Plan coverage: ${pct}% (${report.matchedAcs.length}/${report.matchedAcs.length + report.missedAcs.length})`));
76
- if (report.missedAcs.length > 0) {
77
- logError(io, chalk.red('Missed items:'));
78
- report.missedAcs.forEach((item: string) => logError(io, chalk.red(` - ${item}`)));
79
- }
80
- return {
81
- kind: 'fail',
82
- reason: `plan coverage ${pct}% < 100%,缺 ${report.missedAcs.length} 项: ${report.missedAcs.join('; ')}`,
83
- };
84
- }
85
-
86
- log(io, chalk.green(`✅ Plan coverage: ${pct}%`));
87
- return { kind: 'ok' };
88
- } catch (error: any) {
89
- // API unreachable after all retries — allow with warning (don't block commits on infra)
90
- logError(io, chalk.yellow(`⚠️ Studio API unreachable after ${MAX_RETRIES} retries, allowing commit with warning`));
91
- logError(io, chalk.yellow(` Plan: ${options.planPath}`));
92
- logError(io, chalk.yellow(` Error: ${error?.code || error?.message || String(error)}`));
93
- return {
94
- kind: 'skip',
95
- reason: `Studio API 不可达(${error?.code || error?.message || String(error)}),未判定`,
96
- };
97
- }
98
- }