pi-jev-wiki 0.2.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.
- package/CHANGELOG.md +52 -0
- package/LICENSE +21 -0
- package/README.md +152 -0
- package/package.json +63 -0
- package/skills/llm-wiki/SKILL.md +143 -0
- package/skills/llm-wiki/references/decision.md +32 -0
- package/skills/llm-wiki/references/flow.md +32 -0
- package/skills/llm-wiki/references/gotcha.md +16 -0
- package/skills/llm-wiki/references/invariant.md +20 -0
- package/skills/llm-wiki/references/module.md +30 -0
- package/src/config.ts +168 -0
- package/src/doctor.ts +171 -0
- package/src/extension.ts +1674 -0
- package/src/git.ts +69 -0
- package/src/grounding.ts +46 -0
- package/src/jev.ts +207 -0
- package/src/ledger.ts +85 -0
- package/src/lint.ts +407 -0
- package/src/metrics.ts +61 -0
- package/src/pipeline/adjudicate.ts +416 -0
- package/src/pipeline/capture.ts +109 -0
- package/src/pipeline/extract.ts +150 -0
- package/src/pipeline/write.ts +263 -0
- package/src/provenance.ts +137 -0
- package/src/redact.ts +46 -0
- package/src/review.ts +146 -0
- package/src/sessionlog.ts +107 -0
- package/src/structure.ts +215 -0
- package/src/sync.ts +295 -0
- package/src/wiki/frontmatter.ts +164 -0
- package/src/wiki/layout.ts +126 -0
- package/src/wiki/links.ts +17 -0
- package/src/wiki/lock.ts +86 -0
- package/src/wiki/search.ts +263 -0
- package/src/wiki/toc.ts +198 -0
package/src/wiki/toc.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wiki table of contents (`index.md`) and the append-only log (`log.md`).
|
|
3
|
+
*
|
|
4
|
+
* Index format (Obsidian-friendly tables, machine-parseable):
|
|
5
|
+
*
|
|
6
|
+
* ## architecture
|
|
7
|
+
*
|
|
8
|
+
* | Page | Type | Tags | Summary | Updated |
|
|
9
|
+
* |------|------|------|---------|---------|
|
|
10
|
+
* | [Auth module](architecture/module-auth.md) | architecture/module | auth core | Owns token validation | 2026-09-19 |
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync } from "node:fs";
|
|
13
|
+
import { readFile } from "node:fs/promises";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { todayISO, writeTextAtomic, type WikiLayout } from "./layout.ts";
|
|
16
|
+
import { withWikiLock } from "./lock.ts";
|
|
17
|
+
|
|
18
|
+
export interface TocEntry {
|
|
19
|
+
path: string;
|
|
20
|
+
title: string;
|
|
21
|
+
type: string;
|
|
22
|
+
tags: string[];
|
|
23
|
+
summary: string;
|
|
24
|
+
updated: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const ROW_RE = /^\|\s*\[(.*?)\]\((.*?)\)\s*\|\s*(.*?)\s*\|\s*(.*?)\s*\|\s*(.*?)\s*\|\s*(.*?)\s*\|\s*$/;
|
|
28
|
+
|
|
29
|
+
export function parseIndex(text: string): TocEntry[] {
|
|
30
|
+
const entries: TocEntry[] = [];
|
|
31
|
+
for (const line of text.split(/\r?\n/)) {
|
|
32
|
+
const match = line.match(ROW_RE);
|
|
33
|
+
if (!match) continue;
|
|
34
|
+
const [, title, path, type, tags, summary, updated] = match;
|
|
35
|
+
if (path.includes("](")) continue;
|
|
36
|
+
entries.push({
|
|
37
|
+
path: path.trim(),
|
|
38
|
+
title: title.trim(),
|
|
39
|
+
type: type.trim(),
|
|
40
|
+
tags: tags.trim() ? tags.trim().split(/\s+/) : [],
|
|
41
|
+
summary: summary.trim(),
|
|
42
|
+
updated: updated.trim(),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return entries;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function escapeCell(value: string): string {
|
|
49
|
+
return value.replace(/\|/g, "\\|").replace(/\r?\n/g, " ").trim();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function topicOf(path: string): string {
|
|
53
|
+
const parts = path.split("/");
|
|
54
|
+
return parts.length > 1 ? parts[0] : "general";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Wiki-generated navigation files that are not knowledge pages. */
|
|
58
|
+
export function isWikiMetaFile(relPath: string): boolean {
|
|
59
|
+
const rel = relPath.split("\\").join("/");
|
|
60
|
+
return rel === "index.md" || rel === "log.md" || rel === "toc.md" || rel.startsWith("toc/");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function topicSlug(topic: string): string {
|
|
64
|
+
return topic.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "") || "general";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function groupEntries(entries: TocEntry[]): Map<string, TocEntry[]> {
|
|
68
|
+
const groups = new Map<string, TocEntry[]>();
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
if (entry.path === "index.md" || entry.path === "log.md") continue;
|
|
71
|
+
const topic = topicOf(entry.path);
|
|
72
|
+
const list = groups.get(topic) ?? [];
|
|
73
|
+
list.push(entry);
|
|
74
|
+
groups.set(topic, list);
|
|
75
|
+
}
|
|
76
|
+
return groups;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function renderTable(entries: TocEntry[]): string[] {
|
|
80
|
+
const lines = ["| Page | Type | Tags | Summary | Updated |", "|------|------|------|---------|---------|"];
|
|
81
|
+
for (const entry of entries) {
|
|
82
|
+
lines.push(
|
|
83
|
+
`| [${escapeCell(entry.title)}](${entry.path}) | ${escapeCell(entry.type)} | ${escapeCell(entry.tags.join(" "))} | ${escapeCell(entry.summary)} | ${escapeCell(entry.updated)} |`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return lines;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function renderIndex(entries: TocEntry[]): string {
|
|
90
|
+
const groups = groupEntries(entries);
|
|
91
|
+
|
|
92
|
+
const lines: string[] = [
|
|
93
|
+
"# Wiki Index",
|
|
94
|
+
"",
|
|
95
|
+
"> Complete machine-readable catalog, generated by jev-wiki; edit pages, not this file.",
|
|
96
|
+
"",
|
|
97
|
+
];
|
|
98
|
+
for (const topic of [...groups.keys()].sort()) {
|
|
99
|
+
const list = groups.get(topic)!.sort((a, b) => a.title.localeCompare(b.title));
|
|
100
|
+
lines.push(`## ${topic}`, "", ...renderTable(list), "");
|
|
101
|
+
}
|
|
102
|
+
return `${lines.join("\n").replace(/\s+$/, "")}\n`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Compact agent-facing table of contents: topics, counts, and links to per-topic tables. */
|
|
106
|
+
export function renderCompactToc(entries: TocEntry[]): string {
|
|
107
|
+
const groups = groupEntries(entries);
|
|
108
|
+
const lines: string[] = [
|
|
109
|
+
"# Wiki TOC",
|
|
110
|
+
"",
|
|
111
|
+
`> ${entries.length} pages across ${groups.size} topics. Per-topic tables: \`toc/<topic>.md\`. Full machine index: \`index.md\`.`,
|
|
112
|
+
"",
|
|
113
|
+
"| Topic | Pages | Table |",
|
|
114
|
+
"|-------|-------|-------|",
|
|
115
|
+
];
|
|
116
|
+
for (const topic of [...groups.keys()].sort()) {
|
|
117
|
+
lines.push(`| ${topic} | ${groups.get(topic)!.length} | [toc/${topicSlug(topic)}.md](toc/${topicSlug(topic)}.md) |`);
|
|
118
|
+
}
|
|
119
|
+
const recent = [...entries].filter((entry) => entry.path !== "index.md" && entry.path !== "log.md").sort((a, b) => b.updated.localeCompare(a.updated)).slice(0, 5);
|
|
120
|
+
if (recent.length > 0) {
|
|
121
|
+
lines.push("", "## Recently updated", ...recent.map((entry) => `- [${entry.title}](${entry.path}) — ${entry.summary} (${entry.updated})`));
|
|
122
|
+
}
|
|
123
|
+
return `${lines.join("\n").replace(/\s+$/, "")}\n`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Full table for a single topic. */
|
|
127
|
+
export function renderTopicToc(entries: TocEntry[], topic: string): string {
|
|
128
|
+
const list = entries.filter((entry) => topicOf(entry.path) === topic).sort((a, b) => a.title.localeCompare(b.title));
|
|
129
|
+
return [`# ${topic}`, "", ...renderTable(list)].join("\n") + "\n";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function upsertEntries(existing: TocEntry[], updates: TocEntry[]): TocEntry[] {
|
|
133
|
+
const byPath = new Map(existing.map((entry) => [entry.path, entry]));
|
|
134
|
+
for (const update of updates) byPath.set(update.path, update);
|
|
135
|
+
return [...byPath.values()];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function readIndex(layout: WikiLayout): Promise<TocEntry[]> {
|
|
139
|
+
const path = join(layout.wikiDir, "index.md");
|
|
140
|
+
if (!existsSync(path)) return [];
|
|
141
|
+
return parseIndex(await readFile(path, "utf8"));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function writeIndex(layout: WikiLayout, entries: TocEntry[]): Promise<void> {
|
|
145
|
+
await writeTextAtomic(join(layout.wikiDir, "index.md"), renderIndex(entries));
|
|
146
|
+
await writeTextAtomic(join(layout.wikiDir, "toc.md"), renderCompactToc(entries));
|
|
147
|
+
for (const topic of groupEntries(entries).keys()) {
|
|
148
|
+
await writeTextAtomic(join(layout.wikiDir, "toc", `${topicSlug(topic)}.md`), renderTopicToc(entries, topic));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Atomic read-modify-write of the index under the wiki lock. */
|
|
153
|
+
export async function updateIndex(
|
|
154
|
+
layout: WikiLayout,
|
|
155
|
+
mutator: (entries: TocEntry[]) => TocEntry[] | Promise<TocEntry[]>,
|
|
156
|
+
): Promise<TocEntry[]> {
|
|
157
|
+
return withWikiLock(layout, async () => {
|
|
158
|
+
const entries = await readIndex(layout);
|
|
159
|
+
const next = await mutator(entries);
|
|
160
|
+
await writeIndex(layout, next);
|
|
161
|
+
return next;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function entryFromPage(relPath: string, data: Record<string, unknown>): TocEntry {
|
|
166
|
+
return {
|
|
167
|
+
path: relPath.split("\\").join("/"),
|
|
168
|
+
title: String(data.title ?? relPath),
|
|
169
|
+
type: String(data.type ?? "concept"),
|
|
170
|
+
tags: Array.isArray(data.tags) ? data.tags.map(String) : [],
|
|
171
|
+
summary: String(data.summary ?? "(no summary)"),
|
|
172
|
+
updated: String(data.updated ?? todayISO()),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function appendLog(layout: WikiLayout, op: string, title: string, details: string[] = []): Promise<void> {
|
|
177
|
+
const path = join(layout.wikiDir, "log.md");
|
|
178
|
+
const header = `## [${todayISO()}] ${op} | ${title}`;
|
|
179
|
+
const block = [header, ...details.map((line) => `- ${line}`), ""].join("\n");
|
|
180
|
+
await withWikiLock(layout, async () => {
|
|
181
|
+
if (existsSync(path)) {
|
|
182
|
+
const existing = await readFile(path, "utf8");
|
|
183
|
+
await writeTextAtomic(path, `${existing.replace(/\s+$/, "")}\n\n${block}\n`);
|
|
184
|
+
} else {
|
|
185
|
+
await writeTextAtomic(path, `# Wiki Log\n\n${block}\n`);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function readRecentLog(layout: WikiLayout, limit = 5): Promise<string[]> {
|
|
191
|
+
const path = join(layout.wikiDir, "log.md");
|
|
192
|
+
if (!existsSync(path)) return [];
|
|
193
|
+
const text = await readFile(path, "utf8");
|
|
194
|
+
return text
|
|
195
|
+
.split(/\r?\n/)
|
|
196
|
+
.filter((line) => line.startsWith("## ["))
|
|
197
|
+
.slice(-limit);
|
|
198
|
+
}
|