llm-wiki-kit 0.2.14 → 0.2.15
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/README.md +12 -4
- package/docs/concepts.md +14 -9
- package/docs/integrations/claude-code.md +4 -2
- package/docs/integrations/codex.md +4 -2
- package/docs/manual.md +51 -3
- package/docs/security.md +4 -0
- package/package.json +1 -1
- package/src/cli.js +52 -2
- package/src/constants.js +2 -0
- package/src/evidence.js +128 -0
- package/src/maintenance.js +127 -22
- package/src/project.js +5 -2
- package/src/templates.js +20 -0
- package/src/wiki-eval.js +110 -0
- package/src/wiki-export.js +214 -0
- package/src/wiki-lint.js +66 -1
- package/src/wiki-model.js +2 -0
- package/src/wiki-search.js +79 -13
- package/src/wiki-visibility.js +34 -4
package/src/wiki-lint.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { readdir } from 'fs/promises';
|
|
2
2
|
import { dirname, isAbsolute, join, parse, relative, resolve, sep } from 'path';
|
|
3
|
+
import { EVIDENCE_PREFIXES, MAX_CMD_EVIDENCE_CHARS, parseEvidenceRef } from './evidence.js';
|
|
3
4
|
import { exists, readText } from './fs-utils.js';
|
|
4
5
|
import { maintenanceSummary } from './maintenance.js';
|
|
5
6
|
import { inspectProjectState } from './project-state.js';
|
|
6
|
-
import { hasSecretLikeText } from './redaction.js';
|
|
7
|
+
import { hasSecretLikeText, isSensitivePath } from './redaction.js';
|
|
7
8
|
import { liveQaMaxBytes, liveQaMaxLines } from './live-qa.js';
|
|
8
9
|
import {
|
|
9
10
|
buildAliasMap,
|
|
@@ -145,6 +146,62 @@ async function sourceExists(projectRoot, sourceId) {
|
|
|
145
146
|
return false;
|
|
146
147
|
}
|
|
147
148
|
|
|
149
|
+
function fileEvidencePath(projectRoot, value) {
|
|
150
|
+
return resolve(projectRoot, String(value || '').replace(/\\/g, '/'));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function validateEvidenceRef(projectRoot, page, ref) {
|
|
154
|
+
const issues = [];
|
|
155
|
+
const parsed = parseEvidenceRef(ref);
|
|
156
|
+
const raw = parsed.raw;
|
|
157
|
+
const value = parsed.value;
|
|
158
|
+
if (!parsed.prefix || !EVIDENCE_PREFIXES.has(parsed.prefix)) {
|
|
159
|
+
issues.push(issue('error', 'invalid-evidence-ref-prefix', page.rel, `unsupported evidence_refs prefix: ${raw || '(empty)'}`));
|
|
160
|
+
return issues;
|
|
161
|
+
}
|
|
162
|
+
if (hasSecretLikeText(raw)) {
|
|
163
|
+
issues.push(issue('error', 'secret-like-evidence-ref', page.rel, 'evidence_refs contains token, credential, private-key, or secret-like text'));
|
|
164
|
+
return issues;
|
|
165
|
+
}
|
|
166
|
+
if (parsed.prefix === 'file') {
|
|
167
|
+
const normalized = value.replace(/\\/g, '/');
|
|
168
|
+
if (!normalized) {
|
|
169
|
+
issues.push(issue('error', 'invalid-file-evidence-ref', page.rel, 'file evidence ref is empty'));
|
|
170
|
+
} else if (isAbsolute(normalized) || /^[a-z][a-z0-9+.-]*:/i.test(normalized) || normalized.split('/').includes('..') || isSensitivePath(normalized)) {
|
|
171
|
+
issues.push(issue('error', 'invalid-file-evidence-ref', page.rel, `file evidence ref must be safe repo-relative path: ${normalized}`));
|
|
172
|
+
} else if (!(await pathExistsNormalized(fileEvidencePath(projectRoot, normalized)))) {
|
|
173
|
+
issues.push(issue('warning', 'missing-file-evidence-ref', page.rel, `file evidence ref has no matching project file: ${normalized}`));
|
|
174
|
+
}
|
|
175
|
+
} else if (parsed.prefix === 'raw') {
|
|
176
|
+
if (invalidSourceId(value)) {
|
|
177
|
+
issues.push(issue('error', 'invalid-raw-evidence-ref', page.rel, 'raw evidence ref points outside the project or uses unsupported syntax'));
|
|
178
|
+
} else if (!(await sourceExists(projectRoot, value))) {
|
|
179
|
+
issues.push(issue('warning', 'missing-raw-evidence-ref', page.rel, `raw evidence ref has no matching source file: ${value}`));
|
|
180
|
+
}
|
|
181
|
+
} else if (parsed.prefix === 'url') {
|
|
182
|
+
try {
|
|
183
|
+
const url = new URL(value);
|
|
184
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
185
|
+
issues.push(issue('error', 'invalid-url-evidence-ref', page.rel, `url evidence ref must use http or https: ${value}`));
|
|
186
|
+
}
|
|
187
|
+
if (url.username || url.password) {
|
|
188
|
+
issues.push(issue('error', 'credential-url-evidence-ref', page.rel, 'url evidence ref must not include credentials'));
|
|
189
|
+
}
|
|
190
|
+
} catch {
|
|
191
|
+
issues.push(issue('error', 'invalid-url-evidence-ref', page.rel, `url evidence ref is not a valid URL: ${value}`));
|
|
192
|
+
}
|
|
193
|
+
} else if (parsed.prefix === 'cmd') {
|
|
194
|
+
if (!value) {
|
|
195
|
+
issues.push(issue('error', 'invalid-cmd-evidence-ref', page.rel, 'cmd evidence ref is empty'));
|
|
196
|
+
} else if (value.length > MAX_CMD_EVIDENCE_CHARS) {
|
|
197
|
+
issues.push(issue('error', 'invalid-cmd-evidence-ref', page.rel, `cmd evidence ref exceeds ${MAX_CMD_EVIDENCE_CHARS} characters`));
|
|
198
|
+
} else if (/[\r\n]/.test(value)) {
|
|
199
|
+
issues.push(issue('error', 'invalid-cmd-evidence-ref', page.rel, 'cmd evidence ref must be a single line'));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return issues;
|
|
203
|
+
}
|
|
204
|
+
|
|
148
205
|
function titleKey(page) {
|
|
149
206
|
return normalizeTarget(page.title);
|
|
150
207
|
}
|
|
@@ -250,6 +307,14 @@ export async function runLint(projectRoot, options = {}) {
|
|
|
250
307
|
if (page.frontmatter.last_verified && !isDateLike(page.frontmatter.last_verified)) {
|
|
251
308
|
issues.push(issue('warning', 'invalid-last-verified', page.rel, `last_verified should be YYYY-MM-DD or unknown: ${page.frontmatter.last_verified}`));
|
|
252
309
|
}
|
|
310
|
+
if (page.frontmatter.evidence_refs !== undefined && !Array.isArray(page.frontmatter.evidence_refs)) {
|
|
311
|
+
issues.push(issue('warning', 'invalid-evidence-refs', page.rel, 'evidence_refs should be a YAML array'));
|
|
312
|
+
}
|
|
313
|
+
if (Array.isArray(page.frontmatter.evidence_refs)) {
|
|
314
|
+
for (const ref of page.evidenceRefs) {
|
|
315
|
+
issues.push(...await validateEvidenceRef(projectRoot, page, ref));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
253
318
|
if (page.status === 'stale') {
|
|
254
319
|
issues.push(issue('warning', 'stale-page', page.rel, 'page is marked stale'));
|
|
255
320
|
}
|
package/src/wiki-model.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { basename, join, posix, relative } from 'path';
|
|
2
|
+
import { normalizeEvidenceRefs } from './evidence.js';
|
|
2
3
|
import { exists, listMarkdownFiles, readText } from './fs-utils.js';
|
|
3
4
|
import { normalizeForStorage } from './redaction.js';
|
|
4
5
|
|
|
@@ -156,6 +157,7 @@ export function parseWikiPage(projectRoot, file, content) {
|
|
|
156
157
|
wikilinks: extractWikilinks(content),
|
|
157
158
|
markdownLinks: extractMarkdownLinks(content),
|
|
158
159
|
sourceIds: Array.isArray(data.source_ids) ? data.source_ids : [],
|
|
160
|
+
evidenceRefs: normalizeEvidenceRefs(data.evidence_refs),
|
|
159
161
|
};
|
|
160
162
|
}
|
|
161
163
|
|
package/src/wiki-search.js
CHANGED
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
isStalePage,
|
|
18
18
|
isSupersededPage,
|
|
19
19
|
searchWeight,
|
|
20
|
+
visibleWikiPages,
|
|
21
|
+
wikiVisibility,
|
|
20
22
|
} from './wiki-visibility.js';
|
|
21
23
|
|
|
22
24
|
const DEFAULT_LIMIT = 5;
|
|
@@ -69,7 +71,41 @@ function snippetFor(page, terms) {
|
|
|
69
71
|
return text.slice(start, start + SNIPPET_CHARS);
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
function matchedFieldsFor(page, terms) {
|
|
75
|
+
const fields = [];
|
|
76
|
+
const title = normalizeText(page.title);
|
|
77
|
+
const path = normalizeText(page.rel);
|
|
78
|
+
const body = normalizeText(page.body);
|
|
79
|
+
if (terms.some((term) => title.includes(term))) fields.push('title');
|
|
80
|
+
if (terms.some((term) => path.includes(term))) fields.push('path');
|
|
81
|
+
if (terms.some((term) => body.includes(term))) fields.push('body');
|
|
82
|
+
return fields;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function rankReason(page, fields = {}) {
|
|
86
|
+
const parts = [];
|
|
87
|
+
if (fields.source === 'linked') {
|
|
88
|
+
parts.push(`linked from ${fields.via?.join(', ') || 'direct hit'}`);
|
|
89
|
+
} else if ((fields.matchedFields || []).length > 0) {
|
|
90
|
+
parts.push(`matched ${fields.matchedFields.join(', ')}`);
|
|
91
|
+
} else {
|
|
92
|
+
parts.push('selected by retrieval score');
|
|
93
|
+
}
|
|
94
|
+
if (page.memoryType) parts.push(`memory:${page.memoryType}`);
|
|
95
|
+
if (page.frontmatter?.importance) parts.push(`importance:${page.frontmatter.importance}`);
|
|
96
|
+
if (page.status) parts.push(`status:${page.status}`);
|
|
97
|
+
return parts.join('; ');
|
|
98
|
+
}
|
|
99
|
+
|
|
72
100
|
function resultRecord(page, score, fields = {}) {
|
|
101
|
+
const matchedFields = fields.matchedFields || [];
|
|
102
|
+
const scoreBreakdown = {
|
|
103
|
+
miniSearchScore: fields.miniSearchScore || 0,
|
|
104
|
+
substringScore: fields.substringScore || 0,
|
|
105
|
+
directScore: fields.directScore || 0,
|
|
106
|
+
linkScore: fields.linkScore || 0,
|
|
107
|
+
weight: fields.weight || searchWeight(page),
|
|
108
|
+
};
|
|
73
109
|
return {
|
|
74
110
|
path: page.rel,
|
|
75
111
|
title: page.title,
|
|
@@ -82,6 +118,11 @@ function resultRecord(page, score, fields = {}) {
|
|
|
82
118
|
source: fields.source || 'direct',
|
|
83
119
|
via: fields.via || [],
|
|
84
120
|
matchedTerms: fields.matchedTerms || [],
|
|
121
|
+
matchedFields,
|
|
122
|
+
scoreBreakdown,
|
|
123
|
+
rankReason: fields.rankReason || rankReason(page, { ...fields, matchedFields }),
|
|
124
|
+
visibilityReason: fields.visibilityReason || wikiVisibility(page, fields.visibilityOptions || {}).reason,
|
|
125
|
+
evidenceRefs: page.evidenceRefs || [],
|
|
85
126
|
snippet: fields.snippet || '',
|
|
86
127
|
};
|
|
87
128
|
}
|
|
@@ -108,18 +149,19 @@ function searchVisibility(pages, options = {}) {
|
|
|
108
149
|
includeArchived: Boolean(options.includeArchived),
|
|
109
150
|
};
|
|
110
151
|
const visible = [];
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
if (
|
|
116
|
-
if (
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (isStalePage(page)) metadata.stalePagesSearched += 1;
|
|
120
|
-
visible.push(page);
|
|
152
|
+
const visibility = visibleWikiPages(pages, options);
|
|
153
|
+
for (const item of visibility.hidden) {
|
|
154
|
+
const page = item.page;
|
|
155
|
+
if (!options.includeArchived && isArchivedPage(page)) metadata.hiddenArchivedPages += 1;
|
|
156
|
+
if (!options.includeArchived && isSupersededPage(page)) metadata.hiddenSupersededPages += 1;
|
|
157
|
+
if (!options.includeEpisodic && isEpisodicLayerPage(page) && !isDefaultSearchCandidate(page, options)) metadata.hiddenEpisodicPages += 1;
|
|
158
|
+
}
|
|
159
|
+
for (const item of visibility.visible) {
|
|
160
|
+
if (isStalePage(item.page)) metadata.stalePagesSearched += 1;
|
|
161
|
+
visible.push(item.page);
|
|
121
162
|
}
|
|
122
163
|
metadata.searchedPages = visible.length;
|
|
164
|
+
metadata.visibilityPolicy = visibility.policy;
|
|
123
165
|
return { visible, metadata };
|
|
124
166
|
}
|
|
125
167
|
|
|
@@ -181,22 +223,34 @@ async function performSearch(projectRoot, query, options = {}) {
|
|
|
181
223
|
const page = byPath.get(item.id);
|
|
182
224
|
if (!page) continue;
|
|
183
225
|
const subScore = substringScore(page, terms);
|
|
184
|
-
const
|
|
226
|
+
const weight = searchWeight(page);
|
|
227
|
+
const score = (item.score + subScore) * weight;
|
|
185
228
|
hits.set(page.rel, resultRecord(page, score, {
|
|
186
229
|
directScore: score,
|
|
230
|
+
miniSearchScore: item.score,
|
|
231
|
+
substringScore: subScore,
|
|
232
|
+
weight,
|
|
233
|
+
matchedFields: matchedFieldsFor(page, terms),
|
|
187
234
|
matchedTerms: item.terms || terms,
|
|
188
235
|
snippet: snippetFor(page, terms),
|
|
236
|
+
visibilityOptions: opts,
|
|
189
237
|
}));
|
|
190
238
|
}
|
|
191
239
|
}
|
|
192
240
|
|
|
193
241
|
for (const page of pages) {
|
|
194
|
-
const
|
|
242
|
+
const subScore = substringScore(page, terms);
|
|
243
|
+
const weight = searchWeight(page);
|
|
244
|
+
const score = subScore * weight;
|
|
195
245
|
if (score <= 0 || hits.has(page.rel)) continue;
|
|
196
246
|
hits.set(page.rel, resultRecord(page, score, {
|
|
197
247
|
directScore: score,
|
|
248
|
+
substringScore: subScore,
|
|
249
|
+
weight,
|
|
250
|
+
matchedFields: matchedFieldsFor(page, terms),
|
|
198
251
|
matchedTerms: terms,
|
|
199
252
|
snippet: snippetFor(page, terms),
|
|
253
|
+
visibilityOptions: opts,
|
|
200
254
|
}));
|
|
201
255
|
}
|
|
202
256
|
|
|
@@ -212,13 +266,17 @@ async function performSearch(projectRoot, query, options = {}) {
|
|
|
212
266
|
if (hits.has(neighborPath)) continue;
|
|
213
267
|
const page = byPath.get(neighborPath);
|
|
214
268
|
if (!page) continue;
|
|
215
|
-
const
|
|
269
|
+
const weight = searchWeight(page);
|
|
270
|
+
const linkScore = seed.score * 0.2 * weight;
|
|
216
271
|
hits.set(neighborPath, resultRecord(page, linkScore, {
|
|
217
272
|
linkScore,
|
|
273
|
+
weight,
|
|
218
274
|
source: 'linked',
|
|
219
275
|
via: [seed.path],
|
|
220
276
|
matchedTerms: [],
|
|
277
|
+
matchedFields: [],
|
|
221
278
|
snippet: snippetFor(page, terms),
|
|
279
|
+
visibilityOptions: opts,
|
|
222
280
|
}));
|
|
223
281
|
}
|
|
224
282
|
}
|
|
@@ -238,6 +296,10 @@ function redactHit(hit) {
|
|
|
238
296
|
title: redactText(hit.title, 300),
|
|
239
297
|
via: Array.isArray(hit.via) ? hit.via.map((item) => redactText(item, 300)) : [],
|
|
240
298
|
matchedTerms: Array.isArray(hit.matchedTerms) ? hit.matchedTerms.map((item) => redactText(item, 120)) : [],
|
|
299
|
+
matchedFields: Array.isArray(hit.matchedFields) ? hit.matchedFields.map((item) => redactText(item, 120)) : [],
|
|
300
|
+
rankReason: redactText(hit.rankReason || '', 500),
|
|
301
|
+
visibilityReason: redactText(hit.visibilityReason || '', 500),
|
|
302
|
+
evidenceRefs: Array.isArray(hit.evidenceRefs) ? hit.evidenceRefs.map((item) => redactText(item, 700)) : [],
|
|
241
303
|
snippet: redactText(hit.snippet, SNIPPET_CHARS),
|
|
242
304
|
};
|
|
243
305
|
}
|
|
@@ -392,6 +454,10 @@ export function formatContextPack(pack) {
|
|
|
392
454
|
? `, linked via ${hit.via.join(', ')}`
|
|
393
455
|
: '';
|
|
394
456
|
lines.push(`- ${hit.path} (score ${hit.score.toFixed(2)}, ${hit.source}${suffix}): ${hit.snippet}`);
|
|
457
|
+
lines.push(` why selected: ${hit.rankReason || hit.visibilityReason || 'retrieval match'}`);
|
|
458
|
+
if ((hit.evidenceRefs || []).length > 0) {
|
|
459
|
+
lines.push(` evidence_refs: ${hit.evidenceRefs.slice(0, 3).join(', ')}`);
|
|
460
|
+
}
|
|
395
461
|
}
|
|
396
462
|
}
|
|
397
463
|
if (pack.logExcerpt) {
|
package/src/wiki-visibility.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export const DURABLE_MEMORY_TYPES = new Set(['semantic', 'procedural']);
|
|
2
|
+
export const DURABLE_VISIBILITY_POLICY = 'durable-default-v1';
|
|
2
3
|
|
|
3
4
|
export function frontmatterValues(value) {
|
|
4
5
|
if (Array.isArray(value)) return value.map((item) => String(item || '').trim()).filter(Boolean);
|
|
@@ -60,11 +61,40 @@ export function includeEpisodicPages(options = {}) {
|
|
|
60
61
|
return Boolean(options.includeEpisodic);
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
export function wikiVisibility(page, options = {}) {
|
|
65
|
+
if (!includeArchivedPages(options) && isArchivedPage(page)) {
|
|
66
|
+
return { visible: false, reason: 'hidden: archived page; use --include-archived to inspect it' };
|
|
67
|
+
}
|
|
68
|
+
if (!includeArchivedPages(options) && isSupersededPage(page)) {
|
|
69
|
+
return { visible: false, reason: 'hidden: superseded page; use --include-archived to inspect it' };
|
|
70
|
+
}
|
|
71
|
+
if (includeEpisodicPages(options)) {
|
|
72
|
+
return { visible: true, reason: 'visible: --include-episodic includes episodic/context pages' };
|
|
73
|
+
}
|
|
74
|
+
if (isEpisodicLayerPage(page) && !isPromotedDurablePage(page)) {
|
|
75
|
+
return { visible: false, reason: 'hidden: default episodic query/context/session page' };
|
|
76
|
+
}
|
|
77
|
+
const memoryType = String(page?.memoryType || '').toLowerCase();
|
|
78
|
+
if (DURABLE_MEMORY_TYPES.has(memoryType)) {
|
|
79
|
+
return { visible: true, reason: `visible: durable ${memoryType} wiki page` };
|
|
80
|
+
}
|
|
81
|
+
if (isStalePage(page)) return { visible: true, reason: 'visible: stale page kept searchable with lower ranking' };
|
|
82
|
+
return { visible: true, reason: 'visible: durable non-episodic wiki page' };
|
|
83
|
+
}
|
|
84
|
+
|
|
63
85
|
export function isDefaultSearchCandidate(page, options = {}) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
86
|
+
return wikiVisibility(page, options).visible;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function visibleWikiPages(pages, options = {}) {
|
|
90
|
+
const visible = [];
|
|
91
|
+
const hidden = [];
|
|
92
|
+
for (const page of pages) {
|
|
93
|
+
const visibility = wikiVisibility(page, options);
|
|
94
|
+
if (visibility.visible) visible.push({ page, visibility });
|
|
95
|
+
else hidden.push({ page, visibility });
|
|
96
|
+
}
|
|
97
|
+
return { visible, hidden, policy: DURABLE_VISIBILITY_POLICY };
|
|
68
98
|
}
|
|
69
99
|
|
|
70
100
|
export function searchWeight(page) {
|