@zhuan-ai/zhuanspec 2.16.6 → 2.17.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/dist/cli/hooks.js +10 -0
- package/dist/cli/index.js +4 -0
- package/dist/commands/knowledge.d.ts +12 -2
- package/dist/commands/knowledge.js +277 -3
- package/dist/core/hooks/context-load-hook.js +2 -0
- package/dist/core/hooks/knowledge-index.d.ts +54 -2
- package/dist/core/hooks/knowledge-index.js +247 -11
- package/dist/core/hooks/knowledge-usage.d.ts +90 -0
- package/dist/core/hooks/knowledge-usage.js +341 -0
- package/dist/core/hooks/post-archive.js +128 -29
- package/dist/core/hooks/review-hooks.js +198 -43
- package/dist/core/hooks/summarize.js +93 -1
- package/dist/core/hooks/user-input-hook.d.ts +12 -0
- package/dist/core/hooks/user-input-hook.js +38 -0
- package/dist/core/init.js +10 -0
- package/dist/core/metrics/knowledge-funnel.d.ts +51 -0
- package/dist/core/metrics/knowledge-funnel.js +144 -0
- package/dist/core/templates/agents-template.d.ts +1 -1
- package/dist/core/templates/agents-template.js +29 -10
- package/dist/core/templates/codex-hooks-template.js +5 -0
- package/dist/core/templates/slash-command-templates.js +91 -12
- package/package.json +22 -20
package/dist/cli/hooks.js
CHANGED
|
@@ -15,6 +15,7 @@ import { initHook } from '../core/hooks/init.js';
|
|
|
15
15
|
import { deviationCheckHook } from '../core/hooks/deviation-check.js';
|
|
16
16
|
import { recordProgressHook, initializeProgress } from '../core/hooks/record-progress.js';
|
|
17
17
|
import { collectKnowledgeHook } from '../core/hooks/collect-knowledge.js';
|
|
18
|
+
import { knowledgeUsageHook } from '../core/hooks/knowledge-usage.js';
|
|
18
19
|
import { summarizeHook } from '../core/hooks/summarize.js';
|
|
19
20
|
import { runUserInputHook } from '../core/hooks/user-input-hook.js';
|
|
20
21
|
import { preArchiveHook } from '../core/hooks/pre-archive.js';
|
|
@@ -94,6 +95,15 @@ program
|
|
|
94
95
|
.action(async (options) => {
|
|
95
96
|
await collectKnowledgeHook(options);
|
|
96
97
|
});
|
|
98
|
+
// PostToolUse hook - knowledge usage(引用上报,旁路指标)
|
|
99
|
+
program
|
|
100
|
+
.command('knowledge-usage')
|
|
101
|
+
.description('PostToolUse hook - 记录 AI 引用 knowledge/ 知识文件的次数(旁路指标)')
|
|
102
|
+
.option('--json', 'Output as JSON for Claude Code consumption')
|
|
103
|
+
.option('--file <path>', 'File that was read (override stdin tool_input.file_path)')
|
|
104
|
+
.action(async (options) => {
|
|
105
|
+
await knowledgeUsageHook(options);
|
|
106
|
+
});
|
|
97
107
|
// Stop hook
|
|
98
108
|
program
|
|
99
109
|
.command('summarize')
|
package/dist/cli/index.js
CHANGED
|
@@ -21,6 +21,7 @@ import { discoverSkills, formatSkillsList } from '../core/skill-discovery.js';
|
|
|
21
21
|
import { registerConfigCommand } from '../commands/config.js';
|
|
22
22
|
import { registerArtifactWorkflowCommands } from '../commands/artifact-workflow.js';
|
|
23
23
|
import { DesignCommand } from '../commands/design.js';
|
|
24
|
+
import { registerKnowledgeCommands } from '../commands/knowledge.js';
|
|
24
25
|
const program = new Command();
|
|
25
26
|
const require = createRequire(import.meta.url);
|
|
26
27
|
const { version } = require('../../package.json');
|
|
@@ -635,5 +636,8 @@ program
|
|
|
635
636
|
});
|
|
636
637
|
// Register artifact workflow commands
|
|
637
638
|
registerArtifactWorkflowCommands(program);
|
|
639
|
+
// Register knowledge management commands (reindex, stats)
|
|
640
|
+
const knowledgeCmd = program.command('knowledge').description('管理项目知识库 zhuanspec/knowledge/');
|
|
641
|
+
registerKnowledgeCommands(knowledgeCmd);
|
|
638
642
|
program.parse();
|
|
639
643
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,19 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Knowledge Command
|
|
3
3
|
*
|
|
4
|
-
* 管理项目级知识库 zhuanspec/knowledge
|
|
4
|
+
* 管理项目级知识库 zhuanspec/knowledge/。提供以下子命令:
|
|
5
5
|
* - reindex:扫描三类目录下的 *.md,解析 Front Matter,重建 index.md 三分区
|
|
6
6
|
* (保留 `## Archive Log` 既有内容)。用于回补历史漂移。
|
|
7
|
+
* - stats:输出知识库画像(条数/三分区分布/完整率/缺字段清单 + 沉淀漏斗指标),支持 --json。
|
|
7
8
|
*/
|
|
8
9
|
import { Command } from 'commander';
|
|
9
10
|
interface ReindexOptions {
|
|
10
11
|
dryRun?: boolean;
|
|
11
12
|
}
|
|
13
|
+
interface StatsOptions {
|
|
14
|
+
json?: boolean;
|
|
15
|
+
}
|
|
12
16
|
export declare class KnowledgeCommand {
|
|
13
17
|
reindex(options?: ReindexOptions): Promise<void>;
|
|
18
|
+
stats(options?: StatsOptions): Promise<void>;
|
|
19
|
+
/** 打印知识库画像区块(人类可读)。 */
|
|
20
|
+
private printKnowledgeOverview;
|
|
21
|
+
/** 打印沉淀漏斗区块(人类可读)。 */
|
|
22
|
+
private printFunnel;
|
|
23
|
+
private printServiceBreakdown;
|
|
14
24
|
}
|
|
15
25
|
/**
|
|
16
|
-
* 在给定的 parent commander Command 上挂载
|
|
26
|
+
* 在给定的 parent commander Command 上挂载 knowledge 子命令。
|
|
17
27
|
* 父命令由 cli/index.ts 创建,形如 `zhuanspec knowledge`。
|
|
18
28
|
*/
|
|
19
29
|
export declare function registerKnowledgeCommands(parent: Command): void;
|
|
@@ -1,15 +1,119 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Knowledge Command
|
|
3
3
|
*
|
|
4
|
-
* 管理项目级知识库 zhuanspec/knowledge
|
|
4
|
+
* 管理项目级知识库 zhuanspec/knowledge/。提供以下子命令:
|
|
5
5
|
* - reindex:扫描三类目录下的 *.md,解析 Front Matter,重建 index.md 三分区
|
|
6
6
|
* (保留 `## Archive Log` 既有内容)。用于回补历史漂移。
|
|
7
|
+
* - stats:输出知识库画像(条数/三分区分布/完整率/缺字段清单 + 沉淀漏斗指标),支持 --json。
|
|
7
8
|
*/
|
|
8
9
|
import path from 'path';
|
|
10
|
+
import { promises as fs } from 'fs';
|
|
9
11
|
import chalk from 'chalk';
|
|
10
12
|
import { FileSystemUtils } from '../utils/file-system.js';
|
|
11
13
|
import { resolveZhuanSpecRoot } from '../utils/resolve-root.js';
|
|
12
14
|
import { rebuildIndexContent, scanKnowledgeEntries, } from '../core/hooks/knowledge-index.js';
|
|
15
|
+
import { computeFunnelStats } from '../core/metrics/knowledge-funnel.js';
|
|
16
|
+
const PROJECT_WIKI_FEATURE_MARKERS = ['功能说明:', '功能说明**'];
|
|
17
|
+
const PROJECT_WIKI_EXCLUDED_DIRS = new Set([
|
|
18
|
+
'.git',
|
|
19
|
+
'.hg',
|
|
20
|
+
'.svn',
|
|
21
|
+
'node_modules',
|
|
22
|
+
'dist',
|
|
23
|
+
'build',
|
|
24
|
+
'coverage',
|
|
25
|
+
'changes',
|
|
26
|
+
'specs',
|
|
27
|
+
'knowledge',
|
|
28
|
+
'test',
|
|
29
|
+
]);
|
|
30
|
+
async function listProjectWikiDirs(rootDir) {
|
|
31
|
+
const result = [];
|
|
32
|
+
async function visit(dir) {
|
|
33
|
+
let entries;
|
|
34
|
+
try {
|
|
35
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
if (!entry.isDirectory())
|
|
42
|
+
continue;
|
|
43
|
+
const fullPath = path.join(dir, entry.name);
|
|
44
|
+
if (entry.name === '.project-wiki') {
|
|
45
|
+
result.push(fullPath);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (PROJECT_WIKI_EXCLUDED_DIRS.has(entry.name))
|
|
49
|
+
continue;
|
|
50
|
+
await visit(fullPath);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
await visit(rootDir);
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
async function listMarkdownFiles(rootDir) {
|
|
57
|
+
const result = [];
|
|
58
|
+
async function visit(dir) {
|
|
59
|
+
let entries;
|
|
60
|
+
try {
|
|
61
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
for (const entry of entries) {
|
|
67
|
+
const fullPath = path.join(dir, entry.name);
|
|
68
|
+
if (entry.isDirectory()) {
|
|
69
|
+
await visit(fullPath);
|
|
70
|
+
}
|
|
71
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
72
|
+
result.push(fullPath);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
await visit(rootDir);
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
function countLiteralOccurrences(text, literal) {
|
|
80
|
+
if (!literal)
|
|
81
|
+
return 0;
|
|
82
|
+
let count = 0;
|
|
83
|
+
let index = text.indexOf(literal);
|
|
84
|
+
while (index !== -1) {
|
|
85
|
+
count += 1;
|
|
86
|
+
index = text.indexOf(literal, index + literal.length);
|
|
87
|
+
}
|
|
88
|
+
return count;
|
|
89
|
+
}
|
|
90
|
+
function countProjectWikiFeatureMarkers(text) {
|
|
91
|
+
return PROJECT_WIKI_FEATURE_MARKERS.reduce((total, marker) => total + countLiteralOccurrences(text, marker), 0);
|
|
92
|
+
}
|
|
93
|
+
async function computeProjectWikiStats(repoRoot) {
|
|
94
|
+
let total = 0;
|
|
95
|
+
const byService = {};
|
|
96
|
+
const wikiDirs = await listProjectWikiDirs(repoRoot);
|
|
97
|
+
for (const wikiDir of wikiDirs) {
|
|
98
|
+
const service = path.basename(path.dirname(wikiDir));
|
|
99
|
+
const markdownFiles = await listMarkdownFiles(wikiDir);
|
|
100
|
+
for (const file of markdownFiles) {
|
|
101
|
+
try {
|
|
102
|
+
const content = await FileSystemUtils.readFile(file);
|
|
103
|
+
const count = countProjectWikiFeatureMarkers(content);
|
|
104
|
+
total += count;
|
|
105
|
+
byService[service] = (byService[service] ?? 0) + count;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// ProjectWiki stats are informational; unreadable files should not fail stats.
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
total,
|
|
114
|
+
byService: Object.fromEntries(Object.entries(byService).sort(([a], [b]) => a.localeCompare(b))),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
13
117
|
export class KnowledgeCommand {
|
|
14
118
|
async reindex(options = {}) {
|
|
15
119
|
const repoRoot = resolveZhuanSpecRoot(process.cwd());
|
|
@@ -25,7 +129,6 @@ export class KnowledgeCommand {
|
|
|
25
129
|
? await FileSystemUtils.readFile(indexPath)
|
|
26
130
|
: null;
|
|
27
131
|
const rebuilt = rebuildIndexContent(scan, existing);
|
|
28
|
-
// 输出扫描摘要
|
|
29
132
|
const counts = {
|
|
30
133
|
troubleshooting: scan.entries.filter((e) => e.category === 'troubleshooting').length,
|
|
31
134
|
bestPractices: scan.entries.filter((e) => e.category === 'best-practices').length,
|
|
@@ -35,6 +138,11 @@ export class KnowledgeCommand {
|
|
|
35
138
|
console.log(chalk.gray(` - troubleshooting : ${counts.troubleshooting} 条`));
|
|
36
139
|
console.log(chalk.gray(` - best-practices : ${counts.bestPractices} 条`));
|
|
37
140
|
console.log(chalk.gray(` - implicit-conventions : ${counts.implicitConventions} 条`));
|
|
141
|
+
if (scan.errors.length > 0) {
|
|
142
|
+
console.log(chalk.red(`\n✗ 发现 ${scan.errors.length} 条 Front Matter 严重缺失(YAML 格式缺 id/title):`));
|
|
143
|
+
for (const e of scan.errors)
|
|
144
|
+
console.log(chalk.red(` ${e}`));
|
|
145
|
+
}
|
|
38
146
|
if (scan.warnings.length > 0) {
|
|
39
147
|
console.log(chalk.yellow(`\n⚠ 发现 ${scan.warnings.length} 条 Front Matter 不完整,建议尽快补齐:`));
|
|
40
148
|
for (const w of scan.warnings) {
|
|
@@ -55,9 +163,160 @@ export class KnowledgeCommand {
|
|
|
55
163
|
console.log(chalk.green(`\n✓ 已重建 ${path.relative(process.cwd(), indexPath)}`));
|
|
56
164
|
console.log(chalk.gray(' (## Archive Log 既有内容已保留)'));
|
|
57
165
|
}
|
|
166
|
+
async stats(options = {}) {
|
|
167
|
+
const repoRoot = resolveZhuanSpecRoot(process.cwd());
|
|
168
|
+
const zhuanspecDir = path.join(repoRoot, 'zhuanspec');
|
|
169
|
+
const knowledgeDir = path.join(zhuanspecDir, 'knowledge');
|
|
170
|
+
const projectWiki = await computeProjectWikiStats(repoRoot);
|
|
171
|
+
if (!(await FileSystemUtils.directoryExists(knowledgeDir))) {
|
|
172
|
+
// 知识目录不存在:仍输出漏斗(纠偏信号可能已产生,落库数为 0)
|
|
173
|
+
const funnel = await computeFunnelStats(zhuanspecDir, knowledgeDir, 0);
|
|
174
|
+
if (options.json) {
|
|
175
|
+
const empty = {
|
|
176
|
+
total: projectWiki.total,
|
|
177
|
+
byCategory: { projectWiki: projectWiki.total },
|
|
178
|
+
yamlCount: 0, legacyCount: 0, deprecatedCount: 0,
|
|
179
|
+
completenessRate: 0, missingFields: [], errors: [], warnings: [], projectWiki, funnel,
|
|
180
|
+
};
|
|
181
|
+
console.log(JSON.stringify(empty, null, 2));
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
console.log(chalk.yellow('⚠ 知识目录不存在,尚未沉淀任何项目知识。'));
|
|
185
|
+
this.printKnowledgeOverview({
|
|
186
|
+
total: projectWiki.total,
|
|
187
|
+
byCategory: { projectWiki: projectWiki.total },
|
|
188
|
+
yamlCount: 0,
|
|
189
|
+
legacyCount: 0,
|
|
190
|
+
deprecatedCount: 0,
|
|
191
|
+
completenessRate: 0,
|
|
192
|
+
projectWiki,
|
|
193
|
+
});
|
|
194
|
+
this.printFunnel(funnel);
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const scan = await scanKnowledgeEntries(knowledgeDir);
|
|
199
|
+
const { entries, warnings, errors } = scan;
|
|
200
|
+
const byCategory = {
|
|
201
|
+
troubleshooting: entries.filter((e) => e.category === 'troubleshooting').length,
|
|
202
|
+
'best-practices': entries.filter((e) => e.category === 'best-practices').length,
|
|
203
|
+
'implicit-conventions': entries.filter((e) => e.category === 'implicit-conventions').length,
|
|
204
|
+
projectWiki: projectWiki.total,
|
|
205
|
+
};
|
|
206
|
+
const knowledgeTotal = entries.length;
|
|
207
|
+
const overviewTotal = knowledgeTotal + projectWiki.total;
|
|
208
|
+
// 完整率:无任何 warning/error 的条目 / 总条目
|
|
209
|
+
const problemFiles = new Set([
|
|
210
|
+
...warnings.map((w) => w.match(/\[WARN\] ([^\s]+)/)?.[1] ?? ''),
|
|
211
|
+
...errors.map((e) => e.match(/\[ERROR\] ([^\s]+)/)?.[1] ?? ''),
|
|
212
|
+
].filter(Boolean));
|
|
213
|
+
const completeCount = entries.length - problemFiles.size;
|
|
214
|
+
const completenessRate = entries.length > 0
|
|
215
|
+
? Math.round((completeCount / entries.length) * 100)
|
|
216
|
+
: 100;
|
|
217
|
+
// 按文件聚合缺失字段清单
|
|
218
|
+
const missingMap = new Map();
|
|
219
|
+
for (const line of [...errors, ...warnings]) {
|
|
220
|
+
const m = line.match(/\[(ERROR|WARN)\] ([^\s]+) (.+)/);
|
|
221
|
+
if (!m)
|
|
222
|
+
continue;
|
|
223
|
+
const file = m[2];
|
|
224
|
+
const reason = m[3];
|
|
225
|
+
if (!missingMap.has(file))
|
|
226
|
+
missingMap.set(file, []);
|
|
227
|
+
missingMap.get(file).push(reason);
|
|
228
|
+
}
|
|
229
|
+
const missingFields = Array.from(missingMap.entries()).map(([file, missing]) => ({ file, missing }));
|
|
230
|
+
const funnel = await computeFunnelStats(zhuanspecDir, knowledgeDir, entries.length);
|
|
231
|
+
const output = {
|
|
232
|
+
total: overviewTotal,
|
|
233
|
+
byCategory,
|
|
234
|
+
yamlCount: entries.filter((e) => e.id).length,
|
|
235
|
+
legacyCount: entries.filter((e) => !e.id).length,
|
|
236
|
+
deprecatedCount: entries.filter((e) => e.status === 'deprecated').length,
|
|
237
|
+
completenessRate,
|
|
238
|
+
missingFields,
|
|
239
|
+
errors,
|
|
240
|
+
warnings,
|
|
241
|
+
projectWiki,
|
|
242
|
+
funnel,
|
|
243
|
+
};
|
|
244
|
+
if (options.json) {
|
|
245
|
+
console.log(JSON.stringify(output, null, 2));
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
this.printKnowledgeOverview({
|
|
249
|
+
total: output.total,
|
|
250
|
+
byCategory,
|
|
251
|
+
yamlCount: output.yamlCount,
|
|
252
|
+
legacyCount: output.legacyCount,
|
|
253
|
+
deprecatedCount: output.deprecatedCount,
|
|
254
|
+
completenessRate,
|
|
255
|
+
projectWiki,
|
|
256
|
+
});
|
|
257
|
+
if (errors.length > 0) {
|
|
258
|
+
console.log(chalk.red(`\n ✗ 严重缺失 ${errors.length} 条(YAML 格式缺 id/title):`));
|
|
259
|
+
for (const { file, missing } of missingFields.filter((f) => errors.some((e) => e.includes(f.file)))) {
|
|
260
|
+
console.log(chalk.red(` ${file}: ${missing.join('; ')}`));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (warnings.length > 0) {
|
|
264
|
+
console.log(chalk.yellow(`\n ⚠ 软问题 ${warnings.length} 条(缺关键词/适用场景):`));
|
|
265
|
+
for (const { file, missing } of missingFields.filter((f) => !errors.some((e) => e.includes(f.file)))) {
|
|
266
|
+
console.log(chalk.yellow(` ${file}: ${missing.join('; ')}`));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
270
|
+
console.log(chalk.green('\n ✓ 所有条目 Front Matter 完整。'));
|
|
271
|
+
}
|
|
272
|
+
this.printFunnel(funnel);
|
|
273
|
+
console.log();
|
|
274
|
+
}
|
|
275
|
+
/** 打印知识库画像区块(人类可读)。 */
|
|
276
|
+
printKnowledgeOverview(output) {
|
|
277
|
+
console.log(chalk.cyan('\n📚 知识库画像'));
|
|
278
|
+
console.log(chalk.gray('─────────────────────────────────'));
|
|
279
|
+
console.log(chalk.white(` 总条数 : ${output.total}`));
|
|
280
|
+
console.log(chalk.gray(` troubleshooting : ${output.byCategory['troubleshooting'] ?? 0} 条`));
|
|
281
|
+
console.log(chalk.gray(` best-practices : ${output.byCategory['best-practices'] ?? 0} 条`));
|
|
282
|
+
console.log(chalk.gray(` implicit-conv. : ${output.byCategory['implicit-conventions'] ?? 0} 条`));
|
|
283
|
+
console.log(chalk.gray(` projectWiki : ${output.byCategory['projectWiki'] ?? 0} 条`));
|
|
284
|
+
if (output.projectWiki) {
|
|
285
|
+
this.printServiceBreakdown(output.projectWiki.byService, '条');
|
|
286
|
+
}
|
|
287
|
+
console.log(chalk.gray('─────────────────────────────────'));
|
|
288
|
+
console.log(chalk.white(` YAML 新格式 : ${output.yamlCount} 条`));
|
|
289
|
+
console.log(chalk.white(` 旧散文格式 : ${output.legacyCount} 条`));
|
|
290
|
+
console.log(chalk.white(` 已失效(deprecated) : ${output.deprecatedCount} 条`));
|
|
291
|
+
console.log(chalk.white(` Front Matter 完整率 : ${output.completenessRate}%`));
|
|
292
|
+
}
|
|
293
|
+
/** 打印沉淀漏斗区块(人类可读)。 */
|
|
294
|
+
printFunnel(funnel) {
|
|
295
|
+
const pct = (r) => `${(r * 100).toFixed(2)}%`;
|
|
296
|
+
console.log(chalk.cyan('\n📊 沉淀漏斗'));
|
|
297
|
+
console.log(chalk.gray('─────────────────────────────────'));
|
|
298
|
+
console.log(chalk.white(` ① 纠偏信号数 : ${funnel.correctionSignals}`));
|
|
299
|
+
console.log(chalk.white(` ② 追问数 : ${funnel.askedPitfalls}`));
|
|
300
|
+
console.log(chalk.white(` ③ 落库数 : ${funnel.archivedEntries}`));
|
|
301
|
+
console.log(chalk.white(` ④ 被引用数 : ${funnel.referencedRefs}`));
|
|
302
|
+
console.log(chalk.white(` ⑤ ProjectWiki 被引用数 : ${funnel.projectWikiReferencedRefs}`));
|
|
303
|
+
this.printServiceBreakdown(funnel.projectWikiReferencedRefsByService, '');
|
|
304
|
+
console.log(chalk.gray('─────────────────────────────────'));
|
|
305
|
+
console.log(chalk.gray(` ①→② 信号→追问 : ${pct(funnel.conversions.signalToAsked)}`));
|
|
306
|
+
console.log(chalk.gray(` ②→③ 追问→落库 : ${pct(funnel.conversions.askedToArchived)}`));
|
|
307
|
+
console.log(chalk.gray(` ③→④ 落库→引用 : ${pct(funnel.conversions.archivedToReferenced)}`));
|
|
308
|
+
console.log(chalk.gray(` ①→③ 端到端落库 : ${pct(funnel.conversions.signalToArchived)}`));
|
|
309
|
+
console.log(chalk.gray(` ①→④ 端到端引用 : ${pct(funnel.conversions.signalToReferenced)}`));
|
|
310
|
+
console.log(chalk.gray(` (扫描 change 数:${funnel.scannedChanges})`));
|
|
311
|
+
}
|
|
312
|
+
printServiceBreakdown(byService, suffix) {
|
|
313
|
+
for (const [service, count] of Object.entries(byService)) {
|
|
314
|
+
console.log(chalk.gray(` ${service.padEnd(17)}: ${count}${suffix ? ` ${suffix}` : ''}`));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
58
317
|
}
|
|
59
318
|
/**
|
|
60
|
-
* 在给定的 parent commander Command 上挂载
|
|
319
|
+
* 在给定的 parent commander Command 上挂载 knowledge 子命令。
|
|
61
320
|
* 父命令由 cli/index.ts 创建,形如 `zhuanspec knowledge`。
|
|
62
321
|
*/
|
|
63
322
|
export function registerKnowledgeCommands(parent) {
|
|
@@ -76,5 +335,20 @@ export function registerKnowledgeCommands(parent) {
|
|
|
76
335
|
process.exit(1);
|
|
77
336
|
}
|
|
78
337
|
});
|
|
338
|
+
parent
|
|
339
|
+
.command('stats')
|
|
340
|
+
.description('输出知识库画像:总条数、三分区分布、Front Matter 完整率、缺字段清单')
|
|
341
|
+
.option('--json', '以 JSON 格式输出(供脚本/analytics 消费)')
|
|
342
|
+
.action(async (options) => {
|
|
343
|
+
try {
|
|
344
|
+
const cmd = new KnowledgeCommand();
|
|
345
|
+
await cmd.stats(options ?? {});
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
console.log();
|
|
349
|
+
console.log(chalk.red(`✗ stats 失败: ${error.message}`));
|
|
350
|
+
process.exit(1);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
79
353
|
}
|
|
80
354
|
//# sourceMappingURL=knowledge.js.map
|
|
@@ -20,6 +20,8 @@ function determineResourceType(filePath) {
|
|
|
20
20
|
return 'spec';
|
|
21
21
|
if (filePath.includes('zhuanspec/knowledge/'))
|
|
22
22
|
return 'knowledge';
|
|
23
|
+
if (filePath.includes('.project-wiki/'))
|
|
24
|
+
return 'project-wiki';
|
|
23
25
|
if (filePath.includes('AGENTS.md'))
|
|
24
26
|
return 'AGENTS.md';
|
|
25
27
|
if (filePath.includes('CLAUDE.md'))
|
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
/** 知识库三类分类目录(不得新增) */
|
|
12
12
|
export type KnowledgeCategory = 'troubleshooting' | 'best-practices' | 'implicit-conventions';
|
|
13
13
|
export declare const KNOWLEDGE_CATEGORIES: readonly KnowledgeCategory[];
|
|
14
|
+
/** 严重度(YAML schema 字段,缺省视为 medium) */
|
|
15
|
+
export type KnowledgeSeverity = 'high' | 'medium' | 'low';
|
|
16
|
+
/** 知识生命周期状态(YAML schema 字段,缺省视为 active) */
|
|
17
|
+
export type KnowledgeStatus = 'active' | 'deprecated';
|
|
14
18
|
/** 单条知识条目(来源于知识文件的 Front Matter 解析) */
|
|
15
19
|
export interface KnowledgeEntry {
|
|
16
20
|
category: KnowledgeCategory;
|
|
@@ -22,6 +26,12 @@ export interface KnowledgeEntry {
|
|
|
22
26
|
keywords: string;
|
|
23
27
|
/** 适用场景:一句话"何时引用本条知识" */
|
|
24
28
|
useCase: string;
|
|
29
|
+
/** 稳定主键(YAML 格式才有;旧格式为空) */
|
|
30
|
+
id?: string;
|
|
31
|
+
/** 严重度(YAML 格式才有) */
|
|
32
|
+
severity?: KnowledgeSeverity;
|
|
33
|
+
/** 生命周期状态(YAML 格式才有,缺省 active) */
|
|
34
|
+
status?: KnowledgeStatus;
|
|
25
35
|
}
|
|
26
36
|
/** Front Matter 解析结果 */
|
|
27
37
|
export interface KnowledgeFrontMatter {
|
|
@@ -30,6 +40,16 @@ export interface KnowledgeFrontMatter {
|
|
|
30
40
|
useCase: string;
|
|
31
41
|
missingUseCase: boolean;
|
|
32
42
|
missingKeywords: boolean;
|
|
43
|
+
/** 是否为新版 YAML frontmatter(`---` 包裹);false 表示旧版散文式 */
|
|
44
|
+
isYaml: boolean;
|
|
45
|
+
/** 稳定主键(仅 YAML 格式解析) */
|
|
46
|
+
id: string;
|
|
47
|
+
/** YAML 格式下缺少 id(强校验依据) */
|
|
48
|
+
missingId: boolean;
|
|
49
|
+
/** 严重度(仅 YAML 格式解析,缺省 medium) */
|
|
50
|
+
severity: KnowledgeSeverity;
|
|
51
|
+
/** 生命周期状态(仅 YAML 格式解析,缺省 active) */
|
|
52
|
+
status: KnowledgeStatus;
|
|
33
53
|
}
|
|
34
54
|
/**
|
|
35
55
|
* 生成默认的空 index.md 骨架(含三分区 + Archive Log)。
|
|
@@ -58,21 +78,53 @@ export declare function readOrInitIndex(indexPath: string): Promise<string>;
|
|
|
58
78
|
export declare function appendIndexEntry(indexPath: string, entry: KnowledgeEntry): Promise<boolean>;
|
|
59
79
|
/**
|
|
60
80
|
* 从一段 markdown 文本中解析知识条目 Front Matter。
|
|
61
|
-
*
|
|
81
|
+
*
|
|
82
|
+
* 两种格式自动识别:
|
|
83
|
+
* - **新版 YAML**(`---` 包裹):解析 id / title / keywords / trigger / severity / status,
|
|
84
|
+
* 适用场景优先取 `use_case` 字段,否则回退正文 `## 适用场景` 首行;缺 id 记 missingId。
|
|
85
|
+
* - **旧版散文**(无 frontmatter):沿用 `# 标题` + `**关键词**:` + `**适用场景**:`,
|
|
86
|
+
* 不参与 id 强校验,保证历史知识文件向后兼容。
|
|
62
87
|
*/
|
|
63
88
|
export declare function parseFrontMatter(markdown: string): KnowledgeFrontMatter;
|
|
64
89
|
/**
|
|
65
|
-
*
|
|
90
|
+
* 从归档文档文本解析 deprecates 声明(哪些知识 id 被本次 change 推翻)。
|
|
91
|
+
* 支持两种来源(任一命中即收集,去重):
|
|
92
|
+
* 1. YAML frontmatter 顶层 `deprecates: bp-x, bp-y` 或数组形式
|
|
93
|
+
* 2. 正文行 `deprecates: bp-x, bp-y`(大小写不敏感,逗号/中文逗号/空格分隔)
|
|
94
|
+
* @returns 去重后的 id 列表
|
|
95
|
+
*/
|
|
96
|
+
export declare function parseDeprecatesDeclaration(markdown: string): string[];
|
|
97
|
+
/**
|
|
98
|
+
* 扫描结果:每类目录下发现的知识条目。
|
|
99
|
+
* - warnings:软问题(缺关键词/适用场景、旧格式缺标题),有兜底、不阻断。
|
|
100
|
+
* - errors:硬问题(YAML 格式缺 id / title),强校验依据,供命令决定是否阻断。
|
|
66
101
|
*/
|
|
67
102
|
export interface ScanResult {
|
|
68
103
|
entries: KnowledgeEntry[];
|
|
69
104
|
warnings: string[];
|
|
105
|
+
errors: string[];
|
|
70
106
|
}
|
|
71
107
|
/**
|
|
72
108
|
* 扫描 knowledge/{troubleshooting,best-practices,implicit-conventions}/*.md,
|
|
73
109
|
* 解析 Front Matter 组装 KnowledgeEntry 列表,遇缺字段记录 warning(不阻断)。
|
|
74
110
|
*/
|
|
75
111
|
export declare function scanKnowledgeEntries(knowledgeDir: string): Promise<ScanResult>;
|
|
112
|
+
/**
|
|
113
|
+
* 在 knowledgeDir 内按 id 定位知识文件绝对路径(扫三类目录匹配 frontmatter.id)。
|
|
114
|
+
* 命中多个同 id 时返回首个(id 应唯一,重复属数据问题,由 scan 的 missingId 校验体系覆盖)。
|
|
115
|
+
*/
|
|
116
|
+
export declare function findEntryFileById(knowledgeDir: string, id: string): Promise<string | null>;
|
|
117
|
+
/**
|
|
118
|
+
* 将指定知识文件的 YAML frontmatter status 改为目标值,保留正文与其他字段。
|
|
119
|
+
*
|
|
120
|
+
* 算法(不做整体 YAML 序列化,避免丢注释/改键序/改引号风格破坏文件):
|
|
121
|
+
* 1. FRONT_MATTER_RE 切出 yaml 块与正文 body;无 frontmatter(旧散文式)→ 返回 false,不动正文。
|
|
122
|
+
* 2. 仅当存在 `status:` 行时替换该行值,否则在 yaml 块末尾追加 `status: <status>`。
|
|
123
|
+
* 3. 改写后用 parseFrontMatter 复核 status 命中目标值再落盘(自检,防正则误伤)。
|
|
124
|
+
*
|
|
125
|
+
* @returns 是否实际写入(已是目标状态或旧散文式均返回 false)
|
|
126
|
+
*/
|
|
127
|
+
export declare function setEntryStatus(filePath: string, status: KnowledgeStatus): Promise<boolean>;
|
|
76
128
|
/**
|
|
77
129
|
* 全量重建 index.md 的三大分区(保留 Archive Log 既有内容)。
|
|
78
130
|
* 算法:
|