@zhuan-ai/zhuanspec 2.16.6 → 2.17.0

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 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/。当前仅提供 reindex 子命令:
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 上挂载 `reindex` 子命令。
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/。当前仅提供 reindex 子命令:
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 上挂载 `reindex` 子命令。
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
- * 支持项目现行的 `**字段**: 值` 样式;字段可选,缺失时返回对应 missing 标记。
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
  * 算法: