@dommaker/harness 0.16.0 → 0.16.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.
Files changed (79) hide show
  1. package/bin/harness.js +71 -0
  2. package/dist/cli/commands/doc-freshness-check.d.ts +53 -0
  3. package/dist/cli/commands/doc-freshness-check.d.ts.map +1 -0
  4. package/dist/cli/commands/doc-freshness-check.js +366 -0
  5. package/dist/cli/commands/doc-freshness-check.js.map +1 -0
  6. package/dist/cli/commands/index.d.ts +4 -1
  7. package/dist/cli/commands/index.d.ts.map +1 -1
  8. package/dist/cli/commands/index.js +13 -1
  9. package/dist/cli/commands/index.js.map +1 -1
  10. package/dist/cli/commands/knowledge.d.ts +12 -0
  11. package/dist/cli/commands/knowledge.d.ts.map +1 -1
  12. package/dist/cli/commands/knowledge.js +134 -0
  13. package/dist/cli/commands/knowledge.js.map +1 -1
  14. package/dist/cli/commands/release.d.ts +11 -0
  15. package/dist/cli/commands/release.d.ts.map +1 -1
  16. package/dist/cli/commands/release.js +43 -12
  17. package/dist/cli/commands/release.js.map +1 -1
  18. package/dist/cli/commands/sdd.d.ts +8 -0
  19. package/dist/cli/commands/sdd.d.ts.map +1 -0
  20. package/dist/cli/commands/sdd.js +68 -0
  21. package/dist/cli/commands/sdd.js.map +1 -0
  22. package/dist/cli/commands/spec-baseline-check.d.ts +30 -0
  23. package/dist/cli/commands/spec-baseline-check.d.ts.map +1 -0
  24. package/dist/cli/commands/spec-baseline-check.js +366 -0
  25. package/dist/cli/commands/spec-baseline-check.js.map +1 -0
  26. package/dist/knowledge/audit.d.ts +1 -1
  27. package/dist/knowledge/audit.d.ts.map +1 -1
  28. package/dist/knowledge/audit.js +59 -7
  29. package/dist/knowledge/audit.js.map +1 -1
  30. package/dist/knowledge/index-generator.d.ts +36 -0
  31. package/dist/knowledge/index-generator.d.ts.map +1 -0
  32. package/dist/knowledge/index-generator.js +241 -0
  33. package/dist/knowledge/index-generator.js.map +1 -0
  34. package/dist/knowledge/index.d.ts +2 -0
  35. package/dist/knowledge/index.d.ts.map +1 -1
  36. package/dist/knowledge/index.js +3 -1
  37. package/dist/knowledge/index.js.map +1 -1
  38. package/dist/knowledge/ingest.js +2 -2
  39. package/dist/knowledge/ingest.js.map +1 -1
  40. package/dist/knowledge/lifecycle.d.ts +15 -0
  41. package/dist/knowledge/lifecycle.d.ts.map +1 -1
  42. package/dist/knowledge/lifecycle.js +70 -0
  43. package/dist/knowledge/lifecycle.js.map +1 -1
  44. package/dist/knowledge/lint.js +3 -3
  45. package/dist/knowledge/lint.js.map +1 -1
  46. package/dist/knowledge/primitives/code-structure.d.ts +37 -0
  47. package/dist/knowledge/primitives/code-structure.d.ts.map +1 -0
  48. package/dist/knowledge/primitives/code-structure.js +168 -0
  49. package/dist/knowledge/primitives/code-structure.js.map +1 -0
  50. package/dist/knowledge/types.d.ts +3 -1
  51. package/dist/knowledge/types.d.ts.map +1 -1
  52. package/dist/knowledge/types.js.map +1 -1
  53. package/dist/sdd/index-generator.d.ts +22 -0
  54. package/dist/sdd/index-generator.d.ts.map +1 -0
  55. package/dist/sdd/index-generator.js +118 -0
  56. package/dist/sdd/index-generator.js.map +1 -0
  57. package/package.json +1 -1
  58. package/src/cli/commands/__tests__/doc-freshness-check.test.ts +168 -0
  59. package/src/cli/commands/__tests__/knowledge.test.ts +69 -1
  60. package/src/cli/commands/__tests__/spec-baseline-check.test.ts +235 -0
  61. package/src/cli/commands/doc-freshness-check.ts +397 -0
  62. package/src/cli/commands/index.ts +4 -1
  63. package/src/cli/commands/knowledge.ts +150 -0
  64. package/src/cli/commands/release.ts +48 -12
  65. package/src/cli/commands/sdd.ts +44 -0
  66. package/src/cli/commands/spec-baseline-check.ts +384 -0
  67. package/src/knowledge/__tests__/audit.test.ts +56 -0
  68. package/src/knowledge/__tests__/index-generator.test.ts +335 -0
  69. package/src/knowledge/audit.ts +64 -8
  70. package/src/knowledge/index-generator.ts +233 -0
  71. package/src/knowledge/index.ts +2 -0
  72. package/src/knowledge/ingest.ts +2 -2
  73. package/src/knowledge/lifecycle.ts +71 -0
  74. package/src/knowledge/lint.ts +3 -3
  75. package/src/knowledge/primitives/__tests__/code-structure.test.ts +120 -0
  76. package/src/knowledge/primitives/code-structure.ts +161 -0
  77. package/src/knowledge/types.ts +4 -1
  78. package/src/sdd/__tests__/index-generator.test.ts +150 -0
  79. package/src/sdd/index-generator.ts +109 -0
@@ -2,7 +2,7 @@
2
2
  * knowledge 命令测试 — knowledgeAudit
3
3
  */
4
4
 
5
- import { knowledgeAudit, knowledgeStats } from '../knowledge';
5
+ import { knowledgeAudit, knowledgeStats, knowledgeHealth } from '../knowledge';
6
6
 
7
7
  // Mock chalk
8
8
  jest.mock('chalk', () => ({
@@ -22,6 +22,13 @@ jest.mock('../../../knowledge/audit', () => ({
22
22
  })),
23
23
  }));
24
24
 
25
+ // Mock KnowledgeIndexGenerator (avoids real fs writes in CLI tests)
26
+ jest.mock('../../../knowledge/index-generator', () => ({
27
+ KnowledgeIndexGenerator: jest.fn().mockImplementation(() => ({
28
+ regenerate: jest.fn(),
29
+ })),
30
+ }));
31
+
25
32
  const MOCK_REPORT = {
26
33
  timestamp: '2026-06-02T00:00:00.000Z',
27
34
  totalEntries: 100,
@@ -165,3 +172,64 @@ describe('knowledgeAudit CLI', () => {
165
172
  }));
166
173
  });
167
174
  });
175
+
176
+ describe('knowledgeHealth CLI', () => {
177
+ const storeModule = require('../../../knowledge/store');
178
+ let consoleSpy: jest.SpyInstance;
179
+ let storeSpy: jest.SpyInstance;
180
+
181
+ beforeEach(() => {
182
+ jest.clearAllMocks();
183
+ consoleSpy = jest.spyOn(console, 'log').mockImplementation();
184
+ });
185
+
186
+ afterEach(() => {
187
+ consoleSpy.mockRestore();
188
+ if (storeSpy) storeSpy.mockRestore();
189
+ });
190
+
191
+ it('should output health score and summary in JSON mode', async () => {
192
+ storeSpy = jest.spyOn(storeModule, 'FileKnowledgeStore').mockImplementation(() => ({
193
+ list: jest.fn().mockReturnValue([
194
+ { id: 'GUI-001', maturity: 'verified', sourceReferences: [], referencedBy: ['GUI-002'], created: new Date().toISOString() },
195
+ { id: 'GUI-002', maturity: 'draft', sourceReferences: [], referencedBy: [], created: new Date().toISOString() },
196
+ ]),
197
+ getBaseDir: jest.fn().mockReturnValue('/tmp/knowledge'),
198
+ }));
199
+
200
+ await knowledgeHealth({ json: true });
201
+ const output = consoleSpy.mock.calls.map((c: any[]) => c[0]).join('\n');
202
+ const parsed = JSON.parse(output);
203
+ expect(parsed).toHaveProperty('healthScore');
204
+ expect(parsed.summary).toHaveProperty('total', 2);
205
+ });
206
+
207
+ it('should detect low-reference verified entries', async () => {
208
+ storeSpy = jest.spyOn(storeModule, 'FileKnowledgeStore').mockImplementation(() => ({
209
+ list: jest.fn().mockReturnValue([
210
+ { id: 'GUI-001', maturity: 'verified', sourceReferences: [], referencedBy: [], created: new Date().toISOString() },
211
+ ]),
212
+ getBaseDir: jest.fn().mockReturnValue('/tmp/knowledge'),
213
+ }));
214
+
215
+ await knowledgeHealth({ json: true });
216
+ const output = consoleSpy.mock.calls.map((c: any[]) => c[0]).join('\n');
217
+ const parsed = JSON.parse(output);
218
+ expect(parsed.summary.lowRefEntries).toBe(1);
219
+ });
220
+
221
+ it('should detect stale draft entries', async () => {
222
+ const oldDate = new Date(Date.now() - 100 * 24 * 60 * 60 * 1000).toISOString(); // 100 days ago
223
+ storeSpy = jest.spyOn(storeModule, 'FileKnowledgeStore').mockImplementation(() => ({
224
+ list: jest.fn().mockReturnValue([
225
+ { id: 'GUI-001', maturity: 'draft', sourceReferences: [], referencedBy: [], created: oldDate },
226
+ ]),
227
+ getBaseDir: jest.fn().mockReturnValue('/tmp/knowledge'),
228
+ }));
229
+
230
+ await knowledgeHealth({ json: true });
231
+ const output = consoleSpy.mock.calls.map((c: any[]) => c[0]).join('\n');
232
+ const parsed = JSON.parse(output);
233
+ expect(parsed.summary.staleEntries).toBe(1);
234
+ });
235
+ });
@@ -0,0 +1,235 @@
1
+ /**
2
+ * spec-baseline-check 命令测试
3
+ */
4
+
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ import { extractBaselineSection, specBaselineCheck } from '../spec-baseline-check';
8
+
9
+ describe('spec-baseline-check', () => {
10
+ const tempDir = path.join(process.cwd(), 'temp-test-spec-baseline');
11
+ let consoleSpy: jest.SpyInstance;
12
+ let errorSpy: jest.SpyInstance;
13
+ let originalExitCode: string | number | undefined;
14
+
15
+ beforeAll(() => {
16
+ fs.mkdirSync(tempDir, { recursive: true });
17
+ });
18
+
19
+ afterAll(() => {
20
+ try {
21
+ fs.rmSync(tempDir, { recursive: true, force: true });
22
+ } catch {
23
+ // ignore
24
+ }
25
+ });
26
+
27
+ beforeEach(() => {
28
+ jest.clearAllMocks();
29
+ consoleSpy = jest.spyOn(console, 'log').mockImplementation();
30
+ errorSpy = jest.spyOn(console, 'error').mockImplementation();
31
+ originalExitCode = process.exitCode;
32
+ process.exitCode = 0;
33
+ });
34
+
35
+ afterEach(() => {
36
+ consoleSpy.mockRestore();
37
+ errorSpy.mockRestore();
38
+ process.exitCode = originalExitCode;
39
+ });
40
+
41
+ describe('extractBaselineSection', () => {
42
+ it('应该提取 ## Baseline section 中的列表项', () => {
43
+ const content = [
44
+ '# Spec Title',
45
+ '',
46
+ '## Overview',
47
+ 'Some overview text',
48
+ '',
49
+ '## Baseline',
50
+ '- Node.js >= 18',
51
+ '- TypeScript 5.x installed',
52
+ '- `src/core/` directory exists',
53
+ '',
54
+ '## Details',
55
+ 'More details here',
56
+ ].join('\n');
57
+
58
+ const results = extractBaselineSection(content);
59
+ expect(results).toHaveLength(3);
60
+ expect(results[0]).toBe('Node.js >= 18');
61
+ expect(results[1]).toBe('TypeScript 5.x installed');
62
+ expect(results[2]).toBe('`src/core/` directory exists');
63
+ });
64
+
65
+ it('应该提取 ## 前置条件 section', () => {
66
+ const content = [
67
+ '# 设计文档',
68
+ '',
69
+ '## 前置条件',
70
+ '- 依赖包 `@dommaker/harness` 已安装',
71
+ '- `package.json` 存在',
72
+ '',
73
+ '## 方案',
74
+ '方案描述',
75
+ ].join('\n');
76
+
77
+ const results = extractBaselineSection(content);
78
+ expect(results).toHaveLength(2);
79
+ expect(results[0]).toContain('@dommaker/harness');
80
+ });
81
+
82
+ it('应该支持编号列表', () => {
83
+ const content = [
84
+ '## Baseline',
85
+ '1. Node.js 18+',
86
+ '2. pnpm installed',
87
+ '3. Docker available',
88
+ ].join('\n');
89
+
90
+ const results = extractBaselineSection(content);
91
+ expect(results).toHaveLength(3);
92
+ expect(results[0]).toBe('Node.js 18+');
93
+ });
94
+
95
+ it('应该忽略非目标 section', () => {
96
+ const content = [
97
+ '## Baseline',
98
+ '- Item 1',
99
+ '',
100
+ '## Other Section',
101
+ '- Should not be included',
102
+ ].join('\n');
103
+
104
+ const results = extractBaselineSection(content);
105
+ expect(results).toHaveLength(1);
106
+ expect(results[0]).toBe('Item 1');
107
+ });
108
+
109
+ it('没有 Baseline section 应返回空数组', () => {
110
+ const content = [
111
+ '# Spec',
112
+ '## Overview',
113
+ 'No baseline here',
114
+ ].join('\n');
115
+
116
+ const results = extractBaselineSection(content);
117
+ expect(results).toHaveLength(0);
118
+ });
119
+
120
+ it('应该在遇到同级标题时停止', () => {
121
+ const content = [
122
+ '## Baseline',
123
+ '- Item 1',
124
+ '- Item 2',
125
+ '## Next Section',
126
+ '- Should not appear',
127
+ '### Sub section',
128
+ '- Also not',
129
+ ].join('\n');
130
+
131
+ const results = extractBaselineSection(content);
132
+ expect(results).toHaveLength(2);
133
+ });
134
+
135
+ it('应该支持嵌套在更高级标题下', () => {
136
+ const content = [
137
+ '# Main',
138
+ '## Setup',
139
+ '### Baseline',
140
+ '- Prereq 1',
141
+ '- Prereq 2',
142
+ '### Details', // 同级标题,应该停止
143
+ '- Not included',
144
+ '## More', // 更高级标题,也应该停止
145
+ '- Also not',
146
+ ].join('\n');
147
+
148
+ const results = extractBaselineSection(content);
149
+ expect(results).toHaveLength(2);
150
+ expect(results[0]).toBe('Prereq 1');
151
+ });
152
+ });
153
+
154
+ describe('specBaselineCheck CLI', () => {
155
+ it('应该在文件不存在时报错', async () => {
156
+ await specBaselineCheck('/nonexistent/spec.md');
157
+
158
+ expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('文件不存在'));
159
+ expect(process.exitCode).toBe(1);
160
+ });
161
+
162
+ it('没有 Baseline section 应该提示', async () => {
163
+ const specPath = path.join(tempDir, 'no-baseline.md');
164
+ fs.writeFileSync(specPath, '# Spec\n## Overview\nNo baseline here.');
165
+
166
+ await specBaselineCheck(specPath, { projectPath: tempDir });
167
+
168
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('未找到'));
169
+ });
170
+
171
+ it('应该输出 table 格式结果', async () => {
172
+ // 创建测试用的文件和目录
173
+ const subDir = path.join(tempDir, 'test-project');
174
+ fs.mkdirSync(subDir, { recursive: true });
175
+ fs.writeFileSync(path.join(subDir, 'package.json'), JSON.stringify({ dependencies: {} }));
176
+
177
+ const specPath = path.join(tempDir, 'with-baseline.md');
178
+ fs.writeFileSync(specPath, [
179
+ '# Test Spec',
180
+ '## Baseline',
181
+ '- `package.json` 文件存在',
182
+ '- 普通文本描述',
183
+ ].join('\n'));
184
+
185
+ await specBaselineCheck(specPath, { projectPath: subDir });
186
+
187
+ const output = consoleSpy.mock.calls.map((c: any[]) => c[0]).join('\n');
188
+ expect(output).toContain('前置条件检查');
189
+ });
190
+
191
+ it('应该输出 JSON 格式', async () => {
192
+ const specPath = path.join(tempDir, 'json-test.md');
193
+ fs.writeFileSync(specPath, [
194
+ '# JSON Test',
195
+ '## Baseline',
196
+ '- `package.json` exists',
197
+ ].join('\n'));
198
+
199
+ await specBaselineCheck(specPath, { json: true, projectPath: tempDir });
200
+
201
+ const jsonCall = consoleSpy.mock.calls.find((c: any[]) => {
202
+ try {
203
+ const parsed = JSON.parse(c[0]);
204
+ return Array.isArray(parsed);
205
+ } catch {
206
+ return false;
207
+ }
208
+ });
209
+ expect(jsonCall).toBeTruthy();
210
+ const parsed = JSON.parse(jsonCall[0]);
211
+ expect(parsed.length).toBe(1);
212
+ expect(parsed[0]).toHaveProperty('prerequisite');
213
+ expect(parsed[0]).toHaveProperty('satisfied');
214
+ expect(parsed[0]).toHaveProperty('evidence');
215
+ });
216
+
217
+ it('空 Baseline section 应该输出空结果', async () => {
218
+ const specPath = path.join(tempDir, 'empty-baseline.md');
219
+ fs.writeFileSync(specPath, '# Test\n## Baseline\n\n## Next');
220
+
221
+ await specBaselineCheck(specPath, { json: true, projectPath: tempDir });
222
+
223
+ const jsonCall = consoleSpy.mock.calls.find((c: any[]) => {
224
+ try {
225
+ const parsed = JSON.parse(c[0]);
226
+ return parsed.prerequisites !== undefined;
227
+ } catch {
228
+ return false;
229
+ }
230
+ });
231
+ expect(jsonCall).toBeTruthy();
232
+ expect(JSON.parse(jsonCall[0]).prerequisites).toHaveLength(0);
233
+ });
234
+ });
235
+ });