@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdsrs/llm-wiki",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "Compile messy document directories into Karpathy-style llm_wiki knowledge bases — standalone Q&A CLI + Claude Code/Codex skills",
5
5
  "directories": {
6
6
  "test": "test"
package/src/export.mjs CHANGED
@@ -236,13 +236,71 @@ step()
236
236
  `
237
237
  }
238
238
 
239
- const RENDERERS = { graphml: toGraphML, cypher: toCypher, html: toHtml }
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 === 'graphml' ? 'graphml' : format === 'cypher' ? 'cypher' : 'html'}`)
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
- const topShare = Math.max(...counts.values()) / pages.length
37
- tags = { pages: pages.length, distinct: counts.size, topShare: Number(topShare.toFixed(2)), flagged: topShare < TAG_TOP_SHARE_MIN }
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
  }