@sdsrs/llm-wiki 0.6.7 → 0.7.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 +21 -0
- package/package.json +1 -1
- package/src/ask.mjs +10 -3
- package/src/scanner.mjs +11 -0
- package/src/templates.mjs +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.0 (2026-07-11)
|
|
4
|
+
|
|
5
|
+
Two retrieval-default changes (hence a minor bump). Both are opt-out via
|
|
6
|
+
`wiki.config.json`; no KB migration, no re-index needed. Suite 210 → 213.
|
|
7
|
+
|
|
8
|
+
**What changes for you:**
|
|
9
|
+
|
|
10
|
+
- **BM25 now weights page titles.** A query term in a page's title outranks the
|
|
11
|
+
same term buried in another page's body. Measured on the dogfood KB (24 probes):
|
|
12
|
+
Recall@5 0.688 → 0.729, MRR 0.646 → 0.675 (the lift is entirely on fact-style
|
|
13
|
+
queries; cross-language and multi-hop are unchanged). **Opt out** by setting
|
|
14
|
+
`"bm25TitleWeight": 1` in `wiki.config.json` (default `3`) — `1` restores the
|
|
15
|
+
previous flat single-field indexing.
|
|
16
|
+
- **`ask` now budgets tokens pessimistically.** Page size against `askTokenBudget`
|
|
17
|
+
is now estimated at a worst-case ~2 chars/token (was ~4), so dense
|
|
18
|
+
markdown/code pages no longer silently overflow a small model context window.
|
|
19
|
+
The visible effect: on a tight budget, `ask` may load fewer whole pages before
|
|
20
|
+
trimming the lowest-ranked ones. **To include more pages**, raise
|
|
21
|
+
`"askTokenBudget"` in `wiki.config.json` (default `32000`) — pages are still
|
|
22
|
+
always sent whole, never truncated.
|
|
23
|
+
|
|
3
24
|
## 0.6.7 (2026-07-11)
|
|
4
25
|
|
|
5
26
|
Contract/skill clarifications, an env-var convenience, and an explicit platform
|
package/package.json
CHANGED
package/src/ask.mjs
CHANGED
|
@@ -4,17 +4,22 @@ import { kbPaths } from './paths.mjs'
|
|
|
4
4
|
import { loadKbConfig } from './templates.mjs'
|
|
5
5
|
import { listWikiPages, isInvalidated } from './pages.mjs'
|
|
6
6
|
import { buildBm25Index, searchBm25 } from './bm25.mjs'
|
|
7
|
-
import {
|
|
7
|
+
import { worstCaseTokens } from './scanner.mjs'
|
|
8
8
|
import { loadLlmConfig, makeTransport } from './llm-config.mjs'
|
|
9
9
|
import { loadVectorStore, normalize, cosineTopK } from './vector.mjs'
|
|
10
10
|
import { embedTexts } from './embed.mjs'
|
|
11
11
|
import { fetchWithRetry } from './retry.mjs'
|
|
12
12
|
|
|
13
13
|
export function retrievePages(kbRoot, question, k = 6) {
|
|
14
|
+
// Title is a field: repeat it bm25TitleWeight times in the indexed text so a
|
|
15
|
+
// title term outweighs the same term buried in the body (measured +0.04 Recall@5
|
|
16
|
+
// on the dogfood KB at ×3). Set bm25TitleWeight: 1 in wiki.config.json to restore
|
|
17
|
+
// flat single-field indexing.
|
|
18
|
+
const w = Math.max(1, Math.trunc(loadKbConfig(kbRoot).bm25TitleWeight ?? 1))
|
|
14
19
|
const pages = listWikiPages(kbRoot).filter(p => !p.error && !isInvalidated(p))
|
|
15
20
|
const idx = buildBm25Index(pages.map(p => ({
|
|
16
21
|
id: p.relPath,
|
|
17
|
-
text: [p.data.title, p.data.description, (p.data.tags ?? []).join(' '), p.body].join('\n'),
|
|
22
|
+
text: [Array(w).fill(p.data.title ?? '').join('\n'), p.data.description, (p.data.tags ?? []).join(' '), p.body].join('\n'),
|
|
18
23
|
})))
|
|
19
24
|
return searchBm25(idx, question, k).map(h => ({ relPath: h.id, score: h.score }))
|
|
20
25
|
}
|
|
@@ -164,7 +169,9 @@ export async function askKb(kbRoot, question, { k = 6, retrieveOnly = false, fet
|
|
|
164
169
|
// off every lower-ranked page too (no greedy backfill with smaller pages).
|
|
165
170
|
if (trimmed.length > 0) { trimmed.push(h.relPath); continue }
|
|
166
171
|
const text = fs.readFileSync(path.join(p.wiki, h.relPath), 'utf8')
|
|
167
|
-
|
|
172
|
+
// Pessimistic estimate (worst-case ~2 chars/token) so dense pages don't overflow
|
|
173
|
+
// a small context window; raise askTokenBudget to include more pages per query.
|
|
174
|
+
const tokens = worstCaseTokens(text)
|
|
168
175
|
if (loaded.length > 0 && used + tokens > kbCfg.askTokenBudget) { trimmed.push(h.relPath); continue }
|
|
169
176
|
used += tokens
|
|
170
177
|
loaded.push({ ...h, text })
|
package/src/scanner.mjs
CHANGED
|
@@ -49,6 +49,17 @@ export function estimateTokens(text) {
|
|
|
49
49
|
return Math.round(cjk / 1.6 + (text.length - cjk) / 4)
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
// Pessimistic token estimate for budgeting model input (ask). Dense markdown/code
|
|
53
|
+
// runs ~2-3.5 chars/BPE-token, so estimateTokens' chars/4 under-counts and can
|
|
54
|
+
// overflow a small context window; assume ~2 chars/token for non-CJK and ~1
|
|
55
|
+
// token/char for CJK. (Mirror of embed.mjs's local worstCaseEmbedTokens, which
|
|
56
|
+
// stays local by its own note; this is the shared budgeting copy.)
|
|
57
|
+
export function worstCaseTokens(text) {
|
|
58
|
+
let cjk = 0
|
|
59
|
+
for (const ch of text) if (/[ -鿿豈-]/.test(ch)) cjk++
|
|
60
|
+
return Math.round(cjk + (text.length - cjk) / 2)
|
|
61
|
+
}
|
|
62
|
+
|
|
52
63
|
// Symlinked directories are not followed (loop safety) but are recorded in
|
|
53
64
|
// `skippedDirs` so they surface in the scan report instead of vanishing silently.
|
|
54
65
|
// Symlinked files are followed ONLY when their target resolves inside the source
|
package/src/templates.mjs
CHANGED
|
@@ -12,6 +12,7 @@ export const DEFAULT_CONFIG = {
|
|
|
12
12
|
linkStyle: 'wikilink',
|
|
13
13
|
askTokenBudget: 32000,
|
|
14
14
|
maxFileBytes: 52428800,
|
|
15
|
+
bm25TitleWeight: 3,
|
|
15
16
|
vectorEnabled: false,
|
|
16
17
|
relationTypes: ['implements', 'uses', 'depends_on', 'part_of', 'instance_of', 'derived_from', 'contrasts_with', 'causes'],
|
|
17
18
|
}
|