@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
@@ -4,6 +4,17 @@
4
4
  * 完整 npm 发布流水线:tsc → dist 验证 → npm version → git push → npm publish → gh release。
5
5
  * 与 studio MCP tool `publishPackage` 共享同一逻辑,但此 CLI 命令不依赖 Studio API 运行。
6
6
  * 当 studio API 因 harness 包损坏而无法启动时,此 CLI 命令仍可用。
7
+ *
8
+ * 流程:
9
+ * 1. Verify package
10
+ * 2. Check branch (must be master/main) + remote sync + clean tree
11
+ * 3. tsc build
12
+ * 4. Verify dist files
13
+ * 5. Bump version (npm version → commit + tag)
14
+ * 6. Push (protected branch → create release PR instead)
15
+ * 7. Check private flag
16
+ * 8. npm publish (or skip if PR flow)
17
+ * 9. GitHub Release
7
18
  */
8
19
 
9
20
  import { execSync } from 'child_process';
@@ -114,8 +125,7 @@ export async function release(options: ReleaseOptions): Promise<void> {
114
125
  process.exit(0);
115
126
  }
116
127
 
117
- // ── 5. Bump version (npm version creates git commit + tag atomically,
118
- // including package-lock.json — no desync possible) ──
128
+ // ── 5. Bump version (npm version creates git commit + tag atomically) ──
119
129
  console.log(chalk.cyan('🔢 Bumping version...'));
120
130
  const [major, minor, patch] = oldVersion.split('.').map(Number);
121
131
  let expectedNew: string;
@@ -138,9 +148,40 @@ export async function release(options: ReleaseOptions): Promise<void> {
138
148
  const tag = `v${newVersion}`;
139
149
  console.log(chalk.green(`✅ version: ${oldVersion} → ${newVersion} (committed + tagged)`));
140
150
 
141
- // ── 6. Push ──
151
+ // ── 6. Push (handle protected branch via PR) ──
142
152
  console.log(chalk.cyan('⬆️ Pushing...'));
143
153
  const push = await run(`git push origin ${currentBranch}`, pkgPath, 30_000);
154
+ const isProtected = push.stderr.includes('GH006') || push.stderr.includes('protected branch');
155
+
156
+ if (isProtected) {
157
+ // Protected branch: create release branch + PR
158
+ const releaseBranch = `release/v${newVersion}`;
159
+ console.log(chalk.yellow(`⚠️ ${currentBranch} is protected. Creating PR via ${releaseBranch}...`));
160
+
161
+ await run(`git checkout -b ${releaseBranch}`, pkgPath);
162
+ await run(`git push origin ${releaseBranch}`, pkgPath, 30_000);
163
+
164
+ // Create PR
165
+ const prBody = `## Release ${pkgName}@${newVersion}\n\n- Bump version: ${oldVersion} → ${newVersion}\n- Changelog updated\n\nAfter merge, push tag to trigger npm publish:\n\`\`\`\ngit push origin ${tag}\n\`\`\``;
166
+ const pr = await run(`gh pr create --title "release: ${pkgName}@${newVersion}" --body "${prBody}" --base ${currentBranch}`, pkgPath, 30_000);
167
+ if (pr.stdout) {
168
+ console.log(chalk.green(`✅ PR created: ${pr.stdout.trim()}`));
169
+ } else {
170
+ console.error(chalk.red('❌ PR creation failed:'), pr.stderr.slice(0, 500));
171
+ process.exit(1);
172
+ }
173
+
174
+ // Switch back to original branch
175
+ await run(`git checkout ${currentBranch}`, pkgPath);
176
+
177
+ console.log(chalk.cyan('\n📋 Next steps:'));
178
+ console.log(chalk.gray(` 1. Merge the PR: ${pr.stdout.trim()}`));
179
+ console.log(chalk.gray(` 2. After merge, push tag to trigger npm publish:`));
180
+ console.log(chalk.gray(` git push origin ${tag}`));
181
+ console.log(chalk.gray(` 3. Or run: gh release create ${tag} --generate-notes`));
182
+ return;
183
+ }
184
+
144
185
  if (push.stderr && push.stderr.includes('error')) {
145
186
  console.error(chalk.red('❌ git push failed:'), push.stderr.slice(0, 500));
146
187
  process.exit(1);
@@ -148,7 +189,7 @@ export async function release(options: ReleaseOptions): Promise<void> {
148
189
  await run(`git push origin ${tag}`, pkgPath, 30_000);
149
190
  console.log(chalk.green('✅ git: pushed'));
150
191
 
151
- // ── 8. Check private flag ──
192
+ // ── 7. Check private flag ──
152
193
  if (pkgJson.private) {
153
194
  console.error(chalk.red('❌ package.json has "private": true. Remove it before publishing.'));
154
195
  console.error(chalk.gray(' The release tool will NOT auto-remove private flag.'));
@@ -156,25 +197,21 @@ export async function release(options: ReleaseOptions): Promise<void> {
156
197
  }
157
198
  console.log(chalk.green('✅ package: not private'));
158
199
 
159
- // ── 7. npm publish ──
160
- // Switch to npmjs.org for publishing (npmmirror is read-only mirror)
200
+ // ── 8. npm publish ──
161
201
  const origRegistry = await run('npm config get registry', pkgPath);
162
202
  await run('npm config set registry https://registry.npmjs.org/', pkgPath);
163
203
  console.log(chalk.cyan('📤 Publishing to npm...'));
164
- // npm publish to npmjs.org (the write registry — npmmirror is read-only)
165
204
  const pub = await run('npm publish --registry https://registry.npmjs.org/', pkgPath, 180_000);
166
- // Restore original registry
167
205
  await run(`npm config set registry ${origRegistry.stdout}`, pkgPath);
168
- // Don't string-match npm output — verify by querying the registry
169
206
  const verify = await run(`npm view ${pkgName} version --registry https://registry.npmjs.org/`, pkgPath);
170
207
  if (verify.stdout.trim() === newVersion) {
171
208
  console.log(chalk.green(`✅ npm: published ${pkgName}@${newVersion}`));
172
209
  } else {
173
- console.log(chalk.red(`❌ npm publish verification failed. Expected ${newVersion}, registry has ${verify.stdout.trim() || '???'}`));
210
+ console.error(chalk.red(`❌ npm publish verification failed. Expected ${newVersion}, registry has ${verify.stdout.trim() || '???'}`));
174
211
  process.exit(1);
175
212
  }
176
213
 
177
- // ── 8. GitHub Release ──
214
+ // ── 9. GitHub Release ──
178
215
  console.log(chalk.cyan('🐙 Creating GitHub Release...'));
179
216
  const gh = await run(`gh release create ${tag} --generate-notes`, pkgPath, 30_000);
180
217
  if (gh.stderr && gh.stderr.includes('already exists')) {
@@ -182,7 +219,6 @@ export async function release(options: ReleaseOptions): Promise<void> {
182
219
  } else if (gh.stderr) {
183
220
  console.log(chalk.yellow(`⚠️ gh: release may have failed (non-fatal): ${gh.stderr.slice(0, 200)}`));
184
221
  } else {
185
- // Derive repo URL from git remote
186
222
  let repoUrl = '';
187
223
  try {
188
224
  const remoteUrl = execSync('git remote get-url origin', { cwd: pkgPath, encoding: 'utf-8', stdio: 'pipe', timeout: 5_000 }).trim();
@@ -0,0 +1,44 @@
1
+ // SDD CLI commands
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { SDDIndexGenerator } from '../../sdd/index-generator';
5
+
6
+ export interface SDDOptions {
7
+ projectPath?: string;
8
+ json?: boolean;
9
+ }
10
+
11
+ function getSDDDir(projectPath?: string): string {
12
+ // Default: projectPath/docs or cwd/docs
13
+ const base = projectPath || process.cwd();
14
+ return base;
15
+ }
16
+
17
+ export function sddIndex(options: SDDOptions & { dir?: string }): void {
18
+ const baseDir = options.dir || getSDDDir(options.projectPath);
19
+ const gen = new SDDIndexGenerator(baseDir);
20
+
21
+ const sddDir = path.join(baseDir, 'docs', 'sdd');
22
+ const indexPath = path.join(sddDir, '_index.md');
23
+ const beforeSize = fs.existsSync(indexPath) ? fs.statSync(indexPath).size : 0;
24
+
25
+ const result = gen.regenerate();
26
+
27
+ const afterSize = Buffer.byteLength(
28
+ result.entries.map(e => `${e.slug}|${e.pmoNumber}|${e.status}|${e.title}|${e.tags}`).join('\n'),
29
+ 'utf-8',
30
+ );
31
+
32
+ if (options.json) {
33
+ console.log(JSON.stringify({
34
+ path: indexPath,
35
+ entries: result.count,
36
+ size: afterSize,
37
+ previousSize: beforeSize,
38
+ }));
39
+ } else {
40
+ console.log(`SDD Index regenerated: ${indexPath}`);
41
+ console.log(` Entries: ${result.count}`);
42
+ console.log(` Size: ${beforeSize} → ${afterSize} bytes`);
43
+ }
44
+ }
@@ -0,0 +1,384 @@
1
+ /**
2
+ * harness spec-baseline-check 命令
3
+ *
4
+ * 验证 spec 文件的前置条件(Baseline / 前置条件)是否满足。
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 SpecBaselineCheckOptions {
13
+ /** 项目路径 */
14
+ projectPath?: string;
15
+ /** 输出 JSON 格式 */
16
+ json?: boolean;
17
+ }
18
+
19
+ /** 前置条件验证结果 */
20
+ export interface PrerequisiteResult {
21
+ /** 前置条件描述 */
22
+ prerequisite: string;
23
+ /** 是否满足 */
24
+ satisfied: boolean;
25
+ /** 证据 */
26
+ evidence: string;
27
+ }
28
+
29
+ // ============================================
30
+ // 解析器
31
+ // ============================================
32
+
33
+ /**
34
+ * 从 spec 内容中提取 Baseline/前置条件 section
35
+ */
36
+ export function extractBaselineSection(content: string): string[] {
37
+ const lines = content.split('\n');
38
+ const prerequisites: string[] = [];
39
+
40
+ // 查找 ## Baseline 或 ## 前置条件 section
41
+ let inSection = false;
42
+ let sectionLevel = 0;
43
+
44
+ for (const line of lines) {
45
+ const headerMatch = line.match(/^(#{1,6})\s+(.+)/);
46
+
47
+ if (headerMatch) {
48
+ const level = headerMatch[1].length;
49
+ const title = headerMatch[2].trim();
50
+
51
+ // 检查是否是目标 section
52
+ if (/^(Baseline|前置条件|Prerequisites|Prereqs)$/i.test(title)) {
53
+ inSection = true;
54
+ sectionLevel = level;
55
+ continue;
56
+ }
57
+
58
+ // 同级或更高级的标题结束 section
59
+ if (inSection && level <= sectionLevel) {
60
+ inSection = false;
61
+ continue;
62
+ }
63
+ }
64
+
65
+ // 在 section 中提取列表项
66
+ if (inSection) {
67
+ const listMatch = line.match(/^\s*[-*+]\s+(.+)/);
68
+ if (listMatch) {
69
+ prerequisites.push(listMatch[1].trim());
70
+ }
71
+ // 也支持编号列表
72
+ const numberedMatch = line.match(/^\s*\d+\.\s+(.+)/);
73
+ if (numberedMatch) {
74
+ prerequisites.push(numberedMatch[1].trim());
75
+ }
76
+ }
77
+ }
78
+
79
+ return prerequisites;
80
+ }
81
+
82
+ // ============================================
83
+ // 验证器
84
+ // ============================================
85
+
86
+ /**
87
+ * 检查文件/目录是否存在
88
+ */
89
+ function checkFileExists(pattern: string, projectPath: string): { exists: boolean; evidence: string } {
90
+ // 提取路径引用(反引号中的路径、引号中的路径、或直接的路径模式)
91
+ const pathPatterns = [
92
+ /`([^`]+\.[a-z]+)`/g, // `src/foo.ts`
93
+ /["']([^"']+\.[a-z]+)["']/g, // "src/foo.ts"
94
+ /`([^`]+\/)`/g, // `src/foo/`
95
+ /(\S+\/\S+\.[a-z]+)/g, // src/foo.ts
96
+ ];
97
+
98
+ const paths: string[] = [];
99
+ for (const p of pathPatterns) {
100
+ p.lastIndex = 0;
101
+ let m;
102
+ while ((m = p.exec(pattern)) !== null) {
103
+ paths.push(m[1]);
104
+ }
105
+ }
106
+
107
+ if (paths.length === 0) {
108
+ return { exists: false, evidence: '无法从描述中提取路径' };
109
+ }
110
+
111
+ const found: string[] = [];
112
+ const missing: string[] = [];
113
+
114
+ for (const p of paths) {
115
+ const fullPath = path.resolve(projectPath, p);
116
+ if (fs.existsSync(fullPath)) {
117
+ found.push(p);
118
+ } else {
119
+ missing.push(p);
120
+ }
121
+ }
122
+
123
+ if (missing.length === 0 && found.length > 0) {
124
+ return { exists: true, evidence: `文件存在: ${found.join(', ')}` };
125
+ }
126
+ if (found.length > 0) {
127
+ return { exists: false, evidence: `部分存在: ${found.join(', ')}; 缺失: ${missing.join(', ')}` };
128
+ }
129
+ return { exists: false, evidence: `文件不存在: ${missing.join(', ')}` };
130
+ }
131
+
132
+ /**
133
+ * 检查代码中是否存在特定模式
134
+ */
135
+ function checkCodePattern(pattern: string, projectPath: string): { exists: boolean; evidence: string } {
136
+ // 从描述中提取关键词
137
+ const keywords: string[] = [];
138
+
139
+ // 提取反引号中的代码
140
+ const codePatterns = pattern.match(/`([^`]+)`/g);
141
+ if (codePatterns) {
142
+ for (const cp of codePatterns) {
143
+ const inner = cp.slice(1, -1);
144
+ if (inner.length >= 2) {
145
+ keywords.push(inner);
146
+ }
147
+ }
148
+ }
149
+
150
+ // 提取 "implemented" / "exists" / "已实现" 等关键词后的内容
151
+ const implMatch = pattern.match(/(?:implemented|exists|已实现|已完成|已存在)[\s::]+(.+)/i);
152
+ if (implMatch) {
153
+ keywords.push(implMatch[1].trim());
154
+ }
155
+
156
+ if (keywords.length === 0) {
157
+ return { exists: false, evidence: '无法从描述中提取搜索关键词' };
158
+ }
159
+
160
+ const found: string[] = [];
161
+ const notFound: string[] = [];
162
+
163
+ for (const kw of keywords) {
164
+ let count = 0;
165
+ function walkDir(dir: string) {
166
+ let entries: fs.Dirent[];
167
+ try {
168
+ entries = fs.readdirSync(dir, { withFileTypes: true });
169
+ } catch {
170
+ return;
171
+ }
172
+ for (const entry of entries) {
173
+ if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === 'dist') continue;
174
+ const fullPath = path.join(dir, entry.name);
175
+ if (entry.isDirectory()) {
176
+ walkDir(fullPath);
177
+ } else if (entry.isFile() && (fullPath.endsWith('.ts') || fullPath.endsWith('.js'))) {
178
+ try {
179
+ const content = fs.readFileSync(fullPath, 'utf-8');
180
+ if (content.includes(kw)) count++;
181
+ } catch { /* skip */ }
182
+ }
183
+ }
184
+ }
185
+ walkDir(projectPath);
186
+ if (count > 0) {
187
+ found.push(`${kw} (${count} files)`);
188
+ } else {
189
+ notFound.push(kw);
190
+ }
191
+ }
192
+
193
+ if (notFound.length === 0 && found.length > 0) {
194
+ return { exists: true, evidence: `代码中找到: ${found.join(', ')}` };
195
+ }
196
+ if (found.length > 0) {
197
+ return { exists: false, evidence: `找到: ${found.join(', ')}; 未找到: ${notFound.join(', ')}` };
198
+ }
199
+ return { exists: false, evidence: `代码中未找到: ${notFound.join(', ')}` };
200
+ }
201
+
202
+ /**
203
+ * 检查包依赖是否安装
204
+ */
205
+ function checkDependency(pattern: string, projectPath: string): { exists: boolean; evidence: string } {
206
+ // 提取包名
207
+ const pkgPatterns = [
208
+ /`(@?[\w-]+\/[\w-]+)`/g, // `@scope/pkg` or `pkg-name`
209
+ /`([\w-]+)`/g, // `pkg`
210
+ /install(?:ed)?\s+(\S+)/gi, // installed pkg-name
211
+ ];
212
+
213
+ const packages: string[] = [];
214
+ for (const p of pkgPatterns) {
215
+ p.lastIndex = 0;
216
+ let m;
217
+ while ((m = p.exec(pattern)) !== null) {
218
+ const pkg = m[1];
219
+ if (pkg.length >= 2 && !pkg.includes('/') && !pkg.includes('.')) {
220
+ packages.push(pkg);
221
+ }
222
+ }
223
+ }
224
+
225
+ if (packages.length === 0) {
226
+ return { exists: false, evidence: '无法从描述中提取包名' };
227
+ }
228
+
229
+ const found: string[] = [];
230
+ const missing: string[] = [];
231
+
232
+ const pkgJsonPath = path.join(projectPath, 'package.json');
233
+ let deps: Record<string, string> = {};
234
+
235
+ try {
236
+ const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
237
+ deps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
238
+ } catch {
239
+ return { exists: false, evidence: '无法读取 package.json' };
240
+ }
241
+
242
+ for (const pkg of packages) {
243
+ if (deps[pkg] || deps[`@types/${pkg}`]) {
244
+ found.push(pkg);
245
+ } else {
246
+ // 检查 node_modules
247
+ const nmPath = path.join(projectPath, 'node_modules', pkg);
248
+ if (fs.existsSync(nmPath)) {
249
+ found.push(pkg);
250
+ } else {
251
+ missing.push(pkg);
252
+ }
253
+ }
254
+ }
255
+
256
+ if (missing.length === 0 && found.length > 0) {
257
+ return { exists: true, evidence: `依赖已安装: ${found.join(', ')}` };
258
+ }
259
+ if (found.length > 0) {
260
+ return { exists: false, evidence: `已安装: ${found.join(', ')}; 缺失: ${missing.join(', ')}` };
261
+ }
262
+ return { exists: false, evidence: `依赖缺失: ${missing.join(', ')}` };
263
+ }
264
+
265
+ /**
266
+ * 综合验证一条前置条件
267
+ */
268
+ function verifyPrerequisite(prereq: string, projectPath: string): PrerequisiteResult {
269
+ const lower = prereq.toLowerCase();
270
+
271
+ // 路径/文件存在性检查
272
+ if (lower.includes('文件') || lower.includes('file') || lower.includes('目录') || lower.includes('directory')
273
+ || /`[^`]+\.[a-z]+`/.test(prereq) || /\//.test(prereq)) {
274
+ const { exists, evidence } = checkFileExists(prereq, projectPath);
275
+ if (exists || evidence.includes('文件不存在')) {
276
+ return { prerequisite: prereq, satisfied: exists, evidence };
277
+ }
278
+ }
279
+
280
+ // 依赖检查
281
+ if (lower.includes('依赖') || lower.includes('install') || lower.includes('package') || lower.includes('npm')) {
282
+ const { exists, evidence } = checkDependency(prereq, projectPath);
283
+ return { prerequisite: prereq, satisfied: exists, evidence };
284
+ }
285
+
286
+ // 代码模式检查
287
+ if (lower.includes('实现') || lower.includes('implement') || lower.includes('exist')
288
+ || lower.includes('已') || lower.includes('必须')
289
+ || /`[^`]+`/.test(prereq)) {
290
+ const { exists, evidence } = checkCodePattern(prereq, projectPath);
291
+ return { prerequisite: prereq, satisfied: exists, evidence };
292
+ }
293
+
294
+ // 默认:无法自动验证
295
+ return {
296
+ prerequisite: prereq,
297
+ satisfied: true,
298
+ evidence: '无法自动验证(需人工确认)',
299
+ };
300
+ }
301
+
302
+ // ============================================
303
+ // 输出格式化
304
+ // ============================================
305
+
306
+ function formatTable(results: PrerequisiteResult[]): string {
307
+ if (results.length === 0) return chalk.yellow('未找到 Baseline / 前置条件 section');
308
+
309
+ const lines: string[] = [];
310
+ lines.push(chalk.blue(`前置条件检查 (${results.length} 条)\n`));
311
+
312
+ const satisfied = results.filter(r => r.satisfied).length;
313
+ const failed = results.filter(r => !r.satisfied);
314
+
315
+ lines.push(chalk.bold(` 满足: ${satisfied}/${results.length}`));
316
+ if (failed.length > 0) {
317
+ lines.push(chalk.red(` 未满足: ${failed.length}\n`));
318
+ } else {
319
+ lines.push(chalk.green(' 全部满足\n'));
320
+ }
321
+
322
+ for (const r of results) {
323
+ const icon = r.satisfied ? chalk.green('✓') : chalk.red('✗');
324
+ lines.push(` ${icon} ${r.prerequisite}`);
325
+ if (!r.satisfied) {
326
+ lines.push(` ${chalk.yellow(r.evidence)}`);
327
+ }
328
+ }
329
+
330
+ return lines.join('\n');
331
+ }
332
+
333
+ // ============================================
334
+ // 主入口
335
+ // ============================================
336
+
337
+ /**
338
+ * spec-baseline-check CLI 命令
339
+ */
340
+ export async function specBaselineCheck(
341
+ specPath: string,
342
+ options: SpecBaselineCheckOptions = {},
343
+ ): Promise<void> {
344
+ // 验证文件存在
345
+ const resolvedPath = path.resolve(specPath);
346
+ if (!fs.existsSync(resolvedPath)) {
347
+ console.error(chalk.red(`文件不存在: ${resolvedPath}`));
348
+ process.exitCode = 1;
349
+ return;
350
+ }
351
+
352
+ const projectPath = options.projectPath || path.dirname(resolvedPath);
353
+
354
+ // 读取 spec
355
+ const content = fs.readFileSync(resolvedPath, 'utf-8');
356
+
357
+ // 提取前置条件
358
+ const prerequisites = extractBaselineSection(content);
359
+
360
+ if (prerequisites.length === 0) {
361
+ if (options.json) {
362
+ console.log(JSON.stringify({ prerequisites: [], message: '未找到 Baseline / 前置条件 section' }, null, 2));
363
+ } else {
364
+ console.log(chalk.yellow('未找到 ## Baseline 或 ## 前置条件 section'));
365
+ }
366
+ return;
367
+ }
368
+
369
+ // 验证每条前置条件
370
+ const results = prerequisites.map(p => verifyPrerequisite(p, projectPath));
371
+
372
+ // 输出
373
+ if (options.json) {
374
+ console.log(JSON.stringify(results, null, 2));
375
+ } else {
376
+ console.log(formatTable(results));
377
+ }
378
+
379
+ // 有未满足的前置条件时 exit 1
380
+ const failed = results.filter(r => !r.satisfied);
381
+ if (failed.length > 0) {
382
+ process.exitCode = 1;
383
+ }
384
+ }
@@ -558,6 +558,62 @@ describe('auto-fix', () => {
558
558
  });
559
559
  });
560
560
 
561
+ // ── D2b: deprecated-domain ──────────────────────────────
562
+
563
+ describe('D2b: deprecated-domain', () => {
564
+ it('flags entry with "pipeline" tag', () => {
565
+ const dir = makeTmpDir();
566
+ const audit = new KnowledgeAudit({ baseDir: dir });
567
+ const entry = makeEntry({ tags: ['pipeline', 'deployment'] });
568
+ const issues = audit.validate(entry);
569
+ const dd = issues.filter(i => i.rule === 'deprecated-domain');
570
+ expect(dd.length).toBe(1);
571
+ expect(dd[0].action).toBe('archive');
572
+ expect(dd[0].severity).toBe('high');
573
+ fs.rmSync(dir, { recursive: true });
574
+ });
575
+
576
+ it('flags entry with "pipeline" in title', () => {
577
+ const dir = makeTmpDir();
578
+ const audit = new KnowledgeAudit({ baseDir: dir });
579
+ const entry = makeEntry({ title: 'Pipeline 工作单元不可见导致协作效率低' });
580
+ const issues = audit.validate(entry);
581
+ const dd = issues.filter(i => i.rule === 'deprecated-domain');
582
+ expect(dd.length).toBe(1);
583
+ fs.rmSync(dir, { recursive: true });
584
+ });
585
+
586
+ it('flags entry with "管线" in title', () => {
587
+ const dir = makeTmpDir();
588
+ const audit = new KnowledgeAudit({ baseDir: dir });
589
+ const entry = makeEntry({ title: 'B51 管线端到端验证常量与测试' });
590
+ const issues = audit.validate(entry);
591
+ const dd = issues.filter(i => i.rule === 'deprecated-domain');
592
+ expect(dd.length).toBe(1);
593
+ fs.rmSync(dir, { recursive: true });
594
+ });
595
+
596
+ it('does not flag entry without deprecated domain references', () => {
597
+ const dir = makeTmpDir();
598
+ const audit = new KnowledgeAudit({ baseDir: dir });
599
+ const entry = makeEntry({ title: 'Agent Network 核心概念', tags: ['agent', 'architecture'] });
600
+ const issues = audit.validate(entry);
601
+ const dd = issues.filter(i => i.rule === 'deprecated-domain');
602
+ expect(dd.length).toBe(0);
603
+ fs.rmSync(dir, { recursive: true });
604
+ });
605
+
606
+ it('skips already archived entries', () => {
607
+ const dir = makeTmpDir();
608
+ const audit = new KnowledgeAudit({ baseDir: dir });
609
+ const entry = makeEntry({ title: 'Pipeline legacy entry', maturity: 'archived', tags: ['pipeline'] });
610
+ const issues = audit.validate(entry);
611
+ const dd = issues.filter(i => i.rule === 'deprecated-domain');
612
+ expect(dd.length).toBe(0);
613
+ fs.rmSync(dir, { recursive: true });
614
+ });
615
+ });
616
+
561
617
  // ── run() report structure ─────────────────────────────
562
618
 
563
619
  describe('run() report', () => {