@saasontools/strauss-kb 0.1.9 → 0.1.11
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/ARCHITECTURE.md +17 -0
- package/README.md +155 -62
- package/dist/{chunk-MWWDD23L.js → chunk-CWWXMD35.js} +2 -2
- package/dist/{chunk-KVEEISYQ.js → chunk-I3WW4F6X.js} +2 -2
- package/dist/{chunk-OFDWRMY6.js → chunk-OVRQCQ6P.js} +1257 -304
- package/dist/chunk-OVRQCQ6P.js.map +1 -0
- package/dist/cli-main.cjs +1266 -321
- package/dist/cli-main.cjs.map +1 -1
- package/dist/cli-main.js +2 -2
- package/dist/index.cjs +1145 -186
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +294 -13
- package/dist/index.d.ts +294 -13
- package/dist/index.js +19 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp-main.cjs +1262 -317
- package/dist/mcp-main.cjs.map +1 -1
- package/dist/mcp-main.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-OFDWRMY6.js.map +0 -1
- /package/dist/{chunk-MWWDD23L.js.map → chunk-CWWXMD35.js.map} +0 -0
- /package/dist/{chunk-KVEEISYQ.js.map → chunk-I3WW4F6X.js.map} +0 -0
|
@@ -18,7 +18,31 @@ var kbVerifiedEventSchema = kbActorStampSchema.extend({
|
|
|
18
18
|
});
|
|
19
19
|
var kbAnchorSchema = z.object({
|
|
20
20
|
file: z.string().min(1),
|
|
21
|
-
symbol: z.string().min(1).optional()
|
|
21
|
+
symbol: z.string().min(1).optional(),
|
|
22
|
+
/**
|
|
23
|
+
* Which repository the file lives in — a remote URL
|
|
24
|
+
* (`https://github.com/org/name`) or a short name. Absent means the base's
|
|
25
|
+
* own repository, which is what nearly every anchor means.
|
|
26
|
+
*
|
|
27
|
+
* Unvalidated beyond not-blank: one repository has many spellings.
|
|
28
|
+
* Matched after normalisation; see ARCHITECTURE.
|
|
29
|
+
*/
|
|
30
|
+
repo: z.string().trim().min(1).optional(),
|
|
31
|
+
/**
|
|
32
|
+
* The git rev the evidence was taken at. Prefer a commit SHA: a branch
|
|
33
|
+
* name is a moving pointer, so an anchor pinned to one says the evidence
|
|
34
|
+
* came from wherever that branch happens to be now, which is not a
|
|
35
|
+
* baseline. Recorded and preserved in v1; ref-pinned reads land with
|
|
36
|
+
* SAA-709.
|
|
37
|
+
*/
|
|
38
|
+
ref: z.string().trim().min(1).optional(),
|
|
39
|
+
hash: z.string().regex(/^sha256:[0-9a-f]{64}$/, {
|
|
40
|
+
message: "hash must be sha256:<64 hex chars>"
|
|
41
|
+
}).optional(),
|
|
42
|
+
/** ISO 8601 timestamp of the last successful resolution. */
|
|
43
|
+
resolved_at: z.string().min(1).optional(),
|
|
44
|
+
/** Line count of the text the hash was taken over. */
|
|
45
|
+
lines: z.number().int().positive().optional()
|
|
22
46
|
}).strict();
|
|
23
47
|
var KB_RECORD_TYPES = [
|
|
24
48
|
"fact",
|
|
@@ -295,6 +319,609 @@ function selectDecisions(records) {
|
|
|
295
319
|
);
|
|
296
320
|
}
|
|
297
321
|
|
|
322
|
+
// src/anchor-resolver.ts
|
|
323
|
+
import { execFile } from "child_process";
|
|
324
|
+
import { createHash } from "crypto";
|
|
325
|
+
import { readFile, realpath, stat } from "fs/promises";
|
|
326
|
+
import { isAbsolute, relative, resolve, sep } from "path";
|
|
327
|
+
import { promisify } from "util";
|
|
328
|
+
|
|
329
|
+
// src/concurrency.ts
|
|
330
|
+
var DEFAULT_IO_CONCURRENCY = 16;
|
|
331
|
+
async function mapLimit(items, limit, fn) {
|
|
332
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
333
|
+
throw new RangeError(
|
|
334
|
+
`mapLimit: "limit" must be a positive integer, got ${limit}`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
const out = new Array(items.length);
|
|
338
|
+
let next = 0;
|
|
339
|
+
let failed = false;
|
|
340
|
+
const runners = Array.from(
|
|
341
|
+
{ length: Math.min(limit, items.length) },
|
|
342
|
+
async () => {
|
|
343
|
+
while (!failed && next < items.length) {
|
|
344
|
+
const at = next++;
|
|
345
|
+
try {
|
|
346
|
+
out[at] = await fn(items[at], at);
|
|
347
|
+
} catch (error) {
|
|
348
|
+
failed = true;
|
|
349
|
+
throw error;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
);
|
|
354
|
+
await Promise.all(runners);
|
|
355
|
+
return out;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/anchor-resolver.ts
|
|
359
|
+
var execFileAsync = promisify(execFile);
|
|
360
|
+
var MAX_ANCHOR_FILE_BYTES = 1048576;
|
|
361
|
+
var PARENT_SCOPE_LINES = 50;
|
|
362
|
+
var CLEAN_STATE = { blockComment: false, template: false };
|
|
363
|
+
function stripLine(line, state) {
|
|
364
|
+
let out = "";
|
|
365
|
+
let index = 0;
|
|
366
|
+
let { blockComment, template } = state;
|
|
367
|
+
while (index < line.length) {
|
|
368
|
+
const char = line[index];
|
|
369
|
+
const next = line[index + 1];
|
|
370
|
+
if (blockComment) {
|
|
371
|
+
if (char === "*" && next === "/") {
|
|
372
|
+
blockComment = false;
|
|
373
|
+
index += 2;
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
index += 1;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (template) {
|
|
380
|
+
if (char === "\\") {
|
|
381
|
+
index += 2;
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (char === "`") template = false;
|
|
385
|
+
index += 1;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (char === "/" && next === "*") {
|
|
389
|
+
blockComment = true;
|
|
390
|
+
index += 2;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
if (char === "/" && next === "/") break;
|
|
394
|
+
if (char === "`") {
|
|
395
|
+
template = true;
|
|
396
|
+
index += 1;
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (char === "'" || char === '"') {
|
|
400
|
+
const quote = char;
|
|
401
|
+
index += 1;
|
|
402
|
+
while (index < line.length) {
|
|
403
|
+
if (line[index] === "\\") {
|
|
404
|
+
index += 2;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
if (line[index] === quote) {
|
|
408
|
+
index += 1;
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
index += 1;
|
|
412
|
+
}
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
out += char;
|
|
416
|
+
index += 1;
|
|
417
|
+
}
|
|
418
|
+
return { code: out, state: { blockComment, template } };
|
|
419
|
+
}
|
|
420
|
+
function span(lines, from, to) {
|
|
421
|
+
return {
|
|
422
|
+
text: lines.slice(from, to + 1).join("\n"),
|
|
423
|
+
startLine: from + 1,
|
|
424
|
+
endLine: to + 1
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
function captureBraceBlock(lines, matchLine) {
|
|
428
|
+
let depth = 0;
|
|
429
|
+
let opened = false;
|
|
430
|
+
let state = CLEAN_STATE;
|
|
431
|
+
for (let index = matchLine; index < lines.length; index++) {
|
|
432
|
+
const stripped = stripLine(lines[index] ?? "", state);
|
|
433
|
+
state = stripped.state;
|
|
434
|
+
for (const char of stripped.code) {
|
|
435
|
+
if (char === "{") {
|
|
436
|
+
depth += 1;
|
|
437
|
+
opened = true;
|
|
438
|
+
} else if (char === "}") {
|
|
439
|
+
depth = Math.max(0, depth - 1);
|
|
440
|
+
} else if (char === ";" && !opened) {
|
|
441
|
+
return span(lines, matchLine, index);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (opened && depth === 0) return span(lines, matchLine, index);
|
|
445
|
+
}
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
var PYTHON_HEADER = /^\s*(?:async\s+)?(?:def|class)\s+[A-Za-z_]\w*\s*[(:]/;
|
|
449
|
+
function captureIndentedBlock(lines, matchLine) {
|
|
450
|
+
const header = lines[matchLine] ?? "";
|
|
451
|
+
const indent = header.length - header.trimStart().length;
|
|
452
|
+
let headerEnd = -1;
|
|
453
|
+
for (let index = matchLine; index < lines.length && index <= matchLine + 20; index++) {
|
|
454
|
+
const code = stripLine(lines[index] ?? "", CLEAN_STATE).code.trimEnd();
|
|
455
|
+
if (code.endsWith(":")) {
|
|
456
|
+
headerEnd = index;
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
if (code.includes(":")) return span(lines, matchLine, index);
|
|
460
|
+
}
|
|
461
|
+
if (headerEnd === -1) return null;
|
|
462
|
+
let end = headerEnd;
|
|
463
|
+
for (let index = headerEnd + 1; index < lines.length; index++) {
|
|
464
|
+
const line = lines[index] ?? "";
|
|
465
|
+
if (line.trim() === "") continue;
|
|
466
|
+
const lineIndent = line.length - line.trimStart().length;
|
|
467
|
+
if (lineIndent <= indent) break;
|
|
468
|
+
end = index;
|
|
469
|
+
}
|
|
470
|
+
return end === headerEnd ? null : span(lines, matchLine, end);
|
|
471
|
+
}
|
|
472
|
+
var TIERS = [
|
|
473
|
+
(name) => new RegExp(
|
|
474
|
+
`(?:function|class|interface|type|enum|const|let|var|def)\\s+${name}\\b`
|
|
475
|
+
),
|
|
476
|
+
(name) => new RegExp(`\\b${name}\\s*[:=]`),
|
|
477
|
+
(name) => new RegExp(`\\b${name}\\s*\\(`),
|
|
478
|
+
(name) => new RegExp(`\\b${name}\\b`)
|
|
479
|
+
];
|
|
480
|
+
var regexResolver = {
|
|
481
|
+
name: "regex",
|
|
482
|
+
resolve(source, symbol) {
|
|
483
|
+
const segments = symbol.split(".");
|
|
484
|
+
const name = segments[segments.length - 1];
|
|
485
|
+
if (!name) return null;
|
|
486
|
+
const parent = segments.length > 1 ? segments[segments.length - 2] : void 0;
|
|
487
|
+
const escaped = escapeRegExp(name);
|
|
488
|
+
const parentPattern = parent ? new RegExp(`\\b${escapeRegExp(parent)}\\b`) : null;
|
|
489
|
+
const lines = source.split("\n");
|
|
490
|
+
for (const tier of TIERS) {
|
|
491
|
+
const pattern = tier(escaped);
|
|
492
|
+
let candidates = lines.map((line, index) => ({ line, index })).filter((entry) => pattern.test(entry.line)).map((entry) => entry.index);
|
|
493
|
+
if (!candidates.length) continue;
|
|
494
|
+
if (parentPattern && candidates.length > 1) {
|
|
495
|
+
const distances = candidates.map(
|
|
496
|
+
(index) => distanceToParent(lines, index, parentPattern)
|
|
497
|
+
);
|
|
498
|
+
const nearest = Math.min(...distances);
|
|
499
|
+
if (Number.isFinite(nearest)) {
|
|
500
|
+
candidates = candidates.filter((_, at) => distances[at] === nearest);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (candidates.length !== 1) return null;
|
|
504
|
+
const matchLine = candidates[0];
|
|
505
|
+
return PYTHON_HEADER.test(lines[matchLine] ?? "") ? captureIndentedBlock(lines, matchLine) : captureBraceBlock(lines, matchLine);
|
|
506
|
+
}
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
function escapeRegExp(value) {
|
|
511
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
512
|
+
}
|
|
513
|
+
function distanceToParent(lines, index, parent) {
|
|
514
|
+
const floor = Math.max(0, index - PARENT_SCOPE_LINES);
|
|
515
|
+
for (let at = index; at >= floor; at--) {
|
|
516
|
+
if (parent.test(lines[at] ?? "")) return index - at;
|
|
517
|
+
}
|
|
518
|
+
return Number.POSITIVE_INFINITY;
|
|
519
|
+
}
|
|
520
|
+
function hashAnchorText(text) {
|
|
521
|
+
return `sha256:${createHash("sha256").update(text.replace(/\r\n/g, "\n")).digest("hex")}`;
|
|
522
|
+
}
|
|
523
|
+
function resolveAnchor(source, anchor, resolver = regexResolver) {
|
|
524
|
+
const normalized = source.replace(/\r\n/g, "\n");
|
|
525
|
+
if (!anchor.symbol) {
|
|
526
|
+
const lines = normalized.split("\n");
|
|
527
|
+
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
528
|
+
return {
|
|
529
|
+
text: normalized,
|
|
530
|
+
startLine: 1,
|
|
531
|
+
endLine: Math.max(1, lines.length)
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
return resolver.resolve(normalized, anchor.symbol);
|
|
535
|
+
}
|
|
536
|
+
function anchorFilePath(repoRoot, file) {
|
|
537
|
+
const path = resolve(repoRoot, file.replace(/^\.\//, ""));
|
|
538
|
+
const rel = relative(resolve(repoRoot), path);
|
|
539
|
+
if (rel === "" || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
return path;
|
|
543
|
+
}
|
|
544
|
+
function contains(root, path) {
|
|
545
|
+
const rel = relative(root, path);
|
|
546
|
+
return rel !== "" && rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
547
|
+
}
|
|
548
|
+
function normalizeRepoUrl(value) {
|
|
549
|
+
let url = value.trim().replace(/^git\+/, "");
|
|
550
|
+
const scp = /^[\w.-]+@([\w.-]+):(.+)$/.exec(url);
|
|
551
|
+
if (scp) url = `https://${scp[1]}/${scp[2]}`;
|
|
552
|
+
url = url.replace(/^ssh:\/\/(?:[^@/]+@)?/, "https://");
|
|
553
|
+
url = trimTrailingSlashes(url);
|
|
554
|
+
if (url.endsWith(".git")) url = url.slice(0, -4);
|
|
555
|
+
return trimTrailingSlashes(url).toLowerCase();
|
|
556
|
+
}
|
|
557
|
+
function trimTrailingSlashes(value) {
|
|
558
|
+
let end = value.length;
|
|
559
|
+
while (end > 0 && value[end - 1] === "/") end -= 1;
|
|
560
|
+
return value.slice(0, end);
|
|
561
|
+
}
|
|
562
|
+
function repoPath(normalized) {
|
|
563
|
+
const withoutScheme = normalized.replace(/^[a-z0-9+.-]+:\/\//, "");
|
|
564
|
+
const segments = withoutScheme.split("/").filter(Boolean);
|
|
565
|
+
return segments.length > 1 ? segments.slice(1).join("/") : "";
|
|
566
|
+
}
|
|
567
|
+
function repoIdentifies(declared, originUrl) {
|
|
568
|
+
if (!originUrl) return false;
|
|
569
|
+
const origin = normalizeRepoUrl(originUrl);
|
|
570
|
+
const want = normalizeRepoUrl(declared);
|
|
571
|
+
if (!want || !origin) return false;
|
|
572
|
+
if (want === origin) return true;
|
|
573
|
+
const path = repoPath(origin);
|
|
574
|
+
if (!path) return false;
|
|
575
|
+
return want === path || want === (path.split("/").pop() ?? "");
|
|
576
|
+
}
|
|
577
|
+
async function repoOriginUrl(repoRoot) {
|
|
578
|
+
try {
|
|
579
|
+
const { stdout } = await execFileAsync(
|
|
580
|
+
"git",
|
|
581
|
+
["-C", repoRoot, "config", "--get", "remote.origin.url"],
|
|
582
|
+
{ timeout: 5e3 }
|
|
583
|
+
);
|
|
584
|
+
return stdout.trim() || null;
|
|
585
|
+
} catch {
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
var LazyOrigin = class {
|
|
590
|
+
constructor(repoRoot) {
|
|
591
|
+
this.repoRoot = repoRoot;
|
|
592
|
+
}
|
|
593
|
+
repoRoot;
|
|
594
|
+
url = null;
|
|
595
|
+
asked = false;
|
|
596
|
+
/** Asks git once, so later `isForeign` calls need no await. */
|
|
597
|
+
async prime() {
|
|
598
|
+
if (this.asked) return;
|
|
599
|
+
this.url = await repoOriginUrl(this.repoRoot);
|
|
600
|
+
this.asked = true;
|
|
601
|
+
}
|
|
602
|
+
/** Only meaningful after `prime`; an unprimed origin identifies nothing. */
|
|
603
|
+
isForeign(anchor) {
|
|
604
|
+
if (!anchor.repo) return false;
|
|
605
|
+
return !repoIdentifies(anchor.repo, this.url);
|
|
606
|
+
}
|
|
607
|
+
async foreign(anchor) {
|
|
608
|
+
if (!anchor.repo) return false;
|
|
609
|
+
await this.prime();
|
|
610
|
+
return this.isForeign(anchor);
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
function errorCode(error) {
|
|
614
|
+
return typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0;
|
|
615
|
+
}
|
|
616
|
+
function anchorFileReader(repoRoot) {
|
|
617
|
+
let rootOnce;
|
|
618
|
+
const realRoot = () => {
|
|
619
|
+
rootOnce ??= realpath(resolve(repoRoot)).catch((error) => {
|
|
620
|
+
rootOnce = void 0;
|
|
621
|
+
throw error;
|
|
622
|
+
});
|
|
623
|
+
return rootOnce;
|
|
624
|
+
};
|
|
625
|
+
return (file) => readAnchorFileWithRoot(repoRoot, file, realRoot);
|
|
626
|
+
}
|
|
627
|
+
async function readAnchorFileWithRoot(repoRoot, file, realRoot) {
|
|
628
|
+
const lexical = anchorFilePath(repoRoot, file);
|
|
629
|
+
if (lexical === null) return { ok: false, reason: "outside-repo" };
|
|
630
|
+
let root;
|
|
631
|
+
let path;
|
|
632
|
+
try {
|
|
633
|
+
root = await realRoot();
|
|
634
|
+
path = await realpath(lexical);
|
|
635
|
+
} catch (error) {
|
|
636
|
+
const code = errorCode(error);
|
|
637
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
638
|
+
return { ok: false, reason: "file-missing" };
|
|
639
|
+
}
|
|
640
|
+
return { ok: false, reason: "file-unreadable" };
|
|
641
|
+
}
|
|
642
|
+
if (!contains(root, path)) return { ok: false, reason: "outside-repo" };
|
|
643
|
+
try {
|
|
644
|
+
const stats = await stat(path);
|
|
645
|
+
if (!stats.isFile()) return { ok: false, reason: "file-unreadable" };
|
|
646
|
+
if (stats.size > MAX_ANCHOR_FILE_BYTES) {
|
|
647
|
+
return { ok: false, reason: "file-too-large" };
|
|
648
|
+
}
|
|
649
|
+
return { ok: true, source: await readFile(path, "utf8") };
|
|
650
|
+
} catch (error) {
|
|
651
|
+
const code = errorCode(error);
|
|
652
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
653
|
+
return { ok: false, reason: "file-missing" };
|
|
654
|
+
}
|
|
655
|
+
return { ok: false, reason: "file-unreadable" };
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
function looksLikeWrongRepoRoot(drift) {
|
|
659
|
+
let checked = 0;
|
|
660
|
+
for (const entries of drift.values()) {
|
|
661
|
+
for (const entry of entries) {
|
|
662
|
+
if (entry.reason === "foreign-repo") continue;
|
|
663
|
+
checked += 1;
|
|
664
|
+
if (entry.state !== "unresolved" || entry.reason !== "file-missing") {
|
|
665
|
+
return false;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return checked > 0;
|
|
670
|
+
}
|
|
671
|
+
async function readAnchorFiles(files, read, concurrency = DEFAULT_IO_CONCURRENCY) {
|
|
672
|
+
if (!Number.isInteger(concurrency) || concurrency < 1) {
|
|
673
|
+
throw new RangeError(
|
|
674
|
+
`readAnchorFiles: option "concurrency" must be a positive integer, got ${concurrency}`
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
const wanted = [...new Set(files)];
|
|
678
|
+
const results = await mapLimit(wanted, concurrency, async (file) => {
|
|
679
|
+
try {
|
|
680
|
+
return await read(file);
|
|
681
|
+
} catch {
|
|
682
|
+
return { ok: false, reason: "file-unreadable" };
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
return new Map(wanted.map((file, at) => [file, results[at]]));
|
|
686
|
+
}
|
|
687
|
+
async function detectAnchorDrift(records, options = {}) {
|
|
688
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
689
|
+
const resolver = options.resolver ?? regexResolver;
|
|
690
|
+
const origin = new LazyOrigin(repoRoot);
|
|
691
|
+
const planned = /* @__PURE__ */ new Map();
|
|
692
|
+
let declaresRepo = false;
|
|
693
|
+
for (const record of records) {
|
|
694
|
+
const anchors = (record.frontmatter.strauss_anchors ?? []).filter(
|
|
695
|
+
(anchor) => anchor.hash
|
|
696
|
+
);
|
|
697
|
+
if (!anchors.length) continue;
|
|
698
|
+
if (anchors.some((anchor) => anchor.repo)) declaresRepo = true;
|
|
699
|
+
planned.set(
|
|
700
|
+
record.conceptId,
|
|
701
|
+
anchors.map((anchor) => ({ anchor, foreign: false }))
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
if (declaresRepo) {
|
|
705
|
+
await origin.prime();
|
|
706
|
+
for (const entries of planned.values()) {
|
|
707
|
+
for (const entry of entries)
|
|
708
|
+
entry.foreign = origin.isForeign(entry.anchor);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
const files = [];
|
|
712
|
+
for (const entries of planned.values()) {
|
|
713
|
+
for (const entry of entries) {
|
|
714
|
+
if (!entry.foreign) files.push(entry.anchor.file);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
const reads = await readAnchorFiles(
|
|
718
|
+
files,
|
|
719
|
+
options.reader ?? anchorFileReader(repoRoot),
|
|
720
|
+
options.concurrency ?? DEFAULT_IO_CONCURRENCY
|
|
721
|
+
);
|
|
722
|
+
const drift = /* @__PURE__ */ new Map();
|
|
723
|
+
for (const record of records) {
|
|
724
|
+
const entries = [];
|
|
725
|
+
for (const { anchor, foreign } of planned.get(record.conceptId) ?? []) {
|
|
726
|
+
const base = {
|
|
727
|
+
file: anchor.file,
|
|
728
|
+
...anchor.symbol ? { symbol: anchor.symbol } : {},
|
|
729
|
+
storedHash: anchor.hash
|
|
730
|
+
};
|
|
731
|
+
if (foreign) {
|
|
732
|
+
entries.push({
|
|
733
|
+
...base,
|
|
734
|
+
state: "unresolved",
|
|
735
|
+
diffSize: null,
|
|
736
|
+
reason: "foreign-repo"
|
|
737
|
+
});
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
const read = reads.get(anchor.file);
|
|
741
|
+
if (!read.ok) {
|
|
742
|
+
entries.push({
|
|
743
|
+
...base,
|
|
744
|
+
state: "unresolved",
|
|
745
|
+
diffSize: null,
|
|
746
|
+
reason: read.reason
|
|
747
|
+
});
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
const resolved = resolveAnchor(read.source, anchor, resolver);
|
|
751
|
+
if (!resolved) {
|
|
752
|
+
entries.push({
|
|
753
|
+
...base,
|
|
754
|
+
state: "unresolved",
|
|
755
|
+
diffSize: null,
|
|
756
|
+
reason: "symbol-not-found"
|
|
757
|
+
});
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
const currentHash = hashAnchorText(resolved.text);
|
|
761
|
+
const currentLines = resolved.endLine - resolved.startLine + 1;
|
|
762
|
+
entries.push({
|
|
763
|
+
...base,
|
|
764
|
+
state: currentHash === anchor.hash ? "match" : "drifted",
|
|
765
|
+
currentHash,
|
|
766
|
+
diffSize: anchor.lines === void 0 ? null : Math.abs(currentLines - anchor.lines)
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
if (entries.length) drift.set(record.conceptId, entries);
|
|
770
|
+
}
|
|
771
|
+
return drift;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// src/errors.ts
|
|
775
|
+
var Fault = /* @__PURE__ */ ((Fault2) => {
|
|
776
|
+
Fault2["Configuration"] = "Configuration";
|
|
777
|
+
Fault2["System"] = "System";
|
|
778
|
+
Fault2["User"] = "User";
|
|
779
|
+
return Fault2;
|
|
780
|
+
})(Fault || {});
|
|
781
|
+
var ErrorTypes = /* @__PURE__ */ ((ErrorTypes2) => {
|
|
782
|
+
ErrorTypes2["KbRecordAlreadyExists"] = "KbRecordAlreadyExists";
|
|
783
|
+
ErrorTypes2["KbInvalidConceptId"] = "KbInvalidConceptId";
|
|
784
|
+
ErrorTypes2["KbMissingFlagValue"] = "KbMissingFlagValue";
|
|
785
|
+
ErrorTypes2["KbPackBudgetExceeded"] = "KbPackBudgetExceeded";
|
|
786
|
+
ErrorTypes2["KbRecordNotFound"] = "KbRecordNotFound";
|
|
787
|
+
ErrorTypes2["KbSelfVerification"] = "KbSelfVerification";
|
|
788
|
+
ErrorTypes2["KbWriteConflict"] = "KbWriteConflict";
|
|
789
|
+
return ErrorTypes2;
|
|
790
|
+
})(ErrorTypes || {});
|
|
791
|
+
var BaseError = class extends Error {
|
|
792
|
+
code;
|
|
793
|
+
errorType;
|
|
794
|
+
fault;
|
|
795
|
+
retriable;
|
|
796
|
+
reportToUser;
|
|
797
|
+
details;
|
|
798
|
+
constructor(props) {
|
|
799
|
+
super(props.message);
|
|
800
|
+
this.name = props.name ?? this.constructor.name;
|
|
801
|
+
this.code = props.code ?? 500;
|
|
802
|
+
this.errorType = props.errorType;
|
|
803
|
+
this.fault = props.fault;
|
|
804
|
+
this.retriable = props.retriable ?? true;
|
|
805
|
+
this.reportToUser = props.reportToUser ?? false;
|
|
806
|
+
this.details = props.details;
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
// src/kb-errors.ts
|
|
811
|
+
var KbRecordAlreadyExistsError = class extends BaseError {
|
|
812
|
+
constructor(conceptId2) {
|
|
813
|
+
super({
|
|
814
|
+
message: `kb: ${conceptId2} already exists \u2014 choose a more specific slug, or write with overwrite`,
|
|
815
|
+
errorType: "KbRecordAlreadyExists" /* KbRecordAlreadyExists */,
|
|
816
|
+
code: 409,
|
|
817
|
+
fault: "User" /* User */,
|
|
818
|
+
retriable: false,
|
|
819
|
+
reportToUser: true,
|
|
820
|
+
details: { conceptId: conceptId2, action: "refused" }
|
|
821
|
+
});
|
|
822
|
+
this.conceptId = conceptId2;
|
|
823
|
+
}
|
|
824
|
+
conceptId;
|
|
825
|
+
};
|
|
826
|
+
var KbRecordNotFoundError = class extends BaseError {
|
|
827
|
+
constructor(conceptId2) {
|
|
828
|
+
super({
|
|
829
|
+
message: `kb: ${conceptId2} does not exist`,
|
|
830
|
+
errorType: "KbRecordNotFound" /* KbRecordNotFound */,
|
|
831
|
+
code: 404,
|
|
832
|
+
fault: "User" /* User */,
|
|
833
|
+
retriable: false,
|
|
834
|
+
reportToUser: true,
|
|
835
|
+
details: { conceptId: conceptId2 }
|
|
836
|
+
});
|
|
837
|
+
this.conceptId = conceptId2;
|
|
838
|
+
}
|
|
839
|
+
conceptId;
|
|
840
|
+
};
|
|
841
|
+
var KbWriteConflictError = class extends BaseError {
|
|
842
|
+
constructor(conceptId2) {
|
|
843
|
+
super({
|
|
844
|
+
message: `kb: ${conceptId2} changed while it was being updated \u2014 re-read and retry`,
|
|
845
|
+
errorType: "KbWriteConflict" /* KbWriteConflict */,
|
|
846
|
+
code: 409,
|
|
847
|
+
fault: "System" /* System */,
|
|
848
|
+
retriable: true,
|
|
849
|
+
reportToUser: true,
|
|
850
|
+
details: { conceptId: conceptId2 }
|
|
851
|
+
});
|
|
852
|
+
this.conceptId = conceptId2;
|
|
853
|
+
}
|
|
854
|
+
conceptId;
|
|
855
|
+
};
|
|
856
|
+
var KbSelfVerificationError = class extends BaseError {
|
|
857
|
+
constructor(conceptId2, actor, generatedBy) {
|
|
858
|
+
super({
|
|
859
|
+
message: `kb: ${conceptId2} was generated by ${generatedBy}, and a record's generator cannot verify it \u2014 only a human or a different actor can`,
|
|
860
|
+
errorType: "KbSelfVerification" /* KbSelfVerification */,
|
|
861
|
+
code: 400,
|
|
862
|
+
fault: "User" /* User */,
|
|
863
|
+
retriable: false,
|
|
864
|
+
reportToUser: true,
|
|
865
|
+
details: { conceptId: conceptId2, actor, generatedBy, action: "refused" }
|
|
866
|
+
});
|
|
867
|
+
this.conceptId = conceptId2;
|
|
868
|
+
this.actor = actor;
|
|
869
|
+
this.generatedBy = generatedBy;
|
|
870
|
+
}
|
|
871
|
+
conceptId;
|
|
872
|
+
actor;
|
|
873
|
+
generatedBy;
|
|
874
|
+
};
|
|
875
|
+
var KbPackBudgetExceededError = class extends BaseError {
|
|
876
|
+
constructor(recordCount, approxTokens2, budgetTokens, excluded) {
|
|
877
|
+
super({
|
|
878
|
+
message: `kb: a pack of ${recordCount} records is ~${approxTokens2} tokens against a budget of ${budgetTokens} \u2014 lower hops or maxNodes, or raise the budget`,
|
|
879
|
+
errorType: "KbPackBudgetExceeded" /* KbPackBudgetExceeded */,
|
|
880
|
+
code: 400,
|
|
881
|
+
fault: "User" /* User */,
|
|
882
|
+
retriable: false,
|
|
883
|
+
reportToUser: true,
|
|
884
|
+
details: { recordCount, approxTokens: approxTokens2, budgetTokens, excluded }
|
|
885
|
+
});
|
|
886
|
+
this.recordCount = recordCount;
|
|
887
|
+
this.approxTokens = approxTokens2;
|
|
888
|
+
this.budgetTokens = budgetTokens;
|
|
889
|
+
this.excluded = excluded;
|
|
890
|
+
}
|
|
891
|
+
recordCount;
|
|
892
|
+
approxTokens;
|
|
893
|
+
budgetTokens;
|
|
894
|
+
excluded;
|
|
895
|
+
};
|
|
896
|
+
var KbMissingFlagValueError = class extends BaseError {
|
|
897
|
+
constructor(flag) {
|
|
898
|
+
super({
|
|
899
|
+
message: `kb: ${flag} needs a value \u2014 pass ${flag} <value> or ${flag}=<value>`,
|
|
900
|
+
errorType: "KbMissingFlagValue" /* KbMissingFlagValue */,
|
|
901
|
+
code: 400,
|
|
902
|
+
fault: "User" /* User */,
|
|
903
|
+
retriable: false,
|
|
904
|
+
reportToUser: true,
|
|
905
|
+
details: { flag }
|
|
906
|
+
});
|
|
907
|
+
this.flag = flag;
|
|
908
|
+
}
|
|
909
|
+
flag;
|
|
910
|
+
};
|
|
911
|
+
var KbInvalidConceptIdError = class extends BaseError {
|
|
912
|
+
constructor(message, details) {
|
|
913
|
+
super({
|
|
914
|
+
message: `kb: ${message}`,
|
|
915
|
+
errorType: "KbInvalidConceptId" /* KbInvalidConceptId */,
|
|
916
|
+
code: 400,
|
|
917
|
+
fault: "User" /* User */,
|
|
918
|
+
retriable: false,
|
|
919
|
+
reportToUser: true,
|
|
920
|
+
details
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
|
|
298
925
|
// src/kb-pins/budgets.ts
|
|
299
926
|
function asBudgets(value) {
|
|
300
927
|
if (value === null || typeof value !== "object") return {};
|
|
@@ -393,14 +1020,14 @@ var pinsManifestSchema = z4.object({
|
|
|
393
1020
|
}).passthrough();
|
|
394
1021
|
|
|
395
1022
|
// src/kb-pins/layers.ts
|
|
396
|
-
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
1023
|
+
import { mkdir, readFile as readFile2, writeFile } from "fs/promises";
|
|
397
1024
|
import { homedir } from "os";
|
|
398
|
-
import { dirname, isAbsolute, join as join2, relative, resolve, sep } from "path";
|
|
1025
|
+
import { dirname, isAbsolute as isAbsolute2, join as join2, relative as relative2, resolve as resolve2, sep as sep2 } from "path";
|
|
399
1026
|
function userRoot() {
|
|
400
1027
|
return process.env.STRAUSS_KB_USER_ROOT || homedir();
|
|
401
1028
|
}
|
|
402
1029
|
function layerRoot(workspaceDir, layer) {
|
|
403
|
-
return layer === "user" ? userRoot() :
|
|
1030
|
+
return layer === "user" ? userRoot() : resolve2(workspaceDir);
|
|
404
1031
|
}
|
|
405
1032
|
function layerFile(workspaceDir, layer) {
|
|
406
1033
|
return join2(
|
|
@@ -412,7 +1039,7 @@ async function readPinsLayer(workspaceDir, layer) {
|
|
|
412
1039
|
const file = layerFile(workspaceDir, layer);
|
|
413
1040
|
let raw;
|
|
414
1041
|
try {
|
|
415
|
-
raw = await
|
|
1042
|
+
raw = await readFile2(file, "utf8");
|
|
416
1043
|
} catch {
|
|
417
1044
|
return { pins: [] };
|
|
418
1045
|
}
|
|
@@ -441,11 +1068,11 @@ async function writePinsLayer(workspaceDir, layer, manifest) {
|
|
|
441
1068
|
`, "utf8");
|
|
442
1069
|
}
|
|
443
1070
|
function resolvePinPath(rootDir, path) {
|
|
444
|
-
return
|
|
1071
|
+
return isAbsolute2(path) ? resolve2(path) : resolve2(rootDir, path.split("/").join(sep2));
|
|
445
1072
|
}
|
|
446
1073
|
function storablePath(rootDir, bundlePath2) {
|
|
447
|
-
const rel =
|
|
448
|
-
return (rel === "" ? "." : rel).split(
|
|
1074
|
+
const rel = relative2(resolve2(rootDir), resolve2(bundlePath2));
|
|
1075
|
+
return (rel === "" ? "." : rel).split(sep2).join("/");
|
|
449
1076
|
}
|
|
450
1077
|
async function readMergedPins(workspaceDir) {
|
|
451
1078
|
const manifests = {};
|
|
@@ -471,10 +1098,10 @@ async function readMergedPins(workspaceDir) {
|
|
|
471
1098
|
}
|
|
472
1099
|
|
|
473
1100
|
// src/kb-pins/frozen.ts
|
|
474
|
-
import { resolve as
|
|
1101
|
+
import { resolve as resolve3 } from "path";
|
|
475
1102
|
async function assertBaseNotFrozen(workspaceDir, bundlePath2) {
|
|
476
1103
|
const merged = await readMergedPins(workspaceDir);
|
|
477
|
-
const absolute =
|
|
1104
|
+
const absolute = resolve3(bundlePath2);
|
|
478
1105
|
const pin = merged.pins.find((entry) => entry.absolutePath === absolute);
|
|
479
1106
|
if (pin?.frozen === true) {
|
|
480
1107
|
throw new KbBaseFrozenError(pin.path, pin.layer);
|
|
@@ -559,7 +1186,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
|
|
|
559
1186
|
}
|
|
560
1187
|
|
|
561
1188
|
// src/kb-pins/unpin.ts
|
|
562
|
-
import { resolve as
|
|
1189
|
+
import { resolve as resolve4 } from "path";
|
|
563
1190
|
async function unpinBase(workspaceDir, bundlePath2) {
|
|
564
1191
|
const layers = [];
|
|
565
1192
|
for (const layer of PIN_LAYERS) {
|
|
@@ -580,7 +1207,7 @@ async function unpinBase(workspaceDir, bundlePath2) {
|
|
|
580
1207
|
}
|
|
581
1208
|
}
|
|
582
1209
|
return {
|
|
583
|
-
path: storablePath(
|
|
1210
|
+
path: storablePath(resolve4(workspaceDir), bundlePath2),
|
|
584
1211
|
removed: layers.length > 0,
|
|
585
1212
|
layers
|
|
586
1213
|
};
|
|
@@ -596,7 +1223,7 @@ var STANDING = {
|
|
|
596
1223
|
rejected: "rejected",
|
|
597
1224
|
superseded: "superseded"
|
|
598
1225
|
};
|
|
599
|
-
function adjudicate(hits, bundle, now = /* @__PURE__ */ new Date()) {
|
|
1226
|
+
function adjudicate(hits, bundle, now = /* @__PURE__ */ new Date(), anchorDrift) {
|
|
600
1227
|
const byId = new Map(bundle.map((record) => [record.conceptId, record]));
|
|
601
1228
|
return hits.map((record) => {
|
|
602
1229
|
const status = record.frontmatter.strauss_status;
|
|
@@ -626,6 +1253,20 @@ function adjudicate(hits, bundle, now = /* @__PURE__ */ new Date()) {
|
|
|
626
1253
|
if (!record.frontmatter.verified?.length) {
|
|
627
1254
|
warnings.push({ kind: "unverified" });
|
|
628
1255
|
}
|
|
1256
|
+
const moved = (anchorDrift?.get(record.conceptId) ?? []).filter(
|
|
1257
|
+
(entry) => entry.state !== "match" && entry.reason !== "foreign-repo"
|
|
1258
|
+
);
|
|
1259
|
+
if (moved.length) {
|
|
1260
|
+
warnings.push({
|
|
1261
|
+
kind: "drifted",
|
|
1262
|
+
anchors: moved.map(({ file, symbol, diffSize, reason }) => ({
|
|
1263
|
+
file,
|
|
1264
|
+
...symbol !== void 0 ? { symbol } : {},
|
|
1265
|
+
diffSize,
|
|
1266
|
+
...reason !== void 0 ? { reason } : {}
|
|
1267
|
+
}))
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
629
1270
|
return { record, standing: STANDING[status], heads, warnings };
|
|
630
1271
|
});
|
|
631
1272
|
}
|
|
@@ -678,6 +1319,52 @@ function successors(record, byId) {
|
|
|
678
1319
|
return { records, missing };
|
|
679
1320
|
}
|
|
680
1321
|
|
|
1322
|
+
// src/catalog.ts
|
|
1323
|
+
var EMPTY_STANDINGS = {
|
|
1324
|
+
current: 0,
|
|
1325
|
+
superseded: 0,
|
|
1326
|
+
rejected: 0,
|
|
1327
|
+
unsettled: 0,
|
|
1328
|
+
open: 0
|
|
1329
|
+
};
|
|
1330
|
+
function catalog(bundle, options = {}) {
|
|
1331
|
+
const wanted = options.type ? bundle.filter((record) => record.frontmatter.type === options.type) : bundle;
|
|
1332
|
+
const entries = adjudicate(wanted, bundle, options.now ?? /* @__PURE__ */ new Date()).map((hit) => ({
|
|
1333
|
+
conceptId: hit.record.conceptId,
|
|
1334
|
+
type: hit.record.frontmatter.type,
|
|
1335
|
+
title: hit.record.frontmatter.title ?? null,
|
|
1336
|
+
standing: hit.standing,
|
|
1337
|
+
supersededBy: hit.heads.map((head) => head.conceptId),
|
|
1338
|
+
stale: hit.warnings.some((warning) => warning.kind === "stale")
|
|
1339
|
+
})).sort(byTypeThenTitle);
|
|
1340
|
+
const standings = { ...EMPTY_STANDINGS };
|
|
1341
|
+
for (const entry of entries) standings[entry.standing] += 1;
|
|
1342
|
+
return {
|
|
1343
|
+
entries,
|
|
1344
|
+
recordCount: entries.length,
|
|
1345
|
+
standings,
|
|
1346
|
+
currentCount: standings.current,
|
|
1347
|
+
supersededCount: standings.superseded,
|
|
1348
|
+
staleCount: entries.filter((entry) => entry.stale).length
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
function byTypeThenTitle(left, right) {
|
|
1352
|
+
return byCodeUnit(left.type, right.type) || byCodeUnit(left.title ?? "", right.title ?? "") || byCodeUnit(left.conceptId, right.conceptId);
|
|
1353
|
+
}
|
|
1354
|
+
function byCodeUnit(left, right) {
|
|
1355
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
1356
|
+
}
|
|
1357
|
+
function renderCatalogLine(entry) {
|
|
1358
|
+
const parts = [
|
|
1359
|
+
entry.conceptId,
|
|
1360
|
+
entry.type,
|
|
1361
|
+
entry.title ?? "(untitled)",
|
|
1362
|
+
entry.standing === "superseded" ? `superseded \u2192 ${entry.supersededBy.join(", ") || "(no surviving head)"}` : entry.standing
|
|
1363
|
+
];
|
|
1364
|
+
if (entry.stale) parts.push("stale");
|
|
1365
|
+
return `- ${parts.join(" \xB7 ")}`;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
681
1368
|
// src/kb-index.ts
|
|
682
1369
|
var INDEX_FILE = "INDEX.md";
|
|
683
1370
|
var HEADING = "# KB Index";
|
|
@@ -700,7 +1387,7 @@ function indexIsStale(stored, expected) {
|
|
|
700
1387
|
}
|
|
701
1388
|
|
|
702
1389
|
// src/kb-context.ts
|
|
703
|
-
import { readFile as
|
|
1390
|
+
import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
704
1391
|
var HEADING2 = "## Knowledge bases (pinned)";
|
|
705
1392
|
var DEFAULT_CONTEXT_BUDGET = 4e3;
|
|
706
1393
|
var CONTEXT_PROFILES = {
|
|
@@ -904,7 +1591,7 @@ function toHookJson(block, event) {
|
|
|
904
1591
|
var CONTEXT_BEGIN = "<!-- strauss-kb:begin -->";
|
|
905
1592
|
var CONTEXT_END = "<!-- strauss-kb:end -->";
|
|
906
1593
|
async function syncInstructions(file, block) {
|
|
907
|
-
const existing = await
|
|
1594
|
+
const existing = await readFile3(file, "utf8").catch(() => null);
|
|
908
1595
|
const region = block ? `${CONTEXT_BEGIN}
|
|
909
1596
|
${block.trim()}
|
|
910
1597
|
${CONTEXT_END}` : null;
|
|
@@ -1056,7 +1743,8 @@ var KB_DOCTOR_CHECKS = [
|
|
|
1056
1743
|
"aging",
|
|
1057
1744
|
"orphaned",
|
|
1058
1745
|
"broken-supersession",
|
|
1059
|
-
"superseded-but-cited"
|
|
1746
|
+
"superseded-but-cited",
|
|
1747
|
+
"drifted"
|
|
1060
1748
|
];
|
|
1061
1749
|
var CHECK_HEADLINES = {
|
|
1062
1750
|
expired: "past its stale_after date",
|
|
@@ -1065,7 +1753,8 @@ var CHECK_HEADLINES = {
|
|
|
1065
1753
|
aging: "still open or still proposed long after it was written",
|
|
1066
1754
|
orphaned: "no other record links to it",
|
|
1067
1755
|
"broken-supersession": "the supersession pointers do not resolve",
|
|
1068
|
-
"superseded-but-cited": "a live record's body links to one that no longer holds"
|
|
1756
|
+
"superseded-but-cited": "a live record's body links to one that no longer holds",
|
|
1757
|
+
drifted: "the code an anchor points at moved out from under its hash"
|
|
1069
1758
|
};
|
|
1070
1759
|
var DAY_MS = 864e5;
|
|
1071
1760
|
function doctor(bundle, options = {}) {
|
|
@@ -1075,7 +1764,7 @@ function doctor(bundle, options = {}) {
|
|
|
1075
1764
|
agingDays: options.agingDays ?? DEFAULT_AGING_DAYS
|
|
1076
1765
|
};
|
|
1077
1766
|
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
1078
|
-
const adjudicated = adjudicate(bundle, bundle, now);
|
|
1767
|
+
const adjudicated = adjudicate(bundle, bundle, now, options.anchorDrift);
|
|
1079
1768
|
const standings = new Map(
|
|
1080
1769
|
adjudicated.map((hit) => [hit.record.conceptId, hit.standing])
|
|
1081
1770
|
);
|
|
@@ -1089,7 +1778,8 @@ function doctor(bundle, options = {}) {
|
|
|
1089
1778
|
group("aging", aging(inForce, now, thresholds.agingDays)),
|
|
1090
1779
|
group("orphaned", orphaned(bundle)),
|
|
1091
1780
|
group("broken-supersession", brokenSupersession(bundle, adjudicated)),
|
|
1092
|
-
group("superseded-but-cited", supersededButCited(bundle, standings))
|
|
1781
|
+
group("superseded-but-cited", supersededButCited(bundle, standings)),
|
|
1782
|
+
group("drifted", drifted(inForce))
|
|
1093
1783
|
];
|
|
1094
1784
|
const counts = Object.fromEntries(
|
|
1095
1785
|
groups.map((entry) => [entry.check, entry.count])
|
|
@@ -1275,6 +1965,29 @@ function supersededButCited(bundle, standings) {
|
|
|
1275
1965
|
}
|
|
1276
1966
|
return findings;
|
|
1277
1967
|
}
|
|
1968
|
+
function drifted(hits) {
|
|
1969
|
+
const findings = [];
|
|
1970
|
+
for (const hit of hits) {
|
|
1971
|
+
const warning = hit.warnings.find((entry) => entry.kind === "drifted");
|
|
1972
|
+
if (!warning) continue;
|
|
1973
|
+
findings.push(
|
|
1974
|
+
finding(
|
|
1975
|
+
hit.record,
|
|
1976
|
+
`${warning.anchors.length} ${warning.anchors.length === 1 ? "anchor no longer matches" : "anchors no longer match"}: ${warning.anchors.map((anchor) => {
|
|
1977
|
+
const at = anchor.symbol ? `${anchor.file}:${anchor.symbol}` : anchor.file;
|
|
1978
|
+
if (anchor.reason) return `${at} (${anchor.reason})`;
|
|
1979
|
+
if (anchor.diffSize === null) {
|
|
1980
|
+
return `${at} (changed, size unrecorded)`;
|
|
1981
|
+
}
|
|
1982
|
+
return anchor.diffSize === 0 ? `${at} (content changed, same line count)` : `${at} (${anchor.diffSize} line${anchor.diffSize === 1 ? "" : "s"} apart)`;
|
|
1983
|
+
}).join(", ")}`
|
|
1984
|
+
)
|
|
1985
|
+
);
|
|
1986
|
+
}
|
|
1987
|
+
return findings.sort(
|
|
1988
|
+
(left, right) => left.conceptId.localeCompare(right.conceptId)
|
|
1989
|
+
);
|
|
1990
|
+
}
|
|
1278
1991
|
function replaces(later, earlier) {
|
|
1279
1992
|
return (later.frontmatter.strauss_supersedes ?? []).includes(earlier.conceptId) || earlier.frontmatter.strauss_superseded_by === later.conceptId;
|
|
1280
1993
|
}
|
|
@@ -1399,28 +2112,204 @@ function byGeneratedAt(left, right) {
|
|
|
1399
2112
|
return at(left).localeCompare(at(right)) || left.depth - right.depth;
|
|
1400
2113
|
}
|
|
1401
2114
|
|
|
1402
|
-
// src/commands/
|
|
2115
|
+
// src/commands/anchor-resolve.ts
|
|
1403
2116
|
import { z as z8 } from "zod";
|
|
1404
2117
|
|
|
1405
2118
|
// src/commands/model.ts
|
|
1406
2119
|
import { z as z7 } from "zod";
|
|
1407
2120
|
var bundlePath = z7.string().min(1).describe("Absolute path to the knowledge base directory.");
|
|
1408
2121
|
var conceptId = z7.string().min(1).describe("e.g. decision.cursor-v2");
|
|
2122
|
+
var REPO_ROOT = z7.string().min(1).optional().describe(
|
|
2123
|
+
"Where the anchored source lives, for the drift check. Defaults to the working directory."
|
|
2124
|
+
);
|
|
1409
2125
|
function define(command) {
|
|
1410
2126
|
return command;
|
|
1411
2127
|
}
|
|
1412
2128
|
function argvFlag(argv, name) {
|
|
2129
|
+
const joined = argv.find((arg) => arg.startsWith(`${name}=`));
|
|
2130
|
+
if (joined !== void 0) {
|
|
2131
|
+
const value2 = joined.slice(name.length + 1);
|
|
2132
|
+
if (!value2) throw new KbMissingFlagValueError(name);
|
|
2133
|
+
return value2;
|
|
2134
|
+
}
|
|
1413
2135
|
const at = argv.indexOf(name);
|
|
1414
|
-
|
|
2136
|
+
if (at === -1) return void 0;
|
|
2137
|
+
const value = argv[at + 1];
|
|
2138
|
+
if (value === void 0 || value.startsWith("--")) {
|
|
2139
|
+
throw new KbMissingFlagValueError(name);
|
|
2140
|
+
}
|
|
2141
|
+
return value;
|
|
1415
2142
|
}
|
|
1416
2143
|
|
|
2144
|
+
// src/commands/anchor-resolve.ts
|
|
2145
|
+
var anchorResolveCommand = define({
|
|
2146
|
+
name: "anchor-resolve",
|
|
2147
|
+
tool: "kb_anchor_resolve",
|
|
2148
|
+
usage: "anchor-resolve <concept-id> [--repo-root <path>] [--rebaseline] [--restamp]",
|
|
2149
|
+
description: "Resolve a record's anchors against the working tree: stamp a hash onto anchors that lack one, report drift where the code moved. kb_verify's mechanical counterpart \u2014 reach for it when the question is whether the code still is what it was, not whether the claim still holds. Anchors naming another repository are skipped. Exits non-zero on drift.",
|
|
2150
|
+
input: z8.object({
|
|
2151
|
+
bundlePath,
|
|
2152
|
+
conceptId,
|
|
2153
|
+
repoRoot: z8.string().min(1).optional(),
|
|
2154
|
+
rebaseline: z8.boolean().optional().describe(
|
|
2155
|
+
"Accept the current code as the new baseline for anchors that drifted."
|
|
2156
|
+
),
|
|
2157
|
+
restamp: z8.boolean().optional().describe(
|
|
2158
|
+
"Refresh `resolved_at` on anchors that already match. Off by default, so a green run writes nothing."
|
|
2159
|
+
)
|
|
2160
|
+
}),
|
|
2161
|
+
fromArgv: (argv, path) => ({
|
|
2162
|
+
bundlePath: path,
|
|
2163
|
+
conceptId: argv[1],
|
|
2164
|
+
repoRoot: argvFlag(argv, "--repo-root"),
|
|
2165
|
+
rebaseline: argv.includes("--rebaseline"),
|
|
2166
|
+
restamp: argv.includes("--restamp")
|
|
2167
|
+
}),
|
|
2168
|
+
run: async ({ store, actor, now }, { bundlePath: path, conceptId: id, repoRoot, rebaseline, restamp }) => {
|
|
2169
|
+
const root = repoRoot ?? process.cwd();
|
|
2170
|
+
const record = await store.read(path, id);
|
|
2171
|
+
if (!record) throw new KbRecordNotFoundError(id);
|
|
2172
|
+
const anchors = record.frontmatter.strauss_anchors ?? [];
|
|
2173
|
+
if (!anchors.length) {
|
|
2174
|
+
return {
|
|
2175
|
+
conceptId: id,
|
|
2176
|
+
results: [],
|
|
2177
|
+
verified: false,
|
|
2178
|
+
note: "record has no anchors"
|
|
2179
|
+
};
|
|
2180
|
+
}
|
|
2181
|
+
const results = [];
|
|
2182
|
+
const updated = [];
|
|
2183
|
+
const origin = new LazyOrigin(root);
|
|
2184
|
+
let dirty = false;
|
|
2185
|
+
if (anchors.some((anchor) => anchor.repo)) await origin.prime();
|
|
2186
|
+
const foreign = new Map(
|
|
2187
|
+
anchors.map((anchor) => [anchor, origin.isForeign(anchor)])
|
|
2188
|
+
);
|
|
2189
|
+
const reads = await readAnchorFiles(
|
|
2190
|
+
anchors.filter((anchor) => !foreign.get(anchor)).map((anchor) => anchor.file),
|
|
2191
|
+
anchorFileReader(root)
|
|
2192
|
+
);
|
|
2193
|
+
for (const anchor of anchors) {
|
|
2194
|
+
const base = {
|
|
2195
|
+
file: anchor.file,
|
|
2196
|
+
...anchor.symbol ? { symbol: anchor.symbol } : {},
|
|
2197
|
+
// Carried onto unresolved findings too: an anchor that once hashed
|
|
2198
|
+
// and now resolves to nothing is a broken anchor, and the exit code
|
|
2199
|
+
// has to be able to tell it from one nobody ever stamped.
|
|
2200
|
+
...anchor.hash ? { storedHash: anchor.hash } : {}
|
|
2201
|
+
};
|
|
2202
|
+
if (foreign.get(anchor)) {
|
|
2203
|
+
results.push({ ...base, state: "unresolved", reason: "foreign-repo" });
|
|
2204
|
+
updated.push(anchor);
|
|
2205
|
+
continue;
|
|
2206
|
+
}
|
|
2207
|
+
const fileRead = reads.get(anchor.file);
|
|
2208
|
+
if (!fileRead.ok) {
|
|
2209
|
+
results.push({ ...base, state: "unresolved", reason: fileRead.reason });
|
|
2210
|
+
updated.push(anchor);
|
|
2211
|
+
continue;
|
|
2212
|
+
}
|
|
2213
|
+
const resolved = resolveAnchor(fileRead.source, anchor);
|
|
2214
|
+
if (!resolved) {
|
|
2215
|
+
results.push({
|
|
2216
|
+
...base,
|
|
2217
|
+
state: "unresolved",
|
|
2218
|
+
reason: "symbol-not-found"
|
|
2219
|
+
});
|
|
2220
|
+
updated.push(anchor);
|
|
2221
|
+
continue;
|
|
2222
|
+
}
|
|
2223
|
+
const currentHash = hashAnchorText(resolved.text);
|
|
2224
|
+
const currentLines = resolved.endLine - resolved.startLine + 1;
|
|
2225
|
+
const stamped = {
|
|
2226
|
+
...anchor,
|
|
2227
|
+
hash: currentHash,
|
|
2228
|
+
lines: currentLines,
|
|
2229
|
+
resolved_at: now()
|
|
2230
|
+
};
|
|
2231
|
+
if (!anchor.hash) {
|
|
2232
|
+
results.push({ ...base, state: "stamped", currentHash });
|
|
2233
|
+
updated.push(stamped);
|
|
2234
|
+
dirty = true;
|
|
2235
|
+
} else if (anchor.hash === currentHash) {
|
|
2236
|
+
results.push({
|
|
2237
|
+
...base,
|
|
2238
|
+
state: "match",
|
|
2239
|
+
currentHash
|
|
2240
|
+
});
|
|
2241
|
+
const refresh = restamp || anchor.resolved_at === void 0;
|
|
2242
|
+
updated.push(refresh ? { ...anchor, resolved_at: now() } : anchor);
|
|
2243
|
+
if (refresh) dirty = true;
|
|
2244
|
+
} else {
|
|
2245
|
+
results.push({
|
|
2246
|
+
...base,
|
|
2247
|
+
state: "drifted",
|
|
2248
|
+
currentHash,
|
|
2249
|
+
diffSize: anchor.lines === void 0 ? null : Math.abs(currentLines - anchor.lines),
|
|
2250
|
+
...rebaseline ? { rebaselined: true } : {}
|
|
2251
|
+
});
|
|
2252
|
+
updated.push(rebaseline ? stamped : anchor);
|
|
2253
|
+
if (rebaseline) dirty = true;
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
let frozen = false;
|
|
2257
|
+
if (dirty) {
|
|
2258
|
+
try {
|
|
2259
|
+
await assertBaseNotFrozen(process.cwd(), path);
|
|
2260
|
+
} catch (error) {
|
|
2261
|
+
if (!(error instanceof KbBaseFrozenError)) throw error;
|
|
2262
|
+
frozen = true;
|
|
2263
|
+
}
|
|
2264
|
+
if (!frozen) await store.updateAnchors(path, id, updated, actor);
|
|
2265
|
+
}
|
|
2266
|
+
const frozenNote = frozen ? { frozen: true, note: "base is frozen: nothing was stamped" } : {};
|
|
2267
|
+
const checked = results.filter((entry) => entry.reason !== "foreign-repo");
|
|
2268
|
+
const skipped = results.length - checked.length;
|
|
2269
|
+
const matches2 = checked.filter((entry) => entry.state === "match").length;
|
|
2270
|
+
const clean = checked.length > 0 && checked.every((entry) => entry.state === "match");
|
|
2271
|
+
if (clean) {
|
|
2272
|
+
try {
|
|
2273
|
+
await store.verify(
|
|
2274
|
+
path,
|
|
2275
|
+
id,
|
|
2276
|
+
`anchor-resolve: ${matches2}/${checked.length} anchors match${skipped ? `, ${skipped} in another repo` : ""} (regex resolver)`,
|
|
2277
|
+
actor,
|
|
2278
|
+
now()
|
|
2279
|
+
);
|
|
2280
|
+
} catch (error) {
|
|
2281
|
+
if (!(error instanceof KbSelfVerificationError)) throw error;
|
|
2282
|
+
return {
|
|
2283
|
+
conceptId: id,
|
|
2284
|
+
results,
|
|
2285
|
+
verified: false,
|
|
2286
|
+
verifyRefused: "self-verification",
|
|
2287
|
+
...frozenNote
|
|
2288
|
+
};
|
|
2289
|
+
}
|
|
2290
|
+
return { conceptId: id, results, verified: true, ...frozenNote };
|
|
2291
|
+
}
|
|
2292
|
+
return { conceptId: id, results, verified: false, ...frozenNote };
|
|
2293
|
+
},
|
|
2294
|
+
// A stored hash that no longer resolves is a broken anchor, not an absence:
|
|
2295
|
+
// the file was deleted or the symbol renamed, and exiting zero on it would
|
|
2296
|
+
// let the one edit that destroys an anchor pass the gate that exists to
|
|
2297
|
+
// catch it. An anchor nobody ever stamped is still just unstamped, and one
|
|
2298
|
+
// belonging to another repository was never this run's to check — failing CI
|
|
2299
|
+
// on either would gate on work this command did not do.
|
|
2300
|
+
failsWhen: (result) => result.results.some(
|
|
2301
|
+
(entry) => entry.state === "drifted" || entry.state === "unresolved" && entry.storedHash !== void 0 && entry.reason !== "foreign-repo"
|
|
2302
|
+
)
|
|
2303
|
+
});
|
|
2304
|
+
|
|
1417
2305
|
// src/commands/answer.ts
|
|
2306
|
+
import { z as z9 } from "zod";
|
|
1418
2307
|
var answerCommand = define({
|
|
1419
2308
|
name: "answer",
|
|
1420
2309
|
tool: "kb_answer",
|
|
1421
2310
|
usage: "answer <concept-id> <answer...>",
|
|
1422
2311
|
description: "Resolve an open question: sets the status, stamps who answered and when, and appends an Answer section. If the answer overturns an assumption or a decision, that is a supersession \u2014 do it explicitly.",
|
|
1423
|
-
input:
|
|
2312
|
+
input: z9.object({ bundlePath, conceptId, answer: z9.string().min(1) }),
|
|
1424
2313
|
fromArgv: (argv, path) => ({
|
|
1425
2314
|
bundlePath: path,
|
|
1426
2315
|
conceptId: argv[1],
|
|
@@ -1433,27 +2322,90 @@ var answerCommand = define({
|
|
|
1433
2322
|
}
|
|
1434
2323
|
});
|
|
1435
2324
|
|
|
2325
|
+
// src/commands/catalog.ts
|
|
2326
|
+
import { z as z10 } from "zod";
|
|
2327
|
+
var catalogCommand = define({
|
|
2328
|
+
name: "catalog",
|
|
2329
|
+
tool: "kb_catalog",
|
|
2330
|
+
usage: "catalog [type]",
|
|
2331
|
+
description: "Lists every record as one line \u2014 concept id, type, title, standing, and a stale flag \u2014 at roughly thirty tokens each. Pick this over kb_load once kb_load refuses: kb_catalog never refuses. Superseded records show only their replacement; fetch bodies with kb_load, kb_pack, kb_query, or kb_trace.",
|
|
2332
|
+
input: z10.object({
|
|
2333
|
+
bundlePath,
|
|
2334
|
+
type: z10.enum(KB_RECORD_TYPES).optional()
|
|
2335
|
+
}),
|
|
2336
|
+
fromArgv: (argv, path) => ({
|
|
2337
|
+
bundlePath: path,
|
|
2338
|
+
...argv[1] && !argv[1].startsWith("--") ? { type: argv[1] } : {}
|
|
2339
|
+
}),
|
|
2340
|
+
run: async ({ store }, { bundlePath: path, type }) => render(
|
|
2341
|
+
await store.catalog(path, { ...type ? { type } : {} }),
|
|
2342
|
+
path,
|
|
2343
|
+
type
|
|
2344
|
+
)
|
|
2345
|
+
});
|
|
2346
|
+
function render(result, bundle, type) {
|
|
2347
|
+
const lines = [
|
|
2348
|
+
`# KB Catalog${type ? ` \u2014 ${type}` : ""}`,
|
|
2349
|
+
`bundle: ${bundle}`,
|
|
2350
|
+
`${count(result.recordCount, "record")}: ${standingCounts(result)}`
|
|
2351
|
+
];
|
|
2352
|
+
if (result.staleCount) {
|
|
2353
|
+
lines.push(
|
|
2354
|
+
`${result.staleCount} stale \u2014 a flag over the standings above, not one of them`
|
|
2355
|
+
);
|
|
2356
|
+
}
|
|
2357
|
+
lines.push("");
|
|
2358
|
+
if (!result.entries.length) {
|
|
2359
|
+
lines.push(
|
|
2360
|
+
type ? `(no records of type ${type})` : "(no records \u2014 this base is empty)"
|
|
2361
|
+
);
|
|
2362
|
+
} else {
|
|
2363
|
+
for (const entry of result.entries) lines.push(renderCatalogLine(entry));
|
|
2364
|
+
}
|
|
2365
|
+
lines.push(
|
|
2366
|
+
"",
|
|
2367
|
+
"Bodies are not here: kb_pack <conceptId> for the neighbourhood around one record, kb_load for the whole base when it fits the budget, kb_query for a lookup by wording, kb_trace <conceptId> for how a position was arrived at."
|
|
2368
|
+
);
|
|
2369
|
+
return lines.join("\n");
|
|
2370
|
+
}
|
|
2371
|
+
function standingCounts(result) {
|
|
2372
|
+
const ORDER = [
|
|
2373
|
+
"current",
|
|
2374
|
+
"open",
|
|
2375
|
+
"unsettled",
|
|
2376
|
+
"rejected",
|
|
2377
|
+
"superseded"
|
|
2378
|
+
];
|
|
2379
|
+
const parts = ORDER.filter((standing) => result.standings[standing]).map(
|
|
2380
|
+
(standing) => `${result.standings[standing]} ${standing}`
|
|
2381
|
+
);
|
|
2382
|
+
return parts.length ? parts.join(" \xB7 ") : "none";
|
|
2383
|
+
}
|
|
2384
|
+
function count(value, noun) {
|
|
2385
|
+
return `${value} ${value === 1 ? noun : `${noun}s`}`;
|
|
2386
|
+
}
|
|
2387
|
+
|
|
1436
2388
|
// src/commands/context.ts
|
|
1437
|
-
import { z as
|
|
2389
|
+
import { z as z11 } from "zod";
|
|
1438
2390
|
var contextCommand = define({
|
|
1439
2391
|
name: "context",
|
|
1440
2392
|
tool: "kb_context",
|
|
1441
2393
|
usage: "context [--profile NAME] [--budget N] [--full-under N] [--format json] [--event NAME]",
|
|
1442
2394
|
description: "The pinned-base index block, for injection at every context birth \u2014 startup, clear, resume, and after compaction. An index, not the content: concept ids, titles and standing, with the bodies left behind kb_load at the point of use. Emits nothing when nothing is pinned. Refuses with the list of bases and their sizes rather than truncating past its budget. Budgets resolve most-specific-first: explicit flags, then the workspace manifests' `context` tables (per profile, over their `default`), then the built-in profile (session-start, compact, turn), then package defaults \u2014 so a repo tunes its own numbers in .strauss/kb-pins.json without touching hook commands. Like kb_schema and kb_types this takes no bundlePath \u2014 it reads the workspace pin manifests, because which bases a session should see is workspace state, not a property of one base.",
|
|
1443
|
-
input:
|
|
1444
|
-
budgetTokens:
|
|
2395
|
+
input: z11.object({
|
|
2396
|
+
budgetTokens: z11.number().int().positive().optional().describe(
|
|
1445
2397
|
"Ceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000."
|
|
1446
2398
|
),
|
|
1447
|
-
fullUnderTokens:
|
|
2399
|
+
fullUnderTokens: z11.number().int().positive().optional().describe(
|
|
1448
2400
|
"Per-base rendering threshold, applied before the budget: a base whose complete load fits under this arrives as full records instead of index lines, and the whole block still answers to budgetTokens. Off by default \u2014 index-only is the safe default at a context birth, because injected bodies outlive the qualifiers on them; the session-start profile opts tiny bases in at 1500."
|
|
1449
2401
|
),
|
|
1450
|
-
profile:
|
|
2402
|
+
profile: z11.string().optional().describe(
|
|
1451
2403
|
"Named budget set: built-ins are session-start (full-under 1500), compact and turn (budget 2500); the manifests' `context` tables override per repo. Unknown names fall through to defaults rather than failing."
|
|
1452
2404
|
),
|
|
1453
|
-
format:
|
|
2405
|
+
format: z11.enum(["markdown", "json"]).optional().describe(
|
|
1454
2406
|
"CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
|
|
1455
2407
|
),
|
|
1456
|
-
event:
|
|
2408
|
+
event: z11.string().optional().describe(
|
|
1457
2409
|
"hookEventName stamped into the JSON envelope. Only meaningful with format=json."
|
|
1458
2410
|
)
|
|
1459
2411
|
}),
|
|
@@ -1489,15 +2441,16 @@ var contextCommand = define({
|
|
|
1489
2441
|
});
|
|
1490
2442
|
|
|
1491
2443
|
// src/commands/doctor.ts
|
|
1492
|
-
import { z as
|
|
1493
|
-
var days = (what, fallback) =>
|
|
2444
|
+
import { z as z12 } from "zod";
|
|
2445
|
+
var days = (what, fallback) => z12.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
|
|
1494
2446
|
var doctorCommand = define({
|
|
1495
2447
|
name: "doctor",
|
|
1496
2448
|
tool: "kb_doctor",
|
|
1497
|
-
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--strict]",
|
|
1498
|
-
description: "
|
|
1499
|
-
input:
|
|
2449
|
+
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--strict]",
|
|
2450
|
+
description: "Read-only health sweep: expired, expiring, unverified, aging, orphaned, broken-supersession, superseded-but-cited, drifted anchors. Every group is reported even when empty; nothing is written or re-stamped. Use it when picking up a base you have not touched in a while; kb_validate only checks that pointers between records agree.",
|
|
2451
|
+
input: z12.object({
|
|
1500
2452
|
bundlePath,
|
|
2453
|
+
repoRoot: REPO_ROOT,
|
|
1501
2454
|
expiringDays: days(
|
|
1502
2455
|
"How far ahead `expiring` looks, in days.",
|
|
1503
2456
|
DEFAULT_EXPIRING_DAYS
|
|
@@ -1510,7 +2463,7 @@ var doctorCommand = define({
|
|
|
1510
2463
|
"How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
|
|
1511
2464
|
DEFAULT_AGING_DAYS
|
|
1512
2465
|
),
|
|
1513
|
-
strict:
|
|
2466
|
+
strict: z12.boolean().optional().describe(
|
|
1514
2467
|
"Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
|
|
1515
2468
|
)
|
|
1516
2469
|
}),
|
|
@@ -1522,32 +2475,39 @@ var doctorCommand = define({
|
|
|
1522
2475
|
const expiring2 = argvFlag(argv, "--expiring-days");
|
|
1523
2476
|
const unverified2 = argvFlag(argv, "--unverified-days");
|
|
1524
2477
|
const agingDays = argvFlag(argv, "--aging-days");
|
|
2478
|
+
const repoRoot = argvFlag(argv, "--repo-root");
|
|
1525
2479
|
return {
|
|
1526
2480
|
bundlePath: path,
|
|
2481
|
+
...repoRoot !== void 0 ? { repoRoot } : {},
|
|
1527
2482
|
...expiring2 !== void 0 ? { expiringDays: Number(expiring2) } : {},
|
|
1528
2483
|
...unverified2 !== void 0 ? { unverifiedDays: Number(unverified2) } : {},
|
|
1529
2484
|
...agingDays !== void 0 ? { agingDays: Number(agingDays) } : {},
|
|
1530
2485
|
...argv.includes("--strict") ? { strict: true } : {}
|
|
1531
2486
|
};
|
|
1532
2487
|
},
|
|
1533
|
-
run: async ({ store, now }, { bundlePath: path, expiringDays, unverifiedDays, agingDays }) => {
|
|
2488
|
+
run: async ({ store, now }, { bundlePath: path, expiringDays, unverifiedDays, agingDays, repoRoot }) => {
|
|
1534
2489
|
const checkedAt = now();
|
|
1535
|
-
const
|
|
2490
|
+
const records = await store.list(path);
|
|
2491
|
+
const anchorDrift = await store.detectDrift(records, repoRoot);
|
|
2492
|
+
const report = doctor(records, {
|
|
1536
2493
|
...expiringDays !== void 0 ? { expiringDays } : {},
|
|
1537
2494
|
...unverifiedDays !== void 0 ? { unverifiedDays } : {},
|
|
1538
2495
|
...agingDays !== void 0 ? { agingDays } : {},
|
|
2496
|
+
...anchorDrift !== void 0 ? { anchorDrift } : {},
|
|
1539
2497
|
now: new Date(checkedAt)
|
|
1540
2498
|
});
|
|
1541
2499
|
return { bundlePath: path, checkedAt, ...report };
|
|
1542
2500
|
},
|
|
1543
|
-
render: (result) =>
|
|
1544
|
-
// Only expiry, and only under --strict. The other
|
|
2501
|
+
render: (result) => render2(result),
|
|
2502
|
+
// Only expiry, and only under --strict. The other seven checks report debt a
|
|
1545
2503
|
// reader decides about; an expired record is the base asserting something it
|
|
1546
2504
|
// already said it would stop standing behind, which is the one finding a
|
|
1547
|
-
// pipeline can act on without a judgment call.
|
|
2505
|
+
// pipeline can act on without a judgment call. Drift has its own gate —
|
|
2506
|
+
// `anchor-resolve` exits non-zero on it, against a repo root the caller
|
|
2507
|
+
// named, which is the run a CI pipeline should be making anyway.
|
|
1548
2508
|
failsWhen: (result, input) => input.strict === true && result.counts.expired > 0
|
|
1549
2509
|
});
|
|
1550
|
-
function
|
|
2510
|
+
function render2(result) {
|
|
1551
2511
|
const { thresholds } = result;
|
|
1552
2512
|
const lines = [
|
|
1553
2513
|
`# KB Doctor \u2014 ${result.bundlePath}`,
|
|
@@ -1579,13 +2539,13 @@ function render(result) {
|
|
|
1579
2539
|
}
|
|
1580
2540
|
|
|
1581
2541
|
// src/commands/list.ts
|
|
1582
|
-
import { z as
|
|
2542
|
+
import { z as z13 } from "zod";
|
|
1583
2543
|
var listCommand = define({
|
|
1584
2544
|
name: "list",
|
|
1585
2545
|
tool: "kb_list",
|
|
1586
2546
|
usage: "list [type]",
|
|
1587
2547
|
description: "Every record, optionally narrowed to one type. Use kb_query when you have a question; this is for enumerating.",
|
|
1588
|
-
input:
|
|
2548
|
+
input: z13.object({ bundlePath, type: z13.enum(KB_RECORD_TYPES).optional() }),
|
|
1589
2549
|
fromArgv: (argv, path) => ({ bundlePath: path, type: argv[1] }),
|
|
1590
2550
|
run: async ({ store }, { bundlePath: path, type }) => (await store.list(path, type)).map((record) => ({
|
|
1591
2551
|
conceptId: record.conceptId,
|
|
@@ -1597,36 +2557,40 @@ var listCommand = define({
|
|
|
1597
2557
|
});
|
|
1598
2558
|
|
|
1599
2559
|
// src/commands/load.ts
|
|
1600
|
-
import { z as
|
|
2560
|
+
import { z as z14 } from "zod";
|
|
1601
2561
|
var loadCommand = define({
|
|
1602
2562
|
name: "load",
|
|
1603
2563
|
tool: "kb_load",
|
|
1604
|
-
usage: "load [type] [--budget N | --all]",
|
|
1605
|
-
description: "
|
|
1606
|
-
input:
|
|
2564
|
+
usage: "load [type] [--budget N | --all] [--repo-root PATH]",
|
|
2565
|
+
description: "Loads the whole knowledge base at once, each record with its standing. Superseded records arrive as stubs (name, replacement, date); rejected and open records arrive whole. Refuses past the token budget rather than truncating \u2014 call kb_catalog, then kb_pack on the record that matters, or narrow with `type`; kb_query for a lookup by wording. `all` bypasses the budget.",
|
|
2566
|
+
input: z14.object({
|
|
1607
2567
|
bundlePath,
|
|
1608
|
-
type:
|
|
1609
|
-
budgetTokens:
|
|
1610
|
-
all:
|
|
1611
|
-
"
|
|
1612
|
-
)
|
|
2568
|
+
type: z14.enum(KB_RECORD_TYPES).optional(),
|
|
2569
|
+
budgetTokens: z14.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
|
|
2570
|
+
all: z14.boolean().optional().describe(
|
|
2571
|
+
"Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
|
|
2572
|
+
),
|
|
2573
|
+
repoRoot: REPO_ROOT
|
|
1613
2574
|
}).refine((value) => !(value.all && value.budgetTokens !== void 0), {
|
|
1614
|
-
message: "all
|
|
2575
|
+
message: "all is mutually exclusive with budgetTokens: pass a ceiling or none, not both."
|
|
1615
2576
|
}),
|
|
1616
2577
|
fromArgv: (argv, path) => {
|
|
1617
2578
|
const budget = argvFlag(argv, "--budget");
|
|
2579
|
+
const repoRoot = argvFlag(argv, "--repo-root");
|
|
1618
2580
|
return {
|
|
1619
2581
|
bundlePath: path,
|
|
1620
2582
|
...argv[1] && !argv[1].startsWith("--") ? { type: argv[1] } : {},
|
|
1621
2583
|
...budget ? { budgetTokens: Number(budget) } : {},
|
|
1622
|
-
...argv.includes("--all") ? { all: true } : {}
|
|
2584
|
+
...argv.includes("--all") ? { all: true } : {},
|
|
2585
|
+
...repoRoot !== void 0 ? { repoRoot } : {}
|
|
1623
2586
|
};
|
|
1624
2587
|
},
|
|
1625
|
-
run: async ({ store }, { bundlePath: path, type, budgetTokens, all }) => {
|
|
2588
|
+
run: async ({ store }, { bundlePath: path, type, budgetTokens, all, repoRoot }) => {
|
|
1626
2589
|
const result = await store.load(path, {
|
|
1627
2590
|
...type ? { type } : {},
|
|
1628
2591
|
...budgetTokens ? { budgetTokens } : {},
|
|
1629
|
-
...all ? { all } : {}
|
|
2592
|
+
...all ? { all } : {},
|
|
2593
|
+
...repoRoot !== void 0 ? { repoRoot } : {}
|
|
1630
2594
|
});
|
|
1631
2595
|
if (!result.loaded) return result;
|
|
1632
2596
|
return {
|
|
@@ -1645,25 +2609,25 @@ var loadCommand = define({
|
|
|
1645
2609
|
});
|
|
1646
2610
|
|
|
1647
2611
|
// src/commands/log.ts
|
|
1648
|
-
import { z as
|
|
2612
|
+
import { z as z15 } from "zod";
|
|
1649
2613
|
var logCommand = define({
|
|
1650
2614
|
name: "log",
|
|
1651
2615
|
tool: "kb_log",
|
|
1652
2616
|
usage: "log",
|
|
1653
2617
|
description: "What touched what, and when. The only artifact here that cannot be reconstructed from the records, so malformed lines are reported rather than repaired.",
|
|
1654
|
-
input:
|
|
2618
|
+
input: z15.object({ bundlePath }),
|
|
1655
2619
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
1656
2620
|
run: ({ store }, { bundlePath: path }) => store.readLog(path)
|
|
1657
2621
|
});
|
|
1658
2622
|
|
|
1659
2623
|
// src/commands/no-decision.ts
|
|
1660
|
-
import { z as
|
|
2624
|
+
import { z as z16 } from "zod";
|
|
1661
2625
|
var noDecisionCommand = define({
|
|
1662
2626
|
name: "no-decision",
|
|
1663
2627
|
tool: "kb_no_decision",
|
|
1664
2628
|
usage: "no-decision <reason...>",
|
|
1665
2629
|
description: 'Claim in one sentence that there was nothing to decide. Gating on "did you write a decision?" rewards writing a junk one; gating on "did you answer?" does not, so silence has to be expressible. Idempotent \u2014 restating it is not a collision.',
|
|
1666
|
-
input:
|
|
2630
|
+
input: z16.object({ bundlePath, reason: z16.string().min(1) }),
|
|
1667
2631
|
fromArgv: (argv, path) => ({
|
|
1668
2632
|
bundlePath: path,
|
|
1669
2633
|
reason: argv.slice(1).join(" ").trim()
|
|
@@ -1680,20 +2644,20 @@ var noDecisionCommand = define({
|
|
|
1680
2644
|
});
|
|
1681
2645
|
|
|
1682
2646
|
// src/commands/pack.ts
|
|
1683
|
-
import { z as
|
|
2647
|
+
import { z as z17 } from "zod";
|
|
1684
2648
|
var packCommand = define({
|
|
1685
2649
|
name: "pack",
|
|
1686
2650
|
tool: "kb_pack",
|
|
1687
2651
|
usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
|
|
1688
2652
|
description: "The bounded neighbourhood around one record: everything within `hops` of the root, ranked and cut to `maxNodes`, with every cut record named under Excluded \u2014 a named gap is knowable, a silent one is not. Prefer this over kb_load when the base is too large to hold whole and the work centres on one record; prefer it over kb_query when the question needs the governed neighbourhood \u2014 what was settled and what binds near this record \u2014 rather than a lookup by wording. Superseded records arrive as name, replacement and date stubs exactly as kb_load emits them: their bodies no longer hold, and kb_trace has the history. Refuses outright rather than truncating when the pack would exceed its token budget \u2014 a partial pack is indistinguishable from a complete one \u2014 reporting the record count and every already-cut id so the caller can lower hops or maxNodes, or raise the budget. The header carries the bundle, root, budget and a timestamp; everything below the header is byte-identical across runs over an unchanged base, so two packs can be diffed and a changed byte means changed knowledge. This tool (with kb_load, kb_query and kb_trace) is the only supported way to read a base; a raw file read bypasses supersession resolution and returns replaced records as if current.",
|
|
1689
|
-
input:
|
|
2653
|
+
input: z17.object({
|
|
1690
2654
|
bundlePath,
|
|
1691
2655
|
conceptId,
|
|
1692
|
-
hops:
|
|
1693
|
-
maxNodes:
|
|
2656
|
+
hops: z17.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
|
|
2657
|
+
maxNodes: z17.number().int().positive().optional().describe(
|
|
1694
2658
|
"How many records the pack may hold, root included. Defaults to 20."
|
|
1695
2659
|
),
|
|
1696
|
-
budgetTokens:
|
|
2660
|
+
budgetTokens: z17.number().int().positive().optional().describe(
|
|
1697
2661
|
"Approximate token ceiling over what is actually emitted. Defaults to 25000."
|
|
1698
2662
|
)
|
|
1699
2663
|
}),
|
|
@@ -1715,10 +2679,10 @@ var packCommand = define({
|
|
|
1715
2679
|
...maxNodes !== void 0 ? { maxNodes } : {},
|
|
1716
2680
|
...budgetTokens !== void 0 ? { budgetTokens } : {}
|
|
1717
2681
|
});
|
|
1718
|
-
return
|
|
2682
|
+
return render3(result, path, now());
|
|
1719
2683
|
}
|
|
1720
2684
|
});
|
|
1721
|
-
function
|
|
2685
|
+
function render3(result, bundle, at) {
|
|
1722
2686
|
const lines = [
|
|
1723
2687
|
`# KB Pack \u2014 ${result.root}`,
|
|
1724
2688
|
`bundle: ${bundle}`,
|
|
@@ -1780,22 +2744,22 @@ function warningLabel(warning) {
|
|
|
1780
2744
|
}
|
|
1781
2745
|
|
|
1782
2746
|
// src/commands/pin.ts
|
|
1783
|
-
import { z as
|
|
2747
|
+
import { z as z18 } from "zod";
|
|
1784
2748
|
var pinCommand = define({
|
|
1785
2749
|
name: "pin",
|
|
1786
2750
|
tool: "kb_pin",
|
|
1787
2751
|
usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
|
|
1788
2752
|
description: "Pin a base into a workspace pin manifest, so `context` surfaces it at every context birth. Three layers, nearest wins: the committed project manifest (.strauss/kb-pins.json, the default), `--local` (.strauss/kb-pins.local.json, personal and gitignored), and `--user` (~/.strauss/kb-pins.json, every workspace). Idempotent \u2014 re-pinning changes nothing unless --mode, --profiles, or --frozen/--unfreeze are given, which update just those fields. `--mode full` preloads the whole base into the block regardless of the full-under threshold; `--mode index` never upgrades. `--profiles` scopes the pin to named context profiles. `--frozen` marks the base concluded: write commands against it refuse and `context` labels it read-only. A path with no records yet succeeds with a warning; bases are routinely pinned before they are populated. Pins are workspace state: the pinned base itself is never touched.",
|
|
1789
|
-
input:
|
|
2753
|
+
input: z18.object({
|
|
1790
2754
|
bundlePath,
|
|
1791
|
-
mode:
|
|
2755
|
+
mode: z18.enum(["full", "index"]).optional().describe(
|
|
1792
2756
|
"full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
|
|
1793
2757
|
),
|
|
1794
|
-
profiles:
|
|
1795
|
-
layer:
|
|
2758
|
+
profiles: z18.array(z18.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
|
|
2759
|
+
layer: z18.enum(["project", "local", "user"]).optional().describe(
|
|
1796
2760
|
"Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
|
|
1797
2761
|
),
|
|
1798
|
-
frozen:
|
|
2762
|
+
frozen: z18.boolean().optional().describe(
|
|
1799
2763
|
"true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
|
|
1800
2764
|
)
|
|
1801
2765
|
}),
|
|
@@ -1824,38 +2788,48 @@ var pinCommand = define({
|
|
|
1824
2788
|
});
|
|
1825
2789
|
|
|
1826
2790
|
// src/commands/pins.ts
|
|
1827
|
-
import { z as
|
|
2791
|
+
import { z as z19 } from "zod";
|
|
1828
2792
|
var pinsCommand = define({
|
|
1829
2793
|
name: "pins",
|
|
1830
2794
|
tool: "kb_pins",
|
|
1831
2795
|
usage: "pins",
|
|
1832
2796
|
description: "Every pinned base across the manifest layers, each with its layer and whether it currently resolves to readable records. Reads the workspace manifests rather than any one base, like kb_context.",
|
|
1833
|
-
input:
|
|
2797
|
+
input: z19.object({}),
|
|
1834
2798
|
fromArgv: () => ({}),
|
|
1835
2799
|
run: ({ store }) => listPins(store, process.cwd())
|
|
1836
2800
|
});
|
|
1837
2801
|
|
|
1838
2802
|
// src/commands/query.ts
|
|
1839
|
-
import { z as
|
|
2803
|
+
import { z as z20 } from "zod";
|
|
1840
2804
|
var queryCommand = define({
|
|
1841
2805
|
name: "query",
|
|
1842
2806
|
tool: "kb_query",
|
|
1843
|
-
usage: "query <text...>",
|
|
1844
|
-
description: "Search and return each match with its standing. Results are flagged, never filtered: a superseded record comes back alongside whatever replaced it, and a rejected one is marked as something explicitly not adopted.
|
|
1845
|
-
input:
|
|
2807
|
+
usage: "query <text...> [--repo-root PATH]",
|
|
2808
|
+
description: "Search and return each match with its standing. Results are flagged, never filtered: a superseded record comes back alongside whatever replaced it, and a rejected one is marked as something explicitly not adopted. This is the lookup-by-wording rung, and the narrowest of the three: use it when you know roughly what the record says. The decision rule around it \u2014 while the base fits kb_load's token budget, kb_load it whole, because on this package's measurements a reader holding the whole base answered eight of nine questions whose wording appears in no record where embedding search answered four; once kb_load refuses, kb_catalog for one line per record and then kb_pack on the record the work centres on; and kb_query when the question is a point lookup rather than a neighbourhood. A query cannot tell you that nothing was decided \u2014 it returns its nearest hit whatever the distance \u2014 so reach for kb_catalog when the question is what exists. Never read record files directly: this tool (with kb_load, kb_catalog, kb_pack and kb_trace) is the only supported way to read a base; a file read bypasses supersession resolution and returns replaced records as if current.",
|
|
2809
|
+
input: z20.object({
|
|
1846
2810
|
bundlePath,
|
|
1847
|
-
text:
|
|
1848
|
-
type:
|
|
1849
|
-
includeNonCurrent:
|
|
1850
|
-
|
|
1851
|
-
fromArgv: (argv, path) => ({
|
|
1852
|
-
bundlePath: path,
|
|
1853
|
-
text: argv.slice(1).join(" ").trim(),
|
|
1854
|
-
includeNonCurrent: true
|
|
2811
|
+
text: z20.string().optional(),
|
|
2812
|
+
type: z20.enum(KB_RECORD_TYPES).optional(),
|
|
2813
|
+
includeNonCurrent: z20.boolean().optional(),
|
|
2814
|
+
repoRoot: REPO_ROOT
|
|
1855
2815
|
}),
|
|
1856
|
-
|
|
2816
|
+
// `--repo-root` is a flag, so its value must not fall into the search text.
|
|
2817
|
+
fromArgv: (argv, path) => {
|
|
2818
|
+
const repoRoot = argvFlag(argv, "--repo-root");
|
|
2819
|
+
const words = argv.slice(1);
|
|
2820
|
+
const flag = words.indexOf("--repo-root");
|
|
2821
|
+
if (flag !== -1) words.splice(flag, 2);
|
|
2822
|
+
return {
|
|
2823
|
+
bundlePath: path,
|
|
2824
|
+
text: words.join(" ").trim(),
|
|
2825
|
+
includeNonCurrent: true,
|
|
2826
|
+
...repoRoot !== void 0 ? { repoRoot } : {}
|
|
2827
|
+
};
|
|
2828
|
+
},
|
|
2829
|
+
run: async ({ store }, { bundlePath: path, text, type, includeNonCurrent, repoRoot }) => (await store.query(path, text ?? "", {
|
|
1857
2830
|
...type ? { type } : {},
|
|
1858
|
-
includeNonCurrent: includeNonCurrent === true
|
|
2831
|
+
includeNonCurrent: includeNonCurrent === true,
|
|
2832
|
+
...repoRoot !== void 0 ? { repoRoot } : {}
|
|
1859
2833
|
})).map((hit) => ({
|
|
1860
2834
|
conceptId: hit.record.conceptId,
|
|
1861
2835
|
title: hit.record.frontmatter.title ?? null,
|
|
@@ -1868,40 +2842,40 @@ var queryCommand = define({
|
|
|
1868
2842
|
});
|
|
1869
2843
|
|
|
1870
2844
|
// src/commands/read-index.ts
|
|
1871
|
-
import { z as
|
|
2845
|
+
import { z as z21 } from "zod";
|
|
1872
2846
|
var readIndexCommand = define({
|
|
1873
2847
|
name: "index",
|
|
1874
2848
|
tool: "kb_index",
|
|
1875
2849
|
usage: "index",
|
|
1876
2850
|
description: "The index, rebuilt if it disagrees with the records. One call gives the whole shape of the base: title, type, status, and description per record. The cheap re-orientation call after compaction or deep in a long session \u2014 a few hundred tokens; call it (or kb_context, when bases are pinned) first, then kb_load or fetch by concept id.",
|
|
1877
|
-
input:
|
|
2851
|
+
input: z21.object({ bundlePath }),
|
|
1878
2852
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
1879
2853
|
run: ({ store }, { bundlePath: path }) => store.readIndex(path)
|
|
1880
2854
|
});
|
|
1881
2855
|
|
|
1882
2856
|
// src/commands/schema.ts
|
|
1883
|
-
import { z as
|
|
2857
|
+
import { z as z22 } from "zod";
|
|
1884
2858
|
var schemaCommand = define({
|
|
1885
2859
|
name: "schema",
|
|
1886
2860
|
tool: "kb_schema",
|
|
1887
2861
|
usage: "schema",
|
|
1888
2862
|
description: "JSON Schema for the frontmatter, the write input, and log entries \u2014 generated from the code that enforces them, so it cannot drift from what a write will accept.",
|
|
1889
|
-
input:
|
|
2863
|
+
input: z22.object({}),
|
|
1890
2864
|
fromArgv: () => ({}),
|
|
1891
2865
|
run: () => Promise.resolve(kbJsonSchemas())
|
|
1892
2866
|
});
|
|
1893
2867
|
|
|
1894
2868
|
// src/commands/status.ts
|
|
1895
|
-
import { z as
|
|
2869
|
+
import { z as z23 } from "zod";
|
|
1896
2870
|
var statusCommand = define({
|
|
1897
2871
|
name: "status",
|
|
1898
2872
|
tool: "kb_status",
|
|
1899
2873
|
usage: "status <concept-id> <status>",
|
|
1900
2874
|
description: "Move a record's status, leaving everything else alone. Uses a compare-and-swap, so a concurrent change fails loudly rather than being overwritten.",
|
|
1901
|
-
input:
|
|
2875
|
+
input: z23.object({
|
|
1902
2876
|
bundlePath,
|
|
1903
2877
|
conceptId,
|
|
1904
|
-
status:
|
|
2878
|
+
status: z23.enum(KB_RECORD_STATUSES)
|
|
1905
2879
|
}),
|
|
1906
2880
|
fromArgv: (argv, path) => ({
|
|
1907
2881
|
bundlePath: path,
|
|
@@ -1916,13 +2890,13 @@ var statusCommand = define({
|
|
|
1916
2890
|
});
|
|
1917
2891
|
|
|
1918
2892
|
// src/commands/supersede.ts
|
|
1919
|
-
import { z as
|
|
2893
|
+
import { z as z24 } from "zod";
|
|
1920
2894
|
var supersedeCommand = define({
|
|
1921
2895
|
name: "supersede",
|
|
1922
2896
|
tool: "kb_supersede",
|
|
1923
2897
|
usage: "supersede <concept-id> <replacement-id>",
|
|
1924
2898
|
description: "Mark a record superseded by another, linking both directions. Use this rather than editing a record whose meaning changed \u2014 a record that quietly becomes something else invalidates every reference to it, and the earlier understanding is what a later trace needs.",
|
|
1925
|
-
input:
|
|
2899
|
+
input: z24.object({ bundlePath, conceptId, replacementId: conceptId }),
|
|
1926
2900
|
fromArgv: (argv, path) => ({
|
|
1927
2901
|
bundlePath: path,
|
|
1928
2902
|
conceptId: argv[1],
|
|
@@ -1936,16 +2910,16 @@ var supersedeCommand = define({
|
|
|
1936
2910
|
});
|
|
1937
2911
|
|
|
1938
2912
|
// src/commands/sync-instructions.ts
|
|
1939
|
-
import { z as
|
|
2913
|
+
import { z as z25 } from "zod";
|
|
1940
2914
|
var syncInstructionsCommand = define({
|
|
1941
2915
|
name: "sync-instructions",
|
|
1942
2916
|
usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
|
|
1943
2917
|
description: "Idempotently plant the `context` block between sentinel comments in an instruction file (AGENTS.md, CLAUDE.md), creating the block when absent and leaving everything outside the sentinels alone. CLI-only: this is file plumbing for runtimes whose instruction files are re-read where their conversations are not, not an agent capability \u2014 the capability is kb_context.",
|
|
1944
|
-
input:
|
|
1945
|
-
file:
|
|
1946
|
-
budgetTokens:
|
|
1947
|
-
fullUnderTokens:
|
|
1948
|
-
profile:
|
|
2918
|
+
input: z25.object({
|
|
2919
|
+
file: z25.string().min(1).describe("The instruction file to edit in place."),
|
|
2920
|
+
budgetTokens: z25.number().int().positive().optional(),
|
|
2921
|
+
fullUnderTokens: z25.number().int().positive().optional(),
|
|
2922
|
+
profile: z25.string().optional()
|
|
1949
2923
|
}),
|
|
1950
2924
|
fromArgv: (argv) => {
|
|
1951
2925
|
const budget = argvFlag(argv, "--budget");
|
|
@@ -1971,17 +2945,17 @@ var syncInstructionsCommand = define({
|
|
|
1971
2945
|
});
|
|
1972
2946
|
|
|
1973
2947
|
// src/commands/trace.ts
|
|
1974
|
-
import { z as
|
|
2948
|
+
import { z as z26 } from "zod";
|
|
1975
2949
|
var traceCommand = define({
|
|
1976
2950
|
name: "trace",
|
|
1977
2951
|
tool: "kb_trace",
|
|
1978
2952
|
usage: "trace <concept-id> [edges...]",
|
|
1979
2953
|
description: 'How a position was arrived at, as a timeline ordered by when each record was written. Deliberately includes rejected, draft, and superseded records \u2014 in a history those are the content, not noise. Follows supersession, shared code anchors, and shared sources. Use when the question is "why is this the way it is" rather than "what do we hold now". This tool (with kb_load and kb_query) is the only supported way to read a base; a raw file read bypasses supersession resolution and returns replaced records as if current.',
|
|
1980
|
-
input:
|
|
2954
|
+
input: z26.object({
|
|
1981
2955
|
bundlePath,
|
|
1982
2956
|
conceptId,
|
|
1983
|
-
edges:
|
|
1984
|
-
depth:
|
|
2957
|
+
edges: z26.array(z26.enum(TRACE_EDGES)).optional(),
|
|
2958
|
+
depth: z26.number().int().positive().optional()
|
|
1985
2959
|
}),
|
|
1986
2960
|
fromArgv: (argv, path) => ({
|
|
1987
2961
|
bundlePath: path,
|
|
@@ -2003,53 +2977,53 @@ var traceCommand = define({
|
|
|
2003
2977
|
});
|
|
2004
2978
|
|
|
2005
2979
|
// src/commands/types.ts
|
|
2006
|
-
import { z as
|
|
2980
|
+
import { z as z27 } from "zod";
|
|
2007
2981
|
var typesCommand = define({
|
|
2008
2982
|
name: "types",
|
|
2009
2983
|
tool: "kb_types",
|
|
2010
2984
|
usage: "types",
|
|
2011
2985
|
description: "The twelve record types with their purpose, body sections, and starting status. Read this before writing rather than guessing headings \u2014 a section the type does not define is rejected.",
|
|
2012
|
-
input:
|
|
2986
|
+
input: z27.object({}),
|
|
2013
2987
|
fromArgv: () => ({}),
|
|
2014
2988
|
run: () => Promise.resolve(RECORD_TYPES)
|
|
2015
2989
|
});
|
|
2016
2990
|
|
|
2017
2991
|
// src/commands/unpin.ts
|
|
2018
|
-
import { z as
|
|
2992
|
+
import { z as z28 } from "zod";
|
|
2019
2993
|
var unpinCommand = define({
|
|
2020
2994
|
name: "unpin",
|
|
2021
2995
|
tool: "kb_unpin",
|
|
2022
2996
|
usage: "unpin [bundle-path]",
|
|
2023
2997
|
description: "Remove a base from every pin manifest layer that holds it \u2014 project, local, and user \u2014 because unpinned means gone, not still injected from another file. Reports which layers were touched.",
|
|
2024
|
-
input:
|
|
2998
|
+
input: z28.object({ bundlePath }),
|
|
2025
2999
|
fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
|
|
2026
3000
|
run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
|
|
2027
3001
|
});
|
|
2028
3002
|
|
|
2029
3003
|
// src/commands/validate.ts
|
|
2030
|
-
import { z as
|
|
3004
|
+
import { z as z29 } from "zod";
|
|
2031
3005
|
var validateCommand = define({
|
|
2032
3006
|
name: "validate",
|
|
2033
3007
|
tool: "kb_validate",
|
|
2034
3008
|
usage: "validate",
|
|
2035
3009
|
description: "Check pointers no single record can see: supersession links that disagree between the two records, and assumptions that cite sources. Per-record shape is enforced on every read, so a problem here means someone edited a file by hand.",
|
|
2036
|
-
input:
|
|
3010
|
+
input: z29.object({ bundlePath }),
|
|
2037
3011
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
2038
3012
|
run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
|
|
2039
3013
|
failsWhen: (result) => Array.isArray(result) && result.length > 0
|
|
2040
3014
|
});
|
|
2041
3015
|
|
|
2042
3016
|
// src/commands/verify.ts
|
|
2043
|
-
import { z as
|
|
3017
|
+
import { z as z30 } from "zod";
|
|
2044
3018
|
var verifyCommand = define({
|
|
2045
3019
|
name: "verify",
|
|
2046
3020
|
tool: "kb_verify",
|
|
2047
3021
|
usage: "verify <concept-id> --note <text>",
|
|
2048
3022
|
description: "Append one verified[] event \u2014 who checked the record, when, and what the check found. Appends only; prior events are never rewritten. A record's own generator is refused unless the actor is human: re-reading your own output is not an independent check.",
|
|
2049
|
-
input:
|
|
3023
|
+
input: z30.object({
|
|
2050
3024
|
bundlePath,
|
|
2051
3025
|
conceptId,
|
|
2052
|
-
note:
|
|
3026
|
+
note: z30.string().refine((s) => s.trim().length > 0, {
|
|
2053
3027
|
message: "note must say what the check found"
|
|
2054
3028
|
})
|
|
2055
3029
|
}),
|
|
@@ -2069,7 +3043,7 @@ var verifyCommand = define({
|
|
|
2069
3043
|
});
|
|
2070
3044
|
|
|
2071
3045
|
// src/commands/write.ts
|
|
2072
|
-
import { z as
|
|
3046
|
+
import { z as z31 } from "zod";
|
|
2073
3047
|
var writeCommand = define({
|
|
2074
3048
|
name: "write",
|
|
2075
3049
|
tool: "kb_write",
|
|
@@ -2083,9 +3057,9 @@ var writeCommand = define({
|
|
|
2083
3057
|
"- Prefer a new record over overloading an existing one, and keep each short. A record nobody finishes reading is not durable memory.",
|
|
2084
3058
|
"- Records are never deleted; supersede instead, so the earlier reasoning stays inspectable."
|
|
2085
3059
|
].join("\n"),
|
|
2086
|
-
input:
|
|
3060
|
+
input: z31.object({
|
|
2087
3061
|
bundlePath,
|
|
2088
|
-
type:
|
|
3062
|
+
type: z31.enum(KB_RECORD_TYPES),
|
|
2089
3063
|
input: composeInputSchema
|
|
2090
3064
|
}),
|
|
2091
3065
|
fromArgv: async (argv, path, stdin) => ({
|
|
@@ -2109,7 +3083,7 @@ var writeCommand = define({
|
|
|
2109
3083
|
});
|
|
2110
3084
|
|
|
2111
3085
|
// src/commands/write-decision.ts
|
|
2112
|
-
import { z as
|
|
3086
|
+
import { z as z32 } from "zod";
|
|
2113
3087
|
var writeDecisionCommand = define({
|
|
2114
3088
|
name: "write-decision",
|
|
2115
3089
|
tool: "kb_write_decision",
|
|
@@ -2122,7 +3096,7 @@ var writeDecisionCommand = define({
|
|
|
2122
3096
|
"- `alternative` is what you turned down and why, not a list of everything considered.",
|
|
2123
3097
|
"- A reference to material you read goes in `sources`; a reference to code goes in `anchors`; a reference to another record goes in `relatedConceptIds`."
|
|
2124
3098
|
].join("\n"),
|
|
2125
|
-
input:
|
|
3099
|
+
input: z32.object({ bundlePath, input: decisionInputSchema }),
|
|
2126
3100
|
fromArgv: async (_argv, path, stdin) => ({
|
|
2127
3101
|
bundlePath: path,
|
|
2128
3102
|
input: JSON.parse(await stdin())
|
|
@@ -2151,7 +3125,9 @@ var KB_COMMANDS = [
|
|
|
2151
3125
|
supersedeCommand,
|
|
2152
3126
|
answerCommand,
|
|
2153
3127
|
verifyCommand,
|
|
3128
|
+
anchorResolveCommand,
|
|
2154
3129
|
loadCommand,
|
|
3130
|
+
catalogCommand,
|
|
2155
3131
|
packCommand,
|
|
2156
3132
|
queryCommand,
|
|
2157
3133
|
traceCommand,
|
|
@@ -2197,143 +3173,8 @@ function parseMarkdownWithFrontmatter(text, schema) {
|
|
|
2197
3173
|
};
|
|
2198
3174
|
}
|
|
2199
3175
|
|
|
2200
|
-
// src/errors.ts
|
|
2201
|
-
var Fault = /* @__PURE__ */ ((Fault2) => {
|
|
2202
|
-
Fault2["Configuration"] = "Configuration";
|
|
2203
|
-
Fault2["System"] = "System";
|
|
2204
|
-
Fault2["User"] = "User";
|
|
2205
|
-
return Fault2;
|
|
2206
|
-
})(Fault || {});
|
|
2207
|
-
var ErrorTypes = /* @__PURE__ */ ((ErrorTypes2) => {
|
|
2208
|
-
ErrorTypes2["KbRecordAlreadyExists"] = "KbRecordAlreadyExists";
|
|
2209
|
-
ErrorTypes2["KbInvalidConceptId"] = "KbInvalidConceptId";
|
|
2210
|
-
ErrorTypes2["KbPackBudgetExceeded"] = "KbPackBudgetExceeded";
|
|
2211
|
-
ErrorTypes2["KbRecordNotFound"] = "KbRecordNotFound";
|
|
2212
|
-
ErrorTypes2["KbSelfVerification"] = "KbSelfVerification";
|
|
2213
|
-
ErrorTypes2["KbWriteConflict"] = "KbWriteConflict";
|
|
2214
|
-
return ErrorTypes2;
|
|
2215
|
-
})(ErrorTypes || {});
|
|
2216
|
-
var BaseError = class extends Error {
|
|
2217
|
-
code;
|
|
2218
|
-
errorType;
|
|
2219
|
-
fault;
|
|
2220
|
-
retriable;
|
|
2221
|
-
reportToUser;
|
|
2222
|
-
details;
|
|
2223
|
-
constructor(props) {
|
|
2224
|
-
super(props.message);
|
|
2225
|
-
this.name = props.name ?? this.constructor.name;
|
|
2226
|
-
this.code = props.code ?? 500;
|
|
2227
|
-
this.errorType = props.errorType;
|
|
2228
|
-
this.fault = props.fault;
|
|
2229
|
-
this.retriable = props.retriable ?? true;
|
|
2230
|
-
this.reportToUser = props.reportToUser ?? false;
|
|
2231
|
-
this.details = props.details;
|
|
2232
|
-
}
|
|
2233
|
-
};
|
|
2234
|
-
|
|
2235
|
-
// src/kb-errors.ts
|
|
2236
|
-
var KbRecordAlreadyExistsError = class extends BaseError {
|
|
2237
|
-
constructor(conceptId2) {
|
|
2238
|
-
super({
|
|
2239
|
-
message: `kb: ${conceptId2} already exists \u2014 choose a more specific slug, or write with overwrite`,
|
|
2240
|
-
errorType: "KbRecordAlreadyExists" /* KbRecordAlreadyExists */,
|
|
2241
|
-
code: 409,
|
|
2242
|
-
fault: "User" /* User */,
|
|
2243
|
-
retriable: false,
|
|
2244
|
-
reportToUser: true,
|
|
2245
|
-
details: { conceptId: conceptId2, action: "refused" }
|
|
2246
|
-
});
|
|
2247
|
-
this.conceptId = conceptId2;
|
|
2248
|
-
}
|
|
2249
|
-
conceptId;
|
|
2250
|
-
};
|
|
2251
|
-
var KbRecordNotFoundError = class extends BaseError {
|
|
2252
|
-
constructor(conceptId2) {
|
|
2253
|
-
super({
|
|
2254
|
-
message: `kb: ${conceptId2} does not exist`,
|
|
2255
|
-
errorType: "KbRecordNotFound" /* KbRecordNotFound */,
|
|
2256
|
-
code: 404,
|
|
2257
|
-
fault: "User" /* User */,
|
|
2258
|
-
retriable: false,
|
|
2259
|
-
reportToUser: true,
|
|
2260
|
-
details: { conceptId: conceptId2 }
|
|
2261
|
-
});
|
|
2262
|
-
this.conceptId = conceptId2;
|
|
2263
|
-
}
|
|
2264
|
-
conceptId;
|
|
2265
|
-
};
|
|
2266
|
-
var KbWriteConflictError = class extends BaseError {
|
|
2267
|
-
constructor(conceptId2) {
|
|
2268
|
-
super({
|
|
2269
|
-
message: `kb: ${conceptId2} changed while it was being updated \u2014 re-read and retry`,
|
|
2270
|
-
errorType: "KbWriteConflict" /* KbWriteConflict */,
|
|
2271
|
-
code: 409,
|
|
2272
|
-
fault: "System" /* System */,
|
|
2273
|
-
retriable: true,
|
|
2274
|
-
reportToUser: true,
|
|
2275
|
-
details: { conceptId: conceptId2 }
|
|
2276
|
-
});
|
|
2277
|
-
this.conceptId = conceptId2;
|
|
2278
|
-
}
|
|
2279
|
-
conceptId;
|
|
2280
|
-
};
|
|
2281
|
-
var KbSelfVerificationError = class extends BaseError {
|
|
2282
|
-
constructor(conceptId2, actor, generatedBy) {
|
|
2283
|
-
super({
|
|
2284
|
-
message: `kb: ${conceptId2} was generated by ${generatedBy}, and a record's generator cannot verify it \u2014 only a human or a different actor can`,
|
|
2285
|
-
errorType: "KbSelfVerification" /* KbSelfVerification */,
|
|
2286
|
-
code: 400,
|
|
2287
|
-
fault: "User" /* User */,
|
|
2288
|
-
retriable: false,
|
|
2289
|
-
reportToUser: true,
|
|
2290
|
-
details: { conceptId: conceptId2, actor, generatedBy, action: "refused" }
|
|
2291
|
-
});
|
|
2292
|
-
this.conceptId = conceptId2;
|
|
2293
|
-
this.actor = actor;
|
|
2294
|
-
this.generatedBy = generatedBy;
|
|
2295
|
-
}
|
|
2296
|
-
conceptId;
|
|
2297
|
-
actor;
|
|
2298
|
-
generatedBy;
|
|
2299
|
-
};
|
|
2300
|
-
var KbPackBudgetExceededError = class extends BaseError {
|
|
2301
|
-
constructor(recordCount, approxTokens2, budgetTokens, excluded) {
|
|
2302
|
-
super({
|
|
2303
|
-
message: `kb: a pack of ${recordCount} records is ~${approxTokens2} tokens against a budget of ${budgetTokens} \u2014 lower hops or maxNodes, or raise the budget`,
|
|
2304
|
-
errorType: "KbPackBudgetExceeded" /* KbPackBudgetExceeded */,
|
|
2305
|
-
code: 400,
|
|
2306
|
-
fault: "User" /* User */,
|
|
2307
|
-
retriable: false,
|
|
2308
|
-
reportToUser: true,
|
|
2309
|
-
details: { recordCount, approxTokens: approxTokens2, budgetTokens, excluded }
|
|
2310
|
-
});
|
|
2311
|
-
this.recordCount = recordCount;
|
|
2312
|
-
this.approxTokens = approxTokens2;
|
|
2313
|
-
this.budgetTokens = budgetTokens;
|
|
2314
|
-
this.excluded = excluded;
|
|
2315
|
-
}
|
|
2316
|
-
recordCount;
|
|
2317
|
-
approxTokens;
|
|
2318
|
-
budgetTokens;
|
|
2319
|
-
excluded;
|
|
2320
|
-
};
|
|
2321
|
-
var KbInvalidConceptIdError = class extends BaseError {
|
|
2322
|
-
constructor(message, details) {
|
|
2323
|
-
super({
|
|
2324
|
-
message: `kb: ${message}`,
|
|
2325
|
-
errorType: "KbInvalidConceptId" /* KbInvalidConceptId */,
|
|
2326
|
-
code: 400,
|
|
2327
|
-
fault: "User" /* User */,
|
|
2328
|
-
retriable: false,
|
|
2329
|
-
reportToUser: true,
|
|
2330
|
-
details
|
|
2331
|
-
});
|
|
2332
|
-
}
|
|
2333
|
-
};
|
|
2334
|
-
|
|
2335
3176
|
// src/search-index.ts
|
|
2336
|
-
import { stat } from "fs/promises";
|
|
3177
|
+
import { stat as stat2 } from "fs/promises";
|
|
2337
3178
|
import { join as join3 } from "path";
|
|
2338
3179
|
var SEARCH_INDEX_FILE = ".index.sqlite";
|
|
2339
3180
|
var COLLECTION = "kb";
|
|
@@ -2378,16 +3219,19 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
2378
3219
|
}
|
|
2379
3220
|
}
|
|
2380
3221
|
async function isStale(bundlePath2) {
|
|
2381
|
-
const indexAt = await
|
|
3222
|
+
const indexAt = await stat2(join3(bundlePath2, SEARCH_INDEX_FILE)).then((s) => s.mtimeMs).catch(() => 0);
|
|
2382
3223
|
if (!indexAt) return true;
|
|
2383
3224
|
const { readdir: readdir2 } = await import("fs/promises");
|
|
2384
|
-
const names = await readdir2(bundlePath2).catch(() => [])
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
3225
|
+
const names = (await readdir2(bundlePath2).catch(() => [])).filter(
|
|
3226
|
+
(name) => name.endsWith(".md") && name !== INDEX_FILE
|
|
3227
|
+
);
|
|
3228
|
+
let stale = false;
|
|
3229
|
+
await mapLimit(names, DEFAULT_IO_CONCURRENCY, async (name) => {
|
|
3230
|
+
if (stale) return;
|
|
3231
|
+
const at = await stat2(join3(bundlePath2, name)).then((s) => s.mtimeMs).catch(() => 0);
|
|
3232
|
+
if (at > indexAt) stale = true;
|
|
3233
|
+
});
|
|
3234
|
+
return stale;
|
|
2391
3235
|
}
|
|
2392
3236
|
function resolveHits(hits, records) {
|
|
2393
3237
|
const byName = /* @__PURE__ */ new Map();
|
|
@@ -2419,18 +3263,18 @@ async function loadQmd(logger) {
|
|
|
2419
3263
|
}
|
|
2420
3264
|
|
|
2421
3265
|
// src/kb-store.ts
|
|
2422
|
-
import { createHash } from "crypto";
|
|
3266
|
+
import { createHash as createHash2 } from "crypto";
|
|
2423
3267
|
import {
|
|
2424
3268
|
appendFile,
|
|
2425
3269
|
link,
|
|
2426
3270
|
mkdir as mkdir2,
|
|
2427
3271
|
readdir,
|
|
2428
|
-
readFile as
|
|
3272
|
+
readFile as readFile4,
|
|
2429
3273
|
rename,
|
|
2430
3274
|
unlink,
|
|
2431
3275
|
writeFile as writeFile3
|
|
2432
3276
|
} from "fs/promises";
|
|
2433
|
-
import { join as join4, resolve as
|
|
3277
|
+
import { join as join4, resolve as resolve5, sep as sep3 } from "path";
|
|
2434
3278
|
|
|
2435
3279
|
// src/kb-gitattributes.ts
|
|
2436
3280
|
var GITATTRIBUTES_FILE = ".gitattributes";
|
|
@@ -2527,7 +3371,7 @@ var KbStore = class {
|
|
|
2527
3371
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
2528
3372
|
let raw;
|
|
2529
3373
|
try {
|
|
2530
|
-
raw = await
|
|
3374
|
+
raw = await readFile4(target, "utf8");
|
|
2531
3375
|
} catch {
|
|
2532
3376
|
return null;
|
|
2533
3377
|
}
|
|
@@ -2549,10 +3393,10 @@ var KbStore = class {
|
|
|
2549
3393
|
return [];
|
|
2550
3394
|
}
|
|
2551
3395
|
const wanted = names.sort().filter((name) => name.endsWith(".md") && !STORE_OWNED.has(name)).map((name) => ({ name, conceptId: name.slice(0, -".md".length) })).filter(({ conceptId: conceptId2 }) => !type || conceptId2.startsWith(`${type}.`));
|
|
2552
|
-
const records = await
|
|
2553
|
-
wanted
|
|
2554
|
-
|
|
2555
|
-
)
|
|
3396
|
+
const records = await mapLimit(
|
|
3397
|
+
wanted,
|
|
3398
|
+
DEFAULT_IO_CONCURRENCY,
|
|
3399
|
+
async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await readFile4(join4(root, name), "utf8"))
|
|
2556
3400
|
);
|
|
2557
3401
|
return records.filter((record) => record !== null);
|
|
2558
3402
|
}
|
|
@@ -2574,6 +3418,21 @@ var KbStore = class {
|
|
|
2574
3418
|
{ operation: `status:${status}`, by: actor }
|
|
2575
3419
|
);
|
|
2576
3420
|
}
|
|
3421
|
+
/**
|
|
3422
|
+
* Replaces a record's anchors wholesale, preserving everything else.
|
|
3423
|
+
*
|
|
3424
|
+
* Wholesale rather than merged: the caller just resolved the anchors it is
|
|
3425
|
+
* writing, so it holds the complete current set, and a merge would keep
|
|
3426
|
+
* stale entries the resolution pass deliberately dropped.
|
|
3427
|
+
*/
|
|
3428
|
+
async updateAnchors(bundlePath2, conceptId2, anchors, actor = "unknown") {
|
|
3429
|
+
return this.mutate(
|
|
3430
|
+
bundlePath2,
|
|
3431
|
+
conceptId2,
|
|
3432
|
+
(frontmatter) => ({ ...frontmatter, strauss_anchors: anchors }),
|
|
3433
|
+
{ operation: "anchor-resolve", by: actor }
|
|
3434
|
+
);
|
|
3435
|
+
}
|
|
2577
3436
|
/**
|
|
2578
3437
|
* Appends one `verified[]` event: who checked the record, when, and what the
|
|
2579
3438
|
* check found. Append-only — prior events are history, and are spread into
|
|
@@ -2672,9 +3531,12 @@ ${answer}
|
|
|
2672
3531
|
const bundle = await this.list(bundlePath2);
|
|
2673
3532
|
const needle = text.trim();
|
|
2674
3533
|
const hits = needle ? await this.rank(bundlePath2, needle, bundle) : bundle;
|
|
3534
|
+
const narrowed = options.type ? hits.filter((r) => r.frontmatter.type === options.type) : hits;
|
|
2675
3535
|
const adjudicated = adjudicate(
|
|
2676
|
-
|
|
2677
|
-
bundle
|
|
3536
|
+
narrowed,
|
|
3537
|
+
bundle,
|
|
3538
|
+
/* @__PURE__ */ new Date(),
|
|
3539
|
+
await this.detectDrift(narrowed, options.repoRoot)
|
|
2678
3540
|
);
|
|
2679
3541
|
if (options.includeNonCurrent) return adjudicated;
|
|
2680
3542
|
const present = new Set(adjudicated.map((hit) => hit.record.conceptId));
|
|
@@ -2693,6 +3555,50 @@ ${answer}
|
|
|
2693
3555
|
const lowered = needle.toLowerCase();
|
|
2694
3556
|
return bundle.filter((record) => matches(record, lowered));
|
|
2695
3557
|
}
|
|
3558
|
+
/**
|
|
3559
|
+
* Anchor drift over the records about to be handed back. Like the search
|
|
3560
|
+
* index, this is an enrichment: a filesystem failure degrades to "no drift
|
|
3561
|
+
* reported" rather than failing the read. Anchors without a stored hash are
|
|
3562
|
+
* skipped inside `detectAnchorDrift`, so a base nobody has stamped pays no
|
|
3563
|
+
* fs cost here. `repoRoot` defaults to the working directory — the CLI runs
|
|
3564
|
+
* at the repo root, and the MCP server's cwd is the workspace.
|
|
3565
|
+
*
|
|
3566
|
+
* Public because `doctor` needs the same map with the same degradation: a
|
|
3567
|
+
* sweep that failed to read the tree should report no drift, not fail.
|
|
3568
|
+
*
|
|
3569
|
+
* When no root was given and not one anchored file was found, the finding is
|
|
3570
|
+
* discarded. A base read from somewhere other than the tree it describes
|
|
3571
|
+
* misses every file at once, and that shape is far likelier to be a wrong
|
|
3572
|
+
* default root than a repository where every anchored file was deleted on
|
|
3573
|
+
* the same day. Reporting it would put a drift warning on every record in
|
|
3574
|
+
* the base, which teaches a reader to ignore the warning — the one outcome
|
|
3575
|
+
* worse than not having it. One file found anywhere makes the root
|
|
3576
|
+
* plausible, and the misses become findings again; an explicit `repoRoot` is
|
|
3577
|
+
* taken at its word either way.
|
|
3578
|
+
*/
|
|
3579
|
+
async detectDrift(records, repoRoot) {
|
|
3580
|
+
try {
|
|
3581
|
+
const drift = await detectAnchorDrift(records, {
|
|
3582
|
+
repoRoot: repoRoot ?? process.cwd()
|
|
3583
|
+
});
|
|
3584
|
+
if (repoRoot === void 0 && looksLikeWrongRepoRoot(drift)) {
|
|
3585
|
+
this.logger.warn?.({
|
|
3586
|
+
operation: "kb.anchor-drift",
|
|
3587
|
+
outcome: "skipped",
|
|
3588
|
+
reason: "no anchored file found under the default repo root"
|
|
3589
|
+
});
|
|
3590
|
+
return void 0;
|
|
3591
|
+
}
|
|
3592
|
+
return drift;
|
|
3593
|
+
} catch (error) {
|
|
3594
|
+
this.logger.warn?.({
|
|
3595
|
+
operation: "kb.anchor-drift",
|
|
3596
|
+
outcome: "skipped",
|
|
3597
|
+
error: error instanceof Error ? error.message : "unknown"
|
|
3598
|
+
});
|
|
3599
|
+
return void 0;
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
2696
3602
|
/**
|
|
2697
3603
|
* The whole base, adjudicated, when it is small enough to hand over.
|
|
2698
3604
|
*
|
|
@@ -2712,15 +3618,27 @@ ${answer}
|
|
|
2712
3618
|
* is indistinguishable from a complete one, so a caller would answer "that
|
|
2713
3619
|
* was never decided" from a slice it did not know was a slice.
|
|
2714
3620
|
*
|
|
2715
|
-
*
|
|
2716
|
-
*
|
|
2717
|
-
*
|
|
3621
|
+
* A token budget decides that, measured over what is actually handed back.
|
|
3622
|
+
* The refusal names the estimate and the budget, because a caller told only
|
|
3623
|
+
* "too big" cannot tell whether to narrow the type filter, raise the budget,
|
|
3624
|
+
* or stop loading the base whole altogether. Past the budget the answer is
|
|
3625
|
+
* the catalog and then a pack, which is what the refusal says.
|
|
3626
|
+
*
|
|
3627
|
+
* That refusal is the default guardrail. `all` bypasses the budget outright
|
|
3628
|
+
* and always hands back the whole bundle: an explicit, never-accidental
|
|
3629
|
+
* escape hatch for an operator who has the budget to spend, not a wider
|
|
3630
|
+
* default.
|
|
2718
3631
|
*/
|
|
2719
3632
|
async load(bundlePath2, options = {}) {
|
|
2720
3633
|
const budgetTokens = options.budgetTokens ?? DEFAULT_LOAD_BUDGET;
|
|
2721
3634
|
const bundle = await this.list(bundlePath2);
|
|
2722
3635
|
const wanted = options.type ? bundle.filter((record) => record.frontmatter.type === options.type) : bundle;
|
|
2723
|
-
const adjudicated = adjudicate(
|
|
3636
|
+
const adjudicated = adjudicate(
|
|
3637
|
+
wanted,
|
|
3638
|
+
bundle,
|
|
3639
|
+
/* @__PURE__ */ new Date(),
|
|
3640
|
+
await this.detectDrift(wanted, options.repoRoot)
|
|
3641
|
+
);
|
|
2724
3642
|
const records = adjudicated.filter((hit) => hit.standing !== "superseded");
|
|
2725
3643
|
const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(stub);
|
|
2726
3644
|
const approxTokens2 = records.reduce((total, hit) => total + estimateTokens(hit.record), 0) + superseded.reduce((total, entry) => total + estimateStubTokens(entry), 0);
|
|
@@ -2729,7 +3647,12 @@ ${answer}
|
|
|
2729
3647
|
loaded: false,
|
|
2730
3648
|
recordCount: wanted.length,
|
|
2731
3649
|
approxTokens: approxTokens2,
|
|
2732
|
-
budgetTokens
|
|
3650
|
+
budgetTokens,
|
|
3651
|
+
message: refusalMessage({
|
|
3652
|
+
approxTokens: approxTokens2,
|
|
3653
|
+
budgetTokens,
|
|
3654
|
+
type: options.type
|
|
3655
|
+
})
|
|
2733
3656
|
};
|
|
2734
3657
|
}
|
|
2735
3658
|
return {
|
|
@@ -2745,6 +3668,10 @@ ${answer}
|
|
|
2745
3668
|
async trace(bundlePath2, seedId, options = {}) {
|
|
2746
3669
|
return trace(seedId, await this.list(bundlePath2), options);
|
|
2747
3670
|
}
|
|
3671
|
+
/** Every record named in one line each. See `catalog.ts`. */
|
|
3672
|
+
async catalog(bundlePath2, options = {}) {
|
|
3673
|
+
return catalog(await this.list(bundlePath2), options);
|
|
3674
|
+
}
|
|
2748
3675
|
/** A bounded neighbourhood around one record. See `pack.ts`. */
|
|
2749
3676
|
async pack(bundlePath2, rootId, options = {}) {
|
|
2750
3677
|
return pack(await this.list(bundlePath2), rootId, options);
|
|
@@ -2759,7 +3686,7 @@ ${answer}
|
|
|
2759
3686
|
async readIndex(bundlePath2) {
|
|
2760
3687
|
const root = this.root(bundlePath2);
|
|
2761
3688
|
const expected = renderIndex(await this.list(bundlePath2));
|
|
2762
|
-
const stored = await
|
|
3689
|
+
const stored = await readFile4(join4(root, INDEX_FILE), "utf8").catch(
|
|
2763
3690
|
() => null
|
|
2764
3691
|
);
|
|
2765
3692
|
if (indexIsStale(stored, expected)) {
|
|
@@ -2780,7 +3707,7 @@ ${answer}
|
|
|
2780
3707
|
* knows which agent touched what. So a bad line is surfaced and left alone.
|
|
2781
3708
|
*/
|
|
2782
3709
|
async readLog(bundlePath2) {
|
|
2783
|
-
const raw = await
|
|
3710
|
+
const raw = await readFile4(
|
|
2784
3711
|
join4(this.root(bundlePath2), LOG_FILE),
|
|
2785
3712
|
"utf8"
|
|
2786
3713
|
).catch(() => "");
|
|
@@ -2832,14 +3759,14 @@ ${answer}
|
|
|
2832
3759
|
}
|
|
2833
3760
|
async mutate(bundlePath2, conceptId2, change, entry, changeBody = (body) => body) {
|
|
2834
3761
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
2835
|
-
const before = await
|
|
3762
|
+
const before = await readFile4(target, "utf8").catch(() => null);
|
|
2836
3763
|
if (before === null) throw new KbRecordNotFoundError(conceptId2);
|
|
2837
3764
|
const parsed = this.parse(conceptId2, before);
|
|
2838
3765
|
if (!parsed) throw new KbRecordNotFoundError(conceptId2);
|
|
2839
3766
|
const frontmatter = change(parsed.frontmatter);
|
|
2840
3767
|
const body = changeBody(parsed.body);
|
|
2841
3768
|
const contents = stringifyMarkdownWithFrontmatter(body, frontmatter);
|
|
2842
|
-
const witness = await
|
|
3769
|
+
const witness = await readFile4(target, "utf8").catch(() => null);
|
|
2843
3770
|
if (witness === null || digest(witness) !== digest(before)) {
|
|
2844
3771
|
throw new KbWriteConflictError(conceptId2);
|
|
2845
3772
|
}
|
|
@@ -2926,16 +3853,26 @@ ${answer}
|
|
|
2926
3853
|
try {
|
|
2927
3854
|
let existing;
|
|
2928
3855
|
try {
|
|
2929
|
-
existing = await
|
|
3856
|
+
existing = await readFile4(target, "utf8");
|
|
2930
3857
|
} catch (error) {
|
|
2931
3858
|
if (error.code !== "ENOENT") throw error;
|
|
2932
3859
|
existing = null;
|
|
2933
3860
|
}
|
|
2934
3861
|
if (existing === null) {
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
3862
|
+
try {
|
|
3863
|
+
await writeFile3(target, appendUnionMergeLine(""), {
|
|
3864
|
+
encoding: "utf8",
|
|
3865
|
+
flag: "wx"
|
|
3866
|
+
});
|
|
3867
|
+
} catch (error) {
|
|
3868
|
+
if (error.code !== "EEXIST") throw error;
|
|
3869
|
+
this.logger.info?.({
|
|
3870
|
+
operation: "kb.gitattributes.ensure",
|
|
3871
|
+
bundlePath: root,
|
|
3872
|
+
outcome: "exists"
|
|
3873
|
+
});
|
|
3874
|
+
return;
|
|
3875
|
+
}
|
|
2939
3876
|
this.logger.info?.({
|
|
2940
3877
|
operation: "kb.gitattributes.ensure",
|
|
2941
3878
|
bundlePath: root,
|
|
@@ -2989,12 +3926,12 @@ ${answer}
|
|
|
2989
3926
|
};
|
|
2990
3927
|
}
|
|
2991
3928
|
root(bundlePath2) {
|
|
2992
|
-
return
|
|
3929
|
+
return resolve5(bundlePath2);
|
|
2993
3930
|
}
|
|
2994
3931
|
// Concept ids are `<type>.<slug>` and map to a single file directly under the
|
|
2995
3932
|
// bundle root; anything carrying a separator would escape it.
|
|
2996
3933
|
recordPath(bundlePath2, conceptId2) {
|
|
2997
|
-
if (conceptId2.includes(
|
|
3934
|
+
if (conceptId2.includes(sep3) || conceptId2.includes("/")) {
|
|
2998
3935
|
throw new KbInvalidConceptIdError(
|
|
2999
3936
|
"concept id must not contain a path separator",
|
|
3000
3937
|
{ conceptId: conceptId2 }
|
|
@@ -3011,6 +3948,14 @@ function estimateTokens(record) {
|
|
|
3011
3948
|
function estimateStubTokens(entry) {
|
|
3012
3949
|
return Math.ceil(JSON.stringify(entry).length / 4);
|
|
3013
3950
|
}
|
|
3951
|
+
function refusalMessage(refusal) {
|
|
3952
|
+
const scope = refusal.type ? ` of type ${refusal.type}` : "";
|
|
3953
|
+
return [
|
|
3954
|
+
`Refusing to load this base whole: ~${refusal.approxTokens} tokens is past the ${refusal.budgetTokens}-token budget.`,
|
|
3955
|
+
`Call kb_catalog for one line per record${scope} (id, type, title, standing), then kb_pack on the record that matters; kb_query works for a lookup by wording.`,
|
|
3956
|
+
`To load anyway: raise budgetTokens (currently ${refusal.budgetTokens}), or all=true to bypass the budget.`
|
|
3957
|
+
].join(" ");
|
|
3958
|
+
}
|
|
3014
3959
|
function stub(hit) {
|
|
3015
3960
|
return {
|
|
3016
3961
|
conceptId: hit.record.conceptId,
|
|
@@ -3031,7 +3976,7 @@ function normalizeActor(id) {
|
|
|
3031
3976
|
return id.slice(0, colon + 1).toLowerCase() + id.slice(colon + 1);
|
|
3032
3977
|
}
|
|
3033
3978
|
function digest(contents) {
|
|
3034
|
-
return
|
|
3979
|
+
return createHash2("sha256").update(contents).digest("hex");
|
|
3035
3980
|
}
|
|
3036
3981
|
|
|
3037
3982
|
// src/pack.ts
|
|
@@ -3119,7 +4064,7 @@ function typeRank(record) {
|
|
|
3119
4064
|
}
|
|
3120
4065
|
|
|
3121
4066
|
// src/version.ts
|
|
3122
|
-
var VERSION = true ? "0.1.
|
|
4067
|
+
var VERSION = true ? "0.1.11" : "0.0.0-dev";
|
|
3123
4068
|
|
|
3124
4069
|
export {
|
|
3125
4070
|
kbSourceSchema,
|
|
@@ -3145,6 +4090,21 @@ export {
|
|
|
3145
4090
|
composeNoDecisionRecord,
|
|
3146
4091
|
isNoDecisionRecord,
|
|
3147
4092
|
selectDecisions,
|
|
4093
|
+
regexResolver,
|
|
4094
|
+
hashAnchorText,
|
|
4095
|
+
resolveAnchor,
|
|
4096
|
+
anchorFilePath,
|
|
4097
|
+
detectAnchorDrift,
|
|
4098
|
+
Fault,
|
|
4099
|
+
ErrorTypes,
|
|
4100
|
+
BaseError,
|
|
4101
|
+
KbRecordAlreadyExistsError,
|
|
4102
|
+
KbRecordNotFoundError,
|
|
4103
|
+
KbWriteConflictError,
|
|
4104
|
+
KbSelfVerificationError,
|
|
4105
|
+
KbPackBudgetExceededError,
|
|
4106
|
+
KbMissingFlagValueError,
|
|
4107
|
+
KbInvalidConceptIdError,
|
|
3148
4108
|
contextProfileBudgets,
|
|
3149
4109
|
mergedContextBudgets,
|
|
3150
4110
|
KbPinsMalformedError,
|
|
@@ -3161,6 +4121,8 @@ export {
|
|
|
3161
4121
|
unpinBase,
|
|
3162
4122
|
adjudicate,
|
|
3163
4123
|
resolveHeads,
|
|
4124
|
+
catalog,
|
|
4125
|
+
renderCatalogLine,
|
|
3164
4126
|
INDEX_FILE,
|
|
3165
4127
|
renderIndex,
|
|
3166
4128
|
renderIndexLine,
|
|
@@ -3192,15 +4154,6 @@ export {
|
|
|
3192
4154
|
stringifyMarkdownWithFrontmatter,
|
|
3193
4155
|
splitMarkdownFrontmatter,
|
|
3194
4156
|
parseMarkdownWithFrontmatter,
|
|
3195
|
-
Fault,
|
|
3196
|
-
ErrorTypes,
|
|
3197
|
-
BaseError,
|
|
3198
|
-
KbRecordAlreadyExistsError,
|
|
3199
|
-
KbRecordNotFoundError,
|
|
3200
|
-
KbWriteConflictError,
|
|
3201
|
-
KbSelfVerificationError,
|
|
3202
|
-
KbPackBudgetExceededError,
|
|
3203
|
-
KbInvalidConceptIdError,
|
|
3204
4157
|
SEARCH_INDEX_FILE,
|
|
3205
4158
|
searchBase,
|
|
3206
4159
|
resolveHits,
|
|
@@ -3213,4 +4166,4 @@ export {
|
|
|
3213
4166
|
KbStore,
|
|
3214
4167
|
VERSION
|
|
3215
4168
|
};
|
|
3216
|
-
//# sourceMappingURL=chunk-
|
|
4169
|
+
//# sourceMappingURL=chunk-OVRQCQ6P.js.map
|