@sdsrs/llm-wiki 0.7.2 → 0.7.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,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.3 (2026-07-12)
4
+
5
+ Two robustness fixes from the final targeted dogfooding sweep. No config schema
6
+ change, no KB migration, no retrieval-default change. Suite 228 → 230.
7
+
8
+ **Fixes:**
9
+
10
+ - **`graph`/exports name the file on a corrupt `graph.json`.** `loadGraph` was the
11
+ last JSON reader still using a bare `JSON.parse`: a corrupt `wiki/graph.json`
12
+ threw an unqualified `Unexpected token` SyntaxError, and a valid-JSON-but-wrong-
13
+ shape file like `{}` crashed on `graph.nodes.map`. It now routes through the
14
+ shared `readJsonFile` (names the file) and guards the nodes/edges shape, so both
15
+ corruption modes yield a named, actionable "rerun `llm-wiki index`" error.
16
+ - **`.scan-plan.json` is written atomically.** `scan` wrote the plan with a direct
17
+ `writeFileSync`, but `convert` reads it from a separate process — the same
18
+ truncate-then-write torn read that 0.7.2 fixed for `graph.json`. It (and
19
+ `.lint-report.json`) now write via the shared temp-and-rename `writeFileAtomic`,
20
+ so every JSON state file another process can read is crash-safe.
21
+
3
22
  ## 0.7.2 (2026-07-12)
4
23
 
5
24
  Three defects found by continued end-to-end dogfooding — two robustness/DoS
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdsrs/llm-wiki",
3
- "version": "0.7.2",
3
+ "version": "0.7.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
@@ -2,6 +2,7 @@ import fs from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { kbPaths } from './paths.mjs'
4
4
  import { listWikiPages } from './pages.mjs'
5
+ import { readJsonFile } from './json.mjs'
5
6
 
6
7
  const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
7
8
 
@@ -16,7 +17,18 @@ const cyEscape = (s) => String(s).replace(/\\/g, '\\\\').replace(/\n/g, '\\n').r
16
17
  export function loadGraph(kbRoot) {
17
18
  const p = kbPaths(kbRoot)
18
19
  if (!fs.existsSync(p.graphJson)) throw new Error('wiki/graph.json not found — run `llm-wiki index` first.')
19
- const graph = JSON.parse(fs.readFileSync(p.graphJson, 'utf8'))
20
+ // graph.json is a derived store — route it through readJsonFile (names the file)
21
+ // rather than a bare JSON.parse whose SyntaxError leaks no path, and guard the shape
22
+ // so a hand-corrupted-but-valid `{}` doesn't crash on `graph.nodes.map` (undefined).
23
+ let graph
24
+ try {
25
+ graph = readJsonFile(p.graphJson)
26
+ } catch (err) {
27
+ throw new Error(`${err.message} — rerun \`llm-wiki index\` to rebuild the graph`)
28
+ }
29
+ if (!graph || !Array.isArray(graph.nodes) || !Array.isArray(graph.edges)) {
30
+ throw new Error('wiki/graph.json has an unexpected shape (needs nodes/edges arrays) — rerun `llm-wiki index`')
31
+ }
20
32
  const ids = new Set(graph.nodes.map(n => n.id))
21
33
  for (const e of graph.edges) {
22
34
  for (const end of [e.source, e.target]) {
package/src/lint.mjs CHANGED
@@ -5,6 +5,7 @@ import { loadKbConfig } from './templates.mjs'
5
5
  import { listWikiPages, validatePage, isInvalidated, asList, PAGE_STATUSES, RELATION_CONFIDENCES } from './pages.mjs'
6
6
  import { extractWikilinks, buildIndex } from './indexer.mjs'
7
7
  import { loadManifest } from './manifest.mjs'
8
+ import { writeFileAtomic } from './json.mjs'
8
9
 
9
10
  export async function lintKb(kbRoot, { fix = false } = {}) {
10
11
  const p = kbPaths(kbRoot)
@@ -126,6 +127,8 @@ export async function lintKb(kbRoot, { fix = false } = {}) {
126
127
 
127
128
  if (fix) { buildIndex(kbRoot); autoFixed.push('index-rebuilt') }
128
129
  const report = { autoFixed, mechanical, semantic }
129
- fs.writeFileSync(path.join(kbRoot, '.lint-report.json'), JSON.stringify(report, null, 2) + '\n')
130
+ // Atomic write for uniformity with the other JSON state files (temp+rename). The
131
+ // report has no in-process reader today, so this is preventive, not a live torn-read.
132
+ writeFileAtomic(path.join(kbRoot, '.lint-report.json'), JSON.stringify(report, null, 2) + '\n')
130
133
  return report
131
134
  }
package/src/scanner.mjs CHANGED
@@ -6,6 +6,7 @@ import { SUPPORTED_EXTS } from './convert.mjs'
6
6
  import { sha256File, minhashSignature, jaccardEstimate } from './hashing.mjs'
7
7
  import { loadManifest, diffManifest } from './manifest.mjs'
8
8
  import { listWikiPages, isInvalidated } from './pages.mjs'
9
+ import { writeFileAtomic } from './json.mjs'
9
10
 
10
11
  const TEXT_EXTS = ['.md', '.markdown', '.txt', '.html', '.htm']
11
12
  const NEAR_DUP_THRESHOLD = 0.85
@@ -169,6 +170,9 @@ export async function scanSource(srcDir, kbRoot, { exclude = [], persist = true,
169
170
  batches,
170
171
  estimate,
171
172
  }
172
- if (persist) fs.writeFileSync(kbPaths(kbRoot).scanPlan, JSON.stringify(report, null, 2) + '\n')
173
+ // Atomic write: `convert` reads .scan-plan.json (runConvertPlan) as a separate
174
+ // process, so a direct writeFileSync (truncate-then-write) could hand it a torn
175
+ // file. Same torn-read class as buildIndex's derived stores.
176
+ if (persist) writeFileAtomic(kbPaths(kbRoot).scanPlan, JSON.stringify(report, null, 2) + '\n')
173
177
  return report
174
178
  }