@itradingai/aiwiki 0.2.13 → 0.2.15

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/src/app.js CHANGED
@@ -430,18 +430,29 @@ function printAgentPrompt(stream) {
430
430
  }
431
431
  async function printStatusDetails(stream, root, runCount) {
432
432
  const counts = await contentCounts(root);
433
+ const summary = await statusSummary(root);
433
434
  const lintPath = path.join(root, "dashboards", "Lint Report.md");
434
435
  writeLine(stream, "");
435
- writeLine(stream, "内容统计:");
436
- writeLine(stream, `Wiki 条目: ${counts.wikiEntries}`);
437
- writeLine(stream, `资料卡: ${counts.sourceCards}`);
438
- writeLine(stream, `原文: ${counts.rawFiles}`);
439
- writeLine(stream, `选题: ${counts.topics}`);
440
- writeLine(stream, `大纲: ${counts.outlines}`);
441
- writeLine(stream, `最近 lint: ${await exists(lintPath) ? await relativeMtime(root, lintPath) : "无"}`);
436
+ writeLine(stream, "Content stats:");
437
+ writeLine(stream, `Wiki entries: ${counts.wikiEntries}`);
438
+ writeLine(stream, `Source cards: ${counts.sourceCards}`);
439
+ writeLine(stream, `Raw files: ${counts.rawFiles}`);
440
+ writeLine(stream, `Topics: ${counts.topics}`);
441
+ writeLine(stream, `Outlines: ${counts.outlines}`);
442
+ writeLine(stream, `fallback_entries: ${summary.fallbackCount}`);
443
+ writeLine(stream, `grounding_review_entries: ${summary.groundingReviewCount}`);
444
+ writeLine(stream, `recent_lint: ${await exists(lintPath) ? await relativeMtime(root, lintPath) : "none"}`);
445
+ writeLine(stream, `lint_status: ${summary.lintStatus}`);
446
+ if (summary.lastSuccessRunId) {
447
+ writeLine(stream, `last_success: ${summary.lastSuccessRunId}`);
448
+ }
449
+ if (summary.lastFailureRunId) {
450
+ writeLine(stream, `last_failure: ${summary.lastFailureRunId}`);
451
+ }
452
+ writeLine(stream, `system_files: ${summary.systemFiles.map((item) => `${item.path}=${item.status}`).join(", ")}`);
442
453
  writeLine(stream, "");
443
- writeLine(stream, "下一步建议:");
444
- writeLine(stream, runCount === 0 ? "运行 `aiwiki agent install` 接入宿主 Agent,然后发送 `入库 <url>`。" : "运行 `aiwiki query <主题>` 查询知识,或运行 `aiwiki lint` 检查结构。");
454
+ writeLine(stream, "Next action:");
455
+ writeLine(stream, recommendedNextAction(runCount, summary.lintStatus, summary.systemFiles.some((item) => item.status !== "ok")));
445
456
  }
446
457
  async function printNext(stream, root, runCount, checks, targets, report) {
447
458
  const missing = checks.filter((check) => check.status !== "ok");
@@ -452,42 +463,67 @@ async function printNext(stream, root, runCount, checks, targets, report) {
452
463
  }
453
464
  }
454
465
  writeLine(stream, "AIWiki 下一步建议");
455
- writeLine(stream, `知识库路径: ${root}`);
466
+ writeLine(stream, `workspace: ${root}`);
456
467
  if (missing.length) {
457
468
  writeLine(stream, "");
458
- writeLine(stream, "先修复知识库结构:");
469
+ writeLine(stream, "Repair workspace structure first:");
459
470
  writeLine(stream, `- aiwiki setup --path "${root}" --yes`);
471
+ writeLine(stream, "- repair_order: structure");
460
472
  return;
461
473
  }
462
- if (runCount === 0) {
474
+ const actionableIssues = report?.issues.filter((issue) => issue.severity !== "info") ?? [];
475
+ const errorCount = actionableIssues.filter((issue) => issue.severity === "error").length;
476
+ const warningCount = actionableIssues.filter((issue) => issue.severity === "warning").length;
477
+ if (errorCount > 0) {
463
478
  writeLine(stream, "");
464
- writeLine(stream, "还没有入库记录。");
465
- writeLine(stream, "- aiwiki agent install");
466
- writeLine(stream, "- 然后向宿主 Agent 发送 `入库 <url>`");
467
- writeLine(stream, "- CLI 不抓网页;网页正文由宿主 Agent 提供。");
479
+ writeLine(stream, `结构检查发现 ${errorCount} 个 error 问题。`);
480
+ writeLine(stream, "- aiwiki lint");
481
+ writeLine(stream, "- report: dashboards/Lint Report.md");
482
+ writeLine(stream, "- repair_order: lint_errors");
468
483
  return;
469
484
  }
470
- const actionableIssues = report?.issues.filter((issue) => issue.severity !== "info") ?? [];
471
- if (actionableIssues.length) {
485
+ if (warningCount > 0) {
472
486
  writeLine(stream, "");
473
- writeLine(stream, `结构检查发现 ${actionableIssues.length} 个需要处理的问题。`);
487
+ writeLine(stream, `结构检查发现 ${warningCount} 个 warning 问题。`);
474
488
  writeLine(stream, "- aiwiki lint");
475
- writeLine(stream, `- 查看报告: dashboards/Lint Report.md`);
476
- writeLine(stream, "- 先处理 error / warning,再继续扩展查询或入库。");
489
+ writeLine(stream, "- report: dashboards/Lint Report.md");
490
+ writeLine(stream, "- repair_order: lint_warnings");
491
+ return;
492
+ }
493
+ if (runCount === 0) {
494
+ writeLine(stream, "");
495
+ writeLine(stream, "No ingest records yet.");
496
+ writeLine(stream, "- aiwiki agent install");
497
+ writeLine(stream, "- Then ask the host Agent to ingest a URL.");
498
+ writeLine(stream, "- AIWiki CLI does not fetch webpages; the host Agent supplies content.");
499
+ writeLine(stream, "- repair_order: empty_workspace");
477
500
  return;
478
501
  }
479
502
  writeLine(stream, "");
480
- writeLine(stream, "已有入库记录,可以继续:");
481
- writeLine(stream, "- aiwiki query <主题>");
503
+ writeLine(stream, "Workspace is healthy enough for retrieval:");
504
+ writeLine(stream, "- aiwiki query <topic>");
482
505
  writeLine(stream, "- aiwiki lint");
506
+ writeLine(stream, "- repair_order: healthy_query");
483
507
  if (installableMissing.length) {
484
508
  writeLine(stream, "");
485
- writeLine(stream, "可补充宿主 Agent 接入:");
509
+ writeLine(stream, "Optional host Agent setup:");
486
510
  for (const target of installableMissing) {
487
511
  writeLine(stream, `- aiwiki agent install --agent ${target.id} --yes`);
488
512
  }
489
513
  }
490
514
  }
515
+ function recommendedNextAction(runCount, lintStatus, hasMissingSystemFiles) {
516
+ if (hasMissingSystemFiles) {
517
+ return "next_action: aiwiki setup --path <workspace> --yes";
518
+ }
519
+ if (lintStatus === "needs_attention") {
520
+ return "next_action: aiwiki lint";
521
+ }
522
+ if (runCount === 0) {
523
+ return "next_action: aiwiki agent install";
524
+ }
525
+ return "next_action: aiwiki query <topic>";
526
+ }
491
527
  function renderQuery(context) {
492
528
  const lines = [`AIWiki 查询: ${context.query}`, ""];
493
529
  appendQueryGroup(lines, "Wiki 条目", context.matches.wiki_entries);
@@ -1,6 +1,6 @@
1
1
  import { promises as fs } from "node:fs";
2
2
  import path from "node:path";
3
- import { randomBytes } from "node:crypto";
3
+ import { createHash, randomBytes } from "node:crypto";
4
4
  import { normalizePayload } from "./payload.js";
5
5
  import { buildGroundingReport, groundingFrontmatterLines, groundingWarnings } from "./grounding.js";
6
6
  import { appendRunIdBeforeExt, relativePath, safeJoin, slugify } from "./paths.js";
@@ -27,9 +27,11 @@ export async function ingestPayload(rootPath, rawPayload) {
27
27
  }
28
28
  const slug = slugify(payload.source.title ?? payload.source.url);
29
29
  const content = payload.source.content ?? "";
30
+ const contentFingerprint = createContentFingerprint(content);
30
31
  const collisionWarnings = [];
32
+ await detectDuplicateContent(root, payload, contentFingerprint, collisionWarnings);
31
33
  const longTermTargets = await chooseLongTermTargets(root, slug, runId, collisionWarnings);
32
- const links = buildArtifactLinks(root, slug, runDirName, runStartedAt, longTermTargets);
34
+ const links = buildArtifactLinks(root, slug, runDirName, runStartedAt, contentFingerprint, longTermTargets);
33
35
  const grounding = buildGroundingReport(payload);
34
36
  await writeFile(path.join(runDir, "raw.md"), contentFile(payload, content, links), generatedFiles);
35
37
  await writeFile(path.join(runDir, "source-card.md"), sourceCard(payload, runDirName, links, grounding), generatedFiles);
@@ -102,6 +104,42 @@ async function chooseLongTermTarget(root, dir, fileName, runId, warnings) {
102
104
  warnings.push(`collision renamed: ${relativePath(root, target)} -> ${relativePath(root, renamedTarget)}`);
103
105
  return renamedTarget;
104
106
  }
107
+ async function detectDuplicateContent(root, payload, contentFingerprint, warnings) {
108
+ const rawDir = safeJoin(root, "02-raw", "articles");
109
+ let entries;
110
+ try {
111
+ entries = await fs.readdir(rawDir);
112
+ }
113
+ catch (error) {
114
+ if (error.code === "ENOENT") {
115
+ return;
116
+ }
117
+ throw error;
118
+ }
119
+ const sourceUrl = payload.source.url ?? "";
120
+ for (const entry of entries) {
121
+ if (!entry.toLowerCase().endsWith(".md")) {
122
+ continue;
123
+ }
124
+ const existingPath = path.join(rawDir, entry);
125
+ const existing = await fs.readFile(existingPath, "utf8");
126
+ if (!frontmatterValue(existing, "content_fingerprint", contentFingerprint)) {
127
+ continue;
128
+ }
129
+ const sameSource = sourceUrl
130
+ ? frontmatterValue(existing, "source_url", sourceUrl)
131
+ : frontmatterValue(existing, "title", payload.source.title ?? "Untitled");
132
+ if (sameSource) {
133
+ warnings.push(`duplicate content fingerprint: ${contentFingerprint} already exists at ${relativePath(root, existingPath)}; new run kept separate and long-term files will not overwrite existing files.`);
134
+ return;
135
+ }
136
+ }
137
+ }
138
+ function frontmatterValue(markdown, key, expected) {
139
+ const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
140
+ const escapedExpected = expected.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
141
+ return new RegExp(`^${escapedKey}:\\s*"?${escapedExpected}"?\\s*$`, "m").test(markdown);
142
+ }
105
143
  async function writeFile(target, content, generatedFiles) {
106
144
  try {
107
145
  await fs.writeFile(target, content, { encoding: "utf8", flag: "wx" });
@@ -132,6 +170,7 @@ async function writeSummary(root, runDir, payload, generatedFiles, warnings, lin
132
170
  `created_at: "${escapeYaml(createdAt)}"`,
133
171
  `captured_at: "${escapeYaml(payload.source.captured_at)}"`,
134
172
  `run_id: "${escapeYaml(runId)}"`,
173
+ ...(links ? [`content_fingerprint: "${escapeYaml(links.contentFingerprint)}"`] : []),
135
174
  ...(links ? relationshipFrontmatter(links) : []),
136
175
  ...groundingFrontmatterLines(grounding),
137
176
  `tags: ["aiwiki/run"]`,
@@ -176,6 +215,7 @@ function contentFile(payload, content, links) {
176
215
  `created_at: "${escapeYaml(links.createdAt)}"`,
177
216
  `captured_at: "${escapeYaml(payload.source.captured_at)}"`,
178
217
  `run_id: "${escapeYaml(links.runId)}"`,
218
+ `content_fingerprint: "${escapeYaml(links.contentFingerprint)}"`,
179
219
  ...relationshipFrontmatter(links),
180
220
  `tags: ["aiwiki/raw"]`,
181
221
  "---",
@@ -207,6 +247,7 @@ function sourceCard(payload, runId, links, grounding) {
207
247
  `created_at: "${escapeYaml(links.createdAt)}"`,
208
248
  `captured_at: "${escapeYaml(payload.source.captured_at)}"`,
209
249
  `run_id: "${escapeYaml(runId)}"`,
250
+ `content_fingerprint: "${escapeYaml(links.contentFingerprint)}"`,
210
251
  ...relationshipFrontmatter(links),
211
252
  ...groundingFrontmatterLines(grounding),
212
253
  `aliases: ["${escapeYaml(payload.source.title ?? "Untitled")}"]`,
@@ -229,6 +270,13 @@ function sourceCard(payload, runId, links, grounding) {
229
270
  "",
230
271
  trimPreview(payload.source.content ?? payload.source.fetch_notes ?? ""),
231
272
  "",
273
+ "## Problem / Evidence / Reuse",
274
+ "",
275
+ `- problem_solved: ${payload.analysis?.summary ?? "needs host Agent analysis"}`,
276
+ `- evidence_boundary: ${grounding.needs_review ? "review required before treating analysis as fact" : "host supplied evidence available"}`,
277
+ `- reuse_scenarios: ${payload.analysis?.use_cases.length ? payload.analysis.use_cases.join(", ") : "not specified"}`,
278
+ `- content_fingerprint: ${links.contentFingerprint}`,
279
+ "",
232
280
  "## Grounding 状态",
233
281
  "",
234
282
  `- 证据通道:${grounding.evidence_channel}`,
@@ -253,6 +301,7 @@ function claims(payload, links, grounding) {
253
301
  `created_at: "${escapeYaml(links.createdAt)}"`,
254
302
  `captured_at: "${escapeYaml(payload.source.captured_at)}"`,
255
303
  `run_id: "${escapeYaml(links.runId)}"`,
304
+ `content_fingerprint: "${escapeYaml(links.contentFingerprint)}"`,
256
305
  ...relationshipFrontmatter(links),
257
306
  ...groundingFrontmatterLines(grounding),
258
307
  `tags: ["aiwiki/claims"]`,
@@ -274,6 +323,11 @@ function claims(payload, links, grounding) {
274
323
  "## 建议",
275
324
  "",
276
325
  ...claimLines,
326
+ "## Evidence Boundary",
327
+ "",
328
+ "- Claims with a matching source_quote are traceable to the provided source content.",
329
+ "- Claims without a matching source_quote remain suggestions and need human or host-Agent review before reuse.",
330
+ "",
277
331
  ""
278
332
  ].join("\n");
279
333
  }
@@ -290,6 +344,7 @@ function creativeAssets(payload, links) {
290
344
  `created_at: "${escapeYaml(links.createdAt)}"`,
291
345
  `captured_at: "${escapeYaml(payload.source.captured_at)}"`,
292
346
  `run_id: "${escapeYaml(links.runId)}"`,
347
+ `content_fingerprint: "${escapeYaml(links.contentFingerprint)}"`,
293
348
  ...relationshipFrontmatter(links),
294
349
  `tags: ["aiwiki/assets"]`,
295
350
  "---",
@@ -316,6 +371,7 @@ function topics(payload, links) {
316
371
  `created_at: "${escapeYaml(links.createdAt)}"`,
317
372
  `captured_at: "${escapeYaml(payload.source.captured_at)}"`,
318
373
  `run_id: "${escapeYaml(links.runId)}"`,
374
+ `content_fingerprint: "${escapeYaml(links.contentFingerprint)}"`,
319
375
  ...relationshipFrontmatter(links),
320
376
  `tags: ["aiwiki/topics"]`,
321
377
  "---",
@@ -342,6 +398,7 @@ function outline(payload, links) {
342
398
  `created_at: "${escapeYaml(links.createdAt)}"`,
343
399
  `captured_at: "${escapeYaml(payload.source.captured_at)}"`,
344
400
  `run_id: "${escapeYaml(links.runId)}"`,
401
+ `content_fingerprint: "${escapeYaml(links.contentFingerprint)}"`,
345
402
  ...relationshipFrontmatter(links),
346
403
  `tags: ["aiwiki/outline"]`,
347
404
  "---",
@@ -354,11 +411,28 @@ function outline(payload, links) {
354
411
  "",
355
412
  "1. 背景",
356
413
  "2. 关键观点",
357
- "3. 可复用方法",
358
- `4. 来源:${payload.source.title ?? "Untitled"}`,
414
+ "3. 证据与推断边界",
415
+ "4. 可复用判断与方法",
416
+ "5. 适用场景",
417
+ "6. 可继续链接的条目",
418
+ `7. 来源:${payload.source.title ?? "Untitled"}`,
419
+ "",
420
+ "## Host Agent Outline Hints",
421
+ "",
422
+ ...outlineHintLines(payload),
359
423
  ""
360
424
  ].join("\n");
361
425
  }
426
+ function outlineHintLines(payload) {
427
+ const outline = payload.analysis?.outline?.sections ?? [];
428
+ const links = payload.analysis?.suggested_links ?? [];
429
+ const lines = [
430
+ ...(outline.length ? outline.map((item) => `- outline_section: ${item}`) : []),
431
+ ...(payload.analysis?.reusable_judgments.length ? payload.analysis.reusable_judgments.map((item) => `- reusable_judgment: ${item.judgment}`) : []),
432
+ ...(links.length ? links.map((item) => `- suggested_link: ${item.title}${item.target ? ` -> ${item.target}` : ""}`) : [])
433
+ ];
434
+ return lines.length ? lines : ["- No enriched outline hints supplied by the host Agent."];
435
+ }
362
436
  function claimSuggestionLines(index, claim, confidence, sourceQuote, content) {
363
437
  const quote = sourceQuote?.trim();
364
438
  const supported = Boolean(quote && content.includes(quote));
@@ -447,11 +521,12 @@ function findGeneratedFileInDir(root, files, dir) {
447
521
  const match = files.find((file) => relativePath(root, file).startsWith(`${dir}/`));
448
522
  return match ? relativePath(root, match) : undefined;
449
523
  }
450
- function buildArtifactLinks(root, slug, runDirName, createdAt, longTermTargets) {
524
+ function buildArtifactLinks(root, slug, runDirName, createdAt, contentFingerprint, longTermTargets) {
451
525
  return {
452
526
  slug,
453
527
  runId: runDirName,
454
528
  createdAt,
529
+ contentFingerprint,
455
530
  raw: relativePath(root, longTermTargets.raw),
456
531
  sourceCard: relativePath(root, longTermTargets.sourceCard),
457
532
  wikiEntry: relativePath(root, longTermTargets.wikiEntry),
@@ -462,6 +537,9 @@ function buildArtifactLinks(root, slug, runDirName, createdAt, longTermTargets)
462
537
  runSummary: `09-runs/${runDirName}/processing-summary.md`
463
538
  };
464
539
  }
540
+ function createContentFingerprint(content) {
541
+ return `sha256:${createHash("sha256").update(content.replace(/\r\n/g, "\n"), "utf8").digest("hex")}`;
542
+ }
465
543
  function relationshipFrontmatter(links) {
466
544
  return [
467
545
  `wiki_entry: "${escapeYaml(obsidianLink(links.wikiEntry, "Wiki 条目"))}"`,
@@ -142,10 +142,15 @@ function normalizeAnalysis(value, warnings) {
142
142
  summary: stringValue(value.summary),
143
143
  key_points: stringArray(value.key_points, "analysis.key_points", warnings),
144
144
  reusable_knowledge: reusableKnowledgeArray(value.reusable_knowledge, warnings),
145
+ entities: stringArray(value.entities, "analysis.entities", warnings),
146
+ concepts: stringArray(value.concepts, "analysis.concepts", warnings),
147
+ tensions: stringArray(value.tensions, "analysis.tensions", warnings),
148
+ reusable_judgments: reusableJudgmentsArray(value.reusable_judgments, warnings),
145
149
  related_concepts: stringArray(value.related_concepts, "analysis.related_concepts", warnings),
146
150
  use_cases: stringArray(value.use_cases, "analysis.use_cases", warnings),
147
151
  topic_candidates: stringArray(value.topic_candidates, "analysis.topic_candidates", warnings),
148
152
  claims: claimsArray(value.claims, warnings),
153
+ suggested_links: suggestedLinksArray(value.suggested_links, warnings),
149
154
  outline: outlineValue(value.outline, warnings)
150
155
  };
151
156
  return hasAnalysisContent(analysis) ? analysis : undefined;
@@ -194,6 +199,51 @@ function reusableKnowledgeArray(value, warnings) {
194
199
  return [];
195
200
  });
196
201
  }
202
+ function reusableJudgmentsArray(value, warnings) {
203
+ if (value === undefined) {
204
+ return [];
205
+ }
206
+ if (!Array.isArray(value)) {
207
+ warnings.push("analysis.reusable_judgments ignored: expected an array.");
208
+ return [];
209
+ }
210
+ return value.flatMap((item) => {
211
+ if (typeof item === "string" && item.trim()) {
212
+ return [{ judgment: item.trim() }];
213
+ }
214
+ if (isRecord(item) && typeof item.judgment === "string" && item.judgment.trim()) {
215
+ return [{
216
+ title: stringValue(item.title),
217
+ judgment: item.judgment.trim(),
218
+ rationale: stringValue(item.rationale),
219
+ source_quote: stringValue(item.source_quote)
220
+ }];
221
+ }
222
+ return [];
223
+ });
224
+ }
225
+ function suggestedLinksArray(value, warnings) {
226
+ if (value === undefined) {
227
+ return [];
228
+ }
229
+ if (!Array.isArray(value)) {
230
+ warnings.push("analysis.suggested_links ignored: expected an array.");
231
+ return [];
232
+ }
233
+ return value.flatMap((item) => {
234
+ if (typeof item === "string" && item.trim()) {
235
+ return [{ title: item.trim() }];
236
+ }
237
+ if (isRecord(item) && typeof item.title === "string" && item.title.trim()) {
238
+ return [{
239
+ title: item.title.trim(),
240
+ target: stringValue(item.target),
241
+ reason: stringValue(item.reason)
242
+ }];
243
+ }
244
+ return [];
245
+ });
246
+ }
197
247
  function claimsArray(value, warnings) {
198
248
  if (value === undefined) {
199
249
  return [];
@@ -252,10 +302,15 @@ function hasAnalysisContent(analysis) {
252
302
  return Boolean(analysis.summary ||
253
303
  analysis.key_points.length ||
254
304
  analysis.reusable_knowledge.length ||
305
+ analysis.entities.length ||
306
+ analysis.concepts.length ||
307
+ analysis.tensions.length ||
308
+ analysis.reusable_judgments.length ||
255
309
  analysis.related_concepts.length ||
256
310
  analysis.use_cases.length ||
257
311
  analysis.topic_candidates.length ||
258
312
  analysis.claims.length ||
313
+ analysis.suggested_links.length ||
259
314
  analysis.outline);
260
315
  }
261
316
  function hasCustomOutputRequest(outputs) {
@@ -37,10 +37,11 @@ function wikiFrontmatter(payload, links, title, mode, quality, grounding) {
37
37
  `outline_file: "${escapeYaml(links.outline)}"`,
38
38
  `run_summary: "${escapeYaml(links.runSummary)}"`,
39
39
  `run_id: "${escapeYaml(links.runId)}"`,
40
+ `content_fingerprint: "${escapeYaml(links.contentFingerprint)}"`,
40
41
  `created_at: "${escapeYaml(links.createdAt)}"`,
41
42
  `updated_at: "${escapeYaml(links.createdAt)}"`,
42
43
  ...(mode === "agent_enriched" ? [`summary: "${escapeYaml(payload.wiki_entry?.summary ?? payload.analysis?.summary ?? "")}"`] : []),
43
- `topics: ${yamlStringArray(payload.analysis?.related_concepts ?? [])}`,
44
+ `topics: ${yamlStringArray([...(payload.analysis?.related_concepts ?? []), ...(payload.analysis?.concepts ?? [])])}`,
44
45
  `claims: ${yamlStringArray(payload.analysis?.claims.map((claim) => claim.claim) ?? [])}`,
45
46
  ...groundingFrontmatterLines(grounding),
46
47
  `tags: ["aiwiki/wiki-entry"]`,
@@ -83,9 +84,13 @@ function enrichedBody(payload, links, title, grounding) {
83
84
  else {
84
85
  sections.push("## 核心观点", "", ...listOrFallback(payload.analysis?.key_points ?? [], "待宿主 Agent 补充。"), "");
85
86
  sections.push("## 可复用知识点", "", ...knowledgeList(payload), "");
87
+ sections.push("## Reusable Judgments", "", ...judgmentList(payload), "");
88
+ sections.push("## Entities and Concepts", "", ...entityConceptList(payload), "");
89
+ sections.push("## Tensions", "", ...listOrFallback(payload.analysis?.tensions ?? [], "No explicit tension supplied by the host Agent."), "");
86
90
  sections.push("## 相关概念", "", ...listOrFallback(payload.analysis?.related_concepts ?? [], "待宿主 Agent 补充。"), "");
87
91
  sections.push("## 适合用于什么场景", "", ...listOrFallback(payload.analysis?.use_cases ?? [], "待宿主 Agent 补充。"), "");
88
92
  sections.push("## 可转化的选题", "", ...listOrFallback(payload.analysis?.topic_candidates ?? [], "待宿主 Agent 补充。"), "");
93
+ sections.push("## Suggested Links", "", ...suggestedLinkList(payload), "");
89
94
  }
90
95
  sections.push(sourceSection(links));
91
96
  return sections.join("\n");
@@ -137,6 +142,42 @@ function knowledgeList(payload) {
137
142
  }
138
143
  return items.flatMap((item) => item.title ? [`### ${item.title}`, "", item.content] : [`- ${item.content}`]);
139
144
  }
145
+ function judgmentList(payload) {
146
+ const items = payload.analysis?.reusable_judgments ?? [];
147
+ if (!items.length) {
148
+ return ["No reusable judgment supplied by the host Agent."];
149
+ }
150
+ return items.flatMap((item) => [
151
+ item.title ? `### ${item.title}` : "### Judgment",
152
+ "",
153
+ `- judgment: ${item.judgment}`,
154
+ ...(item.rationale ? [`- rationale: ${item.rationale}`] : []),
155
+ ...(item.source_quote ? ["- evidence boundary: host supplied quote"] : ["- evidence boundary: needs review if reused as a factual claim"]),
156
+ ""
157
+ ]);
158
+ }
159
+ function entityConceptList(payload) {
160
+ const entities = payload.analysis?.entities ?? [];
161
+ const concepts = payload.analysis?.concepts ?? [];
162
+ if (!entities.length && !concepts.length) {
163
+ return ["No explicit entities or concepts supplied by the host Agent."];
164
+ }
165
+ return [
166
+ ...(entities.length ? [`- entities: ${entities.join(", ")}`] : []),
167
+ ...(concepts.length ? [`- concepts: ${concepts.join(", ")}`] : [])
168
+ ];
169
+ }
170
+ function suggestedLinkList(payload) {
171
+ const links = payload.analysis?.suggested_links ?? [];
172
+ if (!links.length) {
173
+ return ["No suggested links supplied by the host Agent."];
174
+ }
175
+ return links.map((link) => {
176
+ const target = link.target ? ` -> ${link.target}` : "";
177
+ const reason = link.reason ? ` (${link.reason})` : "";
178
+ return `- ${link.title}${target}${reason}`;
179
+ });
180
+ }
140
181
  function listOrFallback(values, fallback) {
141
182
  return values.length ? values.map((value) => `- ${value}`) : [fallback];
142
183
  }
@@ -3,7 +3,9 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { createInterface } from "node:readline/promises";
5
5
  import { stdin as input, stdout as output } from "node:process";
6
+ import { frontmatterBoolean, frontmatterString, parseMarkdown } from "./frontmatter.js";
6
7
  import { CliError } from "./output.js";
8
+ import { relativePath } from "./paths.js";
7
9
  export const CONFIG_FILE = "aiwiki.yaml";
8
10
  export const REQUIRED_DIRS = [
9
11
  "02-raw/articles",
@@ -551,6 +553,14 @@ export async function doctor(rootPath) {
551
553
  detail: absolute
552
554
  });
553
555
  }
556
+ for (const file of REQUIRED_FILES) {
557
+ const absolute = path.join(root, file);
558
+ checks.push({
559
+ name: file,
560
+ status: (await exists(absolute)) ? "ok" : "missing",
561
+ detail: absolute
562
+ });
563
+ }
554
564
  const writeTarget = path.join(root, "_system", "logs", ".doctor-write-test");
555
565
  try {
556
566
  await fs.writeFile(writeTarget, "ok", "utf8");
@@ -566,7 +576,16 @@ export async function statusSummary(rootPath) {
566
576
  const root = resolveRoot(rootPath);
567
577
  const runsRoot = path.join(root, "09-runs");
568
578
  if (!(await exists(runsRoot))) {
569
- return { root, runCount: 0, failedCount: 0 };
579
+ return {
580
+ root,
581
+ runCount: 0,
582
+ failedCount: 0,
583
+ fallbackCount: await countWikiEntries(root, "deterministic_fallback"),
584
+ groundingReviewCount: await countGroundingReviewEntries(root),
585
+ lintStatus: await readLintStatus(root),
586
+ lintReportPath: await lintReportPath(root),
587
+ systemFiles: await systemFileSummary(root)
588
+ };
570
589
  }
571
590
  const entries = await fs.readdir(runsRoot, { withFileTypes: true });
572
591
  const dirs = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
@@ -590,11 +609,20 @@ export async function statusSummary(rootPath) {
590
609
  stats.push({ dir, mtimeMs: (await fs.stat(path.join(runsRoot, dir))).mtimeMs });
591
610
  }
592
611
  stats.sort((a, b) => b.mtimeMs - a.mtimeMs);
612
+ const successDirs = dirs.filter((dir) => !dir.endsWith("-fetch-failed"));
613
+ const failureDirs = dirs.filter((dir) => dir.endsWith("-fetch-failed"));
593
614
  return {
594
615
  root,
595
616
  runCount: dirs.length,
596
617
  failedCount,
597
- lastRunId: stats[0]?.dir
618
+ lastRunId: stats[0]?.dir,
619
+ lastSuccessRunId: await newestDir(root, successDirs),
620
+ lastFailureRunId: await newestDir(root, failureDirs),
621
+ fallbackCount: await countWikiEntries(root, "deterministic_fallback"),
622
+ groundingReviewCount: await countGroundingReviewEntries(root),
623
+ lintStatus: await readLintStatus(root),
624
+ lintReportPath: await lintReportPath(root),
625
+ systemFiles: await systemFileSummary(root)
598
626
  };
599
627
  }
600
628
  export async function exists(target) {
@@ -613,3 +641,73 @@ function readScalar(text, key) {
613
641
  function unquote(value) {
614
642
  return value.replace(/^["']|["']$/g, "");
615
643
  }
644
+ const REQUIRED_FILES = ["_system/purpose.md", "_system/index.md", "_system/log.md"];
645
+ async function systemFileSummary(root) {
646
+ const files = [];
647
+ for (const file of REQUIRED_FILES) {
648
+ files.push({ path: file, status: await exists(path.join(root, file)) ? "ok" : "missing" });
649
+ }
650
+ return files;
651
+ }
652
+ async function newestDir(root, dirs) {
653
+ const stats = [];
654
+ for (const dir of dirs) {
655
+ stats.push({ dir, mtimeMs: (await fs.stat(path.join(root, "09-runs", dir))).mtimeMs });
656
+ }
657
+ stats.sort((a, b) => b.mtimeMs - a.mtimeMs);
658
+ return stats[0]?.dir;
659
+ }
660
+ async function countWikiEntries(root, generationMode) {
661
+ const files = await markdownFiles(path.join(root, "05-wiki", "source-knowledge"));
662
+ let count = 0;
663
+ for (const file of files) {
664
+ const parsed = parseMarkdown(await fs.readFile(file, "utf8"));
665
+ if (frontmatterString(parsed.frontmatter, "generation_mode") === generationMode) {
666
+ count += 1;
667
+ }
668
+ }
669
+ return count;
670
+ }
671
+ async function countGroundingReviewEntries(root) {
672
+ const files = await markdownFiles(path.join(root, "05-wiki", "source-knowledge"));
673
+ let count = 0;
674
+ for (const file of files) {
675
+ const parsed = parseMarkdown(await fs.readFile(file, "utf8"));
676
+ if (frontmatterBoolean(parsed.frontmatter, "grounding_needs_review") === true) {
677
+ count += 1;
678
+ }
679
+ }
680
+ return count;
681
+ }
682
+ async function readLintStatus(root) {
683
+ const reportPath = await lintReportPath(root);
684
+ if (!reportPath) {
685
+ return "missing";
686
+ }
687
+ const text = await fs.readFile(path.join(root, reportPath), "utf8");
688
+ if (/\[(error|warning)\]/.test(text)) {
689
+ return "needs_attention";
690
+ }
691
+ return "ok";
692
+ }
693
+ async function lintReportPath(root) {
694
+ const absolute = path.join(root, "dashboards", "Lint Report.md");
695
+ return await exists(absolute) ? relativePath(root, absolute) : undefined;
696
+ }
697
+ async function markdownFiles(dir) {
698
+ if (!(await exists(dir))) {
699
+ return [];
700
+ }
701
+ const entries = await fs.readdir(dir, { withFileTypes: true });
702
+ const files = [];
703
+ for (const entry of entries) {
704
+ const target = path.join(dir, entry.name);
705
+ if (entry.isDirectory()) {
706
+ files.push(...await markdownFiles(target));
707
+ }
708
+ else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
709
+ files.push(target);
710
+ }
711
+ }
712
+ return files;
713
+ }
@@ -101,6 +101,18 @@ AIWiki 会修复常见 UTF-8 mojibake,但这只是兜底;宿主 Agent 仍应
101
101
  }
102
102
  ```
103
103
 
104
+ ## Analysis 增强字段
105
+
106
+ `analysis` 仍然向后兼容;旧 payload 不需要修改。宿主 Agent 能提供时,可以额外写入这些可选字段:
107
+
108
+ - `entities`:文章中可复查的人、产品、组织、地点等实体。
109
+ - `concepts`:可沉淀为知识条目的概念、方法或框架。
110
+ - `tensions`:原文中的冲突、取舍、不确定性或反常识点。
111
+ - `reusable_judgments`:可复用判断,建议同时提供 `rationale` 和 `source_quote`。
112
+ - `suggested_links`:建议关联到已有 Wiki 条目的线索和原因。
113
+
114
+ 不能确认的内容不要编造。AIWiki 会为成功入库的正文写入 `content_fingerprint`;重复入库同一来源同一正文时会保留新 run、输出 warning,并避免覆盖已有长期文件。
115
+
104
116
  ## 失败 payload
105
117
 
106
118
  ```json
package/docs/USAGE.md CHANGED
@@ -12,6 +12,8 @@ AIWiki CLI 也不调用 LLM。高质量 Wiki Entry 来自宿主 Agent 提供的
12
12
 
13
13
  AIWiki 会把证据通道和疑似风险分开记录。`source_quote` 等宿主 Agent 提供的原文引用属于证据通道;`coverage_suspected_incomplete`、`unsupported_claims`、`needs_review` 等属于 AIWiki 生成的启发式复核信号,不等于已经证明内容遗漏。
14
14
 
15
+ 成功入库的正文会写入稳定的 `content_fingerprint`。如果同一来源同一正文重复入库,AIWiki 会保留新的 run 记录、给出重复 fingerprint warning,并把长期文件改名保存,避免静默覆盖已有知识资产。
16
+
15
17
  ## 1. 一次性设置
16
18
 
17
19
  发布后直接运行交互式 setup:
@@ -215,6 +217,8 @@ Wiki Entry 有两种质量模式:
215
217
  - `agent_enriched` / `enriched`:宿主 Agent 提供了 `analysis` 或 `wiki_entry`。
216
218
  - `deterministic_fallback` / `scaffold`:AIWiki 只生成来源、反链、正文预览和待补全区。
217
219
 
220
+ `analysis` 可以继续只传旧字段,也可以补充 `entities`、`concepts`、`tensions`、`reusable_judgments`、`suggested_links`。这些字段会进入 Wiki Entry,帮助用户区分“实体/概念”“可复用判断”“证据边界”和“后续可链接条目”,但不会被 AIWiki 当作已经证实的事实。
221
+
218
222
  Artifact 角色保持固定:
219
223
 
220
224
  - `03-sources/article-cards` 是 trace-first 的资料卡:保留来源、反链、原文预览和 grounding 状态,不承担完整知识正文。
@@ -251,7 +255,7 @@ AIWiki 生成的 Markdown 按 Obsidian vault 内路径组织,文件正文会
251
255
  - `05-wiki/source-knowledge` 是默认知识入口;`03-sources/article-cards` 会链接到 Wiki 条目、原文、Claim 建议、素材建议、选题、大纲和本次处理记录。
252
256
  - `02-raw/articles`、`04-claims/_suggestions`、`06-assets/_suggestions`、`07-topics/ready`、`08-outputs/outlines` 会回链到资料卡,Obsidian 的 Backlinks/Graph View 可以串起同一篇资料。
253
257
  - `09-runs/<run-id>/processing-summary.md` 会把本次生成的 Markdown 文件列成可点击 wikilink;`payload.json` 不是 Markdown,保留普通路径。
254
- - frontmatter 会写入 `aiwiki_id`、`type`、`status`、`slug`、`source_url`、`created_at`、`captured_at`、`run_id`、`source_card`、`raw_note`、`claims_note`、`assets_note`、`topics_note`、`outline_note`、`run_summary`、`tags` 等字段,便于后续用 Obsidian Search / Properties / Dataview 做筛选。
258
+ - frontmatter 会写入 `aiwiki_id`、`type`、`status`、`slug`、`source_url`、`content_fingerprint`、`created_at`、`captured_at`、`run_id`、`source_card`、`raw_note`、`claims_note`、`assets_note`、`topics_note`、`outline_note`、`run_summary`、`tags` 等字段,便于后续用 Obsidian Search / Properties / Dataview 做筛选。
255
259
 
256
260
  ### Obsidian 数据库入口
257
261
 
@@ -406,3 +410,17 @@ aiwiki status
406
410
  # System Purpose Files
407
411
 
408
412
  `aiwiki setup` now also seeds `_system/purpose.md`, `_system/index.md`, and `_system/log.md` when they are missing. These files give humans and host Agents a stable entry point for the knowledge-base goal, scope, common folders, common commands, and lightweight event notes. Re-running setup preserves user edits.
413
+
414
+ ## Diagnostic Commands
415
+
416
+ `aiwiki doctor` checks the workspace directories, write permission, and required system files: `_system/purpose.md`, `_system/index.md`, and `_system/log.md`.
417
+
418
+ `aiwiki status` keeps the existing run-count summary and also reports:
419
+
420
+ - `fallback_entries`: Wiki entries generated as deterministic fallback/scaffold.
421
+ - `grounding_review_entries`: Wiki entries marked for grounding review.
422
+ - `lint_status`: whether a lint report is missing, clean, or needs attention.
423
+ - `system_files`: readiness of purpose, index, and log files.
424
+ - `next_action`: the recommended next command.
425
+
426
+ `aiwiki next` uses the same repair order: fix workspace structure first, then lint errors, lint warnings, empty-workspace onboarding, and finally healthy-state query guidance.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itradingai/aiwiki",
3
- "version": "0.2.13",
3
+ "version": "0.2.15",
4
4
  "type": "module",
5
5
  "description": "Agent-first AI knowledge base CLI for turning articles, links and notes into Obsidian-ready source cards, topics, outlines and reusable knowledge assets.",
6
6
  "license": "MIT",
package/skill/SKILL.md CHANGED
@@ -9,14 +9,27 @@ Use this skill when the user asks an Agent to process one URL, article body, or
9
9
 
10
10
  AIWiki CLI does not fetch webpages and does not call an LLM. The host Agent reads and understands the source; AIWiki validates, writes, links, tracks, queries, and lints local Markdown knowledge files.
11
11
 
12
+ ## Knowledge Base Purpose
13
+
14
+ Before ingesting, querying, linting, or reorganizing material, read `_system/purpose.md` in the target AIWiki workspace when it exists. Treat it as the local contract for:
15
+
16
+ - what this knowledge base is trying to solve
17
+ - what material belongs here
18
+ - what material should stay out
19
+ - how uncertain or off-scope material should be handled
20
+ - how this knowledge base should remain separable from future knowledge bases
21
+
22
+ If the material does not fit the purpose file, do not force it into the knowledge base as confirmed knowledge. Record the mismatch, ask for review when needed, or keep it as a traceable source rather than a claim.
23
+
12
24
  ## Ingest Flow
13
25
 
14
26
  1. Read the URL, message, attachment, or user-provided body.
15
- 2. Build an `aiwiki.agent_payload.v1` payload with `source` and `request`.
16
- 3. If you understand the source, also provide `analysis` and/or `wiki_entry`.
17
- 4. Do not include output paths in the payload. The CLI decides where files are written.
18
- 5. If webpage reading fails, still build a payload with `source.fetch_status` set to `failed` and include `source.fetch_notes`.
19
- 6. Prefer stdin so the user does not need to save a payload file:
27
+ 2. Read `_system/purpose.md` and decide whether the material fits this knowledge base.
28
+ 3. Build an `aiwiki.agent_payload.v1` payload with `source` and `request`.
29
+ 4. If you understand the source, also provide `analysis` and/or `wiki_entry`.
30
+ 5. Do not include output paths in the payload. The CLI decides where files are written.
31
+ 6. If webpage reading fails, still build a payload with `source.fetch_status` set to `failed` and include `source.fetch_notes`.
32
+ 7. Prefer stdin so the user does not need to save a payload file:
20
33
 
21
34
  ```bash
22
35
  aiwiki ingest-agent --stdin