@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.
- package/bin/harness.js +71 -0
- package/dist/cli/commands/doc-freshness-check.d.ts +53 -0
- package/dist/cli/commands/doc-freshness-check.d.ts.map +1 -0
- package/dist/cli/commands/doc-freshness-check.js +366 -0
- package/dist/cli/commands/doc-freshness-check.js.map +1 -0
- package/dist/cli/commands/index.d.ts +4 -1
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +13 -1
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/knowledge.d.ts +12 -0
- package/dist/cli/commands/knowledge.d.ts.map +1 -1
- package/dist/cli/commands/knowledge.js +134 -0
- package/dist/cli/commands/knowledge.js.map +1 -1
- package/dist/cli/commands/release.d.ts +11 -0
- package/dist/cli/commands/release.d.ts.map +1 -1
- package/dist/cli/commands/release.js +43 -12
- package/dist/cli/commands/release.js.map +1 -1
- package/dist/cli/commands/sdd.d.ts +8 -0
- package/dist/cli/commands/sdd.d.ts.map +1 -0
- package/dist/cli/commands/sdd.js +68 -0
- package/dist/cli/commands/sdd.js.map +1 -0
- package/dist/cli/commands/spec-baseline-check.d.ts +30 -0
- package/dist/cli/commands/spec-baseline-check.d.ts.map +1 -0
- package/dist/cli/commands/spec-baseline-check.js +366 -0
- package/dist/cli/commands/spec-baseline-check.js.map +1 -0
- package/dist/knowledge/audit.d.ts +1 -1
- package/dist/knowledge/audit.d.ts.map +1 -1
- package/dist/knowledge/audit.js +59 -7
- package/dist/knowledge/audit.js.map +1 -1
- package/dist/knowledge/index-generator.d.ts +36 -0
- package/dist/knowledge/index-generator.d.ts.map +1 -0
- package/dist/knowledge/index-generator.js +241 -0
- package/dist/knowledge/index-generator.js.map +1 -0
- package/dist/knowledge/index.d.ts +2 -0
- package/dist/knowledge/index.d.ts.map +1 -1
- package/dist/knowledge/index.js +3 -1
- package/dist/knowledge/index.js.map +1 -1
- package/dist/knowledge/ingest.js +2 -2
- package/dist/knowledge/ingest.js.map +1 -1
- package/dist/knowledge/lifecycle.d.ts +15 -0
- package/dist/knowledge/lifecycle.d.ts.map +1 -1
- package/dist/knowledge/lifecycle.js +70 -0
- package/dist/knowledge/lifecycle.js.map +1 -1
- package/dist/knowledge/lint.js +3 -3
- package/dist/knowledge/lint.js.map +1 -1
- package/dist/knowledge/primitives/code-structure.d.ts +37 -0
- package/dist/knowledge/primitives/code-structure.d.ts.map +1 -0
- package/dist/knowledge/primitives/code-structure.js +168 -0
- package/dist/knowledge/primitives/code-structure.js.map +1 -0
- package/dist/knowledge/types.d.ts +3 -1
- package/dist/knowledge/types.d.ts.map +1 -1
- package/dist/knowledge/types.js.map +1 -1
- package/dist/sdd/index-generator.d.ts +22 -0
- package/dist/sdd/index-generator.d.ts.map +1 -0
- package/dist/sdd/index-generator.js +118 -0
- package/dist/sdd/index-generator.js.map +1 -0
- package/package.json +1 -1
- package/src/cli/commands/__tests__/doc-freshness-check.test.ts +168 -0
- package/src/cli/commands/__tests__/knowledge.test.ts +69 -1
- package/src/cli/commands/__tests__/spec-baseline-check.test.ts +235 -0
- package/src/cli/commands/doc-freshness-check.ts +397 -0
- package/src/cli/commands/index.ts +4 -1
- package/src/cli/commands/knowledge.ts +150 -0
- package/src/cli/commands/release.ts +48 -12
- package/src/cli/commands/sdd.ts +44 -0
- package/src/cli/commands/spec-baseline-check.ts +384 -0
- package/src/knowledge/__tests__/audit.test.ts +56 -0
- package/src/knowledge/__tests__/index-generator.test.ts +335 -0
- package/src/knowledge/audit.ts +64 -8
- package/src/knowledge/index-generator.ts +233 -0
- package/src/knowledge/index.ts +2 -0
- package/src/knowledge/ingest.ts +2 -2
- package/src/knowledge/lifecycle.ts +71 -0
- package/src/knowledge/lint.ts +3 -3
- package/src/knowledge/primitives/__tests__/code-structure.test.ts +120 -0
- package/src/knowledge/primitives/code-structure.ts +161 -0
- package/src/knowledge/types.ts +4 -1
- package/src/sdd/__tests__/index-generator.test.ts +150 -0
- 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
|
-
// ──
|
|
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
|
-
// ──
|
|
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.
|
|
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
|
-
// ──
|
|
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', () => {
|