@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 +79 -28
- package/dist/src/lint.js +163 -23
- package/dist/src/workspace.js +100 -2
- package/docs/AGENT_HANDOFF.md +22 -0
- package/docs/README.md +1 -0
- package/docs/USAGE.md +25 -1
- package/docs/development-log.md +97 -0
- package/package.json +1 -1
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
|
|
163
|
-
const
|
|
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
|
|
437
|
-
writeLine(stream,
|
|
438
|
-
writeLine(stream,
|
|
439
|
-
writeLine(stream,
|
|
440
|
-
writeLine(stream,
|
|
441
|
-
writeLine(stream,
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
466
|
-
writeLine(stream, "-
|
|
467
|
-
writeLine(stream, "-
|
|
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
|
-
|
|
471
|
-
if (actionableIssues.length) {
|
|
500
|
+
if (warningCount > 0) {
|
|
472
501
|
writeLine(stream, "");
|
|
473
|
-
writeLine(stream, `结构检查发现 ${
|
|
502
|
+
writeLine(stream, `结构检查发现 ${warningCount} 个 warning 问题。`);
|
|
474
503
|
writeLine(stream, "- aiwiki lint");
|
|
475
|
-
writeLine(stream,
|
|
476
|
-
writeLine(stream, "-
|
|
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, "
|
|
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
|
-
|
|
29
|
-
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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 =
|
|
52
|
-
const hasKeyPoints =
|
|
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({
|
|
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({
|
|
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
|
-
|
|
66
|
-
|
|
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({
|
|
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", "
|
|
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
|
-
"##
|
|
176
|
+
"## Suggested Handling Order",
|
|
108
177
|
"",
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
? [{
|
|
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
|
-
? [{
|
|
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({
|
|
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/");
|
package/dist/src/workspace.js
CHANGED
|
@@ -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 {
|
|
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
|
+
}
|
package/docs/AGENT_HANDOFF.md
CHANGED
|
@@ -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
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
|
-
|
|
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.
|
|
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",
|