@dommaker/harness 0.16.0 → 0.16.2

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 (87) hide show
  1. package/README.md +1 -0
  2. package/bin/harness.js +73 -1
  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 +134 -0
  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 +43 -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/cli/commands/sync-docs.d.ts +2 -0
  28. package/dist/cli/commands/sync-docs.d.ts.map +1 -1
  29. package/dist/cli/commands/sync-docs.js +320 -1
  30. package/dist/cli/commands/sync-docs.js.map +1 -1
  31. package/dist/knowledge/audit.d.ts +1 -1
  32. package/dist/knowledge/audit.d.ts.map +1 -1
  33. package/dist/knowledge/audit.js +59 -7
  34. package/dist/knowledge/audit.js.map +1 -1
  35. package/dist/knowledge/index-generator.d.ts +36 -0
  36. package/dist/knowledge/index-generator.d.ts.map +1 -0
  37. package/dist/knowledge/index-generator.js +241 -0
  38. package/dist/knowledge/index-generator.js.map +1 -0
  39. package/dist/knowledge/index.d.ts +2 -0
  40. package/dist/knowledge/index.d.ts.map +1 -1
  41. package/dist/knowledge/index.js +3 -1
  42. package/dist/knowledge/index.js.map +1 -1
  43. package/dist/knowledge/ingest.js +2 -2
  44. package/dist/knowledge/ingest.js.map +1 -1
  45. package/dist/knowledge/lifecycle.d.ts +15 -0
  46. package/dist/knowledge/lifecycle.d.ts.map +1 -1
  47. package/dist/knowledge/lifecycle.js +70 -0
  48. package/dist/knowledge/lifecycle.js.map +1 -1
  49. package/dist/knowledge/lint.js +3 -3
  50. package/dist/knowledge/lint.js.map +1 -1
  51. package/dist/knowledge/primitives/code-structure.d.ts +37 -0
  52. package/dist/knowledge/primitives/code-structure.d.ts.map +1 -0
  53. package/dist/knowledge/primitives/code-structure.js +168 -0
  54. package/dist/knowledge/primitives/code-structure.js.map +1 -0
  55. package/dist/knowledge/types.d.ts +3 -1
  56. package/dist/knowledge/types.d.ts.map +1 -1
  57. package/dist/knowledge/types.js.map +1 -1
  58. package/dist/sdd/index-generator.d.ts +22 -0
  59. package/dist/sdd/index-generator.d.ts.map +1 -0
  60. package/dist/sdd/index-generator.js +118 -0
  61. package/dist/sdd/index-generator.js.map +1 -0
  62. package/package.json +1 -1
  63. package/src/cli/commands/CONTEXT.md +3 -3
  64. package/src/cli/commands/__tests__/doc-freshness-check.test.ts +168 -0
  65. package/src/cli/commands/__tests__/knowledge.test.ts +69 -1
  66. package/src/cli/commands/__tests__/spec-baseline-check.test.ts +235 -0
  67. package/src/cli/commands/__tests__/sync-docs-agents.test.ts +226 -0
  68. package/src/cli/commands/doc-freshness-check.ts +397 -0
  69. package/src/cli/commands/index.ts +4 -1
  70. package/src/cli/commands/knowledge.ts +150 -0
  71. package/src/cli/commands/release.ts +48 -12
  72. package/src/cli/commands/sdd.ts +44 -0
  73. package/src/cli/commands/spec-baseline-check.ts +384 -0
  74. package/src/cli/commands/sync-docs.ts +355 -1
  75. package/src/knowledge/__tests__/audit.test.ts +56 -0
  76. package/src/knowledge/__tests__/index-generator.test.ts +335 -0
  77. package/src/knowledge/audit.ts +64 -8
  78. package/src/knowledge/index-generator.ts +233 -0
  79. package/src/knowledge/index.ts +2 -0
  80. package/src/knowledge/ingest.ts +2 -2
  81. package/src/knowledge/lifecycle.ts +71 -0
  82. package/src/knowledge/lint.ts +3 -3
  83. package/src/knowledge/primitives/__tests__/code-structure.test.ts +120 -0
  84. package/src/knowledge/primitives/code-structure.ts +161 -0
  85. package/src/knowledge/types.ts +4 -1
  86. package/src/sdd/__tests__/index-generator.test.ts +150 -0
  87. package/src/sdd/index-generator.ts +109 -0
@@ -0,0 +1,226 @@
1
+ /**
2
+ * sync-docs --agents(AGENTS.md 生成)测试
3
+ */
4
+
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ import { syncDocs } from '../sync-docs';
8
+
9
+ describe('sync-docs --agents', () => {
10
+ const tempDir = path.join(process.cwd(), 'temp-test-sync-docs-agents');
11
+
12
+ beforeAll(() => {
13
+ fs.mkdirSync(tempDir, { recursive: true });
14
+ });
15
+
16
+ afterAll(() => {
17
+ try {
18
+ fs.rmSync(tempDir, { recursive: true, force: true });
19
+ } catch {
20
+ // ignore
21
+ }
22
+ });
23
+
24
+ let consoleSpy: jest.SpyInstance;
25
+
26
+ beforeEach(() => {
27
+ jest.clearAllMocks();
28
+ consoleSpy = jest.spyOn(console, 'log').mockImplementation();
29
+ });
30
+
31
+ afterEach(() => {
32
+ consoleSpy.mockRestore();
33
+ });
34
+
35
+ /**
36
+ * 创建夹具仓库:package.json + .harness/config.yml + 知识库 + CLAUDE.md 治理块 + src/docs 目录
37
+ */
38
+ function createFixture(dir: string, opts: { pnpm?: boolean; withCapabilities?: boolean } = {}): void {
39
+ fs.mkdirSync(path.join(dir, 'src'), { recursive: true });
40
+ fs.mkdirSync(path.join(dir, 'docs'), { recursive: true });
41
+ fs.mkdirSync(path.join(dir, '.harness', 'knowledge'), { recursive: true });
42
+
43
+ fs.writeFileSync(path.join(dir, 'src', 'app.ts'), '/**\n * App module\n */\nexport const app = 1;');
44
+ fs.writeFileSync(path.join(dir, 'src', 'CONTEXT.md'), '# src\n\n夹具模块文档\n');
45
+ fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({
46
+ name: 'fixture-app',
47
+ description: '测试夹具项目',
48
+ scripts: {
49
+ dev: 'vite',
50
+ build: 'tsc',
51
+ test: 'jest',
52
+ 'test:e2e': 'jest e2e',
53
+ typecheck: 'tsc --noEmit',
54
+ lint: 'eslint .',
55
+ start: 'node dist/index.js',
56
+ clean: 'rimraf dist',
57
+ },
58
+ }, null, 2));
59
+ fs.writeFileSync(path.join(dir, '.harness', 'config.yml'), 'preset: standard\nenabled: true\n');
60
+ fs.writeFileSync(path.join(dir, '.harness', 'knowledge', 'k1.md'), '# k1\n');
61
+ fs.writeFileSync(path.join(dir, 'CLAUDE.md'), [
62
+ '# CLAUDE.md',
63
+ '',
64
+ '## Governance Rules',
65
+ '<!-- HARNESS_CONSTRAINTS_START -->',
66
+ '### Iron Laws (违反将阻断)',
67
+ '- **law_a**: 描述 a',
68
+ '- **law_b**: 描述 b',
69
+ '### Guidelines (应遵循)',
70
+ '- **g_a**: 描述',
71
+ '- **g_b**: 描述',
72
+ '- **g_c**: 描述',
73
+ '### Tips',
74
+ '- **t_a**: 描述',
75
+ '<!-- HARNESS_CONSTRAINTS_END -->',
76
+ ].join('\n'));
77
+
78
+ if (opts.pnpm) {
79
+ fs.writeFileSync(path.join(dir, 'pnpm-lock.yaml'), 'lockfileVersion: 1\n');
80
+ }
81
+ if (opts.withCapabilities) {
82
+ fs.writeFileSync(
83
+ path.join(dir, 'CAPABILITIES.md'),
84
+ '# Capabilities\n\n| 模块 | 文件 | 说明 |\n|------|------|------|\n| app | src/app.ts | App module |'
85
+ );
86
+ }
87
+ }
88
+
89
+ it('应该从夹具仓库生成 AGENTS.md(pnpm 项目)', async () => {
90
+ const testDir = path.join(tempDir, 'gen-pnpm');
91
+ createFixture(testDir, { pnpm: true });
92
+
93
+ await syncDocs({ projectPath: testDir, agents: true });
94
+
95
+ const agentsPath = path.join(testDir, 'AGENTS.md');
96
+ expect(fs.existsSync(agentsPath)).toBe(true);
97
+ const content = fs.readFileSync(agentsPath, 'utf-8');
98
+
99
+ // 项目简介
100
+ expect(content).toContain('**fixture-app** — 测试夹具项目');
101
+ // 目录结构
102
+ expect(content).toContain('`docs/`');
103
+ expect(content).toContain('项目文档');
104
+ expect(content).toContain('`src/`');
105
+ expect(content).toContain('源码目录');
106
+ // 常用命令(仅 curated,不含 clean;pnpm 前缀)
107
+ expect(content).toContain('pnpm dev');
108
+ expect(content).toContain('pnpm build');
109
+ expect(content).toContain('pnpm test:e2e');
110
+ expect(content).toContain('pnpm typecheck');
111
+ expect(content).toContain('pnpm start');
112
+ expect(content).not.toContain('clean');
113
+ // 约束与治理
114
+ expect(content).toContain('preset: standard');
115
+ expect(content).toContain('Iron Laws 2 条、Guidelines 3 条');
116
+ // 知识入口
117
+ expect(content).toContain('项目知识库(1 条)');
118
+ expect(content).toContain('CONTEXT.md');
119
+ expect(content).toContain('现有 1 个');
120
+
121
+ fs.rmSync(testDir, { recursive: true, force: true });
122
+ });
123
+
124
+ it('npm 项目应该使用 npm run 前缀', async () => {
125
+ const testDir = path.join(tempDir, 'gen-npm');
126
+ createFixture(testDir);
127
+
128
+ await syncDocs({ projectPath: testDir, agents: true });
129
+
130
+ const content = fs.readFileSync(path.join(testDir, 'AGENTS.md'), 'utf-8');
131
+ expect(content).toContain('npm run build');
132
+ expect(content).toContain('npm test');
133
+ expect(content).toContain('npm start');
134
+ expect(content).toContain('npm run typecheck');
135
+
136
+ fs.rmSync(testDir, { recursive: true, force: true });
137
+ });
138
+
139
+ it('重复运行应该幂等(内容一致且 --check 通过)', async () => {
140
+ const testDir = path.join(tempDir, 'idempotent');
141
+ createFixture(testDir, { pnpm: true, withCapabilities: true });
142
+
143
+ await syncDocs({ projectPath: testDir, agents: true });
144
+ const first = fs.readFileSync(path.join(testDir, 'AGENTS.md'), 'utf-8');
145
+
146
+ // 第二次运行:无任何差异
147
+ const secondRun = await syncDocs({ projectPath: testDir, agents: true });
148
+ const second = fs.readFileSync(path.join(testDir, 'AGENTS.md'), 'utf-8');
149
+ expect(second).toBe(first);
150
+ expect(secondRun).toBe(true);
151
+
152
+ // --check 模式应通过
153
+ const checkResult = await syncDocs({ projectPath: testDir, agents: true, check: true });
154
+ expect(checkResult).toBe(true);
155
+
156
+ fs.rmSync(testDir, { recursive: true, force: true });
157
+ });
158
+
159
+ it('--check 模式应该检测 AGENTS.md 缺失', async () => {
160
+ const testDir = path.join(tempDir, 'check-missing');
161
+ createFixture(testDir, { withCapabilities: true });
162
+
163
+ const result = await syncDocs({ projectPath: testDir, agents: true, check: true });
164
+ expect(result).toBe(false);
165
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('缺少 AGENTS.md'));
166
+ // check 模式不写入
167
+ expect(fs.existsSync(path.join(testDir, 'AGENTS.md'))).toBe(false);
168
+
169
+ fs.rmSync(testDir, { recursive: true, force: true });
170
+ });
171
+
172
+ it('--check 模式应该检测内容漂移', async () => {
173
+ const testDir = path.join(tempDir, 'check-drift');
174
+ createFixture(testDir, { pnpm: true, withCapabilities: true });
175
+
176
+ // 先生成
177
+ await syncDocs({ projectPath: testDir, agents: true });
178
+
179
+ // 漂移 1:手改 AGENTS.md
180
+ fs.appendFileSync(path.join(testDir, 'AGENTS.md'), '\n手动追加的一行\n');
181
+ expect(await syncDocs({ projectPath: testDir, agents: true, check: true })).toBe(false);
182
+
183
+ // 重新生成恢复
184
+ await syncDocs({ projectPath: testDir, agents: true });
185
+ expect(await syncDocs({ projectPath: testDir, agents: true, check: true })).toBe(true);
186
+
187
+ // 漂移 2:package.json 删除一个 curated 脚本
188
+ const pkgPath = path.join(testDir, 'package.json');
189
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
190
+ delete pkg.scripts.lint;
191
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
192
+ expect(await syncDocs({ projectPath: testDir, agents: true, check: true })).toBe(false);
193
+
194
+ fs.rmSync(testDir, { recursive: true, force: true });
195
+ });
196
+
197
+ it('未启用 --agents 时不应生成 AGENTS.md', async () => {
198
+ const testDir = path.join(tempDir, 'no-agents');
199
+ createFixture(testDir, { withCapabilities: true });
200
+
201
+ const result = await syncDocs({ projectPath: testDir });
202
+ expect(result).toBe(true);
203
+ expect(fs.existsSync(path.join(testDir, 'AGENTS.md'))).toBe(false);
204
+
205
+ fs.rmSync(testDir, { recursive: true, force: true });
206
+ });
207
+
208
+ it('--json 模式应该输出 agentsMd 状态', async () => {
209
+ const testDir = path.join(tempDir, 'json-agents');
210
+ createFixture(testDir, { withCapabilities: true });
211
+
212
+ const result = await syncDocs({ projectPath: testDir, agents: true, check: true, json: true });
213
+ expect(result).toBe(false);
214
+
215
+ const jsonCall = consoleSpy.mock.calls.find(
216
+ (call: unknown[]) => typeof call[0] === 'string' && call[0].startsWith('{')
217
+ );
218
+ expect(jsonCall).toBeDefined();
219
+ const parsed = JSON.parse(jsonCall![0]);
220
+ expect(parsed.stale).toBe(true);
221
+ expect(parsed.agentsMd).toEqual({ file: 'AGENTS.md', exists: false, stale: true });
222
+ expect(parsed.resolution.some((r: { action: string }) => r.action === 'sync-agents-md')).toBe(true);
223
+
224
+ fs.rmSync(testDir, { recursive: true, force: true });
225
+ });
226
+ });
@@ -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';