@dommaker/harness 0.14.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 (168) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/bin/harness.js +71 -0
  3. package/dist/cli/commands/doc-freshness-check.d.ts +53 -0
  4. package/dist/cli/commands/doc-freshness-check.d.ts.map +1 -0
  5. package/dist/cli/commands/doc-freshness-check.js +366 -0
  6. package/dist/cli/commands/doc-freshness-check.js.map +1 -0
  7. package/dist/cli/commands/index.d.ts +4 -1
  8. package/dist/cli/commands/index.d.ts.map +1 -1
  9. package/dist/cli/commands/index.js +13 -1
  10. package/dist/cli/commands/index.js.map +1 -1
  11. package/dist/cli/commands/knowledge.d.ts +12 -0
  12. package/dist/cli/commands/knowledge.d.ts.map +1 -1
  13. package/dist/cli/commands/knowledge.js +140 -6
  14. package/dist/cli/commands/knowledge.js.map +1 -1
  15. package/dist/cli/commands/release.d.ts +11 -0
  16. package/dist/cli/commands/release.d.ts.map +1 -1
  17. package/dist/cli/commands/release.js +80 -12
  18. package/dist/cli/commands/release.js.map +1 -1
  19. package/dist/cli/commands/sdd.d.ts +8 -0
  20. package/dist/cli/commands/sdd.d.ts.map +1 -0
  21. package/dist/cli/commands/sdd.js +68 -0
  22. package/dist/cli/commands/sdd.js.map +1 -0
  23. package/dist/cli/commands/spec-baseline-check.d.ts +30 -0
  24. package/dist/cli/commands/spec-baseline-check.d.ts.map +1 -0
  25. package/dist/cli/commands/spec-baseline-check.js +366 -0
  26. package/dist/cli/commands/spec-baseline-check.js.map +1 -0
  27. package/dist/core/constraints/checker.js +1 -1
  28. package/dist/core/constraints/checker.js.map +1 -1
  29. package/dist/core/constraints/definitions.d.ts.map +1 -1
  30. package/dist/core/constraints/definitions.js +30 -24
  31. package/dist/core/constraints/definitions.js.map +1 -1
  32. package/dist/knowledge/audit.d.ts +1 -1
  33. package/dist/knowledge/audit.d.ts.map +1 -1
  34. package/dist/knowledge/audit.js +60 -8
  35. package/dist/knowledge/audit.js.map +1 -1
  36. package/dist/knowledge/doctor.d.ts +1 -1
  37. package/dist/knowledge/doctor.d.ts.map +1 -1
  38. package/dist/knowledge/import.d.ts +1 -1
  39. package/dist/knowledge/import.d.ts.map +1 -1
  40. package/dist/knowledge/index-generator.d.ts +36 -0
  41. package/dist/knowledge/index-generator.d.ts.map +1 -0
  42. package/dist/knowledge/index-generator.js +241 -0
  43. package/dist/knowledge/index-generator.js.map +1 -0
  44. package/dist/knowledge/index.d.ts +4 -1
  45. package/dist/knowledge/index.d.ts.map +1 -1
  46. package/dist/knowledge/index.js +4 -2
  47. package/dist/knowledge/index.js.map +1 -1
  48. package/dist/knowledge/ingest.d.ts +1 -1
  49. package/dist/knowledge/ingest.d.ts.map +1 -1
  50. package/dist/knowledge/ingest.js +2 -2
  51. package/dist/knowledge/ingest.js.map +1 -1
  52. package/dist/knowledge/lifecycle-hooks.d.ts +1 -1
  53. package/dist/knowledge/lifecycle-hooks.d.ts.map +1 -1
  54. package/dist/knowledge/lifecycle.d.ts +16 -1
  55. package/dist/knowledge/lifecycle.d.ts.map +1 -1
  56. package/dist/knowledge/lifecycle.js +70 -0
  57. package/dist/knowledge/lifecycle.js.map +1 -1
  58. package/dist/knowledge/lint.d.ts +1 -1
  59. package/dist/knowledge/lint.d.ts.map +1 -1
  60. package/dist/knowledge/lint.js +4 -4
  61. package/dist/knowledge/lint.js.map +1 -1
  62. package/dist/knowledge/primitives/code-structure.d.ts +37 -0
  63. package/dist/knowledge/primitives/code-structure.d.ts.map +1 -0
  64. package/dist/knowledge/primitives/code-structure.js +168 -0
  65. package/dist/knowledge/primitives/code-structure.js.map +1 -0
  66. package/dist/knowledge/query.d.ts +1 -1
  67. package/dist/knowledge/query.d.ts.map +1 -1
  68. package/dist/knowledge/reference-tracker.d.ts +1 -1
  69. package/dist/knowledge/reference-tracker.d.ts.map +1 -1
  70. package/dist/knowledge/store.d.ts +29 -2
  71. package/dist/knowledge/store.d.ts.map +1 -1
  72. package/dist/knowledge/store.js +10 -5
  73. package/dist/knowledge/store.js.map +1 -1
  74. package/dist/knowledge/types.d.ts +17 -1
  75. package/dist/knowledge/types.d.ts.map +1 -1
  76. package/dist/knowledge/types.js.map +1 -1
  77. package/dist/monitoring/knowledge-evolver.d.ts +1 -1
  78. package/dist/monitoring/knowledge-evolver.d.ts.map +1 -1
  79. package/dist/sdd/index-generator.d.ts +22 -0
  80. package/dist/sdd/index-generator.d.ts.map +1 -0
  81. package/dist/sdd/index-generator.js +118 -0
  82. package/dist/sdd/index-generator.js.map +1 -0
  83. package/dist/types/constraint.d.ts +1 -1
  84. package/dist/types/constraint.d.ts.map +1 -1
  85. package/package.json +1 -1
  86. package/src/__tests__/checker.test.ts +4 -4
  87. package/src/cli/commands/__tests__/doc-freshness-check.test.ts +168 -0
  88. package/src/cli/commands/__tests__/knowledge.test.ts +70 -2
  89. package/src/cli/commands/__tests__/spec-baseline-check.test.ts +235 -0
  90. package/src/cli/commands/doc-freshness-check.ts +397 -0
  91. package/src/cli/commands/index.ts +4 -1
  92. package/src/cli/commands/knowledge.ts +151 -1
  93. package/src/cli/commands/release.ts +86 -12
  94. package/src/cli/commands/sdd.ts +44 -0
  95. package/src/cli/commands/spec-baseline-check.ts +384 -0
  96. package/src/context/__tests__/knowledge-injector.test.ts +1 -1
  97. package/src/core/constraints/checker.ts +1 -1
  98. package/src/core/constraints/definitions.ts +31 -25
  99. package/src/knowledge/__tests__/audit.test.ts +57 -1
  100. package/src/knowledge/__tests__/doctor.test.ts +1 -1
  101. package/src/knowledge/__tests__/import.test.ts +1 -1
  102. package/src/knowledge/__tests__/index-generator.test.ts +335 -0
  103. package/src/knowledge/__tests__/ingest.test.ts +1 -1
  104. package/src/knowledge/__tests__/lifecycle-hooks.test.ts +1 -1
  105. package/src/knowledge/__tests__/lifecycle.test.ts +1 -1
  106. package/src/knowledge/__tests__/lint.test.ts +1 -1
  107. package/src/knowledge/__tests__/query.test.ts +1 -1
  108. package/src/knowledge/__tests__/reference-tracker.test.ts +1 -1
  109. package/src/knowledge/__tests__/store.test.ts +1 -1
  110. package/src/knowledge/audit.ts +67 -10
  111. package/src/knowledge/doctor.ts +1 -1
  112. package/src/knowledge/import.ts +1 -1
  113. package/src/knowledge/index-generator.ts +233 -0
  114. package/src/knowledge/index.ts +4 -1
  115. package/src/knowledge/ingest.ts +3 -3
  116. package/src/knowledge/lifecycle-hooks.ts +1 -1
  117. package/src/knowledge/lifecycle.ts +72 -1
  118. package/src/knowledge/lint.ts +5 -5
  119. package/src/knowledge/primitives/__tests__/code-structure.test.ts +120 -0
  120. package/src/knowledge/primitives/code-structure.ts +161 -0
  121. package/src/knowledge/query.ts +1 -1
  122. package/src/knowledge/reference-tracker.ts +1 -1
  123. package/src/knowledge/store.ts +27 -4
  124. package/src/knowledge/types.ts +21 -1
  125. package/src/monitoring/knowledge-evolver.ts +1 -1
  126. package/src/sdd/__tests__/index-generator.test.ts +150 -0
  127. package/src/sdd/index-generator.ts +109 -0
  128. package/src/types/constraint.ts +1 -1
  129. package/dist/cli/commands/diagnose.d.ts +0 -16
  130. package/dist/cli/commands/diagnose.d.ts.map +0 -1
  131. package/dist/cli/commands/diagnose.js +0 -195
  132. package/dist/cli/commands/diagnose.js.map +0 -1
  133. package/dist/cli/commands/propose.d.ts +0 -18
  134. package/dist/cli/commands/propose.d.ts.map +0 -1
  135. package/dist/cli/commands/propose.js +0 -331
  136. package/dist/cli/commands/propose.js.map +0 -1
  137. package/dist/cli/commands/traces.d.ts +0 -15
  138. package/dist/cli/commands/traces.d.ts.map +0 -1
  139. package/dist/cli/commands/traces.js +0 -167
  140. package/dist/cli/commands/traces.js.map +0 -1
  141. package/dist/constraints/definitions.d.ts +0 -39
  142. package/dist/constraints/definitions.d.ts.map +0 -1
  143. package/dist/constraints/definitions.js +0 -71
  144. package/dist/constraints/definitions.js.map +0 -1
  145. package/dist/constraints/quality.d.ts +0 -11
  146. package/dist/constraints/quality.d.ts.map +0 -1
  147. package/dist/constraints/quality.js +0 -257
  148. package/dist/constraints/quality.js.map +0 -1
  149. package/dist/constraints/safety.d.ts +0 -13
  150. package/dist/constraints/safety.d.ts.map +0 -1
  151. package/dist/constraints/safety.js +0 -103
  152. package/dist/constraints/safety.js.map +0 -1
  153. package/dist/extensions/long-running/constraints.d.ts +0 -38
  154. package/dist/extensions/long-running/constraints.d.ts.map +0 -1
  155. package/dist/extensions/long-running/constraints.js +0 -153
  156. package/dist/extensions/long-running/constraints.js.map +0 -1
  157. package/dist/extensions/long-running/index.d.ts +0 -30
  158. package/dist/extensions/long-running/index.d.ts.map +0 -1
  159. package/dist/extensions/long-running/index.js +0 -50
  160. package/dist/extensions/long-running/index.js.map +0 -1
  161. package/dist/extensions/long-running/types.d.ts +0 -154
  162. package/dist/extensions/long-running/types.d.ts.map +0 -1
  163. package/dist/extensions/long-running/types.js +0 -9
  164. package/dist/extensions/long-running/types.js.map +0 -1
  165. package/dist/presets/long-running.d.ts +0 -33
  166. package/dist/presets/long-running.d.ts.map +0 -1
  167. package/dist/presets/long-running.js +0 -52
  168. package/dist/presets/long-running.js.map +0 -1
@@ -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
+ });
@@ -0,0 +1,397 @@
1
+ /**
2
+ * harness doc-freshness-check 命令
3
+ *
4
+ * 从文档中提取可验证的声明(claims),与代码/文件系统对照,输出差异。
5
+ * 纯代码操作,零 LLM 调用。
6
+ */
7
+
8
+ import chalk from 'chalk';
9
+ import * as fs from 'fs';
10
+ import * as path from 'path';
11
+
12
+ export interface DocFreshnessCheckOptions {
13
+ /** 输出 JSON 格式 */
14
+ format?: 'json' | 'table';
15
+ /** 变更文件列表(逗号分隔),用于聚焦检查 */
16
+ changedFiles?: string;
17
+ /** 项目路径 */
18
+ projectPath?: string;
19
+ }
20
+
21
+ /** 声明类型 */
22
+ export type ClaimType = 'numeric' | 'list' | 'status' | 'narrative';
23
+
24
+ /** 单条声明 */
25
+ export interface Claim {
26
+ /** 原始文本片段 */
27
+ text: string;
28
+ /** 声明类型 */
29
+ type: ClaimType;
30
+ /** 行号 */
31
+ line: number;
32
+ }
33
+
34
+ /** 验证结果 */
35
+ export interface ClaimResult {
36
+ /** 原始声明 */
37
+ claim: string;
38
+ /** 声明类型 */
39
+ type: ClaimType;
40
+ /** 文档中的期望值 */
41
+ expected: string;
42
+ /** 实际值 */
43
+ actual: string;
44
+ /** 来源文件 */
45
+ file: string;
46
+ /** 是否匹配 */
47
+ match: boolean;
48
+ }
49
+
50
+ // ============================================
51
+ // Claim 提取器
52
+ // ============================================
53
+
54
+ /** 数字声明:N 个/条/项 + 名词 */
55
+ const NUMERIC_PATTERNS = [
56
+ /(\d+)\s*个\s*(Agent|角色|能力|子系统|模块|端点|命令|组件|层级|阶段|步骤|断点|问题|维度|门|规则|约束)/g,
57
+ /(\d+)\s*条\s*(知识|规则|约束|命令|铁律|指南|提示|断点|问题|结果|记录)/g,
58
+ /(\d+)\s*项\s*(检查|任务|功能|变更|修复|优化|指标|配置)/g,
59
+ /(\d+)\s*(?:大|层)\s*(能力|约束|架构|层级)/g,
60
+ /(\d+)\s*个\s*(?:CLI\s*)?subcommand/gi,
61
+ ];
62
+
63
+ /** 列表声明:A, B, C, ... (至少 3 项) */
64
+ const LIST_PATTERN = /[A-Z][a-zA-Z]+(?:[,,、]\s*[A-Z][a-zA-Z]+){2,}/g;
65
+
66
+ /** 状态标记 */
67
+ const STATUS_PATTERNS = [
68
+ /✅\s*(已完成|已实现|已修复|已部署|已连通|已验证)/g,
69
+ /❌\s*(缺失|未实现|未完成|失败)/g,
70
+ /\bDEPRECATED\b/g,
71
+ /\bTODO\b/g,
72
+ /待(开发|实现|修复|完善|验证)/g,
73
+ /已(废弃|弃用|移除|删除)/g,
74
+ ];
75
+
76
+ /**
77
+ * 从 markdown 内容提取 claims
78
+ */
79
+ export function extractClaims(content: string, filePath: string): ClaimResult[] {
80
+ const lines = content.split('\n');
81
+ const results: ClaimResult[] = [];
82
+
83
+ for (let i = 0; i < lines.length; i++) {
84
+ const line = lines[i];
85
+
86
+ // 数字声明
87
+ for (const pattern of NUMERIC_PATTERNS) {
88
+ pattern.lastIndex = 0;
89
+ let match;
90
+ while ((match = pattern.exec(line)) !== null) {
91
+ const fullMatch = match[0];
92
+ const number = match[1];
93
+ results.push({
94
+ claim: fullMatch,
95
+ type: 'numeric',
96
+ expected: number,
97
+ actual: '', // 待验证
98
+ file: filePath,
99
+ match: false,
100
+ });
101
+ }
102
+ }
103
+
104
+ // 列表声明
105
+ const listMatches = line.match(LIST_PATTERN);
106
+ if (listMatches) {
107
+ for (const m of listMatches) {
108
+ const items = m.split(/[,,、]/).map(s => s.trim()).filter(Boolean);
109
+ if (items.length >= 3) {
110
+ results.push({
111
+ claim: m,
112
+ type: 'list',
113
+ expected: items.join(', '),
114
+ actual: '', // 待验证
115
+ file: filePath,
116
+ match: false,
117
+ });
118
+ }
119
+ }
120
+ }
121
+
122
+ // 状态标记
123
+ for (const pattern of STATUS_PATTERNS) {
124
+ pattern.lastIndex = 0;
125
+ let match;
126
+ while ((match = pattern.exec(line)) !== null) {
127
+ results.push({
128
+ claim: match[0],
129
+ type: 'status',
130
+ expected: match[0],
131
+ actual: '', // 待验证
132
+ file: filePath,
133
+ match: false,
134
+ });
135
+ }
136
+ }
137
+ }
138
+
139
+ return results;
140
+ }
141
+
142
+ // ============================================
143
+ // 验证器
144
+ // ============================================
145
+
146
+ /**
147
+ * 在项目目录中 grep 搜索匹配内容,返回匹配行数
148
+ */
149
+ function countGrepMatches(pattern: string, projectPath: string, fileGlob?: string): number {
150
+ const searchDir = projectPath || process.cwd();
151
+ let count = 0;
152
+
153
+ function walkDir(dir: string) {
154
+ let entries: fs.Dirent[];
155
+ try {
156
+ entries = fs.readdirSync(dir, { withFileTypes: true });
157
+ } catch {
158
+ return;
159
+ }
160
+
161
+ for (const entry of entries) {
162
+ if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === 'dist') continue;
163
+
164
+ const fullPath = path.join(dir, entry.name);
165
+ if (entry.isDirectory()) {
166
+ walkDir(fullPath);
167
+ } else if (entry.isFile()) {
168
+ if (fileGlob && !matchGlob(entry.name, fileGlob)) continue;
169
+ try {
170
+ const content = fs.readFileSync(fullPath, 'utf-8');
171
+ const regex = new RegExp(pattern, 'gi');
172
+ const matches = content.match(regex);
173
+ if (matches) count += matches.length;
174
+ } catch {
175
+ // skip unreadable files
176
+ }
177
+ }
178
+ }
179
+ }
180
+
181
+ walkDir(searchDir);
182
+ return count;
183
+ }
184
+
185
+ /**
186
+ * 简单 glob 匹配(支持 *.ext 和 *)
187
+ */
188
+ function matchGlob(filename: string, glob: string): boolean {
189
+ if (glob === '*') return true;
190
+ if (glob.startsWith('*.')) {
191
+ const ext = glob.slice(1);
192
+ return filename.endsWith(ext);
193
+ }
194
+ return filename === glob;
195
+ }
196
+
197
+ /**
198
+ * 验证数字声明:计数相关文件/代码模式
199
+ */
200
+ function verifyNumericClaim(claim: ClaimResult, projectPath: string): ClaimResult {
201
+ const expected = parseInt(claim.expected, 10);
202
+
203
+ // 从声明文本推断搜索模式
204
+ let searchPattern = '';
205
+ let fileGlob = '*.ts';
206
+
207
+ if (claim.claim.includes('Agent')) {
208
+ searchPattern = 'export (class|interface|function) \\w*Agent';
209
+ } else if (claim.claim.includes('命令') || claim.claim.includes('subcommand')) {
210
+ fileGlob = '*.ts';
211
+ searchPattern = "program\\.command\\(";
212
+ } else if (claim.claim.includes('铁律') || claim.claim.includes('约束') || claim.claim.includes('规则')) {
213
+ searchPattern = "severity:\\s*['\"]?error";
214
+ fileGlob = '*.ts';
215
+ } else if (claim.claim.includes('指南')) {
216
+ searchPattern = "severity:\\s*['\"]?warning";
217
+ fileGlob = '*.ts';
218
+ } else if (claim.claim.includes('端点')) {
219
+ searchPattern = '(router|app)\\.(get|post|put|delete|patch)\\(';
220
+ fileGlob = '*.ts';
221
+ } else {
222
+ // 无法自动验证的数字声明
223
+ return { ...claim, actual: 'N/A (cannot auto-verify)', match: true };
224
+ }
225
+
226
+ const actual = countGrepMatches(searchPattern, projectPath, fileGlob);
227
+
228
+ return {
229
+ ...claim,
230
+ actual: String(actual),
231
+ match: Math.abs(actual - expected) <= Math.max(1, Math.floor(expected * 0.1)), // 10% tolerance
232
+ };
233
+ }
234
+
235
+ /**
236
+ * 验证列表声明:检查列表中的每一项是否存在于代码中
237
+ */
238
+ function verifyListClaim(claim: ClaimResult, projectPath: string): ClaimResult {
239
+ const items = claim.expected.split(',').map(s => s.trim());
240
+ const found: string[] = [];
241
+ const missing: string[] = [];
242
+
243
+ for (const item of items) {
244
+ const count = countGrepMatches(item, projectPath);
245
+ if (count > 0) {
246
+ found.push(item);
247
+ } else {
248
+ missing.push(item);
249
+ }
250
+ }
251
+
252
+ return {
253
+ ...claim,
254
+ actual: missing.length === 0
255
+ ? `all ${items.length} found`
256
+ : `missing: ${missing.join(', ')}`,
257
+ match: missing.length === 0,
258
+ };
259
+ }
260
+
261
+ /**
262
+ * 验证状态声明:检查标记是否与代码/文件一致
263
+ */
264
+ function verifyStatusClaim(claim: ClaimResult, projectPath: string): ClaimResult {
265
+ // 状态声明的验证较复杂,标记为需要人工确认
266
+ return {
267
+ ...claim,
268
+ actual: 'needs manual verification',
269
+ match: true, // 不阻断
270
+ };
271
+ }
272
+
273
+ /**
274
+ * 验证所有 claims
275
+ */
276
+ export function verifyClaims(
277
+ claims: ClaimResult[],
278
+ projectPath: string,
279
+ ): ClaimResult[] {
280
+ return claims.map(claim => {
281
+ switch (claim.type) {
282
+ case 'numeric':
283
+ return verifyNumericClaim(claim, projectPath);
284
+ case 'list':
285
+ return verifyListClaim(claim, projectPath);
286
+ case 'status':
287
+ return verifyStatusClaim(claim, projectPath);
288
+ default:
289
+ return { ...claim, actual: 'N/A', match: true };
290
+ }
291
+ });
292
+ }
293
+
294
+ // ============================================
295
+ // 输出格式化
296
+ // ============================================
297
+
298
+ function formatTable(results: ClaimResult[]): string {
299
+ if (results.length === 0) return chalk.yellow('未发现可验证的声明');
300
+
301
+ const lines: string[] = [];
302
+ lines.push(chalk.blue(`文档新鲜度检查 (${results.length} 条声明)\n`));
303
+
304
+ // 统计
305
+ const mismatches = results.filter(r => !r.match);
306
+ lines.push(chalk.bold(` 匹配: ${results.length - mismatches.length}/${results.length}`));
307
+ if (mismatches.length > 0) {
308
+ lines.push(chalk.red(` 不匹配: ${mismatches.length}\n`));
309
+ } else {
310
+ lines.push(chalk.green(' 全部匹配\n'));
311
+ }
312
+
313
+ // 按类型分组
314
+ const byType: Record<string, ClaimResult[]> = {};
315
+ for (const r of results) {
316
+ if (!byType[r.type]) byType[r.type] = [];
317
+ byType[r.type].push(r);
318
+ }
319
+
320
+ const typeLabels: Record<string, string> = {
321
+ numeric: '数字声明',
322
+ list: '列表声明',
323
+ status: '状态标记',
324
+ narrative: '叙述声明',
325
+ };
326
+
327
+ for (const [type, items] of Object.entries(byType)) {
328
+ lines.push(chalk.bold(` ${typeLabels[type] || type} (${items.length}):`));
329
+ for (const item of items) {
330
+ const icon = item.match ? chalk.green('✓') : chalk.red('✗');
331
+ lines.push(` ${icon} ${item.claim}`);
332
+ if (!item.match) {
333
+ lines.push(` 期望: ${item.expected}`);
334
+ lines.push(` 实际: ${chalk.yellow(item.actual)}`);
335
+ }
336
+ }
337
+ lines.push('');
338
+ }
339
+
340
+ return lines.join('\n');
341
+ }
342
+
343
+ // ============================================
344
+ // 主入口
345
+ // ============================================
346
+
347
+ /**
348
+ * doc-freshness-check CLI 命令
349
+ */
350
+ export async function docFreshnessCheck(
351
+ docPath: string,
352
+ options: DocFreshnessCheckOptions = {},
353
+ ): Promise<void> {
354
+ // 验证文件存在
355
+ const resolvedPath = path.resolve(docPath);
356
+ if (!fs.existsSync(resolvedPath)) {
357
+ console.error(chalk.red(`文件不存在: ${resolvedPath}`));
358
+ process.exitCode = 1;
359
+ return;
360
+ }
361
+
362
+ if (!resolvedPath.endsWith('.md')) {
363
+ console.error(chalk.yellow(`警告: 文件不是 .md 格式,可能无法正确解析`));
364
+ }
365
+
366
+ const projectPath = options.projectPath || path.dirname(resolvedPath);
367
+
368
+ // 读取文档
369
+ const content = fs.readFileSync(resolvedPath, 'utf-8');
370
+
371
+ // 提取声明
372
+ let claims = extractClaims(content, resolvedPath);
373
+
374
+ // 如果指定了变更文件,只验证与变更文件相关的声明
375
+ if (options.changedFiles) {
376
+ const changed = options.changedFiles.split(',').map(s => s.trim());
377
+ // 过滤:保留所有 claims,但只验证与变更文件相关的
378
+ // 实际实现中可以根据声明内容推断相关文件
379
+ void changed; // 预留
380
+ }
381
+
382
+ // 验证
383
+ const results = verifyClaims(claims, projectPath);
384
+
385
+ // 输出
386
+ if (options.format === 'json') {
387
+ console.log(JSON.stringify(results, null, 2));
388
+ } else {
389
+ console.log(formatTable(results));
390
+ }
391
+
392
+ // 有不匹配时 exit 1
393
+ const mismatches = results.filter(r => !r.match);
394
+ if (mismatches.length > 0) {
395
+ process.exitCode = 1;
396
+ }
397
+ }
@@ -17,10 +17,13 @@ export { contract, validateSchema, type ContractOptions } from './contract';
17
17
  export { review, reviewStatus, type ReviewOptions } from './review';
18
18
  export { executeCommand, type CommandCheckOptions } from './command';
19
19
  export { syncDocs, type SyncDocsOptions } from './sync-docs';
20
- export { knowledgeList, knowledgeSearch, knowledgeImport, knowledgeDecay, knowledgeStats, knowledgeUpsert, knowledgeSyncStatus, knowledgeSyncRag, knowledgeAudit, knowledgeSnapshot, knowledgeMigrate, type KnowledgeOptions, type KnowledgeUpsertOptions } from './knowledge';
20
+ export { knowledgeList, knowledgeSearch, knowledgeImport, knowledgeDecay, knowledgeStats, knowledgeUpsert, knowledgeSyncStatus, knowledgeSyncRag, knowledgeAudit, knowledgeSnapshot, knowledgeMigrate, knowledgeHealth, knowledgeIndex, type KnowledgeOptions, type KnowledgeUpsertOptions } from './knowledge';
21
21
  export { failureList, failureStats, failureClear, type FailureOptions } from './failure';
22
22
  export { postevalPlan, type PostEvalPlanOptions } from './posteval-plan';
23
23
  export { release, type ReleaseOptions } from './release';
24
24
  export { analyzeSessions, type AnalyzeSessionsOptions } from './analyze-sessions';
25
25
  export { updateUserModel, type UpdateUserModelOptions } from './update-user-model';
26
26
  export { constraints, getConstraintsMeta, type ConstraintsMeta } from './constraints';
27
+ export { docFreshnessCheck, extractClaims, verifyClaims, type DocFreshnessCheckOptions, type ClaimResult, type ClaimType } from './doc-freshness-check';
28
+ export { specBaselineCheck, extractBaselineSection, type SpecBaselineCheckOptions, type PrerequisiteResult } from './spec-baseline-check';
29
+ export { sddIndex, type SDDOptions } from './sdd';