@sdsrs/llm-wiki 0.7.2 → 0.7.4
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 +43 -0
- package/bin/llm-wiki.mjs +6 -0
- package/package.json +1 -1
- package/src/export.mjs +13 -1
- package/src/hashing.mjs +6 -1
- package/src/indexer.mjs +8 -2
- package/src/lint.mjs +4 -1
- package/src/scanner.mjs +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.4 (2026-07-12)
|
|
4
|
+
|
|
5
|
+
Two robustness fixes surfaced by an adversarial QA pass over the CLI. No config
|
|
6
|
+
schema change, no KB migration, no retrieval-default change. Suite 230 → 234.
|
|
7
|
+
|
|
8
|
+
**Fixes:**
|
|
9
|
+
|
|
10
|
+
- **`index` warns when a page is skipped for invalid frontmatter.** `buildIndex`
|
|
11
|
+
silently excluded any page with unparseable YAML frontmatter from
|
|
12
|
+
`index.md`/`graph.json`/`llms.txt`. Because the docs route hand-editors to
|
|
13
|
+
`index` (not `lint`) after editing, a broken-YAML edit made the page vanish
|
|
14
|
+
from every query surface with no signal — `indexed N pages`, exit 0, nothing
|
|
15
|
+
else. `buildIndex` now returns the skipped pages and the CLI prints a one-line
|
|
16
|
+
stderr warning naming them and pointing at `lint`; stdout stays the clean
|
|
17
|
+
count, and `lint` still owns the detailed per-page report.
|
|
18
|
+
- **`scan` near-duplicate detection uses 128 minhash permutations (was 32).**
|
|
19
|
+
Near-dup similarity was estimated from a 32-permutation minhash and compared
|
|
20
|
+
against the 0.85 threshold, but at 32 perms the estimate's standard error
|
|
21
|
+
(~0.05) let a genuine near-duplicate (true Jaccard 0.898) estimate 0.844 and
|
|
22
|
+
be reported as `near: 0`. 128 perms tightens the estimate (SD ~0.026) so real
|
|
23
|
+
near-dups clear the threshold; the cost is trivial next to the per-file
|
|
24
|
+
sha256+read scan already does, and the signature is stripped before the plan
|
|
25
|
+
is persisted (no on-disk change).
|
|
26
|
+
|
|
27
|
+
## 0.7.3 (2026-07-12)
|
|
28
|
+
|
|
29
|
+
Two robustness fixes from the final targeted dogfooding sweep. No config schema
|
|
30
|
+
change, no KB migration, no retrieval-default change. Suite 228 → 230.
|
|
31
|
+
|
|
32
|
+
**Fixes:**
|
|
33
|
+
|
|
34
|
+
- **`graph`/exports name the file on a corrupt `graph.json`.** `loadGraph` was the
|
|
35
|
+
last JSON reader still using a bare `JSON.parse`: a corrupt `wiki/graph.json`
|
|
36
|
+
threw an unqualified `Unexpected token` SyntaxError, and a valid-JSON-but-wrong-
|
|
37
|
+
shape file like `{}` crashed on `graph.nodes.map`. It now routes through the
|
|
38
|
+
shared `readJsonFile` (names the file) and guards the nodes/edges shape, so both
|
|
39
|
+
corruption modes yield a named, actionable "rerun `llm-wiki index`" error.
|
|
40
|
+
- **`.scan-plan.json` is written atomically.** `scan` wrote the plan with a direct
|
|
41
|
+
`writeFileSync`, but `convert` reads it from a separate process — the same
|
|
42
|
+
truncate-then-write torn read that 0.7.2 fixed for `graph.json`. It (and
|
|
43
|
+
`.lint-report.json`) now write via the shared temp-and-rename `writeFileAtomic`,
|
|
44
|
+
so every JSON state file another process can read is crash-safe.
|
|
45
|
+
|
|
3
46
|
## 0.7.2 (2026-07-12)
|
|
4
47
|
|
|
5
48
|
Three defects found by continued end-to-end dogfooding — two robustness/DoS
|
package/bin/llm-wiki.mjs
CHANGED
|
@@ -65,6 +65,12 @@ program.command('index')
|
|
|
65
65
|
.action((opts) => {
|
|
66
66
|
const r = buildIndex(opts.kb)
|
|
67
67
|
console.log(`indexed ${r.pageCount} pages${r.topicsSplit ? ' (split into topics/)' : ''}`)
|
|
68
|
+
// A page with unparseable frontmatter is excluded from index.md/graph.json/llms.txt. Surface
|
|
69
|
+
// it on stderr (stdout stays the clean count) so a hand-edit that broke the YAML doesn't
|
|
70
|
+
// silently vanish — the README points hand-editors here, not at `lint`.
|
|
71
|
+
if (r.skipped?.length) {
|
|
72
|
+
console.error(`warning: skipped ${r.skipped.length} page(s) with invalid frontmatter (run 'lint' for details): ${r.skipped.map(s => s.relPath).join(', ')}`)
|
|
73
|
+
}
|
|
68
74
|
})
|
|
69
75
|
|
|
70
76
|
program.command('embed')
|
package/package.json
CHANGED
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
|
-
|
|
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/hashing.mjs
CHANGED
|
@@ -13,7 +13,12 @@ function fnv1a(str) {
|
|
|
13
13
|
return h
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
// 128 permutations, not 32: the near-dup estimate's standard error is ~sqrt(J(1-J)/perms),
|
|
17
|
+
// so at 32 perms a genuine near-duplicate (true Jaccard ~0.90) estimated as low as 0.84 and
|
|
18
|
+
// fell below NEAR_DUP_THRESHOLD (0.85), silently missing it (QA73-002). 128 perms tightens the
|
|
19
|
+
// estimate (SD ~0.026) so real near-dups clear the threshold; the cost is trivial next to the
|
|
20
|
+
// per-file sha256+read scan already does, and `_sig` is stripped before the plan is persisted.
|
|
21
|
+
export function minhashSignature(text, perms = 128) {
|
|
17
22
|
const norm = text.toLowerCase().replace(/\s+/g, ' ')
|
|
18
23
|
const shingles = new Set()
|
|
19
24
|
for (let i = 0; i <= norm.length - 5; i++) shingles.add(norm.slice(i, i + 5))
|
package/src/indexer.mjs
CHANGED
|
@@ -26,7 +26,13 @@ const inline = (s) => String(s ?? '').replace(/[\r\n]+/g, ' ')
|
|
|
26
26
|
export function buildIndex(kbRoot) {
|
|
27
27
|
const p = kbPaths(kbRoot)
|
|
28
28
|
const cfg = loadKbConfig(kbRoot)
|
|
29
|
-
const
|
|
29
|
+
const allPages = listWikiPages(kbRoot)
|
|
30
|
+
const pages = allPages.filter(pg => !pg.error)
|
|
31
|
+
// Pages with unparseable/missing frontmatter are excluded from every derived file. Return
|
|
32
|
+
// them so the CLI can warn instead of silently dropping a page — the README routes
|
|
33
|
+
// hand-editors to `index` (not `lint`), so a bad edit would otherwise vanish with no signal
|
|
34
|
+
// (QA73-001). lint still owns the detailed per-page report.
|
|
35
|
+
const skipped = allPages.filter(pg => pg.error).map(pg => ({ relPath: pg.relPath, error: pg.error }))
|
|
30
36
|
|
|
31
37
|
// Preserve the pending section, bounded at the next `## ` heading — a greedy
|
|
32
38
|
// match to EOF would swallow user-added sections into "pending" forever.
|
|
@@ -122,5 +128,5 @@ export function buildIndex(kbRoot) {
|
|
|
122
128
|
...pages.filter(pg => !isInvalidated(pg)).map(pg => `- [${inline(pg.data.title)}](wiki/${pg.relPath}): ${inline(pg.data.description)}`)].join('\n') + '\n'
|
|
123
129
|
writeFileAtomic(p.llmsTxt, llms)
|
|
124
130
|
|
|
125
|
-
return { pageCount: pages.length, topicsSplit }
|
|
131
|
+
return { pageCount: pages.length, topicsSplit, skipped }
|
|
126
132
|
}
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|