llm-wiki-kit 0.2.8 → 0.2.10

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.
@@ -4,10 +4,20 @@ import { redactText } from './redaction.js';
4
4
  import {
5
5
  buildWikiGraph,
6
6
  collectWikiPages,
7
+ MEMORY_BYTE_LIMIT,
8
+ MEMORY_LINE_LIMIT,
7
9
  pageLookup,
8
10
  parseFrontmatter,
9
11
  readMemoryExcerpt,
10
12
  } from './wiki-model.js';
13
+ import {
14
+ isArchivedPage,
15
+ isDefaultSearchCandidate,
16
+ isEpisodicLayerPage,
17
+ isStalePage,
18
+ isSupersededPage,
19
+ searchWeight,
20
+ } from './wiki-visibility.js';
11
21
 
12
22
  const DEFAULT_LIMIT = 5;
13
23
  const SNIPPET_CHARS = 350;
@@ -65,6 +75,7 @@ function resultRecord(page, score, fields = {}) {
65
75
  title: page.title,
66
76
  type: page.type || '',
67
77
  memoryType: page.memoryType || '',
78
+ status: page.status || '',
68
79
  score,
69
80
  directScore: fields.directScore || 0,
70
81
  linkScore: fields.linkScore || 0,
@@ -81,22 +92,35 @@ function sortHits(a, b) {
81
92
  return a.path.localeCompare(b.path);
82
93
  }
83
94
 
84
- function importance(page) {
85
- const value = Number(page?.frontmatter?.importance || 0);
86
- return Number.isFinite(value) ? value : 0;
95
+ export async function searchWiki(projectRoot, query, options = {}) {
96
+ return (await performSearch(projectRoot, query, options)).hits;
87
97
  }
88
98
 
89
- function isDefaultSearchCandidate(page, options = {}) {
90
- if (options.includeEpisodic) return true;
91
- const type = String(page.type || '').toLowerCase();
92
- if (type === 'query' || page.rel.startsWith('wiki/queries/')) {
93
- return ['semantic', 'procedural'].includes(page.memoryType) && importance(page) >= 4;
99
+ function searchVisibility(pages, options = {}) {
100
+ const metadata = {
101
+ totalPages: pages.length,
102
+ searchedPages: 0,
103
+ hiddenEpisodicPages: 0,
104
+ hiddenArchivedPages: 0,
105
+ hiddenSupersededPages: 0,
106
+ stalePagesSearched: 0,
107
+ includeEpisodic: Boolean(options.includeEpisodic),
108
+ includeArchived: Boolean(options.includeArchived),
109
+ };
110
+ const visible = [];
111
+ for (const page of pages) {
112
+ const hiddenArchived = !options.includeArchived && isArchivedPage(page);
113
+ const hiddenSuperseded = !options.includeArchived && isSupersededPage(page);
114
+ const hiddenEpisodic = !options.includeEpisodic && isEpisodicLayerPage(page) && !isDefaultSearchCandidate(page, options);
115
+ if (hiddenArchived) metadata.hiddenArchivedPages += 1;
116
+ if (hiddenSuperseded) metadata.hiddenSupersededPages += 1;
117
+ if (hiddenEpisodic) metadata.hiddenEpisodicPages += 1;
118
+ if (!isDefaultSearchCandidate(page, options)) continue;
119
+ if (isStalePage(page)) metadata.stalePagesSearched += 1;
120
+ visible.push(page);
94
121
  }
95
- return true;
96
- }
97
-
98
- export async function searchWiki(projectRoot, query, options = {}) {
99
- return (await performSearch(projectRoot, query, options)).hits;
122
+ metadata.searchedPages = visible.length;
123
+ return { visible, metadata };
100
124
  }
101
125
 
102
126
  async function performSearch(projectRoot, query, options = {}) {
@@ -109,8 +133,21 @@ async function performSearch(projectRoot, query, options = {}) {
109
133
  }
110
134
 
111
135
  let pages = [];
136
+ let metadata = {
137
+ totalPages: 0,
138
+ searchedPages: 0,
139
+ hiddenEpisodicPages: 0,
140
+ hiddenArchivedPages: 0,
141
+ hiddenSupersededPages: 0,
142
+ stalePagesSearched: 0,
143
+ includeEpisodic: Boolean(opts.includeEpisodic),
144
+ includeArchived: Boolean(opts.includeArchived),
145
+ };
112
146
  try {
113
- pages = (await collectWikiPages(projectRoot, opts)).filter((page) => isDefaultSearchCandidate(page, opts));
147
+ const collected = await collectWikiPages(projectRoot, opts);
148
+ const visibility = searchVisibility(collected, opts);
149
+ pages = visibility.visible;
150
+ metadata = visibility.metadata;
114
151
  } catch {
115
152
  return { hits: [], search: 'missing-wiki' };
116
153
  }
@@ -144,7 +181,7 @@ async function performSearch(projectRoot, query, options = {}) {
144
181
  const page = byPath.get(item.id);
145
182
  if (!page) continue;
146
183
  const subScore = substringScore(page, terms);
147
- const score = item.score + subScore;
184
+ const score = (item.score + subScore) * searchWeight(page);
148
185
  hits.set(page.rel, resultRecord(page, score, {
149
186
  directScore: score,
150
187
  matchedTerms: item.terms || terms,
@@ -154,7 +191,7 @@ async function performSearch(projectRoot, query, options = {}) {
154
191
  }
155
192
 
156
193
  for (const page of pages) {
157
- const score = substringScore(page, terms);
194
+ const score = substringScore(page, terms) * searchWeight(page);
158
195
  if (score <= 0 || hits.has(page.rel)) continue;
159
196
  hits.set(page.rel, resultRecord(page, score, {
160
197
  directScore: score,
@@ -175,7 +212,7 @@ async function performSearch(projectRoot, query, options = {}) {
175
212
  if (hits.has(neighborPath)) continue;
176
213
  const page = byPath.get(neighborPath);
177
214
  if (!page) continue;
178
- const linkScore = seed.score * 0.2;
215
+ const linkScore = seed.score * 0.2 * searchWeight(page);
179
216
  hits.set(neighborPath, resultRecord(page, linkScore, {
180
217
  linkScore,
181
218
  source: 'linked',
@@ -190,6 +227,7 @@ async function performSearch(projectRoot, query, options = {}) {
190
227
  return {
191
228
  hits: [...hits.values()].sort(sortHits).slice(0, limit),
192
229
  search,
230
+ metadata,
193
231
  };
194
232
  }
195
233
 
@@ -282,11 +320,12 @@ export async function buildContextPack(projectRoot, query, options = {}) {
282
320
  const limit = Number(options.limit || DEFAULT_LIMIT);
283
321
  const expand = options.expand !== false;
284
322
  const memoryExcerpt = await readMemoryExcerpt(projectRoot);
285
- const indexExcerpt = (await readText(join(projectRoot, 'llm-wiki', 'wiki', 'index.md'))).slice(0, 1200).trim();
323
+ const rawIndexExcerpt = await readText(join(projectRoot, 'llm-wiki', 'wiki', 'index.md'));
324
+ const indexExcerpt = rawIndexExcerpt.slice(0, 1200).trim();
286
325
  const logExcerpt = options.includeLog
287
326
  ? (await readText(join(projectRoot, 'llm-wiki', 'wiki', 'log.md'))).slice(-1000).trim()
288
327
  : '';
289
- const result = query ? await performSearch(projectRoot, query, { ...options, limit, expand }) : { hits: [], search: 'none' };
328
+ const result = query ? await performSearch(projectRoot, query, { ...options, limit, expand }) : { hits: [], search: 'none', metadata: null };
290
329
  return {
291
330
  workspace: projectRoot,
292
331
  query: redactText(query || '', 1000),
@@ -297,6 +336,32 @@ export async function buildContextPack(projectRoot, query, options = {}) {
297
336
  indexExcerpt: redactText(indexExcerpt, 2000),
298
337
  logExcerpt: redactText(logExcerpt, 2000),
299
338
  hits: result.hits.map(redactHit),
339
+ metadata: {
340
+ search: result.metadata,
341
+ budgets: {
342
+ memory: {
343
+ maxLines: MEMORY_LINE_LIMIT,
344
+ maxBytes: MEMORY_BYTE_LIMIT,
345
+ returnedBytes: Buffer.byteLength(memoryExcerpt.trim(), 'utf8'),
346
+ redactionMaxChars: 30000,
347
+ },
348
+ index: {
349
+ maxChars: 1200,
350
+ sourceChars: rawIndexExcerpt.length,
351
+ returnedChars: indexExcerpt.length,
352
+ redactionMaxChars: 2000,
353
+ },
354
+ hits: {
355
+ requestedLimit: limit,
356
+ returned: result.hits.length,
357
+ snippetChars: SNIPPET_CHARS,
358
+ hookSnippetChars: HOOK_SNIPPET_CHARS,
359
+ expand,
360
+ includeEpisodic: Boolean(options.includeEpisodic),
361
+ includeArchived: Boolean(options.includeArchived),
362
+ },
363
+ },
364
+ },
300
365
  };
301
366
  }
302
367
 
@@ -0,0 +1,76 @@
1
+ export const DURABLE_MEMORY_TYPES = new Set(['semantic', 'procedural']);
2
+
3
+ export function frontmatterValues(value) {
4
+ if (Array.isArray(value)) return value.map((item) => String(item || '').trim()).filter(Boolean);
5
+ const text = String(value || '').trim();
6
+ if (!text || text === '[]') return [];
7
+ return [text];
8
+ }
9
+
10
+ export function pageImportance(page) {
11
+ const value = Number(page?.frontmatter?.importance || 0);
12
+ return Number.isFinite(value) ? value : 0;
13
+ }
14
+
15
+ export function isArchivedPage(page) {
16
+ return String(page?.status || '').toLowerCase() === 'archived';
17
+ }
18
+
19
+ export function isStalePage(page) {
20
+ return String(page?.status || '').toLowerCase() === 'stale';
21
+ }
22
+
23
+ export function supersededBy(page) {
24
+ return frontmatterValues(page?.frontmatter?.superseded_by);
25
+ }
26
+
27
+ export function supersedes(page) {
28
+ return frontmatterValues(page?.frontmatter?.supersedes);
29
+ }
30
+
31
+ export function isSupersededPage(page) {
32
+ return supersededBy(page).length > 0;
33
+ }
34
+
35
+ export function hasSupersession(page) {
36
+ return supersededBy(page).length > 0 || supersedes(page).length > 0;
37
+ }
38
+
39
+ export function isEpisodicLayerPage(page) {
40
+ const type = String(page?.type || '').toLowerCase();
41
+ const rel = String(page?.rel || '');
42
+ return (
43
+ type === 'query' ||
44
+ type === 'context' ||
45
+ type === 'session-log' ||
46
+ rel.startsWith('wiki/queries/') ||
47
+ rel.startsWith('wiki/context/')
48
+ );
49
+ }
50
+
51
+ export function isPromotedDurablePage(page) {
52
+ return DURABLE_MEMORY_TYPES.has(String(page?.memoryType || '').toLowerCase()) && pageImportance(page) >= 4;
53
+ }
54
+
55
+ export function includeArchivedPages(options = {}) {
56
+ return Boolean(options.includeArchived);
57
+ }
58
+
59
+ export function includeEpisodicPages(options = {}) {
60
+ return Boolean(options.includeEpisodic);
61
+ }
62
+
63
+ export function isDefaultSearchCandidate(page, options = {}) {
64
+ if (!includeArchivedPages(options) && (isArchivedPage(page) || isSupersededPage(page))) return false;
65
+ if (includeEpisodicPages(options)) return true;
66
+ if (isEpisodicLayerPage(page)) return isPromotedDurablePage(page);
67
+ return true;
68
+ }
69
+
70
+ export function searchWeight(page) {
71
+ let weight = 1;
72
+ if (DURABLE_MEMORY_TYPES.has(String(page?.memoryType || '').toLowerCase())) weight *= 1.2;
73
+ if (pageImportance(page) >= 4) weight *= 1.1;
74
+ if (isStalePage(page)) weight *= 0.35;
75
+ return weight;
76
+ }