@sdsrs/llm-wiki 0.6.1 → 0.6.3
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 +27 -0
- package/README.md +7 -1
- package/bin/llm-wiki.mjs +2 -2
- package/package.json +1 -1
- package/src/export.mjs +60 -2
- package/src/scanner.mjs +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.3 (2026-07-11)
|
|
4
|
+
|
|
5
|
+
Two cosmetic fixes from an independent review of 0.6.0–0.6.2; no behavior
|
|
6
|
+
change beyond the scan warning's rounding.
|
|
7
|
+
|
|
8
|
+
- fix(scan): the tag-dispersion warning now flags on the same rounded top-tag
|
|
9
|
+
share it displays, so it can no longer print "top tag covers 30%" while
|
|
10
|
+
firing a "below 30%" rule. Effective threshold is 29.5% (was a raw 30% with
|
|
11
|
+
an inconsistent display); negligible for an advisory heuristic. Suite 178 →
|
|
12
|
+
179.
|
|
13
|
+
- docs: note that the JSON Canvas map's `file` cards are vault-relative from the
|
|
14
|
+
KB root, so a canvas written outside the KB via `--out` won't resolve them in
|
|
15
|
+
Obsidian — open the canvas with the KB as its own vault.
|
|
16
|
+
|
|
17
|
+
## 0.6.2 (2026-07-11)
|
|
18
|
+
|
|
19
|
+
No breaking changes, no KB migration, no default-behavior change — one additive
|
|
20
|
+
export format.
|
|
21
|
+
|
|
22
|
+
- feat(export): `--format canvas` writes a JSON Canvas 1.0 file
|
|
23
|
+
(`<kb>/graph.canvas`) — a domain map of `file` cards laid out in columns by
|
|
24
|
+
page type (source / entity / concept / comparison / raw), ordered by link
|
|
25
|
+
degree, colored by type, with invalidated pages flagged red. The cards open
|
|
26
|
+
as live, clickable notes in Obsidian Canvas when the KB is opened as its own
|
|
27
|
+
vault. Zero-LLM, alongside the existing graphml / cypher / html / markdown
|
|
28
|
+
exports. Suite 174 → 178.
|
|
29
|
+
|
|
3
30
|
## 0.6.1 (2026-07-11)
|
|
4
31
|
|
|
5
32
|
No breaking changes, no KB migration, no default-behavior change: the only
|
package/README.md
CHANGED
|
@@ -90,7 +90,7 @@ npx @sdsrs/llm-wiki@0 ask "what did we decide about X?"
|
|
|
90
90
|
| `status` | incremental state: uncompiled raw files, source-dir diff, affected wiki pages |
|
|
91
91
|
| `graph` | query `graph.json` with `path` / `neighbors` / `hubs` (zero-LLM traversal) |
|
|
92
92
|
| `embed` | compute/update page embeddings (`wiki/.vectors.json`) for optional vector location |
|
|
93
|
-
| `export` | export the graph as GraphML, Cypher, or an interactive HTML viewer; or the wiki as standard-markdown copies |
|
|
93
|
+
| `export` | export the graph as GraphML, Cypher, JSON Canvas, or an interactive HTML viewer; or the wiki as standard-markdown copies |
|
|
94
94
|
| `mcp` | run a read-only MCP server (stdio) over the KB |
|
|
95
95
|
|
|
96
96
|
All commands take `--kb <dir>` (default `.`). `ask` supports `-k <n>` (pages to
|
|
@@ -145,6 +145,12 @@ relations:
|
|
|
145
145
|
`lint` validates each relation target and type; `ambiguous` marks edges for a
|
|
146
146
|
human to resolve.
|
|
147
147
|
|
|
148
|
+
`export --format canvas` writes a JSON Canvas (`graph.canvas`) domain map —
|
|
149
|
+
`file` cards laid out by page type that open as live notes in Obsidian Canvas.
|
|
150
|
+
The card paths are vault-relative from the KB root, so open the canvas with the
|
|
151
|
+
KB as its own vault; a canvas written outside the KB via `--out` won't resolve
|
|
152
|
+
its cards.
|
|
153
|
+
|
|
148
154
|
### Skills (Claude Code / Codex)
|
|
149
155
|
|
|
150
156
|
The bundled skills (`wiki-build`, `wiki-ingest`, `wiki-query`, `wiki-lint`,
|
package/bin/llm-wiki.mjs
CHANGED
|
@@ -128,9 +128,9 @@ program.command('install-skills')
|
|
|
128
128
|
})
|
|
129
129
|
|
|
130
130
|
program.command('export')
|
|
131
|
-
.description('export wiki/graph.json (graphml | cypher | html) or a markdown-links copy of the wiki (markdown)')
|
|
131
|
+
.description('export wiki/graph.json (graphml | cypher | html | canvas) or a markdown-links copy of the wiki (markdown)')
|
|
132
132
|
.option('--kb <dir>', 'knowledge base root', '.')
|
|
133
|
-
.requiredOption('--format <format>', 'graphml | cypher | html | markdown')
|
|
133
|
+
.requiredOption('--format <format>', 'graphml | cypher | html | canvas | markdown')
|
|
134
134
|
.option('--out <file>', 'output path (default: <kb>/graph.<ext>, or <kb>/wiki-md/ for markdown)')
|
|
135
135
|
.action((opts) => {
|
|
136
136
|
if (opts.format === 'markdown') {
|
package/package.json
CHANGED
package/src/export.mjs
CHANGED
|
@@ -236,13 +236,71 @@ step()
|
|
|
236
236
|
`
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
-
|
|
239
|
+
// JSON Canvas 1.0 (jsoncanvas.org) — an open node/edge format Obsidian Canvas and
|
|
240
|
+
// other tools read. Nodes are `file` cards pointing at the real KB pages
|
|
241
|
+
// (vault-relative from the KB root, where the .canvas is written), so opening the
|
|
242
|
+
// map in Obsidian gives live, clickable notes. Layout is deterministic — one
|
|
243
|
+
// column per node type, ordered by degree — so exports are stable and diffable.
|
|
244
|
+
const CANVAS_COLORS = { source: '5', entity: '4', concept: '6', comparison: '2' } // JSON Canvas preset ids; raw/unknown → default gray
|
|
245
|
+
const CANVAS_TYPE_ORDER = ['source', 'entity', 'concept', 'comparison', 'raw']
|
|
246
|
+
const CANVAS_NODE_W = 260
|
|
247
|
+
const CANVAS_NODE_H = 60
|
|
248
|
+
const CANVAS_COL_GAP = 340
|
|
249
|
+
const CANVAS_ROW_GAP = 90
|
|
250
|
+
|
|
251
|
+
export function toCanvas(graph) {
|
|
252
|
+
const degree = new Map()
|
|
253
|
+
for (const e of graph.edges) for (const end of [e.source, e.target]) degree.set(end, (degree.get(end) ?? 0) + 1)
|
|
254
|
+
|
|
255
|
+
const cols = new Map()
|
|
256
|
+
for (const n of graph.nodes) {
|
|
257
|
+
const key = CANVAS_TYPE_ORDER.includes(n.type) ? n.type : 'raw'
|
|
258
|
+
if (!cols.has(key)) cols.set(key, [])
|
|
259
|
+
cols.get(key).push(n)
|
|
260
|
+
}
|
|
261
|
+
const rank = (t) => { const i = CANVAS_TYPE_ORDER.indexOf(t); return i === -1 ? CANVAS_TYPE_ORDER.length : i }
|
|
262
|
+
const colKeys = [...cols.keys()].sort((a, b) => rank(a) - rank(b))
|
|
263
|
+
|
|
264
|
+
const nodes = []
|
|
265
|
+
colKeys.forEach((key, ci) => {
|
|
266
|
+
const column = cols.get(key).sort((a, b) =>
|
|
267
|
+
(degree.get(b.id) ?? 0) - (degree.get(a.id) ?? 0) || a.id.localeCompare(b.id))
|
|
268
|
+
column.forEach((n, ri) => {
|
|
269
|
+
const node = {
|
|
270
|
+
id: n.id,
|
|
271
|
+
type: 'file',
|
|
272
|
+
file: n.id.startsWith('raw/') ? n.id : `wiki/${n.id}.md`,
|
|
273
|
+
x: ci * CANVAS_COL_GAP,
|
|
274
|
+
y: ri * CANVAS_ROW_GAP,
|
|
275
|
+
width: CANVAS_NODE_W,
|
|
276
|
+
height: CANVAS_NODE_H,
|
|
277
|
+
}
|
|
278
|
+
const color = n.status === 'invalidated' ? '1' : CANVAS_COLORS[n.type]
|
|
279
|
+
if (color) node.color = color
|
|
280
|
+
nodes.push(node)
|
|
281
|
+
})
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
const present = new Set(nodes.map(n => n.id))
|
|
285
|
+
const edges = graph.edges
|
|
286
|
+
.filter(e => present.has(e.source) && present.has(e.target))
|
|
287
|
+
.map((e, i) => {
|
|
288
|
+
const edge = { id: `e${i}`, fromNode: e.source, toNode: e.target }
|
|
289
|
+
if (e.type) edge.label = e.type
|
|
290
|
+
return edge
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
return JSON.stringify({ nodes, edges }, null, 2) + '\n'
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const RENDERERS = { graphml: toGraphML, cypher: toCypher, html: toHtml, canvas: toCanvas }
|
|
297
|
+
const EXT = { graphml: 'graphml', cypher: 'cypher', html: 'html', canvas: 'canvas' }
|
|
240
298
|
|
|
241
299
|
export function exportGraph(kbRoot, { format, out } = {}) {
|
|
242
300
|
const render = RENDERERS[format]
|
|
243
301
|
if (!render) throw new Error(`unknown format: ${format} (expected ${Object.keys(RENDERERS).join(' | ')} | markdown)`)
|
|
244
302
|
const graph = loadGraph(kbRoot)
|
|
245
|
-
const outPath = out ?? path.join(kbRoot, `graph.${format
|
|
303
|
+
const outPath = out ?? path.join(kbRoot, `graph.${EXT[format]}`)
|
|
246
304
|
fs.writeFileSync(outPath, render(graph))
|
|
247
305
|
return { out: outPath, nodeCount: graph.nodes.length, edgeCount: graph.edges.length }
|
|
248
306
|
}
|
package/src/scanner.mjs
CHANGED
|
@@ -33,8 +33,12 @@ function detectDomainMixture(files, kbRoot) {
|
|
|
33
33
|
if (pages.length >= TAG_MIN_PAGES) {
|
|
34
34
|
const counts = new Map()
|
|
35
35
|
for (const p of pages) for (const t of new Set(p.data.tags)) counts.set(t, (counts.get(t) ?? 0) + 1)
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
// Round once and flag on the rounded value so the stored/displayed share
|
|
37
|
+
// (CLI prints Math.round(topShare*100)) never reads "30%" while the strict
|
|
38
|
+
// `< TAG_TOP_SHARE_MIN` rule fired on a raw 0.29x. Effective threshold is
|
|
39
|
+
// 29.5%; negligible for an advisory heuristic, and flag/display now agree.
|
|
40
|
+
const topShare = Number((Math.max(...counts.values()) / pages.length).toFixed(2))
|
|
41
|
+
tags = { pages: pages.length, distinct: counts.size, topShare, flagged: topShare < TAG_TOP_SHARE_MIN }
|
|
38
42
|
}
|
|
39
43
|
return { language, tags, flagged: language.flagged || (tags?.flagged ?? false) }
|
|
40
44
|
}
|