@zosmaai/pi-llm-wiki 0.11.5 → 0.11.6
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/extensions/llm-wiki/lib/ingest-worker.js +43 -20
- package/dist/extensions/llm-wiki/lib/knowledge-links.js +127 -24
- package/dist/extensions/llm-wiki/lib/metadata.js +5 -5
- package/dist/extensions/llm-wiki/lib/retro.js +38 -4
- package/dist/extensions/llm-wiki/lib/runtime.js +2 -2
- package/dist/extensions/llm-wiki/lib/task-config.js +22 -0
- package/dist/extensions/llm-wiki/lib/tools.js +47 -8
- package/dist/mcp/index.js +2 -1
- package/dist/mcp/operations.js +21 -2
- package/docs/configuration.md +1 -0
- package/docs/obsidian.md +6 -6
- package/docs/superpowers/plans/2026-08-27-wikilink-resolver-normalization.md +735 -0
- package/docs/superpowers/plans/2026-08-29-wikilink-gate-ensure-page-retro.md +642 -0
- package/docs/superpowers/plans/2026-08-29-wikilink-write-validation.md +695 -0
- package/extensions/llm-wiki/lib/ingest-worker.ts +60 -27
- package/extensions/llm-wiki/lib/knowledge-document.ts +1 -0
- package/extensions/llm-wiki/lib/knowledge-links.ts +201 -32
- package/extensions/llm-wiki/lib/metadata.ts +9 -5
- package/extensions/llm-wiki/lib/retro.ts +48 -4
- package/extensions/llm-wiki/lib/runtime.ts +2 -2
- package/extensions/llm-wiki/lib/task-config.ts +36 -0
- package/extensions/llm-wiki/lib/tools.ts +62 -8
- package/mcp/index.ts +12 -1
- package/mcp/operations.ts +32 -2
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
resolveGlobalSettingsPath,
|
|
10
10
|
resolveProjectSettingsPath,
|
|
11
11
|
} from "./host.js";
|
|
12
|
+
import type { WikilinkValidationMode } from "./knowledge-links.js";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Configuration for the background-task lane (issue #64, part of #63).
|
|
@@ -147,6 +148,17 @@ export interface TaskConfig {
|
|
|
147
148
|
* response truncates before commit_synthesis can be called.
|
|
148
149
|
*/
|
|
149
150
|
synthesisMaxTokens?: number;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Wikilink gate applied to the ingested source body before pages are
|
|
154
|
+
* written (issue #172, Layer 2). Reuses the Layer 1 resolver.
|
|
155
|
+
* - "off" : no-op.
|
|
156
|
+
* - "warn" : write proceeds; unresolved/ambiguous links are reported.
|
|
157
|
+
* - "strict" : block the write (commit returns ok:false) if any link is unresolvable.
|
|
158
|
+
* - "normalize" : rewrite resolvable links to their canonical id, then write.
|
|
159
|
+
* Default "warn". See `resolveWikilinkValidation`.
|
|
160
|
+
*/
|
|
161
|
+
wikilinkValidation?: WikilinkValidationMode;
|
|
150
162
|
}
|
|
151
163
|
|
|
152
164
|
export const TASK_DEFAULTS: TaskConfig = {};
|
|
@@ -159,6 +171,24 @@ export function noticesEnabled(config: TaskConfig | undefined): boolean {
|
|
|
159
171
|
return config?.notices !== false;
|
|
160
172
|
}
|
|
161
173
|
|
|
174
|
+
const WIKILINK_VALIDATION_MODES: readonly WikilinkValidationMode[] = [
|
|
175
|
+
"off",
|
|
176
|
+
"warn",
|
|
177
|
+
"strict",
|
|
178
|
+
"normalize",
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Resolve the wikilink write-gate mode (issue #172, Layer 2). Defaults to
|
|
183
|
+
* `warn` — ingest always writes and reports; only an explicit `strict` blocks.
|
|
184
|
+
* Unknown values fall back to `warn` rather than failing the ingest.
|
|
185
|
+
*/
|
|
186
|
+
export function resolveWikilinkValidation(config: TaskConfig | undefined): WikilinkValidationMode {
|
|
187
|
+
const v = config?.wikilinkValidation;
|
|
188
|
+
if (v && (WIKILINK_VALIDATION_MODES as readonly string[]).includes(v)) return v;
|
|
189
|
+
return "warn";
|
|
190
|
+
}
|
|
191
|
+
|
|
162
192
|
/**
|
|
163
193
|
* Resolve whether the personal vault may serve as this project's ambient
|
|
164
194
|
* vault. Explicit `ambientPersonalVault` wins; otherwise the host decides
|
|
@@ -251,6 +281,11 @@ function readNamespacedConfig(path: string): Partial<TaskConfig> {
|
|
|
251
281
|
out.synthesisMaxTokens = Math.floor(maxTokens);
|
|
252
282
|
}
|
|
253
283
|
|
|
284
|
+
const wl = section.wikilinkValidation;
|
|
285
|
+
if (typeof wl === "string" && (WIKILINK_VALIDATION_MODES as readonly string[]).includes(wl)) {
|
|
286
|
+
out.wikilinkValidation = wl as WikilinkValidationMode;
|
|
287
|
+
}
|
|
288
|
+
|
|
254
289
|
return out;
|
|
255
290
|
} catch {
|
|
256
291
|
return {};
|
|
@@ -438,6 +473,7 @@ const KNOWN_KEYS = [
|
|
|
438
473
|
"trajectories",
|
|
439
474
|
"synthesisLanguage",
|
|
440
475
|
"synthesisMaxTokens",
|
|
476
|
+
"wikilinkValidation",
|
|
441
477
|
] as const;
|
|
442
478
|
|
|
443
479
|
export function loadTaskConfigSources(
|
|
@@ -11,12 +11,16 @@ import {
|
|
|
11
11
|
serializeKnowledgeDocument,
|
|
12
12
|
writeKnowledgeDocumentFile,
|
|
13
13
|
} from "./knowledge-document.js";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
applyWikilinkGate,
|
|
16
|
+
buildResolvedBacklinks,
|
|
17
|
+
buildWikilinkIndex,
|
|
18
|
+
} from "./knowledge-links.js";
|
|
15
19
|
import { repairLegacyKnowledgeDocuments } from "./legacy-repair.js";
|
|
16
20
|
import { type Registry, appendEvent, rebuildMetadata, rebuildMetadataLight } from "./metadata.js";
|
|
17
21
|
import type { Runtime } from "./runtime.js";
|
|
18
22
|
import { captureFile, captureText, captureUrl } from "./source-packet.js";
|
|
19
|
-
import { parseModelRef } from "./task-config.js";
|
|
23
|
+
import { loadTaskConfig, parseModelRef, resolveWikilinkValidation } from "./task-config.js";
|
|
20
24
|
import {
|
|
21
25
|
type VaultPaths,
|
|
22
26
|
detectVaultFormat,
|
|
@@ -415,6 +419,7 @@ export function registerWikiIngest(pi: ExtensionAPI, runtime?: Runtime): void {
|
|
|
415
419
|
manifest: s.manifest,
|
|
416
420
|
extracted: s.extracted,
|
|
417
421
|
synthesisLanguage: runtime.config.synthesisLanguage,
|
|
422
|
+
wikilinkValidation: runtime.config.wikilinkValidation,
|
|
418
423
|
});
|
|
419
424
|
if (committed) {
|
|
420
425
|
// Background semantic embeddings (#66): embed the pages this
|
|
@@ -428,8 +433,10 @@ export function registerWikiIngest(pi: ExtensionAPI, runtime?: Runtime): void {
|
|
|
428
433
|
];
|
|
429
434
|
launchEmbedPages(runtime, launchCtx, paths, pageIds, `embed:ingest:${s.id}`);
|
|
430
435
|
}
|
|
436
|
+
const wl = committed?.wikilinkDiagnostics?.length ?? 0;
|
|
437
|
+
const wlNote = wl > 0 ? `, ${wl} wikilink issue${wl === 1 ? "" : "s"}` : "";
|
|
431
438
|
const summary = committed
|
|
432
|
-
? `LLM Wiki: ingested ${s.id} → ${committed.entitiesCreated.length} entit${committed.entitiesCreated.length === 1 ? "y" : "ies"}, ${committed.conceptsCreated.length} concept${committed.conceptsCreated.length === 1 ? "" : "s"}`
|
|
439
|
+
? `LLM Wiki: ingested ${s.id} → ${committed.entitiesCreated.length} entit${committed.entitiesCreated.length === 1 ? "y" : "ies"}, ${committed.conceptsCreated.length} concept${committed.conceptsCreated.length === 1 ? "" : "s"}${wlNote}`
|
|
433
440
|
: `LLM Wiki: ${s.id} produced no synthesis`;
|
|
434
441
|
if (ctx.hasUI) {
|
|
435
442
|
ctx.ui.notify(summary, committed ? "info" : "warning");
|
|
@@ -565,7 +572,42 @@ export function registerWikiEnsurePage(pi: ExtensionAPI, runtime?: Runtime): voi
|
|
|
565
572
|
}
|
|
566
573
|
|
|
567
574
|
const today = fmtDate();
|
|
568
|
-
|
|
575
|
+
let body = params.content ?? buildPageBody(type, params.title);
|
|
576
|
+
|
|
577
|
+
// Pre-write wikilink gate (#172): validate/normalize caller-supplied content.
|
|
578
|
+
const mode = resolveWikilinkValidation(loadTaskConfig(ctx.cwd));
|
|
579
|
+
let wikilinkIssues: string[] = [];
|
|
580
|
+
if (mode !== "off") {
|
|
581
|
+
const registry = readJson<{ pages: Record<string, unknown> }>(
|
|
582
|
+
join(paths.meta, "registry.json"),
|
|
583
|
+
{ pages: {} },
|
|
584
|
+
);
|
|
585
|
+
const gate = applyWikilinkGate(
|
|
586
|
+
body,
|
|
587
|
+
buildWikilinkIndex(Object.keys(registry.pages)),
|
|
588
|
+
`${folder}/${slug}`,
|
|
589
|
+
mode,
|
|
590
|
+
);
|
|
591
|
+
wikilinkIssues = gate.diagnostics.map((d) => d.message);
|
|
592
|
+
if (!gate.ok) {
|
|
593
|
+
return {
|
|
594
|
+
content: [
|
|
595
|
+
{
|
|
596
|
+
type: "text",
|
|
597
|
+
text: `Rejected write — unresolved/ambiguous wikilinks:\n${wikilinkIssues
|
|
598
|
+
.map((m) => `- ${m}`)
|
|
599
|
+
.join("\n")}`,
|
|
600
|
+
},
|
|
601
|
+
],
|
|
602
|
+
details: { error: "link_validation", issues: wikilinkIssues } as Record<
|
|
603
|
+
string,
|
|
604
|
+
unknown
|
|
605
|
+
>,
|
|
606
|
+
isError: true,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
if (mode === "normalize") body = gate.body;
|
|
610
|
+
}
|
|
569
611
|
const doc = createKnowledgeDocument(
|
|
570
612
|
`${folder}/${slug}.md`,
|
|
571
613
|
{
|
|
@@ -595,9 +637,16 @@ export function registerWikiEnsurePage(pi: ExtensionAPI, runtime?: Runtime): voi
|
|
|
595
637
|
rebuildMetadataLight(paths);
|
|
596
638
|
}
|
|
597
639
|
|
|
640
|
+
const gateNote = wikilinkIssues.length
|
|
641
|
+
? `\n\n⚠️ ${wikilinkIssues.length} wikilink issue(s):\n${wikilinkIssues.map((m) => `- ${m}`).join("\n")}`
|
|
642
|
+
: "";
|
|
598
643
|
return {
|
|
599
|
-
content: [{ type: "text", text: `✅ Created ${type} page: \`${pagePath}
|
|
600
|
-
details: {
|
|
644
|
+
content: [{ type: "text", text: `✅ Created ${type} page: \`${pagePath}\`${gateNote}` }],
|
|
645
|
+
details: {
|
|
646
|
+
path: pagePath,
|
|
647
|
+
created: true,
|
|
648
|
+
wikilinkIssues,
|
|
649
|
+
} as Record<string, unknown>,
|
|
601
650
|
};
|
|
602
651
|
},
|
|
603
652
|
});
|
|
@@ -871,7 +920,7 @@ function runWikiLint(paths: VaultPaths, autoFix: boolean): string {
|
|
|
871
920
|
|
|
872
921
|
const discovery = discoverKnowledgeDocuments(paths);
|
|
873
922
|
const pages = discovery.documents;
|
|
874
|
-
const
|
|
923
|
+
const wikilinkIndex = buildWikilinkIndex(pages.map((page) => page.id));
|
|
875
924
|
const inbound = Object.fromEntries(pages.map((page) => [page.id, 0]));
|
|
876
925
|
const gapSources = new Map<string, Set<string>>();
|
|
877
926
|
const findings: string[] = [];
|
|
@@ -879,7 +928,7 @@ function runWikiLint(paths: VaultPaths, autoFix: boolean): string {
|
|
|
879
928
|
let contradictions = 0;
|
|
880
929
|
|
|
881
930
|
for (const page of pages) {
|
|
882
|
-
const resolved = buildResolvedBacklinks(page.id, page.body,
|
|
931
|
+
const resolved = buildResolvedBacklinks(page.id, page.body, wikilinkIndex);
|
|
883
932
|
for (const target of resolved.targets) inbound[target]++;
|
|
884
933
|
for (const unresolved of resolved.unresolved) {
|
|
885
934
|
const sources = gapSources.get(unresolved.target) ?? new Set<string>();
|
|
@@ -888,6 +937,11 @@ function runWikiLint(paths: VaultPaths, autoFix: boolean): string {
|
|
|
888
937
|
missingPages++;
|
|
889
938
|
findings.push(`Missing page: ${unresolved.target} (in ${page.id})`);
|
|
890
939
|
}
|
|
940
|
+
for (const d of resolved.diagnostics) {
|
|
941
|
+
if (d.code === "link_ambiguous") {
|
|
942
|
+
findings.push(d.message.replace("Ambiguous wikilink: ", "Ambiguous: "));
|
|
943
|
+
}
|
|
944
|
+
}
|
|
891
945
|
}
|
|
892
946
|
|
|
893
947
|
let orphans = 0;
|
package/mcp/index.ts
CHANGED
|
@@ -15,6 +15,10 @@ import { join } from "node:path";
|
|
|
15
15
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
16
16
|
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
17
17
|
import * as z from "zod/v4";
|
|
18
|
+
import {
|
|
19
|
+
loadTaskConfig,
|
|
20
|
+
resolveWikilinkValidation,
|
|
21
|
+
} from "../extensions/llm-wiki/lib/task-config.js";
|
|
18
22
|
import { getVaultPaths, resolveVaultPaths } from "../extensions/llm-wiki/lib/utils.js";
|
|
19
23
|
import { createExecApi } from "./exec.js";
|
|
20
24
|
import {
|
|
@@ -265,7 +269,14 @@ server.registerTool(
|
|
|
265
269
|
}
|
|
266
270
|
|
|
267
271
|
const paths = getPaths();
|
|
268
|
-
const result = await retroOperation(
|
|
272
|
+
const result = await retroOperation(
|
|
273
|
+
paths,
|
|
274
|
+
slug,
|
|
275
|
+
title,
|
|
276
|
+
body,
|
|
277
|
+
category,
|
|
278
|
+
resolveWikilinkValidation(loadTaskConfig(process.cwd())),
|
|
279
|
+
);
|
|
269
280
|
|
|
270
281
|
if (!result.ok) {
|
|
271
282
|
return {
|
package/mcp/operations.ts
CHANGED
|
@@ -6,13 +6,20 @@
|
|
|
6
6
|
* registry entries, or builds page strings itself.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { join } from "node:path";
|
|
9
10
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
10
11
|
import { bootstrapVault } from "../extensions/llm-wiki/lib/bootstrap.js";
|
|
12
|
+
import {
|
|
13
|
+
type WikilinkValidationMode,
|
|
14
|
+
applyWikilinkGate,
|
|
15
|
+
buildWikilinkIndex,
|
|
16
|
+
} from "../extensions/llm-wiki/lib/knowledge-links.js";
|
|
11
17
|
import { type ProjectionResult, rebuildMetadata } from "../extensions/llm-wiki/lib/metadata.js";
|
|
12
18
|
import { type RecallResult, searchWikiLayered } from "../extensions/llm-wiki/lib/recall.js";
|
|
13
19
|
import { saveInsight } from "../extensions/llm-wiki/lib/retro.js";
|
|
14
20
|
import { captureFile, captureText, captureUrl } from "../extensions/llm-wiki/lib/source-packet.js";
|
|
15
|
-
import
|
|
21
|
+
import { resolveWikilinkValidation } from "../extensions/llm-wiki/lib/task-config.js";
|
|
22
|
+
import { type VaultPaths, readJson } from "../extensions/llm-wiki/lib/utils.js";
|
|
16
23
|
import {
|
|
17
24
|
VaultWriteError,
|
|
18
25
|
inspectVaultFormat,
|
|
@@ -134,6 +141,7 @@ export async function retroOperation(
|
|
|
134
141
|
title: string,
|
|
135
142
|
body: string,
|
|
136
143
|
category?: string,
|
|
144
|
+
wikilinkValidation?: WikilinkValidationMode,
|
|
137
145
|
): Promise<
|
|
138
146
|
| { ok: true; slug: string; sourcePagePath: string }
|
|
139
147
|
| { ok: false; diagnostics: Array<{ code: string; message: string }> }
|
|
@@ -145,8 +153,30 @@ export async function retroOperation(
|
|
|
145
153
|
diagnostics: vaultCheck.diagnostics.map((d) => ({ code: d.code, message: d.message })),
|
|
146
154
|
};
|
|
147
155
|
}
|
|
156
|
+
// Pre-write wikilink gate (#172): validate/normalize caller-supplied body.
|
|
157
|
+
const mode = resolveWikilinkValidation({ wikilinkValidation });
|
|
158
|
+
let gateBody = body;
|
|
159
|
+
if (mode !== "off") {
|
|
160
|
+
const registry = readJson<{ pages: Record<string, unknown> }>(
|
|
161
|
+
join(paths.meta, "registry.json"),
|
|
162
|
+
{ pages: {} },
|
|
163
|
+
);
|
|
164
|
+
const gate = applyWikilinkGate(
|
|
165
|
+
body,
|
|
166
|
+
buildWikilinkIndex(Object.keys(registry.pages)),
|
|
167
|
+
`sources/${slug}`,
|
|
168
|
+
mode,
|
|
169
|
+
);
|
|
170
|
+
if (!gate.ok) {
|
|
171
|
+
return {
|
|
172
|
+
ok: false,
|
|
173
|
+
diagnostics: gate.diagnostics.map((d) => ({ code: "link_validation", message: d.message })),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (mode === "normalize") gateBody = gate.body;
|
|
177
|
+
}
|
|
148
178
|
try {
|
|
149
|
-
const result = saveInsight(paths, slug, title,
|
|
179
|
+
const result = saveInsight(paths, slug, title, gateBody, category, { rebuild: false });
|
|
150
180
|
const projection = projectionOutcome(rebuildMetadata(paths));
|
|
151
181
|
if (!projection.ok) return projection;
|
|
152
182
|
return { ok: true, slug: result.slug, sourcePagePath: result.sourcePagePath };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zosmaai/pi-llm-wiki",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.6",
|
|
4
4
|
"description": "Self-maintaining LLM Wiki for Pi — Karpathy-pattern knowledge base with immutable source capture, automated ingestion, search, linting, and Obsidian-compatible vault. auto-updating personal & company wiki.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi",
|