@itradingai/aiwiki 0.2.15 → 0.2.18

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/README.md CHANGED
@@ -247,3 +247,37 @@ aiwiki lint --path "F:\knowledge_data\aiwiki-test"
247
247
  ## License
248
248
 
249
249
  MIT. See [LICENSE](LICENSE).
250
+
251
+ ## Query Filters
252
+
253
+ AIWiki retrieval is local Markdown/frontmatter search. It is intentionally lightweight: no vector search, no database, no external search, and no RAG-over-wiki.
254
+
255
+ ```bash
256
+ aiwiki context "AI Agent" --type wiki_entries --source-role input --wiki-type source_knowledge --status active --limit 5
257
+ aiwiki query "AI Agent" --type source_cards --status to-review --limit 3
258
+ ```
259
+
260
+ `context` returns Agent-readable JSON with `query_scope`, `result_quality`, `recommended_next_action`, `match_reasons`, `quality_signals`, and `related_refs`. `query` uses the same retrieval path and shows the match reasons and quality hints for humans.
261
+
262
+ ## Agent Skill Sync and Upgrade
263
+
264
+ AIWiki is Agent-first: after installing or upgrading the npm package, sync the packaged AIWiki skill into the local Agent environment.
265
+
266
+ First install and later upgrades use the same safe command:
267
+
268
+ ```bash
269
+ npm install -g @itradingai/aiwiki@latest
270
+ aiwiki agent sync --yes
271
+ aiwiki agent check
272
+ ```
273
+
274
+ For one Agent:
275
+
276
+ ```bash
277
+ aiwiki agent sync --agent codex --yes
278
+ aiwiki agent sync --agent claude --yes
279
+ ```
280
+
281
+ `agent sync` is idempotent. Missing targets are installed, current targets are left unchanged, and changed old skill files are backed up before overwrite. Use `--dry-run` to preview and `--json` when an AI Agent needs stable machine-readable status.
282
+
283
+ After sync, restart or reload the target Agent so it reads the new AIWiki skill. To roll back, copy the generated `.bak-<timestamp>` file back over the target skill file.
package/dist/src/app.js CHANGED
@@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url";
6
6
  import { flagBool, flagString, parseArgs } from "./args.js";
7
7
  import { buildContext } from "./context.js";
8
8
  import { deriveFileTitle, ingestFile, ingestPayload } from "./ingest.js";
9
- import { lintWorkspace, renderLintReport, writeLintReport } from "./lint.js";
9
+ import { filterLintReport, lintWorkspace, renderLintReport, renderLintSummary, writeLintReport } from "./lint.js";
10
10
  import { CliError, writeLine } from "./output.js";
11
11
  import { confirmInit, directorySummary, doctor, exists, initWorkspace, promptForSetup, promptForInitPath, readConfig, resolveWorkspace, setDefaultWorkspace, statusSummary } from "./workspace.js";
12
12
  export async function runCli(argv, streams = { stdout: process.stdout, stderr: process.stderr }) {
@@ -17,6 +17,14 @@ export async function runCli(argv, streams = { stdout: process.stdout, stderr: p
17
17
  writeLine(streams.stdout, `aiwiki ${await packageVersion()}`);
18
18
  return 0;
19
19
  }
20
+ if (command === "agent" && (subcommand === "help" || args.flags.has("help"))) {
21
+ printAgentHelp(streams.stdout);
22
+ return 0;
23
+ }
24
+ if ((command === "context" || command === "query") && args.flags.has("help")) {
25
+ printContextHelp(streams.stdout);
26
+ return 0;
27
+ }
20
28
  if (args.flags.has("help") || !command || command === "help" || command === "-h") {
21
29
  printHelp(streams.stdout);
22
30
  return 0;
@@ -54,8 +62,24 @@ export async function runCli(argv, streams = { stdout: process.stdout, stderr: p
54
62
  }
55
63
  return 0;
56
64
  }
65
+ if (command === "agent" && subcommand === "sync") {
66
+ const result = await syncAgentSkills({
67
+ agentId: flagString(args, "agent"),
68
+ yes: flagBool(args, "yes"),
69
+ dryRun: flagBool(args, "dry-run"),
70
+ json: flagBool(args, "json"),
71
+ streams
72
+ });
73
+ if (flagBool(args, "json")) {
74
+ writeLine(streams.stdout, JSON.stringify(result, null, 2));
75
+ }
76
+ else {
77
+ printAgentSyncResult(streams.stdout, result);
78
+ }
79
+ return 0;
80
+ }
57
81
  if (command === "agent" && subcommand === "check") {
58
- await printAgentCheck(streams.stdout, await discoverAgentTargets());
82
+ await printAgentCheckDetailed(streams.stdout, await discoverAgentTargets(), flagBool(args, "json"));
59
83
  return 0;
60
84
  }
61
85
  if (command === "agent" && (subcommand === "list" || !subcommand)) {
@@ -137,7 +161,7 @@ export async function runCli(argv, streams = { stdout: process.stdout, stderr: p
137
161
  if (!query) {
138
162
  throw new CliError("请提供查询主题。");
139
163
  }
140
- writeLine(streams.stdout, JSON.stringify(await buildContext(root, query), null, 2));
164
+ writeLine(streams.stdout, JSON.stringify(await buildContext(root, query, contextOptions(args)), null, 2));
141
165
  return 0;
142
166
  }
143
167
  if (command === "query") {
@@ -146,7 +170,7 @@ export async function runCli(argv, streams = { stdout: process.stdout, stderr: p
146
170
  if (!query) {
147
171
  throw new CliError("请提供查询主题。");
148
172
  }
149
- writeLine(streams.stdout, renderQuery(await buildContext(root, query)));
173
+ writeLine(streams.stdout, renderQuery(await buildContext(root, query, contextOptions(args))));
150
174
  return 0;
151
175
  }
152
176
  if (command === "next") {
@@ -159,10 +183,16 @@ export async function runCli(argv, streams = { stdout: process.stdout, stderr: p
159
183
  }
160
184
  if (command === "lint") {
161
185
  const root = await resolveWorkspace(flagString(args, "path"));
162
- const report = await lintWorkspace(root);
163
- const reportPath = await writeLintReport(root, report);
186
+ const severity = parseLintSeverity(flagString(args, "severity"));
187
+ const report = filterLintReport(await lintWorkspace(root), severity);
188
+ if (flagBool(args, "json")) {
189
+ writeLine(streams.stdout, JSON.stringify(report, null, 2));
190
+ return 0;
191
+ }
192
+ const reportPath = flagBool(args, "no-write") ? undefined : await writeLintReport(root, report);
193
+ writeLine(streams.stdout, renderLintSummary(report, reportPath));
194
+ writeLine(streams.stdout, "");
164
195
  writeLine(streams.stdout, renderLintReport(report));
165
- writeLine(streams.stdout, `report: ${reportPath}`);
166
196
  return 0;
167
197
  }
168
198
  if (command === "ingest-agent") {
@@ -241,6 +271,8 @@ function printHelp(stream) {
241
271
  writeLine(stream, " aiwiki agent list");
242
272
  writeLine(stream, " aiwiki agent install");
243
273
  writeLine(stream, " aiwiki agent install --agent codex --yes");
274
+ writeLine(stream, " aiwiki agent sync --yes");
275
+ writeLine(stream, " aiwiki agent check --json");
244
276
  writeLine(stream, " aiwiki prompt agent");
245
277
  writeLine(stream, " aiwiki doctor");
246
278
  writeLine(stream, " aiwiki status");
@@ -256,6 +288,50 @@ function printHelp(stream) {
256
288
  writeLine(stream, " aiwiki ingest-url <url> --content-file <file>");
257
289
  writeLine(stream, " aiwiki agent check");
258
290
  }
291
+ function printAgentHelp(stream) {
292
+ writeLine(stream, "AIWiki Agent commands");
293
+ writeLine(stream, "");
294
+ writeLine(stream, "Agent-first setup and upgrade:");
295
+ writeLine(stream, " aiwiki agent sync --yes");
296
+ writeLine(stream, " aiwiki agent sync --agent codex --yes");
297
+ writeLine(stream, " aiwiki agent sync --agent codex --dry-run");
298
+ writeLine(stream, " aiwiki agent sync --json --yes");
299
+ writeLine(stream, "");
300
+ writeLine(stream, "Status:");
301
+ writeLine(stream, " aiwiki agent check");
302
+ writeLine(stream, " aiwiki agent check --json");
303
+ writeLine(stream, "");
304
+ writeLine(stream, "Compatibility:");
305
+ writeLine(stream, " aiwiki agent install --agent codex --yes");
306
+ writeLine(stream, " aiwiki agent install --agent codex --yes --force");
307
+ writeLine(stream, "");
308
+ writeLine(stream, "sync is idempotent: missing targets are installed, current targets are left unchanged, and different targets are backed up before overwrite.");
309
+ }
310
+ function printContextHelp(stream) {
311
+ writeLine(stream, "AIWiki context/query");
312
+ writeLine(stream, "");
313
+ writeLine(stream, "Local Markdown/frontmatter retrieval for host Agents and humans:");
314
+ writeLine(stream, " aiwiki context <topic> --limit 10");
315
+ writeLine(stream, " aiwiki query <topic> --type wiki_entries --status active");
316
+ writeLine(stream, "");
317
+ writeLine(stream, "Filters:");
318
+ writeLine(stream, " --type wiki_entries|source_cards|claims|topics|outlines|raw_refs");
319
+ writeLine(stream, " --source-role input|processing|output");
320
+ writeLine(stream, " --wiki-type source_knowledge|personal_knowledge");
321
+ writeLine(stream, " --status active|to-review|ready|draft");
322
+ writeLine(stream, " --limit <1-50>");
323
+ writeLine(stream, "");
324
+ writeLine(stream, "context JSON includes query_scope, result_quality, recommended_next_action, match_reasons, quality_signals, and related_refs.");
325
+ }
326
+ function parseLintSeverity(value) {
327
+ if (value === undefined) {
328
+ return undefined;
329
+ }
330
+ if (value === "error" || value === "warning" || value === "info") {
331
+ return value;
332
+ }
333
+ throw new CliError("lint --severity must be error, warning, or info");
334
+ }
259
335
  async function discoverAgentTargets() {
260
336
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
261
337
  const skillSource = path.join(packageRoot, "skill", "SKILL.md");
@@ -347,6 +423,40 @@ async function printAgentCheck(stream, targets) {
347
423
  }
348
424
  }
349
425
  }
426
+ async function printAgentCheckDetailed(stream, targets, json = false) {
427
+ const checked = await Promise.all(targets.map(async (target) => ({
428
+ ...target,
429
+ state: await inspectAgentTarget(target),
430
+ installed: target.target ? await exists(target.target) : false
431
+ })));
432
+ if (json) {
433
+ writeLine(stream, JSON.stringify({
434
+ schema_version: "aiwiki.agent_check.v1",
435
+ generated_at: new Date().toISOString(),
436
+ targets: checked.map((target) => ({
437
+ id: target.id,
438
+ name: target.name,
439
+ detected: target.detected,
440
+ installable: target.installable,
441
+ installed: target.installed,
442
+ state: target.state,
443
+ source: target.source,
444
+ target: target.target
445
+ }))
446
+ }, null, 2));
447
+ return;
448
+ }
449
+ writeLine(stream, "AIWiki Agent check");
450
+ for (const target of checked) {
451
+ writeLine(stream, `${target.id}: ${target.name} | detected=${target.detected ? "yes" : "no"} | installed=${target.installed ? "yes" : "no"} | installable=${target.installable ? "yes" : "no"} | state=${target.state}`);
452
+ if (target.detected && target.installable && (target.state === "missing" || target.state === "different")) {
453
+ writeLine(stream, ` suggested: aiwiki agent sync --agent ${target.id} --yes`);
454
+ }
455
+ else if (target.detected && !target.installable) {
456
+ writeLine(stream, " suggested: aiwiki prompt agent");
457
+ }
458
+ }
459
+ }
350
460
  async function installAgentSkill(options) {
351
461
  const targets = await discoverAgentTargets();
352
462
  const installable = targets.filter((target) => target.detected && target.installable);
@@ -385,8 +495,8 @@ async function installAgentSkill(options) {
385
495
  return undefined;
386
496
  }
387
497
  }
388
- await copyInstallFile(selected.source, selected.target, options.force);
389
- return selected;
498
+ const result = await copyInstallFileSafe(selected.source, selected.target, options.force);
499
+ return { ...selected, ...result };
390
500
  }
391
501
  async function askQuestion(streams, question) {
392
502
  if (!process.stdin.isTTY) {
@@ -400,7 +510,28 @@ async function askQuestion(streams, question) {
400
510
  rl.close();
401
511
  }
402
512
  }
513
+ async function syncAgentSkills(options) {
514
+ const targets = await discoverAgentTargets();
515
+ const selected = options.agentId ? targets.find((target) => target.id === options.agentId) : undefined;
516
+ if (!selected && options.agentId) {
517
+ throw new CliError(`未知宿主 Agent: ${options.agentId}`);
518
+ }
519
+ const syncTargets = selected ? [selected] : targets.filter((target) => target.detected && target.installable);
520
+ if (!syncTargets.length) {
521
+ throw new CliError("No detected installable Agent targets. Run aiwiki agent list or aiwiki prompt agent.");
522
+ }
523
+ if (!options.yes && !options.dryRun) {
524
+ throw new CliError("agent sync modifies Agent skill files. Re-run with --yes, or use --dry-run to preview.");
525
+ }
526
+ return {
527
+ schema_version: "aiwiki.agent_sync.v1",
528
+ generated_at: new Date().toISOString(),
529
+ dry_run: options.dryRun,
530
+ results: await Promise.all(syncTargets.map((target) => syncAgentTarget(target, options.dryRun)))
531
+ };
532
+ }
403
533
  async function copyInstallFile(source, target, force) {
534
+ return copyInstallFileSafe(source, target, force);
404
535
  await fs.access(source);
405
536
  if (!force && (await exists(target))) {
406
537
  throw new CliError(`目标文件已存在: ${target}。如需覆盖,请加 --force。`);
@@ -408,6 +539,84 @@ async function copyInstallFile(source, target, force) {
408
539
  await fs.mkdir(path.dirname(target), { recursive: true });
409
540
  await fs.copyFile(source, target);
410
541
  }
542
+ async function copyInstallFileSafe(source, target, force) {
543
+ await fs.access(source);
544
+ const targetExists = await exists(target);
545
+ if (!force && targetExists) {
546
+ throw new CliError(`目标文件已存在: ${target}。如需覆盖,请运行 aiwiki agent sync --agent <id> --yes,或为 install 加 --force。`);
547
+ }
548
+ if (targetExists && await sameFileContent(source, target)) {
549
+ return { action: "current" };
550
+ }
551
+ await fs.mkdir(path.dirname(target), { recursive: true });
552
+ const backupPath = targetExists ? await backupFile(target) : undefined;
553
+ await fs.copyFile(source, target);
554
+ return { action: targetExists ? "updated" : "installed", backupPath };
555
+ }
556
+ async function inspectAgentTarget(target) {
557
+ if (!target.installable || !target.source || !target.target) {
558
+ return "unsupported";
559
+ }
560
+ if (!(await exists(target.target))) {
561
+ return "missing";
562
+ }
563
+ return await sameFileContent(target.source, target.target) ? "current" : "different";
564
+ }
565
+ async function syncAgentTarget(target, dryRun) {
566
+ const state = await inspectAgentTarget(target);
567
+ const base = {
568
+ id: target.id,
569
+ name: target.name,
570
+ detected: target.detected,
571
+ installable: target.installable,
572
+ state,
573
+ target: target.target,
574
+ source: target.source,
575
+ changed: false,
576
+ dryRun
577
+ };
578
+ if (state === "unsupported" || !target.source || !target.target) {
579
+ return { ...base, action: "unsupported", note: target.note };
580
+ }
581
+ if (state === "current") {
582
+ return { ...base, action: "current" };
583
+ }
584
+ if (dryRun) {
585
+ return { ...base, action: state === "missing" ? "would_install" : "would_update", changed: true };
586
+ }
587
+ const result = await copyInstallFileSafe(target.source, target.target, true);
588
+ return { ...base, action: result.action, backupPath: result.backupPath, changed: result.action !== "current" };
589
+ }
590
+ async function sameFileContent(source, target) {
591
+ try {
592
+ const [sourceText, targetText] = await Promise.all([fs.readFile(source, "utf8"), fs.readFile(target, "utf8")]);
593
+ return sourceText === targetText;
594
+ }
595
+ catch {
596
+ return false;
597
+ }
598
+ }
599
+ async function backupFile(target) {
600
+ const parsed = path.parse(target);
601
+ const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
602
+ const backupPath = path.join(parsed.dir, `${parsed.base}.bak-${stamp}`);
603
+ await fs.copyFile(target, backupPath);
604
+ return backupPath;
605
+ }
606
+ function printAgentSyncResult(stream, report) {
607
+ writeLine(stream, "AIWiki Agent sync");
608
+ writeLine(stream, `dry_run: ${report.dry_run ? "yes" : "no"}`);
609
+ for (const item of report.results) {
610
+ writeLine(stream, `${item.id}: ${item.name} | state=${item.state} | action=${item.action} | changed=${item.changed ? "yes" : "no"}`);
611
+ if (item.target) {
612
+ writeLine(stream, ` target: ${item.target}`);
613
+ }
614
+ if (item.backupPath) {
615
+ writeLine(stream, ` backup: ${item.backupPath}`);
616
+ }
617
+ }
618
+ writeLine(stream, "next: restart or reload the target Agent so it reads the synced AIWiki skill.");
619
+ }
411
620
  function printAgentPrompt(stream) {
412
621
  writeLine(stream, "AIWiki Agent 中文提示");
413
622
  writeLine(stream, "");
@@ -493,7 +702,7 @@ async function printNext(stream, root, runCount, checks, targets, report) {
493
702
  if (runCount === 0) {
494
703
  writeLine(stream, "");
495
704
  writeLine(stream, "No ingest records yet.");
496
- writeLine(stream, "- aiwiki agent install");
705
+ writeLine(stream, "- aiwiki agent sync --yes");
497
706
  writeLine(stream, "- Then ask the host Agent to ingest a URL.");
498
707
  writeLine(stream, "- AIWiki CLI does not fetch webpages; the host Agent supplies content.");
499
708
  writeLine(stream, "- repair_order: empty_workspace");
@@ -520,12 +729,25 @@ function recommendedNextAction(runCount, lintStatus, hasMissingSystemFiles) {
520
729
  return "next_action: aiwiki lint";
521
730
  }
522
731
  if (runCount === 0) {
523
- return "next_action: aiwiki agent install";
732
+ return "next_action: aiwiki agent sync --yes";
524
733
  }
525
734
  return "next_action: aiwiki query <topic>";
526
735
  }
736
+ function contextOptions(args) {
737
+ const limit = flagString(args, "limit");
738
+ return {
739
+ filters: {
740
+ type: flagString(args, "type"),
741
+ source_role: flagString(args, "source-role"),
742
+ wiki_type: flagString(args, "wiki-type"),
743
+ status: flagString(args, "status")
744
+ },
745
+ limit: limit === undefined ? undefined : Number(limit)
746
+ };
747
+ }
527
748
  function renderQuery(context) {
528
749
  const lines = [`AIWiki 查询: ${context.query}`, ""];
750
+ lines.push(`结果质量: matches=${context.result_quality.total_matches}, best_score=${context.result_quality.best_score}, has_wiki_entry=${context.result_quality.has_wiki_entry ? "yes" : "no"}`, `下一步建议: ${context.recommended_next_action}`, `查询范围: groups=${context.query_scope.searched_groups.join(",") || "none"}, limit=${context.query_scope.limit}, filters=${JSON.stringify(context.query_scope.filters)}`, "");
529
751
  appendQueryGroup(lines, "Wiki 条目", context.matches.wiki_entries);
530
752
  appendQueryGroup(lines, "资料卡", context.matches.source_cards);
531
753
  appendQueryGroup(lines, "选题", context.matches.topics);
@@ -546,9 +768,13 @@ function appendQueryGroup(lines, label, items) {
546
768
  }
547
769
  for (const item of items.slice(0, 5)) {
548
770
  lines.push(`- ${item.title} (${item.path})`);
771
+ lines.push(` score=${item.score}; reasons=${item.match_reasons.join(",") || "unknown"}; quality=${item.quality_signals.join(",") || "none"}`);
549
772
  if (item.summary) {
550
773
  lines.push(` ${item.summary}`);
551
774
  }
775
+ if (item.related_refs.length) {
776
+ lines.push(` related: ${item.related_refs.slice(0, 5).join("; ")}`);
777
+ }
552
778
  if (item.warnings.length) {
553
779
  lines.push(` 提示: ${item.warnings.join(";")}`);
554
780
  }
@@ -11,13 +11,28 @@ const GROUPS = [
11
11
  { key: "outlines", dir: "08-outputs/outlines", weight: 2 },
12
12
  { key: "raw_refs", dir: "02-raw/articles", weight: 1 }
13
13
  ];
14
- export async function buildContext(rootPath, query, now = new Date().toISOString()) {
14
+ const DEFAULT_LIMIT = 10;
15
+ export async function buildContext(rootPath, query, options = {}, now = new Date().toISOString()) {
15
16
  const root = path.resolve(rootPath);
16
17
  const tokens = tokenize(query);
18
+ const limit = normalizeLimit(options.limit);
19
+ const filters = normalizeFilters(options.filters);
17
20
  const result = {
18
21
  schema_version: "aiwiki.context.v1",
19
22
  query,
20
23
  generated_at: now,
24
+ query_scope: {
25
+ filters,
26
+ limit,
27
+ searched_groups: []
28
+ },
29
+ result_quality: {
30
+ total_matches: 0,
31
+ best_score: 0,
32
+ has_wiki_entry: false,
33
+ warnings: []
34
+ },
35
+ recommended_next_action: "broaden_query",
21
36
  matches: {
22
37
  wiki_entries: [],
23
38
  source_cards: [],
@@ -26,34 +41,41 @@ export async function buildContext(rootPath, query, now = new Date().toISOString
26
41
  outlines: [],
27
42
  raw_refs: []
28
43
  },
29
- suggested_answer_structure: ["主题概览", "核心观点", "已有资料依据", "可复用判断", "下一步建议"],
44
+ suggested_answer_structure: ["topic overview", "core claims", "available evidence", "reuse judgment", "next action"],
30
45
  warnings: []
31
46
  };
32
47
  if (!tokens.length) {
33
48
  result.warnings.push("query is empty after tokenization");
49
+ finalizeQuality(result);
34
50
  return result;
35
51
  }
36
52
  for (const group of GROUPS.filter((item) => item.key !== "raw_refs")) {
53
+ if (!groupAllowed(group.key, filters.type)) {
54
+ continue;
55
+ }
37
56
  const dir = path.join(root, group.dir);
38
57
  if (!(await exists(dir))) {
39
58
  continue;
40
59
  }
41
- const matches = await searchDir(root, dir, tokens, group.weight);
42
- result.matches[group.key].push(...matches.slice(0, 10));
60
+ result.query_scope.searched_groups.push(group.key);
61
+ const matches = await searchDir(root, dir, tokens, group.weight, group.key, filters);
62
+ result.matches[group.key].push(...matches.slice(0, limit));
43
63
  }
44
- if (!result.matches.wiki_entries.length) {
45
- result.warnings.push("未命中 Wiki Entry,结果可能来自资料卡、选题或原文引用。");
64
+ if (!result.matches.wiki_entries.length && groupAllowed("raw_refs", filters.type)) {
65
+ result.warnings.push("No Wiki Entry matched; results may come from source cards, topics, outlines, or raw references.");
46
66
  const rawGroup = GROUPS.find((item) => item.key === "raw_refs");
47
67
  if (rawGroup) {
48
68
  const rawDir = path.join(root, rawGroup.dir);
49
69
  if (await exists(rawDir)) {
50
- result.matches.raw_refs.push(...(await searchDir(root, rawDir, tokens, rawGroup.weight)).slice(0, 10));
70
+ result.query_scope.searched_groups.push(rawGroup.key);
71
+ result.matches.raw_refs.push(...(await searchDir(root, rawDir, tokens, rawGroup.weight, rawGroup.key, filters)).slice(0, limit));
51
72
  }
52
73
  }
53
74
  }
75
+ finalizeQuality(result);
54
76
  return result;
55
77
  }
56
- async function searchDir(root, dir, tokens, weight) {
78
+ async function searchDir(root, dir, tokens, weight, groupKey, filters) {
57
79
  const files = await listMarkdownFiles(dir);
58
80
  const matches = [];
59
81
  for (const file of files) {
@@ -61,12 +83,24 @@ async function searchDir(root, dir, tokens, weight) {
61
83
  const parsed = parseMarkdown(text);
62
84
  const rel = relativePath(root, file);
63
85
  const title = frontmatterString(parsed.frontmatter, "title") ?? path.basename(file, ".md");
86
+ const type = frontmatterString(parsed.frontmatter, "type") ?? groupKey;
87
+ const status = frontmatterString(parsed.frontmatter, "status");
88
+ const sourceRole = frontmatterString(parsed.frontmatter, "source_role");
89
+ const wikiType = frontmatterString(parsed.frontmatter, "wiki_type");
90
+ if (!passesFilters({ type, groupKey, status, source_role: sourceRole, wiki_type: wikiType }, filters)) {
91
+ continue;
92
+ }
93
+ const topics = frontmatterArray(parsed.frontmatter, "topics");
94
+ const tags = frontmatterArray(parsed.frontmatter, "tags");
95
+ const relatedRefs = relatedReferences(parsed.frontmatter, parsed.body);
96
+ const sourceUrl = frontmatterString(parsed.frontmatter, "source_url") ?? "";
64
97
  const haystack = [
65
98
  rel,
66
99
  title,
67
- frontmatterString(parsed.frontmatter, "source_url") ?? "",
68
- frontmatterArray(parsed.frontmatter, "topics").join(" "),
69
- frontmatterArray(parsed.frontmatter, "tags").join(" "),
100
+ sourceUrl,
101
+ topics.join(" "),
102
+ tags.join(" "),
103
+ relatedRefs.join(" "),
70
104
  parsed.body
71
105
  ].join("\n").toLowerCase();
72
106
  const hits = tokens.filter((token) => haystack.includes(token.toLowerCase())).length;
@@ -77,8 +111,9 @@ async function searchDir(root, dir, tokens, weight) {
77
111
  const quality = frontmatterString(parsed.frontmatter, "quality");
78
112
  const groundingMarkers = frontmatterArray(parsed.frontmatter, "grounding_markers");
79
113
  const groundingNeedsReview = frontmatterBoolean(parsed.frontmatter, "grounding_needs_review");
114
+ const groundingEvidenceAvailable = frontmatterBoolean(parsed.frontmatter, "grounding_evidence_available");
80
115
  const warnings = generationMode === "deterministic_fallback"
81
- ? [" Wiki Entry deterministic fallback,仅包含来源、正文预览和待补全区。"]
116
+ ? ["This Wiki Entry is a deterministic fallback; it may need host-agent enrichment."]
82
117
  : [];
83
118
  if (groundingNeedsReview) {
84
119
  warnings.push(`Grounding needs review${groundingMarkers.length ? `: ${groundingMarkers.join(", ")}` : ""}.`);
@@ -88,11 +123,19 @@ async function searchDir(root, dir, tokens, weight) {
88
123
  path: rel,
89
124
  summary: frontmatterString(parsed.frontmatter, "summary") ?? summarize(parsed.body, quality),
90
125
  score: Number(((hits / tokens.length) * weight).toFixed(2)),
91
- topics: frontmatterArray(parsed.frontmatter, "topics"),
92
- source_url: frontmatterString(parsed.frontmatter, "source_url") ?? "",
126
+ type,
127
+ topics,
128
+ tags,
129
+ source_url: sourceUrl,
130
+ status,
131
+ source_role: sourceRole,
132
+ wiki_type: wikiType,
133
+ match_reasons: matchReasons(tokens, { rel, title, body: parsed.body, topics, tags, relatedRefs, sourceUrl }),
134
+ quality_signals: qualitySignals({ quality, generationMode, groundingEvidenceAvailable, groundingNeedsReview, status, relatedRefs }),
135
+ related_refs: relatedRefs,
93
136
  generation_mode: generationMode,
94
137
  quality,
95
- grounding_evidence_available: frontmatterBoolean(parsed.frontmatter, "grounding_evidence_available"),
138
+ grounding_evidence_available: groundingEvidenceAvailable,
96
139
  grounding_needs_review: groundingNeedsReview,
97
140
  grounding_markers: groundingMarkers,
98
141
  warnings
@@ -125,7 +168,121 @@ function tokenize(value) {
125
168
  function summarize(body, quality) {
126
169
  const compact = body.replace(/\s+/g, " ").trim();
127
170
  if (quality === "scaffold") {
128
- return "仅有正文预览,未生成高质量摘要。";
171
+ return "Only a scaffold preview is available; enrich before relying on it as a final answer.";
129
172
  }
130
173
  return compact.length > 180 ? `${compact.slice(0, 180)}...` : compact;
131
174
  }
175
+ function normalizeLimit(value) {
176
+ if (typeof value !== "number" || !Number.isFinite(value)) {
177
+ return DEFAULT_LIMIT;
178
+ }
179
+ return Math.max(1, Math.min(50, Math.floor(value)));
180
+ }
181
+ function normalizeFilters(filters) {
182
+ return Object.fromEntries(Object.entries(filters ?? {})
183
+ .filter(([, value]) => typeof value === "string" && value.trim())
184
+ .map(([key, value]) => [key, String(value).trim()]));
185
+ }
186
+ function groupAllowed(groupKey, typeFilter) {
187
+ if (!typeFilter) {
188
+ return true;
189
+ }
190
+ return normalizeType(typeFilter) === normalizeType(groupKey);
191
+ }
192
+ function passesFilters(item, filters) {
193
+ if (filters.type && normalizeType(filters.type) !== normalizeType(item.type) && normalizeType(filters.type) !== normalizeType(item.groupKey)) {
194
+ return false;
195
+ }
196
+ if (filters.status && filters.status !== item.status) {
197
+ return false;
198
+ }
199
+ if (filters.source_role && filters.source_role !== item.source_role) {
200
+ return false;
201
+ }
202
+ if (filters.wiki_type && filters.wiki_type !== item.wiki_type) {
203
+ return false;
204
+ }
205
+ return true;
206
+ }
207
+ function normalizeType(value) {
208
+ return value.replace(/-/g, "_").toLowerCase();
209
+ }
210
+ function relatedReferences(frontmatter, body) {
211
+ const refs = [
212
+ frontmatterString(frontmatter, "source_card"),
213
+ frontmatterString(frontmatter, "raw_file"),
214
+ frontmatterString(frontmatter, "claims_note"),
215
+ frontmatterString(frontmatter, "assets_note"),
216
+ frontmatterString(frontmatter, "topics_note"),
217
+ frontmatterString(frontmatter, "outline_note"),
218
+ ...extractWikilinks(body)
219
+ ].filter((value) => Boolean(value));
220
+ return Array.from(new Set(refs));
221
+ }
222
+ function extractWikilinks(body) {
223
+ const refs = [];
224
+ const pattern = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g;
225
+ for (const match of body.matchAll(pattern)) {
226
+ refs.push(match[1].trim());
227
+ }
228
+ return refs;
229
+ }
230
+ function matchReasons(tokens, fields) {
231
+ const reasons = new Set();
232
+ for (const token of tokens.map((item) => item.toLowerCase())) {
233
+ if (fields.title.toLowerCase().includes(token))
234
+ reasons.add("title");
235
+ if (fields.rel.toLowerCase().includes(token))
236
+ reasons.add("path");
237
+ if (fields.sourceUrl.toLowerCase().includes(token))
238
+ reasons.add("source_url");
239
+ if (fields.topics.join(" ").toLowerCase().includes(token))
240
+ reasons.add("topics");
241
+ if (fields.tags.join(" ").toLowerCase().includes(token))
242
+ reasons.add("tags");
243
+ if (fields.relatedRefs.join(" ").toLowerCase().includes(token))
244
+ reasons.add("relationships");
245
+ if (fields.body.toLowerCase().includes(token))
246
+ reasons.add("body");
247
+ }
248
+ return Array.from(reasons);
249
+ }
250
+ function qualitySignals(item) {
251
+ const signals = [];
252
+ if (item.quality)
253
+ signals.push(`quality:${item.quality}`);
254
+ if (item.status)
255
+ signals.push(`status:${item.status}`);
256
+ if (item.generationMode)
257
+ signals.push(`generation_mode:${item.generationMode}`);
258
+ if (item.groundingEvidenceAvailable === true)
259
+ signals.push("grounding:evidence_available");
260
+ if (item.groundingEvidenceAvailable === false)
261
+ signals.push("grounding:no_evidence_flag");
262
+ if (item.groundingNeedsReview)
263
+ signals.push("grounding:needs_review");
264
+ if (item.relatedRefs.length)
265
+ signals.push("relationships:present");
266
+ return signals;
267
+ }
268
+ function finalizeQuality(result) {
269
+ const all = Object.values(result.matches).flat();
270
+ result.result_quality = {
271
+ total_matches: all.length,
272
+ best_score: all.reduce((best, item) => Math.max(best, item.score), 0),
273
+ has_wiki_entry: result.matches.wiki_entries.length > 0,
274
+ warnings: result.warnings
275
+ };
276
+ if (!all.length) {
277
+ result.recommended_next_action = "broaden_query_or_ingest_source";
278
+ }
279
+ else if (!result.matches.wiki_entries.length) {
280
+ result.recommended_next_action = "review_source_cards_then_create_wiki_entry";
281
+ }
282
+ else if (all.some((item) => item.grounding_needs_review || item.quality === "scaffold")) {
283
+ result.recommended_next_action = "review_grounding_or_enrich_entry";
284
+ }
285
+ else {
286
+ result.recommended_next_action = "use_matches_for_answer";
287
+ }
288
+ }