@plebeiantech/lat.md 0.12.3-fork.4 → 0.12.3-fork.6

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.
Files changed (64) hide show
  1. package/dist/src/cli/check-coverage.d.ts +2 -0
  2. package/dist/src/cli/check-coverage.js +71 -0
  3. package/dist/src/cli/check-mode.d.ts +6 -0
  4. package/dist/src/cli/check-mode.js +44 -2
  5. package/dist/src/cli/check.js +5 -1
  6. package/dist/src/cli/fork-instructions.d.ts +19 -0
  7. package/dist/src/cli/fork-instructions.js +183 -0
  8. package/dist/src/cli/fork-scaffold.d.ts +16 -0
  9. package/dist/src/cli/fork-scaffold.js +129 -0
  10. package/dist/src/cli/index.js +29 -4
  11. package/dist/src/cli/init.js +4 -0
  12. package/dist/src/cli/search.js +6 -4
  13. package/dist/src/cli/ui-build.d.ts +4 -0
  14. package/dist/src/cli/ui-build.js +13 -0
  15. package/dist/src/cli/ui.js +18 -2
  16. package/dist/src/code-refs.js +25 -4
  17. package/dist/src/fork/upstream-drift-cli.d.ts +1 -0
  18. package/dist/src/fork/upstream-drift-cli.js +2 -0
  19. package/dist/src/fork/upstream-drift.d.ts +26 -0
  20. package/dist/src/fork/upstream-drift.js +185 -0
  21. package/dist/src/fork/upstream-guard-cli.d.ts +1 -0
  22. package/dist/src/fork/upstream-guard-cli.js +2 -0
  23. package/dist/src/fork/upstream-guard.d.ts +72 -0
  24. package/dist/src/fork/upstream-guard.js +445 -0
  25. package/dist/src/init-version.d.ts +1 -1
  26. package/dist/src/init-version.js +1 -1
  27. package/dist/src/lattice.d.ts +2 -1
  28. package/dist/src/lattice.js +1 -0
  29. package/dist/src/search/search.d.ts +1 -0
  30. package/dist/src/search/search.js +15 -9
  31. package/dist/src/view/client/assets/index-VZwPPiFE.js +334 -0
  32. package/dist/src/view/client/assets/index-W8ktvNj9.css +1 -0
  33. package/dist/src/view/client/assets/logo-CNF8zs47.svg +182 -0
  34. package/dist/src/view/client/index.html +2 -2
  35. package/dist/src/view/git-diff.js +17 -0
  36. package/dist/src/view/graph.d.ts +6 -0
  37. package/dist/src/view/graph.js +176 -0
  38. package/dist/src/view/markdown.d.ts +5 -1
  39. package/dist/src/view/markdown.js +112 -25
  40. package/dist/src/view/protocol.d.ts +43 -0
  41. package/dist/src/view/protocol.js +1 -1
  42. package/dist/src/view/references.d.ts +2 -0
  43. package/dist/src/view/references.js +30 -8
  44. package/dist/src/view/repository.d.ts +5 -2
  45. package/dist/src/view/repository.js +13 -12
  46. package/dist/src/view/search.js +1 -0
  47. package/dist/src/view/server.d.ts +3 -0
  48. package/dist/src/view/server.js +42 -9
  49. package/dist/src/view/source-target.d.ts +8 -0
  50. package/dist/src/view/source-target.js +22 -0
  51. package/dist/src/view/static-build.d.ts +16 -0
  52. package/dist/src/view/static-build.js +408 -0
  53. package/dist/src/view/static-protocol.d.ts +23 -0
  54. package/dist/src/view/static-protocol.js +10 -0
  55. package/dist/src/view/store.d.ts +9 -1
  56. package/dist/src/view/store.js +82 -15
  57. package/dist/src/view/table-of-contents.d.ts +9 -0
  58. package/dist/src/view/table-of-contents.js +103 -0
  59. package/package.json +3 -1
  60. package/templates/AGENTS.md +2 -13
  61. package/templates/fork/conventions.md +95 -0
  62. package/templates/skill/SKILL.md +3 -14
  63. package/dist/src/view/client/assets/index-B0n0u03y.css +0 -1
  64. package/dist/src/view/client/assets/index-xm2_px4q.js +0 -23
@@ -0,0 +1,445 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { isAbsolute, resolve } from 'node:path';
4
+ // @lat: [[fork#The upstream guard]]
5
+ export const SYNC_POINT_FILE = 'fork-upstream-sync-point';
6
+ /**
7
+ * The commit this fork was originally cut from. It is not the guard's
8
+ * baseline — see [[fork#The upstream guard#The sync point]] for why a frozen
9
+ * one cannot tell the fork's changes from upstream's.
10
+ */
11
+ export const ORIGINAL_FORK_POINT = '65d2f5b220853b2b10dca3de18d511f575bc12c7';
12
+ // @lat: [[fork#The upstream guard#The allowlist]]
13
+ export const ALLOWLIST_FILE = 'fork-upstream-allowlist.tsv';
14
+ export const REASON_PLACEHOLDER = 'TODO';
15
+ const ALLOWLIST_HEADER = [
16
+ '# Upstream files this fork is permitted to modify.',
17
+ '#',
18
+ '# Generated by `node dist/src/fork/upstream-guard-cli.js --regenerate`.',
19
+ '# One entry per line: the repo-relative path, a TAB, then the reason the',
20
+ '# fork must diverge in that file. A placeholder reason fails the check —',
21
+ '# regenerate writes the marker, a person replaces it with the real reason.',
22
+ '#',
23
+ '# Paths are relative to the sync point in fork-upstream-sync-point, so an',
24
+ '# upstream merge moves this list. Regenerate in the same commit.',
25
+ '#',
26
+ '# Deleting an upstream file is never allowlistable. See lat.md/fork.md.',
27
+ '',
28
+ ];
29
+ export function parseAllowlist(text) {
30
+ const entries = [];
31
+ const errors = [];
32
+ const seen = new Map();
33
+ text.split('\n').forEach((raw, index) => {
34
+ const line = index + 1;
35
+ const stripped = raw.replace(/\r$/, '');
36
+ if (stripped.trim() === '' || stripped.trimStart().startsWith('#'))
37
+ return;
38
+ const tab = stripped.indexOf('\t');
39
+ const split = tab >= 0 ? tab : stripped.search(/\s/);
40
+ if (split < 0) {
41
+ errors.push(`${ALLOWLIST_FILE}:${line}: entry has no reason: ${stripped}`);
42
+ return;
43
+ }
44
+ const path = stripped.slice(0, split).trim();
45
+ const reason = stripped.slice(split + 1).trim();
46
+ if (path === '') {
47
+ errors.push(`${ALLOWLIST_FILE}:${line}: entry has no path`);
48
+ return;
49
+ }
50
+ if (reason === '') {
51
+ errors.push(`${ALLOWLIST_FILE}:${line}: entry has no reason: ${path}`);
52
+ return;
53
+ }
54
+ const first = seen.get(path);
55
+ if (first !== undefined) {
56
+ errors.push(`${ALLOWLIST_FILE}:${line}: duplicate entry for ${path} ` +
57
+ `(first at line ${first})`);
58
+ return;
59
+ }
60
+ seen.set(path, line);
61
+ entries.push({ path, reason, line });
62
+ });
63
+ return { entries, errors };
64
+ }
65
+ export function renderAllowlist(entries) {
66
+ const sorted = [...entries].sort((a, b) => (a.path < b.path ? -1 : 1));
67
+ const body = sorted.map((e) => `${e.path}\t${e.reason}`);
68
+ return [...ALLOWLIST_HEADER, ...body, ''].join('\n');
69
+ }
70
+ export function needsReason(reason) {
71
+ return reason.trimStart().toUpperCase().startsWith(REASON_PLACEHOLDER);
72
+ }
73
+ export function parseNameStatus(raw) {
74
+ const fields = raw.split('\0').filter((f) => f !== '');
75
+ const out = [];
76
+ for (let i = 0; i + 1 < fields.length; i += 2) {
77
+ out.push({ status: fields[i], path: fields[i + 1] });
78
+ }
79
+ return out;
80
+ }
81
+ export function classify(input) {
82
+ const findings = [];
83
+ const touched = [];
84
+ for (const entry of input.diff) {
85
+ if (!input.upstream.has(entry.path))
86
+ continue;
87
+ touched.push(entry.path);
88
+ if (entry.status.startsWith('D')) {
89
+ findings.push({
90
+ kind: 'deletion',
91
+ path: entry.path,
92
+ message: `${entry.path}: deletes an upstream file.\n` +
93
+ ' No allowlist entry can permit this. A file this fork deletes ' +
94
+ 'conflicts on\n' +
95
+ ' every future merge that touches it upstream; a file this fork ' +
96
+ 'modifies\n' +
97
+ ' conflicts once and stays resolved. Leave the file in place and ' +
98
+ 'change\n' +
99
+ ' what it does, or turn it off out of band.',
100
+ });
101
+ continue;
102
+ }
103
+ const reason = input.allowlist.get(entry.path);
104
+ if (reason === undefined) {
105
+ findings.push({
106
+ kind: 'unallowlisted',
107
+ path: entry.path,
108
+ message: `${entry.path}: modifies an upstream file that is not allowlisted.\n` +
109
+ ' Move the change into a new fork-owned file, or add the path to ' +
110
+ `${ALLOWLIST_FILE}\n` +
111
+ ' with the reason the fork must diverge here.',
112
+ });
113
+ continue;
114
+ }
115
+ if (needsReason(reason)) {
116
+ findings.push({
117
+ kind: 'missing-reason',
118
+ path: entry.path,
119
+ message: `${entry.path}: allowlisted with a placeholder reason.\n` +
120
+ ` Replace the ${REASON_PLACEHOLDER} in ${ALLOWLIST_FILE} with why ` +
121
+ 'this file must diverge.',
122
+ });
123
+ }
124
+ }
125
+ const touchedSet = new Set(touched);
126
+ const warnings = [];
127
+ for (const path of input.allowlist.keys()) {
128
+ if (!touchedSet.has(path)) {
129
+ warnings.push(`${path}: allowlisted but no longer modified — regenerate to drop it.`);
130
+ }
131
+ }
132
+ return { findings, warnings, touched };
133
+ }
134
+ function git(repo, args, stdin) {
135
+ const result = spawnSync('git', ['-C', repo, ...args], {
136
+ input: stdin,
137
+ encoding: 'utf-8',
138
+ maxBuffer: 64 * 1024 * 1024,
139
+ });
140
+ if (result.error)
141
+ throw result.error;
142
+ return result;
143
+ }
144
+ export function repoRoot(from) {
145
+ const result = git(from, ['rev-parse', '--show-toplevel']);
146
+ if (result.status !== 0) {
147
+ throw new Error(`not a git repository: ${from}`);
148
+ }
149
+ return result.stdout.trim();
150
+ }
151
+ export function hasCommit(repo, rev) {
152
+ return git(repo, ['cat-file', '-e', `${rev}^{commit}`]).status === 0;
153
+ }
154
+ export function diffAgainst(repo, rev) {
155
+ const result = git(repo, [
156
+ 'diff',
157
+ '--no-renames',
158
+ '--name-status',
159
+ '-z',
160
+ rev,
161
+ '--',
162
+ ]);
163
+ if (result.status !== 0) {
164
+ throw new Error(`git diff against ${rev} failed: ${result.stderr.trim()}`);
165
+ }
166
+ return parseNameStatus(result.stdout);
167
+ }
168
+ export function upstreamPaths(repo, rev, paths) {
169
+ const out = new Set();
170
+ if (paths.length === 0)
171
+ return out;
172
+ const batchable = paths.filter((p) => !p.includes('\n'));
173
+ const input = batchable.map((p) => `${rev}:${p}`).join('\n') + '\n';
174
+ const result = git(repo, ['cat-file', '--batch-check'], input);
175
+ const lines = result.stdout.split('\n').filter((l) => l !== '');
176
+ const aligned = lines.length === batchable.length;
177
+ if (aligned) {
178
+ lines.forEach((line, index) => {
179
+ if (/ blob \d+$/.test(line))
180
+ out.add(batchable[index]);
181
+ });
182
+ }
183
+ const leftover = aligned ? paths.filter((p) => p.includes('\n')) : paths;
184
+ for (const path of leftover) {
185
+ const probe = git(repo, ['cat-file', '-t', `${rev}:${path}`]);
186
+ if (probe.status === 0 && probe.stdout.trim() === 'blob')
187
+ out.add(path);
188
+ }
189
+ return out;
190
+ }
191
+ // @lat: [[fork#The upstream guard#The sync point]]
192
+ export function parseSyncPoint(text) {
193
+ for (const raw of text.split('\n')) {
194
+ const line = raw.replace(/\r$/, '').trim();
195
+ if (line === '' || line.startsWith('#'))
196
+ continue;
197
+ return line.split(/\s/)[0];
198
+ }
199
+ return undefined;
200
+ }
201
+ export function renderSyncPoint(previous, rev) {
202
+ const kept = previous
203
+ .split('\n')
204
+ .filter((raw) => {
205
+ const line = raw.replace(/\r$/, '').trim();
206
+ return line === '' || line.startsWith('#');
207
+ })
208
+ .join('\n')
209
+ .replace(/\n+$/, '');
210
+ return `${kept}\n\n${rev}\n`;
211
+ }
212
+ export function isAncestorOfHead(repo, rev) {
213
+ return git(repo, ['merge-base', '--is-ancestor', rev, 'HEAD']).status === 0;
214
+ }
215
+ export function isShallow(repo) {
216
+ const result = git(repo, ['rev-parse', '--is-shallow-repository']);
217
+ return result.stdout.trim() === 'true';
218
+ }
219
+ // @lat: [[fork#The upstream guard#Running it in CI]]
220
+ export function fetchSyncPoint(repo, rev) {
221
+ const result = git(repo, ['fetch', '--no-tags', '--depth=1', 'origin', rev]);
222
+ return result.status === 0 && hasCommit(repo, rev);
223
+ }
224
+ function pathFor(repo, file) {
225
+ return isAbsolute(file) ? file : resolve(repo, file);
226
+ }
227
+ function allowlistPathFor(options) {
228
+ return pathFor(options.repo, options.allowlistFile);
229
+ }
230
+ function syncPointPathFor(options) {
231
+ return pathFor(options.repo, options.syncPointFile ?? SYNC_POINT_FILE);
232
+ }
233
+ function loadAllowlist(file) {
234
+ if (!existsSync(file))
235
+ return { entries: [], errors: [] };
236
+ return parseAllowlist(readFileSync(file, 'utf-8'));
237
+ }
238
+ function fail(report) {
239
+ return { ok: false, report, findings: [], warnings: [] };
240
+ }
241
+ export function runGuard(options) {
242
+ const root = repoRoot(options.repo);
243
+ const opts = { ...options, repo: root };
244
+ const syncFile = syncPointPathFor(opts);
245
+ if (opts.setSyncPoint !== undefined) {
246
+ return recordSyncPoint(root, syncFile, opts.setSyncPoint);
247
+ }
248
+ let syncPoint = opts.syncPoint;
249
+ if (syncPoint === undefined) {
250
+ if (!existsSync(syncFile)) {
251
+ return fail(`upstream-guard: no sync point recorded at ${syncFile}\n` +
252
+ ' The guard measures everything from the last upstream commit ' +
253
+ 'merged\n' +
254
+ ' into this fork. Record it:\n' +
255
+ ' node dist/src/fork/upstream-guard-cli.js --set-sync-point ' +
256
+ '<rev>\n');
257
+ }
258
+ syncPoint = parseSyncPoint(readFileSync(syncFile, 'utf-8'));
259
+ if (syncPoint === undefined || syncPoint === '') {
260
+ return fail(`upstream-guard: ${syncFile} holds no revision.\n` +
261
+ ' It must contain one commit id on a line of its own.\n');
262
+ }
263
+ }
264
+ const present = hasCommit(root, syncPoint) ||
265
+ (opts.fetch && fetchSyncPoint(root, syncPoint));
266
+ if (!present) {
267
+ return fail(`upstream-guard: sync point ${syncPoint} is not in this repository.\n` +
268
+ ' A shallow clone does not carry it. Fetch it first:\n' +
269
+ ` git fetch --no-tags --depth=1 origin ${syncPoint}\n`);
270
+ }
271
+ // A sync point off the current history makes every number below
272
+ // meaningless — the diff would report upstream's own commits as the fork's.
273
+ // Truncated history cannot answer the question, so shallow clones skip it.
274
+ if (!isShallow(root) && !isAncestorOfHead(root, syncPoint)) {
275
+ return fail(`upstream-guard: sync point ${syncPoint.slice(0, 7)} is not an ancestor ` +
276
+ 'of HEAD.\n' +
277
+ ' Merge that upstream commit before recording it, or record the ' +
278
+ 'one\n' +
279
+ ' this branch actually descends from.\n');
280
+ }
281
+ const file = allowlistPathFor(opts);
282
+ const diff = diffAgainst(root, syncPoint);
283
+ const upstream = upstreamPaths(root, syncPoint, diff.map((d) => d.path));
284
+ if (opts.regenerate) {
285
+ return regenerateInto(file, diff, upstream);
286
+ }
287
+ const parsed = loadAllowlist(file);
288
+ if (!existsSync(file)) {
289
+ return fail(`upstream-guard: allowlist not found at ${file}\n`);
290
+ }
291
+ const map = new Map(parsed.entries.map((e) => [e.path, e.reason]));
292
+ const result = classify({ diff, upstream, allowlist: map });
293
+ const findings = [
294
+ ...parsed.errors.map((message) => ({ kind: 'allowlist-error', path: file, message })),
295
+ ...result.findings,
296
+ ];
297
+ const lines = [];
298
+ if (findings.length > 0) {
299
+ lines.push(`upstream-guard: ${findings.length} violation(s) against sync point ` +
300
+ `${syncPoint.slice(0, 7)}`, '');
301
+ for (const finding of findings)
302
+ lines.push(` ${finding.message}`, '');
303
+ lines.push('The fork keeps its diff small: new behaviour goes in new fork-owned ' +
304
+ 'files,', 'and an upstream file takes an import plus one call line. See ' +
305
+ 'lat.md/fork.md.');
306
+ }
307
+ else {
308
+ lines.push(`upstream-guard: ok — ${result.touched.length} upstream file(s) ` +
309
+ 'modified, all allowlisted.');
310
+ }
311
+ for (const warning of result.warnings)
312
+ lines.push(` warning: ${warning}`);
313
+ return {
314
+ ok: findings.length === 0,
315
+ report: lines.join('\n') + '\n',
316
+ findings,
317
+ warnings: result.warnings,
318
+ };
319
+ }
320
+ // @lat: [[fork#The upstream guard#The sync point]]
321
+ function recordSyncPoint(repo, file, rev) {
322
+ const resolved = git(repo, ['rev-parse', `${rev}^{commit}`]);
323
+ if (resolved.status !== 0) {
324
+ return fail(`upstream-guard: ${rev} is not a commit in this repository.\n`);
325
+ }
326
+ const sha = resolved.stdout.trim();
327
+ if (!isShallow(repo) && !isAncestorOfHead(repo, sha)) {
328
+ return fail(`upstream-guard: ${sha.slice(0, 7)} is not an ancestor of HEAD.\n` +
329
+ ' Record a sync point only once its merge has landed.\n');
330
+ }
331
+ const previous = existsSync(file) ? readFileSync(file, 'utf-8') : '';
332
+ writeFileSync(file, renderSyncPoint(previous, sha), 'utf-8');
333
+ return {
334
+ ok: true,
335
+ report: `upstream-guard: sync point is now ${sha}\n` +
336
+ ' regenerate the allowlist next — the set of files the fork ' +
337
+ 'diverges in has moved.\n',
338
+ findings: [],
339
+ warnings: [],
340
+ };
341
+ }
342
+ function regenerateInto(file, diff, upstream) {
343
+ const existing = new Map(loadAllowlist(file).entries.map((e) => [e.path, e.reason]));
344
+ const entries = [];
345
+ const deletions = [];
346
+ const fresh = [];
347
+ for (const entry of diff) {
348
+ if (!upstream.has(entry.path))
349
+ continue;
350
+ if (entry.status.startsWith('D')) {
351
+ deletions.push({
352
+ kind: 'deletion',
353
+ path: entry.path,
354
+ message: `${entry.path}: deletes an upstream file, which is never ` +
355
+ 'allowlistable — left out of the regenerated list.',
356
+ });
357
+ continue;
358
+ }
359
+ const reason = existing.get(entry.path) ??
360
+ `${REASON_PLACEHOLDER}: why must this upstream file diverge?`;
361
+ if (!existing.has(entry.path))
362
+ fresh.push(entry.path);
363
+ entries.push({ path: entry.path, reason, line: 0 });
364
+ }
365
+ writeFileSync(file, renderAllowlist(entries), 'utf-8');
366
+ const lines = [
367
+ `upstream-guard: wrote ${entries.length} entr(ies) to ${file}`,
368
+ ];
369
+ for (const path of fresh) {
370
+ lines.push(` new entry needs a reason: ${path}`);
371
+ }
372
+ for (const finding of deletions)
373
+ lines.push(` ${finding.message}`);
374
+ return {
375
+ ok: deletions.length === 0,
376
+ report: lines.join('\n') + '\n',
377
+ findings: deletions,
378
+ warnings: [],
379
+ };
380
+ }
381
+ const USAGE = `Usage: node dist/src/fork/upstream-guard-cli.js [options]
382
+
383
+ --regenerate rewrite the allowlist from the current diff
384
+ --fetch fetch the sync point from origin if it is absent
385
+ --set-sync-point <rev> record <rev> as the last merged upstream commit
386
+ --sync-point <rev> check against <rev> instead of the recorded one
387
+ --repo <dir> default the current working directory
388
+ --allowlist <file> default <repo>/${ALLOWLIST_FILE}
389
+ --sync-point-file <f> default <repo>/${SYNC_POINT_FILE}
390
+ -h, --help this text
391
+ `;
392
+ export function parseArgs(argv) {
393
+ const options = {
394
+ repo: process.cwd(),
395
+ allowlistFile: ALLOWLIST_FILE,
396
+ regenerate: false,
397
+ fetch: false,
398
+ };
399
+ for (let i = 0; i < argv.length; i += 1) {
400
+ const arg = argv[i];
401
+ if (arg === '-h' || arg === '--help')
402
+ return { help: true };
403
+ else if (arg === '--regenerate' || arg === '--write')
404
+ options.regenerate = true;
405
+ else if (arg === '--fetch')
406
+ options.fetch = true;
407
+ else if (arg === '--set-sync-point')
408
+ options.setSyncPoint = argv[++i] ?? '';
409
+ else if (arg === '--sync-point')
410
+ options.syncPoint = argv[++i] ?? '';
411
+ else if (arg === '--sync-point-file')
412
+ options.syncPointFile = argv[++i] ?? '';
413
+ else if (arg === '--repo')
414
+ options.repo = argv[++i] ?? '';
415
+ else if (arg === '--allowlist')
416
+ options.allowlistFile = argv[++i] ?? '';
417
+ else
418
+ throw new Error(`unknown argument: ${arg}`);
419
+ }
420
+ return options;
421
+ }
422
+ export function main(argv) {
423
+ let options;
424
+ try {
425
+ options = parseArgs(argv);
426
+ }
427
+ catch (error) {
428
+ process.stderr.write(`${error.message}\n\n${USAGE}`);
429
+ return 2;
430
+ }
431
+ if ('help' in options) {
432
+ process.stdout.write(USAGE);
433
+ return 0;
434
+ }
435
+ let result;
436
+ try {
437
+ result = runGuard(options);
438
+ }
439
+ catch (error) {
440
+ process.stderr.write(`upstream-guard: ${error.message}\n`);
441
+ return 2;
442
+ }
443
+ process.stdout.write(result.report);
444
+ return result.ok ? 0 : 1;
445
+ }
@@ -3,7 +3,7 @@
3
3
  * requires users to re-run it (e.g. new hooks, generated files, MCP config, or
4
4
  * a configuration migration).
5
5
  */
6
- export declare const INIT_VERSION = 5;
6
+ export declare const INIT_VERSION = 6;
7
7
  export declare function readInitVersion(latDir: string): number | null;
8
8
  export declare function readFileHash(latDir: string, relPath: string): string | null;
9
9
  export declare function contentHash(content: string): string;
@@ -6,7 +6,7 @@ import { join } from 'node:path';
6
6
  * requires users to re-run it (e.g. new hooks, generated files, MCP config, or
7
7
  * a configuration migration).
8
8
  */
9
- export const INIT_VERSION = 5;
9
+ export const INIT_VERSION = 6;
10
10
  function cachePath(latDir) {
11
11
  return join(latDir, '.cache', 'lat_init.json');
12
12
  }
@@ -38,7 +38,7 @@ export type MdLink = {
38
38
  * edit — the trap this list guards against is silent, so it must not depend on
39
39
  * whoever adds the next field remembering to register it twice.
40
40
  */
41
- export declare const LAT_FIELDS: readonly ["require-code-mention", "mode", "status", "reviewed-hash", "tags"];
41
+ export declare const LAT_FIELDS: readonly ["require-code-mention", "mode", "require-mode", "status", "reviewed-hash", "tags"];
42
42
  /**
43
43
  * Something wrong with the frontmatter itself, carried out of the parser for a
44
44
  * caller to report. Both kinds fail OPEN if they stay quiet: a misplaced or
@@ -101,6 +101,7 @@ export declare function resolveRef(target: string, sectionIds: Set<string>, file
101
101
  export type SectionMatch = {
102
102
  section: Section;
103
103
  reason: string;
104
+ score?: number;
104
105
  };
105
106
  export declare function findSections(sections: Section[], query: string): SectionMatch[];
106
107
  export declare function extractRefs(filePath: string, content: string, projectRoot?: string, tree?: Root): Ref[];
@@ -15,6 +15,7 @@ import { visit } from 'unist-util-visit';
15
15
  export const LAT_FIELDS = [
16
16
  'require-code-mention',
17
17
  'mode',
18
+ 'require-mode',
18
19
  'status',
19
20
  'reviewed-hash',
20
21
  'tags',
@@ -5,5 +5,6 @@ export type SearchResult = {
5
5
  file: string;
6
6
  heading: string;
7
7
  content: string;
8
+ score: number;
8
9
  };
9
10
  export declare function searchSections(db: Client, query: string, embedder: Embedder, limit?: number): Promise<SearchResult[]>;
@@ -2,15 +2,21 @@ export async function searchSections(db, query, embedder, limit = 5) {
2
2
  const [queryVec] = await embedder.embed([query]);
3
3
  const vecJson = JSON.stringify(queryVec);
4
4
  const rows = await db.execute({
5
- sql: `SELECT s.id, s.file, s.heading, s.content
5
+ sql: `SELECT s.id, s.file, s.heading, s.content,
6
+ 1.0 - vector_distance_cos(s.embedding, vector(?)) AS score
6
7
  FROM vector_top_k('sections_vec_idx', vector(?), ?) AS v
7
- JOIN sections AS s ON s.rowid = v.id`,
8
- args: [vecJson, limit],
8
+ JOIN sections AS s ON s.rowid = v.id
9
+ ORDER BY score DESC`,
10
+ args: [vecJson, vecJson, limit],
11
+ });
12
+ return rows.rows.map((row) => {
13
+ const score = Number(row.score);
14
+ return {
15
+ id: row.id,
16
+ file: row.file,
17
+ heading: row.heading,
18
+ content: row.content,
19
+ score: Number.isFinite(score) ? score : 0,
20
+ };
9
21
  });
10
- return rows.rows.map((row) => ({
11
- id: row.id,
12
- file: row.file,
13
- heading: row.heading,
14
- content: row.content,
15
- }));
16
22
  }