@itradingai/aiwiki 0.2.14 → 0.2.16

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
@@ -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 }) {
@@ -159,10 +159,16 @@ export async function runCli(argv, streams = { stdout: process.stdout, stderr: p
159
159
  }
160
160
  if (command === "lint") {
161
161
  const root = await resolveWorkspace(flagString(args, "path"));
162
- const report = await lintWorkspace(root);
163
- const reportPath = await writeLintReport(root, report);
162
+ const severity = parseLintSeverity(flagString(args, "severity"));
163
+ const report = filterLintReport(await lintWorkspace(root), severity);
164
+ if (flagBool(args, "json")) {
165
+ writeLine(streams.stdout, JSON.stringify(report, null, 2));
166
+ return 0;
167
+ }
168
+ const reportPath = flagBool(args, "no-write") ? undefined : await writeLintReport(root, report);
169
+ writeLine(streams.stdout, renderLintSummary(report, reportPath));
170
+ writeLine(streams.stdout, "");
164
171
  writeLine(streams.stdout, renderLintReport(report));
165
- writeLine(streams.stdout, `report: ${reportPath}`);
166
172
  return 0;
167
173
  }
168
174
  if (command === "ingest-agent") {
@@ -256,6 +262,15 @@ function printHelp(stream) {
256
262
  writeLine(stream, " aiwiki ingest-url <url> --content-file <file>");
257
263
  writeLine(stream, " aiwiki agent check");
258
264
  }
265
+ function parseLintSeverity(value) {
266
+ if (value === undefined) {
267
+ return undefined;
268
+ }
269
+ if (value === "error" || value === "warning" || value === "info") {
270
+ return value;
271
+ }
272
+ throw new CliError("lint --severity must be error, warning, or info");
273
+ }
259
274
  async function discoverAgentTargets() {
260
275
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
261
276
  const skillSource = path.join(packageRoot, "skill", "SKILL.md");
@@ -430,18 +445,29 @@ function printAgentPrompt(stream) {
430
445
  }
431
446
  async function printStatusDetails(stream, root, runCount) {
432
447
  const counts = await contentCounts(root);
448
+ const summary = await statusSummary(root);
433
449
  const lintPath = path.join(root, "dashboards", "Lint Report.md");
434
450
  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) : "无"}`);
451
+ writeLine(stream, "Content stats:");
452
+ writeLine(stream, `Wiki entries: ${counts.wikiEntries}`);
453
+ writeLine(stream, `Source cards: ${counts.sourceCards}`);
454
+ writeLine(stream, `Raw files: ${counts.rawFiles}`);
455
+ writeLine(stream, `Topics: ${counts.topics}`);
456
+ writeLine(stream, `Outlines: ${counts.outlines}`);
457
+ writeLine(stream, `fallback_entries: ${summary.fallbackCount}`);
458
+ writeLine(stream, `grounding_review_entries: ${summary.groundingReviewCount}`);
459
+ writeLine(stream, `recent_lint: ${await exists(lintPath) ? await relativeMtime(root, lintPath) : "none"}`);
460
+ writeLine(stream, `lint_status: ${summary.lintStatus}`);
461
+ if (summary.lastSuccessRunId) {
462
+ writeLine(stream, `last_success: ${summary.lastSuccessRunId}`);
463
+ }
464
+ if (summary.lastFailureRunId) {
465
+ writeLine(stream, `last_failure: ${summary.lastFailureRunId}`);
466
+ }
467
+ writeLine(stream, `system_files: ${summary.systemFiles.map((item) => `${item.path}=${item.status}`).join(", ")}`);
442
468
  writeLine(stream, "");
443
- writeLine(stream, "下一步建议:");
444
- writeLine(stream, runCount === 0 ? "运行 `aiwiki agent install` 接入宿主 Agent,然后发送 `入库 <url>`。" : "运行 `aiwiki query <主题>` 查询知识,或运行 `aiwiki lint` 检查结构。");
469
+ writeLine(stream, "Next action:");
470
+ writeLine(stream, recommendedNextAction(runCount, summary.lintStatus, summary.systemFiles.some((item) => item.status !== "ok")));
445
471
  }
446
472
  async function printNext(stream, root, runCount, checks, targets, report) {
447
473
  const missing = checks.filter((check) => check.status !== "ok");
@@ -452,42 +478,67 @@ async function printNext(stream, root, runCount, checks, targets, report) {
452
478
  }
453
479
  }
454
480
  writeLine(stream, "AIWiki 下一步建议");
455
- writeLine(stream, `知识库路径: ${root}`);
481
+ writeLine(stream, `workspace: ${root}`);
456
482
  if (missing.length) {
457
483
  writeLine(stream, "");
458
- writeLine(stream, "先修复知识库结构:");
484
+ writeLine(stream, "Repair workspace structure first:");
459
485
  writeLine(stream, `- aiwiki setup --path "${root}" --yes`);
486
+ writeLine(stream, "- repair_order: structure");
460
487
  return;
461
488
  }
462
- if (runCount === 0) {
489
+ const actionableIssues = report?.issues.filter((issue) => issue.severity !== "info") ?? [];
490
+ const errorCount = actionableIssues.filter((issue) => issue.severity === "error").length;
491
+ const warningCount = actionableIssues.filter((issue) => issue.severity === "warning").length;
492
+ if (errorCount > 0) {
463
493
  writeLine(stream, "");
464
- writeLine(stream, "还没有入库记录。");
465
- writeLine(stream, "- aiwiki agent install");
466
- writeLine(stream, "- 然后向宿主 Agent 发送 `入库 <url>`");
467
- writeLine(stream, "- CLI 不抓网页;网页正文由宿主 Agent 提供。");
494
+ writeLine(stream, `结构检查发现 ${errorCount} 个 error 问题。`);
495
+ writeLine(stream, "- aiwiki lint");
496
+ writeLine(stream, "- report: dashboards/Lint Report.md");
497
+ writeLine(stream, "- repair_order: lint_errors");
468
498
  return;
469
499
  }
470
- const actionableIssues = report?.issues.filter((issue) => issue.severity !== "info") ?? [];
471
- if (actionableIssues.length) {
500
+ if (warningCount > 0) {
472
501
  writeLine(stream, "");
473
- writeLine(stream, `结构检查发现 ${actionableIssues.length} 个需要处理的问题。`);
502
+ writeLine(stream, `结构检查发现 ${warningCount} 个 warning 问题。`);
474
503
  writeLine(stream, "- aiwiki lint");
475
- writeLine(stream, `- 查看报告: dashboards/Lint Report.md`);
476
- writeLine(stream, "- 先处理 error / warning,再继续扩展查询或入库。");
504
+ writeLine(stream, "- report: dashboards/Lint Report.md");
505
+ writeLine(stream, "- repair_order: lint_warnings");
506
+ return;
507
+ }
508
+ if (runCount === 0) {
509
+ writeLine(stream, "");
510
+ writeLine(stream, "No ingest records yet.");
511
+ writeLine(stream, "- aiwiki agent install");
512
+ writeLine(stream, "- Then ask the host Agent to ingest a URL.");
513
+ writeLine(stream, "- AIWiki CLI does not fetch webpages; the host Agent supplies content.");
514
+ writeLine(stream, "- repair_order: empty_workspace");
477
515
  return;
478
516
  }
479
517
  writeLine(stream, "");
480
- writeLine(stream, "已有入库记录,可以继续:");
481
- writeLine(stream, "- aiwiki query <主题>");
518
+ writeLine(stream, "Workspace is healthy enough for retrieval:");
519
+ writeLine(stream, "- aiwiki query <topic>");
482
520
  writeLine(stream, "- aiwiki lint");
521
+ writeLine(stream, "- repair_order: healthy_query");
483
522
  if (installableMissing.length) {
484
523
  writeLine(stream, "");
485
- writeLine(stream, "可补充宿主 Agent 接入:");
524
+ writeLine(stream, "Optional host Agent setup:");
486
525
  for (const target of installableMissing) {
487
526
  writeLine(stream, `- aiwiki agent install --agent ${target.id} --yes`);
488
527
  }
489
528
  }
490
529
  }
530
+ function recommendedNextAction(runCount, lintStatus, hasMissingSystemFiles) {
531
+ if (hasMissingSystemFiles) {
532
+ return "next_action: aiwiki setup --path <workspace> --yes";
533
+ }
534
+ if (lintStatus === "needs_attention") {
535
+ return "next_action: aiwiki lint";
536
+ }
537
+ if (runCount === 0) {
538
+ return "next_action: aiwiki agent install";
539
+ }
540
+ return "next_action: aiwiki query <topic>";
541
+ }
491
542
  function renderQuery(context) {
492
543
  const lines = [`AIWiki 查询: ${context.query}`, ""];
493
544
  appendQueryGroup(lines, "Wiki 条目", context.matches.wiki_entries);
package/dist/src/lint.js CHANGED
@@ -19,14 +19,17 @@ export async function lintWorkspace(rootPath, now = new Date().toISOString()) {
19
19
  ...await readNotes(root, "08-outputs/outlines")
20
20
  ];
21
21
  const issues = [];
22
+ issues.push(...await systemFileIssues(root));
22
23
  const wikiSourceCards = new Set(wikiEntries.map((note) => frontmatterString(note.frontmatter, "source_card")).filter(Boolean));
23
24
  for (const card of sourceCards) {
24
25
  if (!wikiSourceCards.has(card.path)) {
25
26
  issues.push({
26
27
  severity: "warning",
27
28
  path: card.path,
28
- message: "Source Card 没有对应 Wiki Entry。",
29
- suggestion: `重新入库或生成 05-wiki/source-knowledge/${path.basename(card.path)}`
29
+ category: "isolated_source_card",
30
+ action: "reingest",
31
+ message: "Source Card has no matching Wiki Entry.",
32
+ suggestion: `Reingest or create a matching 05-wiki/source-knowledge/${path.basename(card.path)} entry.`
30
33
  });
31
34
  }
32
35
  }
@@ -35,26 +38,68 @@ export async function lintWorkspace(rootPath, now = new Date().toISOString()) {
35
38
  const rawFile = frontmatterString(entry.frontmatter, "raw_file");
36
39
  const mode = frontmatterString(entry.frontmatter, "generation_mode");
37
40
  if (!sourceCard) {
38
- issues.push({ severity: "warning", path: entry.path, message: "Wiki Entry 缺少 source_card。", suggestion: "补写 source_card vault 路径。" });
41
+ issues.push({
42
+ severity: "warning",
43
+ path: entry.path,
44
+ category: "missing_source",
45
+ action: "reingest",
46
+ message: "Wiki Entry is missing source_card.",
47
+ suggestion: "Add the vault path of the source card."
48
+ });
39
49
  }
40
50
  if (!rawFile) {
41
- issues.push({ severity: "warning", path: entry.path, message: "Wiki Entry 缺少 raw_file。", suggestion: "补写 raw_file vault 路径。" });
51
+ issues.push({
52
+ severity: "warning",
53
+ path: entry.path,
54
+ category: "missing_source",
55
+ action: "reingest",
56
+ message: "Wiki Entry is missing raw_file.",
57
+ suggestion: "Add the vault path of the raw source file."
58
+ });
42
59
  }
43
60
  if (mode === "deterministic_fallback") {
44
- issues.push({ severity: "info", path: entry.path, message: "Wiki Entry 是 deterministic fallback,仅包含来源和正文预览。", suggestion: "让宿主 Agent 基于原文补充 analysis 或 wiki_entry。" });
61
+ issues.push({
62
+ severity: "info",
63
+ path: entry.path,
64
+ category: "stale_scaffold",
65
+ action: "enrich",
66
+ message: "Wiki Entry is a deterministic fallback and only contains source trace plus a content preview.",
67
+ suggestion: "Ask the host Agent to enrich it with analysis or a full wiki_entry."
68
+ });
45
69
  const createdAt = Date.parse(frontmatterString(entry.frontmatter, "created_at") ?? "");
46
70
  if (Number.isFinite(createdAt) && Date.parse(now) - createdAt > 7 * 24 * 60 * 60 * 1000) {
47
- issues.push({ severity: "warning", path: entry.path, message: "fallback Wiki Entry 超过 7 天未补全。", suggestion: "重新运行宿主 Agent 生成 enriched Wiki Entry。" });
71
+ issues.push({
72
+ severity: "warning",
73
+ path: entry.path,
74
+ category: "stale_scaffold",
75
+ action: "enrich",
76
+ message: "Fallback Wiki Entry is older than 7 days.",
77
+ suggestion: "Reingest with a host Agent to generate an enriched Wiki Entry."
78
+ });
48
79
  }
49
80
  }
50
81
  if (mode === "agent_enriched") {
51
- const hasSummary = /## 一句话总结/.test(entry.body) || Boolean(frontmatterString(entry.frontmatter, "summary"));
52
- const hasKeyPoints = /## 核心观点[\s\S]*-\s+/.test(entry.body);
82
+ const hasSummary = /##\s+.+/.test(entry.body) || Boolean(frontmatterString(entry.frontmatter, "summary"));
83
+ const hasKeyPoints = /-\s+/.test(entry.body);
53
84
  if (!hasSummary) {
54
- issues.push({ severity: "warning", path: entry.path, message: "agent_enriched Wiki Entry 缺少 summary。", suggestion: "让宿主 Agent 提供 analysis.summary。" });
85
+ issues.push({
86
+ severity: "warning",
87
+ path: entry.path,
88
+ category: "weak_entry",
89
+ action: "enrich",
90
+ message: "agent_enriched Wiki Entry is missing a summary.",
91
+ suggestion: "Ask the host Agent to provide analysis.summary."
92
+ });
55
93
  }
56
94
  if (!hasKeyPoints) {
57
- issues.push({ severity: "warning", path: entry.path, message: "agent_enriched Wiki Entry 缺少 key_points。", suggestion: "让宿主 Agent 提供 analysis.key_points。" });
95
+ issues.push({
96
+ severity: "warning",
97
+ path: entry.path,
98
+ category: "weak_entry",
99
+ action: "enrich",
100
+ message: "agent_enriched Wiki Entry is missing key points.",
101
+ suggestion: "Ask the host Agent to provide analysis.key_points."
102
+ });
58
103
  }
59
104
  }
60
105
  if (frontmatterBoolean(entry.frontmatter, "grounding_needs_review") === true) {
@@ -62,15 +107,24 @@ export async function lintWorkspace(rootPath, now = new Date().toISOString()) {
62
107
  issues.push({
63
108
  severity: "warning",
64
109
  path: entry.path,
65
- message: `Wiki Entry 需要 grounding 复核${markers.length ? `: ${markers.join(", ")}` : "。"}`,
66
- suggestion: "检查 source_quote 是否能在 Raw 中找到;coverage_suspected_incomplete 仅代表启发式疑似风险。"
110
+ category: "grounding_review",
111
+ action: "mark_reviewed",
112
+ message: `Wiki Entry needs grounding review${markers.length ? `: ${markers.join(", ")}` : "."}`,
113
+ suggestion: "Check whether source quotes are present in Raw. Heuristic coverage risks are not confirmed facts."
67
114
  });
68
115
  }
69
116
  if (frontmatterBoolean(entry.frontmatter, "represents_user_view") === true && frontmatterString(entry.frontmatter, "source_role") !== "output") {
70
- issues.push({ severity: "warning", path: entry.path, message: "只有 output 角色应标记为代表用户观点。", suggestion: "将 represents_user_view 改为 false,或将 source_role 改为 output。" });
117
+ issues.push({
118
+ severity: "warning",
119
+ path: entry.path,
120
+ category: "metadata_boundary",
121
+ action: "mark_reviewed",
122
+ message: "Only output source_role entries should represent the user's view.",
123
+ suggestion: "Set represents_user_view to false, or change source_role to output when it is user-authored output."
124
+ });
71
125
  }
72
126
  }
73
- issues.push(...duplicateIssues(sourceCards, "source_url", "重复 URL"));
127
+ issues.push(...duplicateIssues(sourceCards, "source_url", "Duplicate URL"));
74
128
  issues.push(...duplicateTitles(allNotes));
75
129
  issues.push(...brokenLinkIssues(root, allNotes));
76
130
  return {
@@ -84,6 +138,15 @@ export async function lintWorkspace(rootPath, now = new Date().toISOString()) {
84
138
  issues
85
139
  };
86
140
  }
141
+ export function filterLintReport(report, severity) {
142
+ if (!severity) {
143
+ return report;
144
+ }
145
+ return {
146
+ ...report,
147
+ issues: report.issues.filter((issue) => issue.severity === severity)
148
+ };
149
+ }
87
150
  export async function writeLintReport(rootPath, report) {
88
151
  const root = path.resolve(rootPath);
89
152
  const target = safeJoin(root, "dashboards", "Lint Report.md");
@@ -92,6 +155,8 @@ export async function writeLintReport(rootPath, report) {
92
155
  return relativePath(root, target);
93
156
  }
94
157
  export function renderLintReport(report) {
158
+ const counts = severityCounts(report.issues);
159
+ const topIssue = report.issues[0];
95
160
  return [
96
161
  "# AIWiki Lint Report",
97
162
  "",
@@ -103,17 +168,49 @@ export function renderLintReport(report) {
103
168
  `- Source Cards: ${report.summary.source_cards}`,
104
169
  `- Raw Files: ${report.summary.raw_files}`,
105
170
  `- Runs: ${report.summary.runs}`,
171
+ `- Errors: ${counts.error}`,
172
+ `- Warnings: ${counts.warning}`,
173
+ `- Info: ${counts.info}`,
174
+ `- Top Issue: ${topIssue ? formatIssueLine(topIssue) : "none"}`,
106
175
  "",
107
- "## Issues",
176
+ "## Suggested Handling Order",
108
177
  "",
109
- ...(report.issues.length ? report.issues.map((issue) => {
110
- const suffix = issue.path ? ` (${issue.path})` : "";
111
- const suggestion = issue.suggestion ? `\n - Suggested Fix: ${issue.suggestion}` : "";
112
- return `- [${issue.severity}] ${issue.message}${suffix}${suggestion}`;
113
- }) : ["- none"]),
178
+ "- Fix error issues first.",
179
+ "- Review warning issues next.",
180
+ "- Use info issues for enrichment and cleanup backlog.",
181
+ "",
182
+ ...renderIssueGroup("Errors", report.issues.filter((issue) => issue.severity === "error")),
183
+ ...renderIssueGroup("Warnings", report.issues.filter((issue) => issue.severity === "warning")),
184
+ ...renderIssueGroup("Info", report.issues.filter((issue) => issue.severity === "info")),
114
185
  ""
115
186
  ].join("\n");
116
187
  }
188
+ export function renderLintSummary(report, reportPath) {
189
+ const counts = severityCounts(report.issues);
190
+ const topIssue = report.issues[0];
191
+ return [
192
+ `lint_summary: errors=${counts.error} warnings=${counts.warning} info=${counts.info}`,
193
+ `top_issue: ${topIssue ? formatIssueLine(topIssue) : "none"}`,
194
+ ...(reportPath ? [`report: ${reportPath}`] : [])
195
+ ].join("\n");
196
+ }
197
+ async function systemFileIssues(root) {
198
+ const issues = [];
199
+ for (const systemFile of ["_system/purpose.md", "_system/index.md", "_system/log.md"]) {
200
+ if (await exists(path.join(root, systemFile))) {
201
+ continue;
202
+ }
203
+ issues.push({
204
+ severity: "error",
205
+ path: systemFile,
206
+ category: "workspace_structure",
207
+ action: "repair_structure",
208
+ message: `Required system file is missing: ${systemFile}`,
209
+ suggestion: `Run aiwiki setup --path "${root}" --yes`
210
+ });
211
+ }
212
+ return issues;
213
+ }
117
214
  async function readNotes(root, dir) {
118
215
  const absolute = path.join(root, dir);
119
216
  if (!(await exists(absolute))) {
@@ -163,7 +260,13 @@ function duplicateIssues(notes, field, label) {
163
260
  seen.set(value, [...(seen.get(value) ?? []), note.path]);
164
261
  }
165
262
  return Array.from(seen.entries()).flatMap(([value, paths]) => paths.length > 1
166
- ? [{ severity: "warning", message: `${label}: ${value}`, suggestion: paths.join(", ") }]
263
+ ? [{
264
+ severity: "warning",
265
+ category: "duplicate",
266
+ action: "mark_reviewed",
267
+ message: `${label}: ${value}`,
268
+ suggestion: paths.join(", ")
269
+ }]
167
270
  : []);
168
271
  }
169
272
  function duplicateTitles(notes) {
@@ -175,7 +278,13 @@ function duplicateTitles(notes) {
175
278
  seen.set(note.title, [...(seen.get(note.title) ?? []), note.path]);
176
279
  }
177
280
  return Array.from(seen.entries()).flatMap(([title, paths]) => paths.length > 1
178
- ? [{ severity: "info", message: `重复标题: ${title}`, suggestion: paths.join(", ") }]
281
+ ? [{
282
+ severity: "info",
283
+ category: "duplicate_title",
284
+ action: "archive",
285
+ message: `Duplicate title: ${title}`,
286
+ suggestion: paths.join(", ")
287
+ }]
179
288
  : []);
180
289
  }
181
290
  function brokenLinkIssues(root, notes) {
@@ -185,12 +294,43 @@ function brokenLinkIssues(root, notes) {
185
294
  for (const link of note.body.matchAll(/\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g)) {
186
295
  const target = link[1].replace(/\\/g, "/").replace(/\.md$/i, "");
187
296
  if (!existing.has(target) && !isRunLocalLink(target)) {
188
- issues.push({ severity: "error", path: note.path, message: `内部链接断裂: ${target}`, suggestion: "检查目标文件是否存在或更新 wikilink。" });
297
+ issues.push({
298
+ severity: "error",
299
+ path: note.path,
300
+ category: "broken_link",
301
+ action: "fix_link",
302
+ message: `Broken wikilink: ${target}`,
303
+ suggestion: "Check whether the target file exists or update the wikilink."
304
+ });
189
305
  }
190
306
  }
191
307
  }
192
308
  return issues;
193
309
  }
310
+ function renderIssueGroup(title, issues) {
311
+ return [
312
+ `## ${title}`,
313
+ "",
314
+ ...(issues.length ? issues.map((issue) => {
315
+ const suggestion = issue.suggestion ? `\n - Suggested Fix: ${issue.suggestion}` : "";
316
+ const action = issue.action ? `\n - Action: ${issue.action}` : "";
317
+ return `- ${formatIssueLine(issue)}${action}${suggestion}`;
318
+ }) : ["- none"]),
319
+ ""
320
+ ];
321
+ }
322
+ function formatIssueLine(issue) {
323
+ const suffix = issue.path ? ` (${issue.path})` : "";
324
+ const category = issue.category ? ` {${issue.category}}` : "";
325
+ return `[${issue.severity}]${category} ${issue.message}${suffix}`;
326
+ }
327
+ function severityCounts(issues) {
328
+ return {
329
+ error: issues.filter((issue) => issue.severity === "error").length,
330
+ warning: issues.filter((issue) => issue.severity === "warning").length,
331
+ info: issues.filter((issue) => issue.severity === "info").length
332
+ };
333
+ }
194
334
  function isRunLocalLink(target) {
195
335
  // Run-local notes are valid trace links but are not part of the long-term note set.
196
336
  return target.startsWith("09-runs/");
@@ -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
+ }
@@ -199,6 +199,28 @@ aiwiki query "<主题>"
199
199
  aiwiki lint
200
200
  ```
201
201
 
202
+ 需要机器读取时用:
203
+
204
+ ```bash
205
+ aiwiki lint --json
206
+ ```
207
+
208
+ 只想看某一级别时用:
209
+
210
+ ```bash
211
+ aiwiki lint --severity error
212
+ aiwiki lint --severity warning
213
+ aiwiki lint --severity info
214
+ ```
215
+
216
+ 只做临时检查、不改 dashboard 时用:
217
+
218
+ ```bash
219
+ aiwiki lint --no-write
220
+ ```
221
+
222
+ Lint 输出会包含摘要、最高优先级问题、分级报告,以及建议动作。把 `error` 当作必须先修的结构问题,把 `warning` 当作需要处理或复核的维护问题,把 `info` 当作富集、归档或后续整理 backlog。
223
+
202
224
  `context` 返回 JSON,注意其中的 `generation_mode`、`quality` 和 `warnings`。如果结果是 `deterministic_fallback` / `scaffold`,回复时要说明它只是可追溯脚手架,不是高质量知识提炼。
203
225
 
204
226
  `context` 也可能返回 grounding 字段。回复用户时可以把 `grounding_needs_review: true` 解释为“这条资料需要复核证据或覆盖度”,不要说成“AIWiki 已确认漏掉重点”。
package/docs/README.md CHANGED
@@ -9,6 +9,7 @@ AIWiki 是一个 Agent-first 的本地知识库工具,用来把宿主 Agent
9
9
  - 示例展示:[SHOWCASE.md](SHOWCASE.md)
10
10
  - 常见问题:[FAQ.md](FAQ.md)
11
11
  - 路线图:[ROADMAP.md](ROADMAP.md)
12
+ - 开发记录:[development-log.md](development-log.md)
12
13
 
13
14
  ## Quick Start
14
15
 
package/docs/USAGE.md CHANGED
@@ -318,13 +318,23 @@ aiwiki query "AI Agent"
318
318
  aiwiki lint
319
319
  ```
320
320
 
321
+ 常用工作台模式:
322
+
323
+ ```bash
324
+ aiwiki lint --severity warning
325
+ aiwiki lint --json
326
+ aiwiki lint --no-write
327
+ ```
328
+
329
+ `lint` 会先输出 `lint_summary`、`top_issue` 和报告路径,再按 Errors / Warnings / Info 分组展示问题。每个问题会尽量给出建议动作,例如 `enrich`、`fix_link`、`reingest`、`archive` 或 `mark_reviewed`。`--severity` 只查看指定级别,`--json` 给宿主 Agent 使用,`--no-write` 只在终端检查而不更新 `dashboards/Lint Report.md`。
330
+
321
331
  查看下一步建议:
322
332
 
323
333
  ```bash
324
334
  aiwiki next
325
335
  ```
326
336
 
327
- `lint` 输出报告并写入 `dashboards/Lint Report.md`。
337
+ 默认情况下,`lint` 输出报告并写入 `dashboards/Lint Report.md`。
328
338
 
329
339
  ## 8. 高级调试
330
340
 
@@ -410,3 +420,17 @@ aiwiki status
410
420
  # System Purpose Files
411
421
 
412
422
  `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.
423
+
424
+ ## Diagnostic Commands
425
+
426
+ `aiwiki doctor` checks the workspace directories, write permission, and required system files: `_system/purpose.md`, `_system/index.md`, and `_system/log.md`.
427
+
428
+ `aiwiki status` keeps the existing run-count summary and also reports:
429
+
430
+ - `fallback_entries`: Wiki entries generated as deterministic fallback/scaffold.
431
+ - `grounding_review_entries`: Wiki entries marked for grounding review.
432
+ - `lint_status`: whether a lint report is missing, clean, or needs attention.
433
+ - `system_files`: readiness of purpose, index, and log files.
434
+ - `next_action`: the recommended next command.
435
+
436
+ `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.
@@ -0,0 +1,97 @@
1
+ # AIWiki Development Log
2
+
3
+ This log records queue-driven AIWiki development milestones that should remain visible to future maintainers, not only in automation chat history.
4
+
5
+ ## 2026-06-05 - AIWIKI-004 lint workbench
6
+
7
+ Status: implemented, locally verified, pushed to GitHub, blocked on npm OTP before publication.
8
+
9
+ Version target: `@itradingai/aiwiki@0.2.16`
10
+
11
+ Commit: `932386c` (`Turn lint into an actionable maintenance workbench`)
12
+
13
+ ### Goal
14
+
15
+ Turn `aiwiki lint` from a plain report writer into a practical structure-maintenance workbench for humans and host Agents.
16
+
17
+ The scoped acceptance criteria were:
18
+
19
+ - summarize lint output in the terminal with errors, warnings, info, top issue, and report path;
20
+ - group the markdown report by severity and provide a handling order;
21
+ - support `--severity error|warning|info`;
22
+ - support `--json` for machine-readable Agent use;
23
+ - support `--no-write` for temporary checks that do not update `dashboards/Lint Report.md`;
24
+ - add lightweight knowledge-gap signals where feasible;
25
+ - attach advisory review actions such as `enrich`, `fix_link`, `archive`, `reingest`, and `mark_reviewed`.
26
+
27
+ ### Implemented
28
+
29
+ - `src/lint.ts`
30
+ - Added `LintSeverity` and advisory `LintAction`.
31
+ - Added issue `category` and `action` fields.
32
+ - Added system-file checks for `_system/purpose.md`, `_system/index.md`, and `_system/log.md`.
33
+ - Added signals for isolated Source Cards, stale deterministic fallback entries, grounding-review entries, metadata-boundary issues, duplicate URLs/titles, and broken wikilinks.
34
+ - Added severity filtering, terminal summary rendering, and severity-grouped report rendering.
35
+
36
+ - `src/app.ts`
37
+ - Added `aiwiki lint --severity error|warning|info`.
38
+ - Added `aiwiki lint --json`.
39
+ - Added `aiwiki lint --no-write`.
40
+ - Kept the default behavior of writing `dashboards/Lint Report.md`.
41
+
42
+ - `tests/cli.test.ts` and `tests/ingest.test.ts`
43
+ - Covered lint summary output, severity filtering, JSON output, no-write behavior, and updated lint issue text assertions.
44
+
45
+ - `docs/USAGE.md` and `docs/AGENT_HANDOFF.md`
46
+ - Documented human and Agent-facing lint modes.
47
+
48
+ ### Verification
49
+
50
+ - `npm test`: passed, 53 tests.
51
+ - `npm run release:check`: passed, including 53 tests and release-check.
52
+ - `npm pack --dry-run`: passed for `@itradingai/aiwiki@0.2.16`.
53
+ - Clean export pack from `C:/tmp/aiwiki-004-publish`: passed, 31 files, shasum `945c70d3d4cf20c9550deaaf92036786b75e62cf`.
54
+
55
+ ### Release State
56
+
57
+ GitHub push succeeded:
58
+
59
+ ```text
60
+ 2e4b253..932386c main -> main
61
+ ```
62
+
63
+ npm publication is blocked by a one-time password challenge:
64
+
65
+ ```text
66
+ npm error code EOTP
67
+ npm error This operation requires a one-time password from your authenticator.
68
+ ```
69
+
70
+ Current registry version remains `0.2.15`, so remote smoke tests for `0.2.16` have not run yet.
71
+
72
+ ### Resume Steps
73
+
74
+ From the clean publish directory:
75
+
76
+ ```powershell
77
+ cd C:\tmp\aiwiki-004-publish
78
+ npm publish --access public --otp=<code>
79
+ npm view @itradingai/aiwiki version
80
+ ```
81
+
82
+ After `npm view` returns `0.2.16`, run remote smoke tests for:
83
+
84
+ ```bash
85
+ aiwiki lint --path <tmp-vault>
86
+ aiwiki lint --severity warning --path <tmp-vault>
87
+ aiwiki lint --json --path <tmp-vault>
88
+ aiwiki lint --no-write --path <tmp-vault>
89
+ ```
90
+
91
+ Then update the queue through `published`, `remote_verified`, and `done`.
92
+
93
+ ### Notes For Future Changes
94
+
95
+ - Lint actions are advisory only. Do not make `archive`, `reingest`, `mark_reviewed`, or related actions mutate files without a separate explicit task.
96
+ - Keep lint local-file-only. Do not add crawling, vector search, RAG-over-wiki, RBAC, RSS, scheduled collection, or browser plugins under this queue item.
97
+ - The original working tree had unrelated `skill/SKILL.md` changes. Publication must use a clean commit export until that WIP is resolved.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itradingai/aiwiki",
3
- "version": "0.2.14",
3
+ "version": "0.2.16",
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",