@zosmaai/pi-llm-wiki 0.1.0

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.
@@ -0,0 +1,40 @@
1
+ ---
2
+ type: synthesis
3
+ topic: analysis topic
4
+ created: YYYY-MM-DD
5
+ updated: YYYY-MM-DD
6
+ sources_count: N
7
+ ---
8
+
9
+ # Synthesis Title
10
+
11
+ > _Cross-cutting analysis derived from [N] sources._
12
+
13
+ ## Question
14
+
15
+ [The question or purpose that drove this analysis.]
16
+
17
+ ## Analysis
18
+
19
+ [Synthesized content drawing from multiple sources. Identify patterns, connections, and contradictions across sources.]
20
+
21
+ ## Key Insights
22
+
23
+ 1. [Insight 1 — novel connection discovered]
24
+ 2. [Insight 2 — pattern across sources]
25
+ 3. [Insight 3 — remaining question or tension]
26
+
27
+ ## Conclusion
28
+
29
+ [Summary of findings and implications.]
30
+
31
+ ## Sources Used
32
+
33
+ - [[source-1]]
34
+ - [[source-2]]
35
+ - [[source-3]]
36
+
37
+ ## Related Pages
38
+
39
+ - [[concept-1]]
40
+ - [[entity-1]]
@@ -0,0 +1,404 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
6
+
7
+ // ─── Helpers ────────────────────────────────────────────
8
+
9
+ let tempDir: string;
10
+ const __fname = typeof __filename !== "undefined" ? __filename : "";
11
+ const __dname =
12
+ typeof __dirname !== "undefined"
13
+ ? __dirname
14
+ : typeof import.meta !== "undefined" && import.meta.dirname
15
+ ? import.meta.dirname
16
+ : dirname(fileURLToPath(__fname || `file://${process.cwd()}/test/dummy.ts`));
17
+
18
+ const rootDir = resolve(__dname, "..");
19
+
20
+ function readFile(path: string): string {
21
+ return readFileSync(path, { encoding: "utf-8" });
22
+ }
23
+
24
+ function createWikiRoot(): string {
25
+ const dir = join(tempDir, `wiki-${Math.random().toString(36).slice(2)}`);
26
+ mkdirSync(dir, { recursive: true });
27
+
28
+ const dirs = [
29
+ "raw/articles",
30
+ "raw/papers",
31
+ "raw/notes",
32
+ "raw/assets",
33
+ "wiki/entities",
34
+ "wiki/concepts",
35
+ "wiki/sources",
36
+ "wiki/syntheses",
37
+ "wiki/changes",
38
+ "outputs",
39
+ ".discoveries",
40
+ ];
41
+ for (const d of dirs) mkdirSync(join(dir, d), { recursive: true });
42
+
43
+ return dir;
44
+ }
45
+
46
+ function createConfig(dir: string, overrides: Record<string, unknown> = {}) {
47
+ const defaults: Record<string, unknown> = {
48
+ wiki: { mode: "personal", topic: "Test Topic" },
49
+ change_detection: false,
50
+ };
51
+ const config = { ...defaults, ...overrides } as Record<string, Record<string, unknown>>;
52
+ const mode = config.wiki?.mode || "personal";
53
+ const topic = config.wiki?.topic || "Test Topic";
54
+ writeFileSync(
55
+ join(dir, "config.yaml"),
56
+ `# LLM Wiki Configuration\nwiki:\n mode: ${mode}\n topic: "${topic}"\n`,
57
+ );
58
+ }
59
+
60
+ function createSourceFile(dir: string, name: string, content: string) {
61
+ writeFileSync(join(dir, "raw", "articles", name), content);
62
+ }
63
+
64
+ function createWikiPage(dir: string, subdir: string | "", name: string, content: string) {
65
+ const target = subdir ? join(dir, "wiki", subdir, name) : join(dir, "wiki", name);
66
+ writeFileSync(target, content);
67
+ }
68
+
69
+ // ─── Package Structure Tests ────────────────────────────
70
+
71
+ describe("package structure", () => {
72
+ it("should have a valid package.json with pi manifest", () => {
73
+ const pkg = JSON.parse(readFile(join(rootDir, "package.json")));
74
+ expect(pkg.name).toBe("@zosmaai/pi-llm-wiki");
75
+ expect(pkg.keywords).toContain("pi-package");
76
+ expect(pkg.pi.extensions).toContain("./extensions");
77
+ expect(pkg.pi.skills).toContain("./skills");
78
+ expect(pkg.pi.prompts).toContain("./prompts");
79
+ expect(pkg.peerDependencies).toBeDefined();
80
+ expect(pkg.peerDependencies["@mariozechner/pi-coding-agent"]).toBe("*");
81
+ expect(pkg.peerDependencies.typebox).toBe("*");
82
+ });
83
+
84
+ it("should have a SKILL.md with valid frontmatter and schema content", () => {
85
+ const skillPath = join(rootDir, "skills", "llm-wiki", "SKILL.md");
86
+ expect(existsSync(skillPath)).toBe(true);
87
+ const content = readFile(skillPath);
88
+ expect(content).toContain("name: llm-wiki");
89
+ expect(content).toContain("## Golden Rules");
90
+ expect(content).toContain("RAW IS IMMUTABLE");
91
+ expect(content).toContain("## Workflows");
92
+ expect(content).toContain("/wiki:ingest");
93
+ expect(content).toContain("Obsidian Integration");
94
+ expect(content).toContain("Personal Wiki");
95
+ expect(content).toContain("Company Wiki");
96
+ });
97
+
98
+ it("should have all 8 prompt templates with frontmatter", () => {
99
+ const prompts = [
100
+ "wiki-init.md",
101
+ "wiki-ingest.md",
102
+ "wiki-query.md",
103
+ "wiki-lint.md",
104
+ "wiki-discover.md",
105
+ "wiki-run.md",
106
+ "wiki-status.md",
107
+ "wiki-digest.md",
108
+ ];
109
+ for (const prompt of prompts) {
110
+ const path = join(rootDir, "prompts", prompt);
111
+ expect(existsSync(path)).toBe(true);
112
+ const content = readFile(path);
113
+ expect(content).toContain("description:");
114
+ expect(content).toContain("section: LLM Wiki");
115
+ expect(content).toContain("topLevelCli: true");
116
+ }
117
+ });
118
+
119
+ it("should have all wiki template files", () => {
120
+ const t = join(rootDir, "skills", "llm-wiki", "templates");
121
+ expect(existsSync(join(t, "INDEX.md"))).toBe(true);
122
+ expect(existsSync(join(t, "LOG.md"))).toBe(true);
123
+ expect(existsSync(join(t, "DASHBOARD.md"))).toBe(true);
124
+ expect(existsSync(join(t, "config.yaml"))).toBe(true);
125
+ expect(existsSync(join(t, "pages", "entity.md"))).toBe(true);
126
+ expect(existsSync(join(t, "pages", "concept.md"))).toBe(true);
127
+ expect(existsSync(join(t, "pages", "source.md"))).toBe(true);
128
+ expect(existsSync(join(t, "pages", "synthesis.md"))).toBe(true);
129
+ });
130
+
131
+ it("should have the extension file with all custom tools", () => {
132
+ const extPath = join(rootDir, "extensions", "llm-wiki-tools.ts");
133
+ expect(existsSync(extPath)).toBe(true);
134
+ const content = readFile(extPath);
135
+ expect(content).toContain("ExtensionAPI");
136
+ expect(content).toContain("registerTool");
137
+ const tools = [
138
+ "wiki_ingest",
139
+ "wiki_status_report",
140
+ "wiki_lint_report",
141
+ "wiki_discover_sources",
142
+ "wiki_watch",
143
+ ];
144
+ for (const tool of tools) {
145
+ expect(content).toContain(tool);
146
+ }
147
+ });
148
+
149
+ it("should have a comprehensive README with install instructions", () => {
150
+ const readme = readFile(join(rootDir, "README.md"));
151
+ expect(readme).toContain("@zosmaai/pi-llm-wiki");
152
+ expect(readme).toContain("pi install npm:@zosmaai/pi-llm-wiki");
153
+ expect(readme).toContain("Karpathy");
154
+ expect(readme).toContain("Obsidian");
155
+ expect(readme).toContain("Personal Wiki");
156
+ expect(readme).toContain("Company Wiki");
157
+ });
158
+ });
159
+
160
+ // ─── SKILL.md Frontmatter Validation ────────────────────
161
+
162
+ describe("skill frontmatter validation", () => {
163
+ const skillPath = join(rootDir, "skills", "llm-wiki", "SKILL.md");
164
+
165
+ it("should have name matching directory, lowercase with hyphens only", () => {
166
+ const content = readFile(skillPath);
167
+ const match = content.match(/^---\n([\s\S]*?)\n---/) as RegExpMatchArray | null;
168
+ expect(match).not.toBeNull();
169
+ const frontmatter = match![1];
170
+ expect(frontmatter).toContain("name: llm-wiki");
171
+
172
+ const nameMatch = frontmatter.match(/name:\s*(\S+)/);
173
+ expect(nameMatch).not.toBeNull();
174
+ const name = nameMatch![1];
175
+ expect(name).toMatch(/^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$/);
176
+ expect(name.length).toBeLessThanOrEqual(64);
177
+ expect(name).not.toContain("--");
178
+ expect(name).not.toMatch(/^-|-$/);
179
+ });
180
+
181
+ it("should have a description under 1024 characters", () => {
182
+ const content = readFile(skillPath);
183
+ const match = content.match(/^---\n([\s\S]*?)\n---/) as RegExpMatchArray | null;
184
+ expect(match).not.toBeNull();
185
+ const descMatch = match![1].match(/description:\s*(.+)/);
186
+ expect(descMatch).not.toBeNull();
187
+ expect(descMatch![1].length).toBeLessThanOrEqual(1024);
188
+ });
189
+ });
190
+
191
+ // ─── Wiki Directory Structure Tests ─────────────────────
192
+
193
+ describe("wiki directory structure", () => {
194
+ let wikiDir: string;
195
+
196
+ beforeEach(() => {
197
+ tempDir = join(tmpdir(), `pi-llm-wiki-test-${Date.now()}`);
198
+ mkdirSync(tempDir, { recursive: true });
199
+ wikiDir = createWikiRoot();
200
+ });
201
+
202
+ afterEach(() => {
203
+ rmSync(tempDir, { recursive: true, force: true });
204
+ });
205
+
206
+ it("should have all required directories", () => {
207
+ expect(existsSync(join(wikiDir, "raw", "articles"))).toBe(true);
208
+ expect(existsSync(join(wikiDir, "raw", "papers"))).toBe(true);
209
+ expect(existsSync(join(wikiDir, "raw", "notes"))).toBe(true);
210
+ expect(existsSync(join(wikiDir, "wiki", "entities"))).toBe(true);
211
+ expect(existsSync(join(wikiDir, "wiki", "concepts"))).toBe(true);
212
+ expect(existsSync(join(wikiDir, "wiki", "sources"))).toBe(true);
213
+ expect(existsSync(join(wikiDir, "wiki", "syntheses"))).toBe(true);
214
+ expect(existsSync(join(wikiDir, "wiki", "changes"))).toBe(true);
215
+ expect(existsSync(join(wikiDir, "outputs"))).toBe(true);
216
+ expect(existsSync(join(wikiDir, ".discoveries"))).toBe(true);
217
+ });
218
+
219
+ it("should create source pages from ingested files", () => {
220
+ createConfig(wikiDir);
221
+ createSourceFile(wikiDir, "test-article.md", "# Test\nContent about AI.");
222
+ expect(existsSync(join(wikiDir, "raw", "articles", "test-article.md"))).toBe(true);
223
+
224
+ createWikiPage(
225
+ wikiDir,
226
+ "sources",
227
+ "test-article.md",
228
+ "---\ntype: source\nformat: article\nraw_path: raw/articles/test-article.md\ningested: 2026-04-27\ntopics: [ai]\n---\n\n# Test Article\n\n## Summary\nAI content.\n",
229
+ );
230
+ const content = readFile(join(wikiDir, "wiki", "sources", "test-article.md"));
231
+ expect(content).toContain("type: source");
232
+ expect(content).toContain("raw_path: raw/articles/test-article.md");
233
+ });
234
+
235
+ it("should create entity pages with correct format", () => {
236
+ createWikiPage(
237
+ wikiDir,
238
+ "entities",
239
+ "test-entity.md",
240
+ "---\ntype: entity\ncategory: person\ncreated: 2026-04-27\nupdated: 2026-04-27\nsources: [raw/articles/test.md]\n---\n\n# Person\n\n## Links\n- [[related-concept]]\n\n## Sources\n- [test](../raw/articles/test.md)\n",
241
+ );
242
+ const content = readFile(join(wikiDir, "wiki", "entities", "test-entity.md"));
243
+ expect(content).toContain("type: entity");
244
+ expect(content).toContain("category: person");
245
+ expect(content).toContain("[[related-concept]]");
246
+ });
247
+
248
+ it("should create concept pages with correct format", () => {
249
+ createWikiPage(
250
+ wikiDir,
251
+ "concepts",
252
+ "test-concept.md",
253
+ "---\ntype: concept\ndomain: engineering\ncreated: 2026-04-27\nupdated: 2026-04-27\nsources: []\n---\n\n# Test Concept\n\n## Links\n- [[other-concept]]\n",
254
+ );
255
+ const content = readFile(join(wikiDir, "wiki", "concepts", "test-concept.md"));
256
+ expect(content).toContain("type: concept");
257
+ expect(content).toContain("domain: engineering");
258
+ expect(content).toContain("[[other-concept]]");
259
+ });
260
+
261
+ it("should create synthesis pages with correct format", () => {
262
+ createWikiPage(
263
+ wikiDir,
264
+ "syntheses",
265
+ "comparison.md",
266
+ "---\ntype: synthesis\ntopic: comparison\ncreated: 2026-04-27\nupdated: 2026-04-27\nsources_count: 2\n---\n\n# Comparison\n\n## Sources Used\n- [[source-1]]\n- [[source-2]]\n",
267
+ );
268
+ const content = readFile(join(wikiDir, "wiki", "syntheses", "comparison.md"));
269
+ expect(content).toContain("type: synthesis");
270
+ expect(content).toContain("sources_count: 2");
271
+ expect(content).toContain("[[source-1]]");
272
+ });
273
+
274
+ it("should maintain INDEX.md catalog", () => {
275
+ createWikiPage(
276
+ wikiDir,
277
+ "",
278
+ "INDEX.md",
279
+ "# Wiki Index\n\n## Entities\n- [test](entities/test.md)\n",
280
+ );
281
+ const content = readFile(join(wikiDir, "wiki", "INDEX.md"));
282
+ expect(content).toContain("test");
283
+ });
284
+
285
+ it("should append to LOG.md", () => {
286
+ writeFileSync(join(wikiDir, "wiki", "LOG.md"), "## [2026-04-27] ingest | 3 pages\n");
287
+ const content = readFile(join(wikiDir, "wiki", "LOG.md"));
288
+ expect(content).toContain("ingest");
289
+ expect(content).toContain("3 pages");
290
+ });
291
+
292
+ it("should handle contradiction markers", () => {
293
+ createWikiPage(
294
+ wikiDir,
295
+ "concepts",
296
+ "conflict.md",
297
+ "---\ntype: concept\ndomain: ai\ncreated: 2026-04-27\nupdated: 2026-04-27\nsources: [a.md, b.md]\n---\n\n> ⚠️ **Contradiction:** A claims X but B claims Y.\n",
298
+ );
299
+ const content = readFile(join(wikiDir, "wiki", "concepts", "conflict.md"));
300
+ expect(content).toContain("Contradiction:");
301
+ });
302
+ });
303
+
304
+ // ─── Cross-Reference Integrity ─────────────────────────
305
+
306
+ describe("cross-reference integrity", () => {
307
+ let wikiDir: string;
308
+
309
+ beforeEach(() => {
310
+ tempDir = join(tmpdir(), `pi-llm-wiki-xref-${Date.now()}`);
311
+ mkdirSync(tempDir, { recursive: true });
312
+ wikiDir = createWikiRoot();
313
+ });
314
+
315
+ afterEach(() => {
316
+ rmSync(tempDir, { recursive: true, force: true });
317
+ });
318
+
319
+ it("should allow orphan detection by absence of inbound wikilinks", () => {
320
+ createWikiPage(
321
+ wikiDir,
322
+ "entities",
323
+ "orphan.md",
324
+ "---\ntype: entity\ncategory: person\ncreated: 2026-04-27\nupdated: 2026-04-27\nsources: []\n---\n# Orphan\n",
325
+ );
326
+ const content = readFile(join(wikiDir, "wiki", "entities", "orphan.md"));
327
+ expect(content).not.toContain("[[orphan");
328
+ });
329
+
330
+ it("should detect broken wikilinks referencing nonexistent pages", () => {
331
+ createWikiPage(
332
+ wikiDir,
333
+ "concepts",
334
+ "main.md",
335
+ "---\ntype: concept\ndomain: ai\ncreated: 2026-04-27\nupdated: 2026-04-27\nsources: []\n---\n\n# Main\n[[missing-page]] and [[another-missing]]\n",
336
+ );
337
+ const content = readFile(join(wikiDir, "wiki", "concepts", "main.md"));
338
+ expect(content).toContain("[[missing-page]]");
339
+ expect(content).toContain("[[another-missing]]");
340
+ expect(existsSync(join(wikiDir, "wiki", "entities", "missing-page.md"))).toBe(false);
341
+ expect(existsSync(join(wikiDir, "wiki", "concepts", "missing-page.md"))).toBe(false);
342
+ });
343
+ });
344
+
345
+ // ─── Configuration Tests ──────────────────────────────
346
+
347
+ describe("configuration", () => {
348
+ let wikiDir: string;
349
+
350
+ beforeEach(() => {
351
+ tempDir = join(tmpdir(), `pi-llm-wiki-config-${Date.now()}`);
352
+ mkdirSync(tempDir, { recursive: true });
353
+ wikiDir = createWikiRoot();
354
+ });
355
+
356
+ afterEach(() => {
357
+ rmSync(tempDir, { recursive: true, force: true });
358
+ });
359
+
360
+ it("should accept personal mode config", () => {
361
+ createConfig(wikiDir, { wiki: { mode: "personal", topic: "Learning" } });
362
+ const config = readFile(join(wikiDir, "config.yaml"));
363
+ expect(config).toContain("mode: personal");
364
+ });
365
+
366
+ it("should accept company mode config", () => {
367
+ createConfig(wikiDir, { wiki: { mode: "company", topic: "Competitors" } });
368
+ const config = readFile(join(wikiDir, "config.yaml"));
369
+ expect(config).toContain("mode: company");
370
+ });
371
+
372
+ it("should support company mode with change detection pages", () => {
373
+ createConfig(wikiDir, { wiki: { mode: "company", topic: "Market" }, change_detection: true });
374
+ const config = readFile(join(wikiDir, "config.yaml"));
375
+ expect(config).toContain("mode: company");
376
+
377
+ createWikiPage(
378
+ wikiDir,
379
+ "changes",
380
+ "competitor-2026-04-27.md",
381
+ "---\ntype: change\nentity: competitor\ndetected: 2026-04-27\n---\n\n# Change\nPricing changed from $99 to $149.\n",
382
+ );
383
+ expect(existsSync(join(wikiDir, "wiki", "changes", "competitor-2026-04-27.md"))).toBe(true);
384
+ const content = readFile(join(wikiDir, "wiki", "changes", "competitor-2026-04-27.md"));
385
+ expect(content).toContain("type: change");
386
+ expect(content).toContain("Pricing changed");
387
+ });
388
+
389
+ it("should track discovery history", () => {
390
+ const history = { processed: [{ path: "raw/articles/a.md", ingested: "2026-04-27" }] };
391
+ writeFileSync(join(wikiDir, ".discoveries", "history.json"), JSON.stringify(history));
392
+ const content = JSON.parse(readFile(join(wikiDir, ".discoveries", "history.json")));
393
+ expect(content.processed).toHaveLength(1);
394
+ expect(content.processed[0].path).toBe("raw/articles/a.md");
395
+ });
396
+
397
+ it("should track knowledge gaps", () => {
398
+ const gaps = { gaps: [{ topic: "reinforcement learning", priority: "high" }] };
399
+ writeFileSync(join(wikiDir, ".discoveries", "gaps.json"), JSON.stringify(gaps));
400
+ const content = JSON.parse(readFile(join(wikiDir, ".discoveries", "gaps.json")));
401
+ expect(content.gaps).toHaveLength(1);
402
+ expect(content.gaps[0].priority).toBe("high");
403
+ });
404
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2022"],
7
+ "types": ["node"],
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "noEmit": true,
13
+ "isolatedModules": true,
14
+ "outDir": "dist",
15
+ "rootDir": "."
16
+ },
17
+ "include": ["extensions/**/*.ts", "test/**/*.ts"],
18
+ "exclude": ["node_modules", "dist"]
19
+ }
@@ -0,0 +1,16 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ include: ["test/**/*.test.ts"],
7
+ environment: "node",
8
+ testTimeout: 10_000,
9
+ hookTimeout: 10_000,
10
+ coverage: {
11
+ provider: "v8",
12
+ reporter: ["text", "lcov", "html"],
13
+ include: ["extensions/**/*.ts", "skills/**/*.md"],
14
+ },
15
+ },
16
+ });