@itradingai/aiwiki 0.2.12 → 0.2.14

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
@@ -416,6 +416,7 @@ function printAgentPrompt(stream) {
416
416
  writeLine(stream, "- 收录 <url>");
417
417
  writeLine(stream, "- 存一下 <url>");
418
418
  writeLine(stream, "- aiwiki <url>");
419
+ writeLine(stream, "Before ingesting, querying, or reorganizing, read `_system/purpose.md` and keep material aligned with the knowledge-base goal, scope, and unsuitable-content rules.");
419
420
  writeLine(stream, "");
420
421
  writeLine(stream, "如果当前会话被用户明确设定为 AIWiki 入库助手,则用户只发送 URL 也默认触发入库。普通会话中不要把所有 URL 都自动入库。");
421
422
  writeLine(stream, "");
@@ -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
  }
@@ -21,6 +21,94 @@ export const REQUIRED_DIRS = [
21
21
  "_system/logs"
22
22
  ];
23
23
  const WORKSPACE_SEEDS = [
24
+ {
25
+ path: "_system/purpose.md",
26
+ content: `# AIWiki Knowledge Base Purpose
27
+
28
+ This file defines what this knowledge base is for. Host Agents should read it before ingesting, querying, or reorganizing content.
29
+
30
+ ## Goal
31
+
32
+ Build a local, traceable AI knowledge base that turns useful articles, notes, and source material into Obsidian-ready Markdown.
33
+
34
+ ## Suitable Materials
35
+
36
+ - Articles, notes, transcripts, and references that can become source cards, wiki entries, claims, topics, outlines, or reusable assets.
37
+ - External materials with clear source information.
38
+ - User-owned drafts or published work when the user explicitly says the material represents their own output.
39
+
40
+ ## Unsuitable Materials
41
+
42
+ - Content without a usable source or context.
43
+ - Purely private, sensitive, illegal, or unsafe material.
44
+ - Generic web noise that cannot become reusable knowledge.
45
+ - Claims that cannot be tied back to evidence.
46
+
47
+ ## Multi-Knowledge-Base Boundary
48
+
49
+ This base AIWiki workspace is a single knowledge base. If the user later creates multiple knowledge bases, each one should have its own purpose file and Agents should route material according to that local purpose.
50
+
51
+ ## Agent Rules
52
+
53
+ - Respect this purpose before ingesting material.
54
+ - Keep evidence and inference separate.
55
+ - Do not treat external input as the user's own view unless the user says so.
56
+ - Prefer traceable source cards and wiki entries over unsupported summaries.
57
+ `
58
+ },
59
+ {
60
+ path: "_system/index.md",
61
+ content: `# AIWiki System Index
62
+
63
+ Use this file as the human and Agent entry point for the knowledge base.
64
+
65
+ ## Core Areas
66
+
67
+ - [[02-raw/articles|Raw Articles]]
68
+ - [[03-sources/article-cards|Source Cards]]
69
+ - [[04-claims/_suggestions|Claim Suggestions]]
70
+ - [[05-wiki|Wiki Entries]]
71
+ - [[06-assets/_suggestions|Asset Suggestions]]
72
+ - [[07-topics/ready|Topic Pipeline]]
73
+ - [[08-outputs/outlines|Draft Outlines]]
74
+ - [[09-runs|Processing Runs]]
75
+
76
+ ## Dashboards
77
+
78
+ - [[dashboards/AIWiki Home|AIWiki Home]]
79
+ - [[dashboards/Review Queue|Review Queue]]
80
+ - [[dashboards/Recent Runs|Recent Runs]]
81
+ - [[dashboards/Lint Report|Lint Report]]
82
+
83
+ ## System Files
84
+
85
+ - [[_system/purpose|Purpose]]
86
+ - [[_system/log|Log]]
87
+ - [[_system/schemas/aiwiki-frontmatter|Frontmatter Schema]]
88
+
89
+ ## Common Commands
90
+
91
+ \`\`\`bash
92
+ aiwiki status
93
+ aiwiki next
94
+ aiwiki query "<topic>"
95
+ aiwiki context "<topic>"
96
+ aiwiki lint
97
+ \`\`\`
98
+ `
99
+ },
100
+ {
101
+ path: "_system/log.md",
102
+ content: `# AIWiki System Log
103
+
104
+ This lightweight log is reserved for important workspace events. It keeps the base edition file-first and does not require a database.
105
+
106
+ ## Entries
107
+
108
+ <!-- Add manual or future automated events below. -->
109
+
110
+ `
111
+ },
24
112
  {
25
113
  path: "dashboards/AIWiki Home.md",
26
114
  content: `# AIWiki 首页
@@ -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
@@ -208,3 +220,8 @@ aiwiki lint
208
220
  ```
209
221
 
210
222
  不要把外部资料标成代表用户观点。
223
+ # Knowledge Base Purpose
224
+
225
+ Before ingesting, querying, or reorganizing material, read `_system/purpose.md` in the target AIWiki workspace. Treat it as the local contract for what belongs in this knowledge base, what should stay out, and how future multi-knowledge-base routing should be handled.
226
+
227
+ 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.
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
 
@@ -403,3 +407,6 @@ aiwiki status
403
407
  - 成功读取时,`03-sources/article-cards` 下出现资料卡。
404
408
  - 成功读取时,`05-wiki/source-knowledge` 下出现 Wiki Entry。
405
409
  - 抓取失败时,`09-runs/<run-id>-fetch-failed` 下出现失败记录。
410
+ # System Purpose Files
411
+
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itradingai/aiwiki",
3
- "version": "0.2.12",
3
+ "version": "0.2.14",
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