llm-wiki-compiler 0.7.0 → 0.8.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 +251 -0
- package/README.md +71 -9
- package/dist/cli.js +3072 -387
- package/dist/cli.js.map +1 -1
- package/dist/viewer/assets/THIRD_PARTY_NOTICES.txt +22 -0
- package/dist/viewer/assets/d3.min.js +2 -0
- package/dist/viewer/assets/index.html +4 -2
- package/dist/viewer/assets/viewer-graph.js +369 -0
- package/dist/viewer/assets/viewer-rail.js +115 -39
- package/dist/viewer/assets/viewer-search.js +48 -16
- package/dist/viewer/assets/viewer-sidebar.js +105 -35
- package/dist/viewer/assets/viewer.css +68 -1
- package/dist/viewer/assets/viewer.js +232 -115
- package/package.json +6 -3
|
@@ -22,43 +22,101 @@
|
|
|
22
22
|
import { wireSearch } from "./viewer-search.js";
|
|
23
23
|
import { renderSidebar, markActive } from "./viewer-sidebar.js";
|
|
24
24
|
import { renderProjectRail, renderSupportRail, clearSupportRail } from "./viewer-rail.js";
|
|
25
|
+
import { loadGraph } from "./viewer-graph.js";
|
|
25
26
|
|
|
26
27
|
const PAGE_INDEX_SELECTOR = "#page-index";
|
|
27
28
|
const MAIN_SELECTOR = "[data-main-pane]";
|
|
28
29
|
const TITLE_SELECTOR = "[data-app-title]";
|
|
30
|
+
const DEFAULT_TITLE = "llmwiki";
|
|
31
|
+
const EMPTY_INDEX = { pages: [] };
|
|
32
|
+
|
|
33
|
+
/** Hashes that all map to the home route — `#`, `#/`, and empty/missing. */
|
|
34
|
+
const HOME_HASHES = new Set(["", "#", "#/"]);
|
|
35
|
+
|
|
36
|
+
/** Static routes whose hash uniquely names the kind (no slug segment). */
|
|
37
|
+
const STATIC_ROUTES = new Map([
|
|
38
|
+
["#/index", { kind: "index" }],
|
|
39
|
+
["#/health", { kind: "health" }],
|
|
40
|
+
["#/graph", { kind: "graph" }],
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
/** Pattern matching `#/(concepts|queries)/<slug>` hash routes. */
|
|
44
|
+
const PAGE_HASH_PATTERN = /^#\/(concepts|queries)\/(.+)$/;
|
|
45
|
+
|
|
46
|
+
/** Rows for the home dashboard counts grid: `[label, envelope.counts key]`. */
|
|
47
|
+
const COUNT_ROWS = [
|
|
48
|
+
["Concepts", "concepts"],
|
|
49
|
+
["Saved queries", "queries"],
|
|
50
|
+
["Source files", "sourceFiles"],
|
|
51
|
+
["Pending reviews", "pendingReviews"],
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
/** Rows for the /api/health metrics block: `[label, health key]`. */
|
|
55
|
+
const HEALTH_METRIC_ROWS = [
|
|
56
|
+
["Concepts", "concepts"],
|
|
57
|
+
["Saved queries", "queries"],
|
|
58
|
+
["Compiled sources", "sources"],
|
|
59
|
+
["Source files", "sourceFiles"],
|
|
60
|
+
["Pending reviews", "pendingReviews"],
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
/** Rows for the lint block: `[label, key, fallback]`. */
|
|
64
|
+
const LINT_METRIC_ROWS = [
|
|
65
|
+
["Warnings", "warnings", 0],
|
|
66
|
+
["Errors", "errors", 0],
|
|
67
|
+
["Last run", "at", ""],
|
|
68
|
+
];
|
|
29
69
|
|
|
30
70
|
/** Parse the server-embedded page-index JSON. Empty list if absent or malformed. */
|
|
31
71
|
function readEmbeddedIndex() {
|
|
32
72
|
const node = document.querySelector(PAGE_INDEX_SELECTOR);
|
|
33
|
-
|
|
73
|
+
const text = node?.textContent;
|
|
74
|
+
if (!text) return EMPTY_INDEX;
|
|
75
|
+
return parsePageIndex(text);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Best-effort JSON.parse of the embedded blob. Always returns a `{pages}` shape. */
|
|
79
|
+
function parsePageIndex(text) {
|
|
34
80
|
try {
|
|
35
|
-
const data = JSON.parse(
|
|
36
|
-
|
|
81
|
+
const data = JSON.parse(text);
|
|
82
|
+
if (Array.isArray(data?.pages)) return { pages: data.pages };
|
|
37
83
|
} catch {
|
|
38
|
-
|
|
84
|
+
// Malformed JSON in the embedded blob is not a user-facing error.
|
|
39
85
|
}
|
|
86
|
+
return EMPTY_INDEX;
|
|
40
87
|
}
|
|
41
88
|
|
|
42
|
-
|
|
43
89
|
/**
|
|
44
|
-
* Parse `location.hash` into a route descriptor.
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
90
|
+
* Parse `location.hash` into a route descriptor. Static routes resolve
|
|
91
|
+
* via `STATIC_ROUTES`; page routes fall through to {@link parsePageRoute}.
|
|
92
|
+
* Malformed percent-encoding in the slug segment falls back to the home
|
|
93
|
+
* route so a hand-edited URL cannot throw from `decodeURIComponent`
|
|
94
|
+
* (`#/concepts/%E0%A4%A` is the canonical bad-input case).
|
|
48
95
|
*/
|
|
49
96
|
function parseRoute(hash) {
|
|
50
|
-
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
97
|
+
const key = hash ?? "";
|
|
98
|
+
if (HOME_HASHES.has(key)) return { kind: "home" };
|
|
99
|
+
const staticRoute = STATIC_ROUTES.get(key);
|
|
100
|
+
if (staticRoute) return staticRoute;
|
|
101
|
+
return parsePageRoute(key);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Resolve a `#/(concepts|queries)/<slug>` hash; non-matches return home. */
|
|
105
|
+
function parsePageRoute(hash) {
|
|
106
|
+
const match = hash.match(PAGE_HASH_PATTERN);
|
|
54
107
|
if (!match) return { kind: "home" };
|
|
55
|
-
|
|
108
|
+
const slug = decodeSlug(match[2]);
|
|
109
|
+
if (slug === null) return { kind: "home" };
|
|
110
|
+
return { kind: "page", directory: match[1], slug };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Safely percent-decode a slug; returns null on malformed input. */
|
|
114
|
+
function decodeSlug(raw) {
|
|
56
115
|
try {
|
|
57
|
-
|
|
116
|
+
return decodeURIComponent(raw);
|
|
58
117
|
} catch {
|
|
59
|
-
return
|
|
118
|
+
return null;
|
|
60
119
|
}
|
|
61
|
-
return { kind: "page", directory: match[1], slug };
|
|
62
120
|
}
|
|
63
121
|
|
|
64
122
|
/** Render the home dashboard from the `/api/pages` envelope. */
|
|
@@ -67,25 +125,46 @@ function renderHome(envelope) {
|
|
|
67
125
|
if (!main) return;
|
|
68
126
|
main.innerHTML = "";
|
|
69
127
|
main.className = "main-pane home-dashboard";
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
128
|
+
appendHomeContent(main, envelope);
|
|
129
|
+
renderProjectRail(envelope);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Append every section of the home dashboard to the main pane. */
|
|
133
|
+
function appendHomeContent(main, envelope) {
|
|
134
|
+
main.appendChild(buildHeading("h1", projectTitle(envelope)));
|
|
135
|
+
main.appendChild(buildCountsBlock(envelope?.counts));
|
|
136
|
+
appendIndexLinkIfAvailable(main, envelope);
|
|
137
|
+
appendRecentBlockIfAny(main, envelope);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Append the compiled-index link, if the envelope flagged it available. */
|
|
141
|
+
function appendIndexLinkIfAvailable(main, envelope) {
|
|
142
|
+
if (envelope?.index?.available) {
|
|
143
|
+
main.appendChild(buildIndexLink(envelope.index.href));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Append the recent-updates block, if the envelope carried any rows. */
|
|
148
|
+
function appendRecentBlockIfAny(main, envelope) {
|
|
149
|
+
if (hasRecentPages(envelope)) {
|
|
76
150
|
main.appendChild(buildRecentBlock(envelope.recentPages));
|
|
77
151
|
}
|
|
78
|
-
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Display title for the envelope, with a stable fallback. */
|
|
155
|
+
function projectTitle(envelope) {
|
|
156
|
+
return envelope?.project?.title || DEFAULT_TITLE;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** True when the envelope carries at least one recent page. */
|
|
160
|
+
function hasRecentPages(envelope) {
|
|
161
|
+
return Array.isArray(envelope?.recentPages) && envelope.recentPages.length > 0;
|
|
79
162
|
}
|
|
80
163
|
|
|
81
164
|
/** Render a `<dl>` of project counts on the home dashboard. */
|
|
82
165
|
function buildCountsBlock(counts) {
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
["Saved queries", counts.queries ?? 0],
|
|
86
|
-
["Source files", counts.sourceFiles ?? 0],
|
|
87
|
-
["Pending reviews", counts.pendingReviews ?? 0],
|
|
88
|
-
]);
|
|
166
|
+
const rows = COUNT_ROWS.map(([label, key]) => [label, counts?.[key] ?? 0]);
|
|
167
|
+
const dl = buildDefinitionList(rows);
|
|
89
168
|
dl.className = "metric-grid";
|
|
90
169
|
return dl;
|
|
91
170
|
}
|
|
@@ -120,8 +199,6 @@ function buildIndexLink(href) {
|
|
|
120
199
|
|
|
121
200
|
/** Render the recent-pages list on the home dashboard. */
|
|
122
201
|
function buildRecentBlock(recent) {
|
|
123
|
-
const h2 = document.createElement("h2");
|
|
124
|
-
h2.textContent = "Recently updated";
|
|
125
202
|
const ul = document.createElement("ul");
|
|
126
203
|
ul.className = "recent-list";
|
|
127
204
|
for (const page of recent) {
|
|
@@ -134,11 +211,34 @@ function buildRecentBlock(recent) {
|
|
|
134
211
|
}
|
|
135
212
|
const wrap = document.createElement("section");
|
|
136
213
|
wrap.className = "recent-section";
|
|
137
|
-
wrap.appendChild(h2);
|
|
214
|
+
wrap.appendChild(buildHeading("h2", "Recently updated"));
|
|
138
215
|
wrap.appendChild(ul);
|
|
139
216
|
return wrap;
|
|
140
217
|
}
|
|
141
218
|
|
|
219
|
+
/** Build a heading element with the given tag and text content. */
|
|
220
|
+
function buildHeading(tag, text) {
|
|
221
|
+
const el = document.createElement(tag);
|
|
222
|
+
el.textContent = text;
|
|
223
|
+
return el;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Build a `<p class="placeholder">` with the given message. */
|
|
227
|
+
function buildPlaceholder(text) {
|
|
228
|
+
const p = document.createElement("p");
|
|
229
|
+
p.className = "placeholder";
|
|
230
|
+
p.textContent = text;
|
|
231
|
+
return p;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Dispatch table: route.kind → handler for routes that fit the (main) signature. */
|
|
235
|
+
const ROUTE_RENDERERS = {
|
|
236
|
+
home: () => loadAndRenderHome(),
|
|
237
|
+
index: (main) => renderIndexPane(main),
|
|
238
|
+
health: (main) => renderHealthPane(main),
|
|
239
|
+
graph: (main) => renderGraphPane(main),
|
|
240
|
+
};
|
|
241
|
+
|
|
142
242
|
/** Fetch and render the page at the current hash route. */
|
|
143
243
|
async function renderRoute() {
|
|
144
244
|
const route = parseRoute(location.hash);
|
|
@@ -146,9 +246,8 @@ async function renderRoute() {
|
|
|
146
246
|
const main = document.querySelector(MAIN_SELECTOR);
|
|
147
247
|
if (!main) return;
|
|
148
248
|
main.className = "main-pane";
|
|
149
|
-
|
|
150
|
-
if (
|
|
151
|
-
if (route.kind === "health") return renderHealthPane(main);
|
|
249
|
+
const handler = ROUTE_RENDERERS[route.kind];
|
|
250
|
+
if (handler) return handler(main);
|
|
152
251
|
return renderPagePane(main, route.directory, route.slug);
|
|
153
252
|
}
|
|
154
253
|
|
|
@@ -157,9 +256,7 @@ async function renderHealthPane(main) {
|
|
|
157
256
|
try {
|
|
158
257
|
const health = await fetchJson("/api/health");
|
|
159
258
|
main.innerHTML = "";
|
|
160
|
-
|
|
161
|
-
h1.textContent = "Health";
|
|
162
|
-
main.appendChild(h1);
|
|
259
|
+
main.appendChild(buildHeading("h1", "Health"));
|
|
163
260
|
main.appendChild(buildHealthDashboard(health));
|
|
164
261
|
clearSupportRail();
|
|
165
262
|
} catch (err) {
|
|
@@ -171,37 +268,24 @@ async function renderHealthPane(main) {
|
|
|
171
268
|
function buildHealthDashboard(health) {
|
|
172
269
|
const wrap = document.createElement("section");
|
|
173
270
|
wrap.className = "health-dashboard";
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
["Saved queries", health.queries ?? 0],
|
|
177
|
-
["Compiled sources", health.sources ?? 0],
|
|
178
|
-
["Source files", health.sourceFiles ?? 0],
|
|
179
|
-
["Pending reviews", health.pendingReviews ?? 0],
|
|
180
|
-
]);
|
|
271
|
+
const rows = HEALTH_METRIC_ROWS.map(([label, key]) => [label, health?.[key] ?? 0]);
|
|
272
|
+
const metrics = buildDefinitionList(rows);
|
|
181
273
|
metrics.className = "metric-list";
|
|
182
274
|
wrap.appendChild(metrics);
|
|
183
|
-
wrap.appendChild(buildLintBlock(health
|
|
275
|
+
wrap.appendChild(buildLintBlock(health?.lint));
|
|
184
276
|
return wrap;
|
|
185
277
|
}
|
|
186
278
|
|
|
187
279
|
/** Render the lint summary, or a "lint has not been run yet" placeholder. */
|
|
188
280
|
function buildLintBlock(lint) {
|
|
189
281
|
const wrap = document.createElement("section");
|
|
190
|
-
|
|
191
|
-
h2.textContent = "Lint";
|
|
192
|
-
wrap.appendChild(h2);
|
|
282
|
+
wrap.appendChild(buildHeading("h2", "Lint"));
|
|
193
283
|
if (!lint) {
|
|
194
|
-
|
|
195
|
-
note.className = "placeholder";
|
|
196
|
-
note.textContent = "No cached lint summary yet — run `llmwiki lint`.";
|
|
197
|
-
wrap.appendChild(note);
|
|
284
|
+
wrap.appendChild(buildPlaceholder("No cached lint summary yet — run `llmwiki lint`."));
|
|
198
285
|
return wrap;
|
|
199
286
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
["Errors", lint.errors ?? 0],
|
|
203
|
-
["Last run", lint.at ?? ""],
|
|
204
|
-
]));
|
|
287
|
+
const rows = LINT_METRIC_ROWS.map(([label, key, fallback]) => [label, lint[key] ?? fallback]);
|
|
288
|
+
wrap.appendChild(buildDefinitionList(rows));
|
|
205
289
|
return wrap;
|
|
206
290
|
}
|
|
207
291
|
|
|
@@ -209,69 +293,89 @@ function buildLintBlock(lint) {
|
|
|
209
293
|
async function loadAndRenderHome() {
|
|
210
294
|
try {
|
|
211
295
|
const envelope = await fetchJson("/api/pages");
|
|
212
|
-
|
|
213
|
-
renderSidebar(envelope.pages || []);
|
|
214
|
-
renderHome(envelope);
|
|
296
|
+
applyHomeEnvelope(envelope);
|
|
215
297
|
} catch (err) {
|
|
216
298
|
renderError(`Could not load /api/pages: ${err.message}`);
|
|
217
299
|
}
|
|
218
300
|
}
|
|
219
301
|
|
|
302
|
+
/** Apply a successfully fetched /api/pages envelope to the chrome + main pane. */
|
|
303
|
+
function applyHomeEnvelope(envelope) {
|
|
304
|
+
const titleEl = document.querySelector(TITLE_SELECTOR);
|
|
305
|
+
titleEl.textContent = projectTitle(envelope);
|
|
306
|
+
renderSidebar(envelope?.pages || []);
|
|
307
|
+
renderHome(envelope);
|
|
308
|
+
}
|
|
309
|
+
|
|
220
310
|
/** Fetch /api/index and render the rendered HTML coming back from the server. */
|
|
221
311
|
async function renderIndexPane(main) {
|
|
222
312
|
clearSupportRail();
|
|
223
313
|
try {
|
|
224
314
|
const payload = await fetchJson("/api/index");
|
|
225
315
|
main.innerHTML = "";
|
|
226
|
-
|
|
227
|
-
h1.textContent = "Index";
|
|
228
|
-
main.appendChild(h1);
|
|
316
|
+
main.appendChild(buildHeading("h1", "Index"));
|
|
229
317
|
appendRenderedBody(main, payload.html);
|
|
230
318
|
} catch (err) {
|
|
231
|
-
|
|
232
|
-
main.innerHTML = "";
|
|
233
|
-
const note = document.createElement("p");
|
|
234
|
-
note.className = "placeholder";
|
|
235
|
-
note.textContent = "wiki/index.md is not available. Run `llmwiki compile`.";
|
|
236
|
-
main.appendChild(note);
|
|
237
|
-
} else {
|
|
238
|
-
renderError(`Could not load /api/index: ${err.message}`);
|
|
239
|
-
}
|
|
319
|
+
handleIndexError(main, err);
|
|
240
320
|
}
|
|
241
321
|
}
|
|
242
322
|
|
|
323
|
+
/** Render either the "wiki/index.md missing" placeholder or a generic error. */
|
|
324
|
+
function handleIndexError(main, err) {
|
|
325
|
+
if (err.status !== 404) {
|
|
326
|
+
renderError(`Could not load /api/index: ${err.message}`);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
main.innerHTML = "";
|
|
330
|
+
main.appendChild(buildPlaceholder("wiki/index.md is not available. Run `llmwiki compile`."));
|
|
331
|
+
}
|
|
332
|
+
|
|
243
333
|
/** Fetch /api/page/:dir/:slug and render. */
|
|
244
334
|
async function renderPagePane(main, directory, slug) {
|
|
245
335
|
try {
|
|
246
|
-
const payload = await fetchJson(
|
|
247
|
-
|
|
248
|
-
);
|
|
249
|
-
main.innerHTML = "";
|
|
250
|
-
const h1 = document.createElement("h1");
|
|
251
|
-
h1.textContent = payload.title || slug;
|
|
252
|
-
main.appendChild(h1);
|
|
253
|
-
if (payload.pageDirectory === "queries") {
|
|
254
|
-
const question = document.createElement("p");
|
|
255
|
-
question.className = "query-question";
|
|
256
|
-
question.textContent = `Question: ${payload.title || slug}`;
|
|
257
|
-
main.appendChild(question);
|
|
258
|
-
}
|
|
259
|
-
appendWarnings(main, payload.warnings || []);
|
|
260
|
-
const body = appendRenderedBody(main, payload.html);
|
|
261
|
-
removeDuplicateLeadingHeading(body, payload.title || slug);
|
|
262
|
-
renderSupportRail(payload);
|
|
336
|
+
const payload = await fetchJson(pageApiPath(directory, slug));
|
|
337
|
+
renderPagePayload(main, payload, slug);
|
|
263
338
|
} catch (err) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
339
|
+
handlePageError(main, err, directory, slug);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Build the `/api/page/:dir/:slug` URL with both segments percent-encoded. */
|
|
344
|
+
function pageApiPath(directory, slug) {
|
|
345
|
+
return `/api/page/${encodeURIComponent(directory)}/${encodeURIComponent(slug)}`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Render the body of a successful /api/page response into the main pane. */
|
|
349
|
+
function renderPagePayload(main, payload, slug) {
|
|
350
|
+
const title = payload.title || slug;
|
|
351
|
+
main.innerHTML = "";
|
|
352
|
+
main.appendChild(buildHeading("h1", title));
|
|
353
|
+
if (payload.pageDirectory === "queries") {
|
|
354
|
+
main.appendChild(buildQueryQuestion(title));
|
|
355
|
+
}
|
|
356
|
+
appendWarnings(main, payload.warnings || []);
|
|
357
|
+
const body = appendRenderedBody(main, payload.html);
|
|
358
|
+
removeDuplicateLeadingHeading(body, title);
|
|
359
|
+
renderSupportRail(payload);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** Question banner shown above the body for saved-query pages. */
|
|
363
|
+
function buildQueryQuestion(title) {
|
|
364
|
+
const p = document.createElement("p");
|
|
365
|
+
p.className = "query-question";
|
|
366
|
+
p.textContent = `Question: ${title}`;
|
|
367
|
+
return p;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Render the 404 placeholder or a generic error for /api/page failures. */
|
|
371
|
+
function handlePageError(main, err, directory, slug) {
|
|
372
|
+
if (err.status !== 404) {
|
|
373
|
+
renderError(`Could not load page: ${err.message}`);
|
|
374
|
+
return;
|
|
274
375
|
}
|
|
376
|
+
main.innerHTML = "";
|
|
377
|
+
main.appendChild(buildPlaceholder(`Page not found: ${directory}/${slug}`));
|
|
378
|
+
clearSupportRail();
|
|
275
379
|
}
|
|
276
380
|
|
|
277
381
|
/**
|
|
@@ -290,18 +394,31 @@ function appendRenderedBody(main, html) {
|
|
|
290
394
|
main.appendChild(body);
|
|
291
395
|
return body;
|
|
292
396
|
}
|
|
293
|
-
const note =
|
|
397
|
+
const note = buildPlaceholder("No rendered content.");
|
|
294
398
|
main.appendChild(note);
|
|
295
399
|
return note;
|
|
296
400
|
}
|
|
297
401
|
|
|
298
402
|
/** Drop a duplicated first Markdown H1 when it matches the viewer page title. */
|
|
299
403
|
function removeDuplicateLeadingHeading(body, title) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
if (!
|
|
303
|
-
|
|
304
|
-
|
|
404
|
+
const heading = leadingH1(body);
|
|
405
|
+
if (!heading) return;
|
|
406
|
+
if (!hasMatchingHeadingText(heading, title)) return;
|
|
407
|
+
heading.remove();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** Return `body.firstElementChild` if it is an H1, else null. */
|
|
411
|
+
function leadingH1(body) {
|
|
412
|
+
const first = body?.firstElementChild;
|
|
413
|
+
if (!first) return null;
|
|
414
|
+
return first.tagName === "H1" ? first : null;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** True when the heading text matches `title` after trimming both sides. */
|
|
418
|
+
function hasMatchingHeadingText(heading, title) {
|
|
419
|
+
if (!title) return false;
|
|
420
|
+
const headingText = heading.textContent?.trim();
|
|
421
|
+
return headingText === title.trim();
|
|
305
422
|
}
|
|
306
423
|
|
|
307
424
|
/** Render every payload warning as a banner above the page body. */
|
|
@@ -314,14 +431,6 @@ function appendWarnings(main, warnings) {
|
|
|
314
431
|
}
|
|
315
432
|
}
|
|
316
433
|
|
|
317
|
-
/** Visible "no content" fallback for pages whose body is empty after frontmatter. */
|
|
318
|
-
function emptyBodyNote() {
|
|
319
|
-
const note = document.createElement("p");
|
|
320
|
-
note.className = "placeholder";
|
|
321
|
-
note.textContent = "No rendered content.";
|
|
322
|
-
return note;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
434
|
/** Render a top-of-main error banner without crashing the rest of the UI. */
|
|
326
435
|
function renderError(message) {
|
|
327
436
|
const main = document.querySelector(MAIN_SELECTOR);
|
|
@@ -334,6 +443,14 @@ function renderError(message) {
|
|
|
334
443
|
clearSupportRail();
|
|
335
444
|
}
|
|
336
445
|
|
|
446
|
+
/** Fetch /api/graph and render the force-directed graph view. */
|
|
447
|
+
async function renderGraphPane(main) {
|
|
448
|
+
clearSupportRail();
|
|
449
|
+
main.innerHTML = "";
|
|
450
|
+
main.className = "main-pane graph-pane";
|
|
451
|
+
await loadGraph(main);
|
|
452
|
+
}
|
|
453
|
+
|
|
337
454
|
/** Promise-returning fetch helper that surfaces non-2xx statuses as errors. */
|
|
338
455
|
async function fetchJson(pathname) {
|
|
339
456
|
const res = await fetch(pathname, { credentials: "same-origin" });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "llm-wiki-compiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "A knowledge compiler CLI — raw sources in, interlinked wiki out",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,10 +10,12 @@
|
|
|
10
10
|
"build": "tsup",
|
|
11
11
|
"dev": "tsup --watch",
|
|
12
12
|
"dev:view": "node scripts/dev-viewer.mjs",
|
|
13
|
+
"release:check-docs": "node scripts/check-release-docs.mjs",
|
|
14
|
+
"release:check-docs:current": "node scripts/check-release-docs.mjs --current-version",
|
|
13
15
|
"test": "vitest run",
|
|
14
16
|
"test:watch": "vitest",
|
|
15
17
|
"fallow:ci": "bash scripts/fallow-ci.sh",
|
|
16
|
-
"prepublishOnly": "npm run build && npm
|
|
18
|
+
"prepublishOnly": "npm run release:check-docs:current && npm run build && npm pack --dry-run --ignore-scripts",
|
|
17
19
|
"prepare": "husky"
|
|
18
20
|
},
|
|
19
21
|
"keywords": [
|
|
@@ -36,6 +38,7 @@
|
|
|
36
38
|
},
|
|
37
39
|
"files": [
|
|
38
40
|
"dist/",
|
|
41
|
+
"CHANGELOG.md",
|
|
39
42
|
"LICENSE",
|
|
40
43
|
"README.md"
|
|
41
44
|
],
|
|
@@ -67,7 +70,7 @@
|
|
|
67
70
|
"@types/markdown-it": "^14.1.2",
|
|
68
71
|
"@types/sanitize-html": "^2.16.1",
|
|
69
72
|
"@types/turndown": "^5.0.0",
|
|
70
|
-
"fallow": "2.
|
|
73
|
+
"fallow": "2.82.0",
|
|
71
74
|
"husky": "^9.1.7",
|
|
72
75
|
"tsup": "^8.0.0",
|
|
73
76
|
"typescript": "^5.7.0",
|