@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
package/dist/cli-main.cjs
CHANGED
|
@@ -24,7 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
));
|
|
25
25
|
|
|
26
26
|
// src/cli.ts
|
|
27
|
-
var
|
|
27
|
+
var import_node_path8 = require("path");
|
|
28
28
|
|
|
29
29
|
// src/decision-record.ts
|
|
30
30
|
var import_zod3 = require("zod");
|
|
@@ -52,7 +52,31 @@ var kbVerifiedEventSchema = kbActorStampSchema.extend({
|
|
|
52
52
|
});
|
|
53
53
|
var kbAnchorSchema = import_zod.z.object({
|
|
54
54
|
file: import_zod.z.string().min(1),
|
|
55
|
-
symbol: import_zod.z.string().min(1).optional()
|
|
55
|
+
symbol: import_zod.z.string().min(1).optional(),
|
|
56
|
+
/**
|
|
57
|
+
* Which repository the file lives in — a remote URL
|
|
58
|
+
* (`https://github.com/org/name`) or a short name. Absent means the base's
|
|
59
|
+
* own repository, which is what nearly every anchor means.
|
|
60
|
+
*
|
|
61
|
+
* Unvalidated beyond not-blank: one repository has many spellings.
|
|
62
|
+
* Matched after normalisation; see ARCHITECTURE.
|
|
63
|
+
*/
|
|
64
|
+
repo: import_zod.z.string().trim().min(1).optional(),
|
|
65
|
+
/**
|
|
66
|
+
* The git rev the evidence was taken at. Prefer a commit SHA: a branch
|
|
67
|
+
* name is a moving pointer, so an anchor pinned to one says the evidence
|
|
68
|
+
* came from wherever that branch happens to be now, which is not a
|
|
69
|
+
* baseline. Recorded and preserved in v1; ref-pinned reads land with
|
|
70
|
+
* SAA-709.
|
|
71
|
+
*/
|
|
72
|
+
ref: import_zod.z.string().trim().min(1).optional(),
|
|
73
|
+
hash: import_zod.z.string().regex(/^sha256:[0-9a-f]{64}$/, {
|
|
74
|
+
message: "hash must be sha256:<64 hex chars>"
|
|
75
|
+
}).optional(),
|
|
76
|
+
/** ISO 8601 timestamp of the last successful resolution. */
|
|
77
|
+
resolved_at: import_zod.z.string().min(1).optional(),
|
|
78
|
+
/** Line count of the text the hash was taken over. */
|
|
79
|
+
lines: import_zod.z.number().int().positive().optional()
|
|
56
80
|
}).strict();
|
|
57
81
|
var KB_RECORD_TYPES = [
|
|
58
82
|
"fact",
|
|
@@ -319,9 +343,596 @@ function composeNoDecisionRecord(reason, writtenBy, writtenAt) {
|
|
|
319
343
|
);
|
|
320
344
|
}
|
|
321
345
|
|
|
322
|
-
// src/commands/
|
|
346
|
+
// src/commands/anchor-resolve.ts
|
|
323
347
|
var import_zod6 = require("zod");
|
|
324
348
|
|
|
349
|
+
// src/anchor-resolver.ts
|
|
350
|
+
var import_node_child_process = require("child_process");
|
|
351
|
+
var import_node_crypto = require("crypto");
|
|
352
|
+
var import_promises = require("fs/promises");
|
|
353
|
+
var import_node_path = require("path");
|
|
354
|
+
var import_node_util = require("util");
|
|
355
|
+
|
|
356
|
+
// src/concurrency.ts
|
|
357
|
+
var DEFAULT_IO_CONCURRENCY = 16;
|
|
358
|
+
async function mapLimit(items, limit, fn) {
|
|
359
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
360
|
+
throw new RangeError(
|
|
361
|
+
`mapLimit: "limit" must be a positive integer, got ${limit}`
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
const out = new Array(items.length);
|
|
365
|
+
let next = 0;
|
|
366
|
+
let failed = false;
|
|
367
|
+
const runners = Array.from(
|
|
368
|
+
{ length: Math.min(limit, items.length) },
|
|
369
|
+
async () => {
|
|
370
|
+
while (!failed && next < items.length) {
|
|
371
|
+
const at = next++;
|
|
372
|
+
try {
|
|
373
|
+
out[at] = await fn(items[at], at);
|
|
374
|
+
} catch (error) {
|
|
375
|
+
failed = true;
|
|
376
|
+
throw error;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
);
|
|
381
|
+
await Promise.all(runners);
|
|
382
|
+
return out;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// src/anchor-resolver.ts
|
|
386
|
+
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
387
|
+
var MAX_ANCHOR_FILE_BYTES = 1048576;
|
|
388
|
+
var PARENT_SCOPE_LINES = 50;
|
|
389
|
+
var CLEAN_STATE = { blockComment: false, template: false };
|
|
390
|
+
function stripLine(line, state) {
|
|
391
|
+
let out = "";
|
|
392
|
+
let index = 0;
|
|
393
|
+
let { blockComment, template } = state;
|
|
394
|
+
while (index < line.length) {
|
|
395
|
+
const char = line[index];
|
|
396
|
+
const next = line[index + 1];
|
|
397
|
+
if (blockComment) {
|
|
398
|
+
if (char === "*" && next === "/") {
|
|
399
|
+
blockComment = false;
|
|
400
|
+
index += 2;
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
index += 1;
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
if (template) {
|
|
407
|
+
if (char === "\\") {
|
|
408
|
+
index += 2;
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
if (char === "`") template = false;
|
|
412
|
+
index += 1;
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (char === "/" && next === "*") {
|
|
416
|
+
blockComment = true;
|
|
417
|
+
index += 2;
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (char === "/" && next === "/") break;
|
|
421
|
+
if (char === "`") {
|
|
422
|
+
template = true;
|
|
423
|
+
index += 1;
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
if (char === "'" || char === '"') {
|
|
427
|
+
const quote = char;
|
|
428
|
+
index += 1;
|
|
429
|
+
while (index < line.length) {
|
|
430
|
+
if (line[index] === "\\") {
|
|
431
|
+
index += 2;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (line[index] === quote) {
|
|
435
|
+
index += 1;
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
index += 1;
|
|
439
|
+
}
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
out += char;
|
|
443
|
+
index += 1;
|
|
444
|
+
}
|
|
445
|
+
return { code: out, state: { blockComment, template } };
|
|
446
|
+
}
|
|
447
|
+
function span(lines, from, to) {
|
|
448
|
+
return {
|
|
449
|
+
text: lines.slice(from, to + 1).join("\n"),
|
|
450
|
+
startLine: from + 1,
|
|
451
|
+
endLine: to + 1
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
function captureBraceBlock(lines, matchLine) {
|
|
455
|
+
let depth = 0;
|
|
456
|
+
let opened = false;
|
|
457
|
+
let state = CLEAN_STATE;
|
|
458
|
+
for (let index = matchLine; index < lines.length; index++) {
|
|
459
|
+
const stripped = stripLine(lines[index] ?? "", state);
|
|
460
|
+
state = stripped.state;
|
|
461
|
+
for (const char of stripped.code) {
|
|
462
|
+
if (char === "{") {
|
|
463
|
+
depth += 1;
|
|
464
|
+
opened = true;
|
|
465
|
+
} else if (char === "}") {
|
|
466
|
+
depth = Math.max(0, depth - 1);
|
|
467
|
+
} else if (char === ";" && !opened) {
|
|
468
|
+
return span(lines, matchLine, index);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (opened && depth === 0) return span(lines, matchLine, index);
|
|
472
|
+
}
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
var PYTHON_HEADER = /^\s*(?:async\s+)?(?:def|class)\s+[A-Za-z_]\w*\s*[(:]/;
|
|
476
|
+
function captureIndentedBlock(lines, matchLine) {
|
|
477
|
+
const header = lines[matchLine] ?? "";
|
|
478
|
+
const indent = header.length - header.trimStart().length;
|
|
479
|
+
let headerEnd = -1;
|
|
480
|
+
for (let index = matchLine; index < lines.length && index <= matchLine + 20; index++) {
|
|
481
|
+
const code = stripLine(lines[index] ?? "", CLEAN_STATE).code.trimEnd();
|
|
482
|
+
if (code.endsWith(":")) {
|
|
483
|
+
headerEnd = index;
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
if (code.includes(":")) return span(lines, matchLine, index);
|
|
487
|
+
}
|
|
488
|
+
if (headerEnd === -1) return null;
|
|
489
|
+
let end = headerEnd;
|
|
490
|
+
for (let index = headerEnd + 1; index < lines.length; index++) {
|
|
491
|
+
const line = lines[index] ?? "";
|
|
492
|
+
if (line.trim() === "") continue;
|
|
493
|
+
const lineIndent = line.length - line.trimStart().length;
|
|
494
|
+
if (lineIndent <= indent) break;
|
|
495
|
+
end = index;
|
|
496
|
+
}
|
|
497
|
+
return end === headerEnd ? null : span(lines, matchLine, end);
|
|
498
|
+
}
|
|
499
|
+
var TIERS = [
|
|
500
|
+
(name) => new RegExp(
|
|
501
|
+
`(?:function|class|interface|type|enum|const|let|var|def)\\s+${name}\\b`
|
|
502
|
+
),
|
|
503
|
+
(name) => new RegExp(`\\b${name}\\s*[:=]`),
|
|
504
|
+
(name) => new RegExp(`\\b${name}\\s*\\(`),
|
|
505
|
+
(name) => new RegExp(`\\b${name}\\b`)
|
|
506
|
+
];
|
|
507
|
+
var regexResolver = {
|
|
508
|
+
name: "regex",
|
|
509
|
+
resolve(source, symbol) {
|
|
510
|
+
const segments = symbol.split(".");
|
|
511
|
+
const name = segments[segments.length - 1];
|
|
512
|
+
if (!name) return null;
|
|
513
|
+
const parent = segments.length > 1 ? segments[segments.length - 2] : void 0;
|
|
514
|
+
const escaped = escapeRegExp(name);
|
|
515
|
+
const parentPattern = parent ? new RegExp(`\\b${escapeRegExp(parent)}\\b`) : null;
|
|
516
|
+
const lines = source.split("\n");
|
|
517
|
+
for (const tier of TIERS) {
|
|
518
|
+
const pattern = tier(escaped);
|
|
519
|
+
let candidates = lines.map((line, index) => ({ line, index })).filter((entry) => pattern.test(entry.line)).map((entry) => entry.index);
|
|
520
|
+
if (!candidates.length) continue;
|
|
521
|
+
if (parentPattern && candidates.length > 1) {
|
|
522
|
+
const distances = candidates.map(
|
|
523
|
+
(index) => distanceToParent(lines, index, parentPattern)
|
|
524
|
+
);
|
|
525
|
+
const nearest = Math.min(...distances);
|
|
526
|
+
if (Number.isFinite(nearest)) {
|
|
527
|
+
candidates = candidates.filter((_, at) => distances[at] === nearest);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (candidates.length !== 1) return null;
|
|
531
|
+
const matchLine = candidates[0];
|
|
532
|
+
return PYTHON_HEADER.test(lines[matchLine] ?? "") ? captureIndentedBlock(lines, matchLine) : captureBraceBlock(lines, matchLine);
|
|
533
|
+
}
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
function escapeRegExp(value) {
|
|
538
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
539
|
+
}
|
|
540
|
+
function distanceToParent(lines, index, parent) {
|
|
541
|
+
const floor = Math.max(0, index - PARENT_SCOPE_LINES);
|
|
542
|
+
for (let at = index; at >= floor; at--) {
|
|
543
|
+
if (parent.test(lines[at] ?? "")) return index - at;
|
|
544
|
+
}
|
|
545
|
+
return Number.POSITIVE_INFINITY;
|
|
546
|
+
}
|
|
547
|
+
function hashAnchorText(text) {
|
|
548
|
+
return `sha256:${(0, import_node_crypto.createHash)("sha256").update(text.replace(/\r\n/g, "\n")).digest("hex")}`;
|
|
549
|
+
}
|
|
550
|
+
function resolveAnchor(source, anchor, resolver = regexResolver) {
|
|
551
|
+
const normalized = source.replace(/\r\n/g, "\n");
|
|
552
|
+
if (!anchor.symbol) {
|
|
553
|
+
const lines = normalized.split("\n");
|
|
554
|
+
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
555
|
+
return {
|
|
556
|
+
text: normalized,
|
|
557
|
+
startLine: 1,
|
|
558
|
+
endLine: Math.max(1, lines.length)
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
return resolver.resolve(normalized, anchor.symbol);
|
|
562
|
+
}
|
|
563
|
+
function anchorFilePath(repoRoot, file) {
|
|
564
|
+
const path = (0, import_node_path.resolve)(repoRoot, file.replace(/^\.\//, ""));
|
|
565
|
+
const rel = (0, import_node_path.relative)((0, import_node_path.resolve)(repoRoot), path);
|
|
566
|
+
if (rel === "" || rel === ".." || rel.startsWith(`..${import_node_path.sep}`) || (0, import_node_path.isAbsolute)(rel)) {
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
return path;
|
|
570
|
+
}
|
|
571
|
+
function contains(root, path) {
|
|
572
|
+
const rel = (0, import_node_path.relative)(root, path);
|
|
573
|
+
return rel !== "" && rel !== ".." && !rel.startsWith(`..${import_node_path.sep}`) && !(0, import_node_path.isAbsolute)(rel);
|
|
574
|
+
}
|
|
575
|
+
function normalizeRepoUrl(value) {
|
|
576
|
+
let url = value.trim().replace(/^git\+/, "");
|
|
577
|
+
const scp = /^[\w.-]+@([\w.-]+):(.+)$/.exec(url);
|
|
578
|
+
if (scp) url = `https://${scp[1]}/${scp[2]}`;
|
|
579
|
+
url = url.replace(/^ssh:\/\/(?:[^@/]+@)?/, "https://");
|
|
580
|
+
url = trimTrailingSlashes(url);
|
|
581
|
+
if (url.endsWith(".git")) url = url.slice(0, -4);
|
|
582
|
+
return trimTrailingSlashes(url).toLowerCase();
|
|
583
|
+
}
|
|
584
|
+
function trimTrailingSlashes(value) {
|
|
585
|
+
let end = value.length;
|
|
586
|
+
while (end > 0 && value[end - 1] === "/") end -= 1;
|
|
587
|
+
return value.slice(0, end);
|
|
588
|
+
}
|
|
589
|
+
function repoPath(normalized) {
|
|
590
|
+
const withoutScheme = normalized.replace(/^[a-z0-9+.-]+:\/\//, "");
|
|
591
|
+
const segments = withoutScheme.split("/").filter(Boolean);
|
|
592
|
+
return segments.length > 1 ? segments.slice(1).join("/") : "";
|
|
593
|
+
}
|
|
594
|
+
function repoIdentifies(declared, originUrl) {
|
|
595
|
+
if (!originUrl) return false;
|
|
596
|
+
const origin = normalizeRepoUrl(originUrl);
|
|
597
|
+
const want = normalizeRepoUrl(declared);
|
|
598
|
+
if (!want || !origin) return false;
|
|
599
|
+
if (want === origin) return true;
|
|
600
|
+
const path = repoPath(origin);
|
|
601
|
+
if (!path) return false;
|
|
602
|
+
return want === path || want === (path.split("/").pop() ?? "");
|
|
603
|
+
}
|
|
604
|
+
async function repoOriginUrl(repoRoot) {
|
|
605
|
+
try {
|
|
606
|
+
const { stdout } = await execFileAsync(
|
|
607
|
+
"git",
|
|
608
|
+
["-C", repoRoot, "config", "--get", "remote.origin.url"],
|
|
609
|
+
{ timeout: 5e3 }
|
|
610
|
+
);
|
|
611
|
+
return stdout.trim() || null;
|
|
612
|
+
} catch {
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
var LazyOrigin = class {
|
|
617
|
+
constructor(repoRoot) {
|
|
618
|
+
this.repoRoot = repoRoot;
|
|
619
|
+
}
|
|
620
|
+
repoRoot;
|
|
621
|
+
url = null;
|
|
622
|
+
asked = false;
|
|
623
|
+
/** Asks git once, so later `isForeign` calls need no await. */
|
|
624
|
+
async prime() {
|
|
625
|
+
if (this.asked) return;
|
|
626
|
+
this.url = await repoOriginUrl(this.repoRoot);
|
|
627
|
+
this.asked = true;
|
|
628
|
+
}
|
|
629
|
+
/** Only meaningful after `prime`; an unprimed origin identifies nothing. */
|
|
630
|
+
isForeign(anchor) {
|
|
631
|
+
if (!anchor.repo) return false;
|
|
632
|
+
return !repoIdentifies(anchor.repo, this.url);
|
|
633
|
+
}
|
|
634
|
+
async foreign(anchor) {
|
|
635
|
+
if (!anchor.repo) return false;
|
|
636
|
+
await this.prime();
|
|
637
|
+
return this.isForeign(anchor);
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
function errorCode(error) {
|
|
641
|
+
return typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0;
|
|
642
|
+
}
|
|
643
|
+
function anchorFileReader(repoRoot) {
|
|
644
|
+
let rootOnce;
|
|
645
|
+
const realRoot = () => {
|
|
646
|
+
rootOnce ??= (0, import_promises.realpath)((0, import_node_path.resolve)(repoRoot)).catch((error) => {
|
|
647
|
+
rootOnce = void 0;
|
|
648
|
+
throw error;
|
|
649
|
+
});
|
|
650
|
+
return rootOnce;
|
|
651
|
+
};
|
|
652
|
+
return (file) => readAnchorFileWithRoot(repoRoot, file, realRoot);
|
|
653
|
+
}
|
|
654
|
+
async function readAnchorFileWithRoot(repoRoot, file, realRoot) {
|
|
655
|
+
const lexical = anchorFilePath(repoRoot, file);
|
|
656
|
+
if (lexical === null) return { ok: false, reason: "outside-repo" };
|
|
657
|
+
let root;
|
|
658
|
+
let path;
|
|
659
|
+
try {
|
|
660
|
+
root = await realRoot();
|
|
661
|
+
path = await (0, import_promises.realpath)(lexical);
|
|
662
|
+
} catch (error) {
|
|
663
|
+
const code = errorCode(error);
|
|
664
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
665
|
+
return { ok: false, reason: "file-missing" };
|
|
666
|
+
}
|
|
667
|
+
return { ok: false, reason: "file-unreadable" };
|
|
668
|
+
}
|
|
669
|
+
if (!contains(root, path)) return { ok: false, reason: "outside-repo" };
|
|
670
|
+
try {
|
|
671
|
+
const stats = await (0, import_promises.stat)(path);
|
|
672
|
+
if (!stats.isFile()) return { ok: false, reason: "file-unreadable" };
|
|
673
|
+
if (stats.size > MAX_ANCHOR_FILE_BYTES) {
|
|
674
|
+
return { ok: false, reason: "file-too-large" };
|
|
675
|
+
}
|
|
676
|
+
return { ok: true, source: await (0, import_promises.readFile)(path, "utf8") };
|
|
677
|
+
} catch (error) {
|
|
678
|
+
const code = errorCode(error);
|
|
679
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
680
|
+
return { ok: false, reason: "file-missing" };
|
|
681
|
+
}
|
|
682
|
+
return { ok: false, reason: "file-unreadable" };
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
function looksLikeWrongRepoRoot(drift) {
|
|
686
|
+
let checked = 0;
|
|
687
|
+
for (const entries of drift.values()) {
|
|
688
|
+
for (const entry of entries) {
|
|
689
|
+
if (entry.reason === "foreign-repo") continue;
|
|
690
|
+
checked += 1;
|
|
691
|
+
if (entry.state !== "unresolved" || entry.reason !== "file-missing") {
|
|
692
|
+
return false;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return checked > 0;
|
|
697
|
+
}
|
|
698
|
+
async function readAnchorFiles(files, read, concurrency = DEFAULT_IO_CONCURRENCY) {
|
|
699
|
+
if (!Number.isInteger(concurrency) || concurrency < 1) {
|
|
700
|
+
throw new RangeError(
|
|
701
|
+
`readAnchorFiles: option "concurrency" must be a positive integer, got ${concurrency}`
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
const wanted = [...new Set(files)];
|
|
705
|
+
const results = await mapLimit(wanted, concurrency, async (file) => {
|
|
706
|
+
try {
|
|
707
|
+
return await read(file);
|
|
708
|
+
} catch {
|
|
709
|
+
return { ok: false, reason: "file-unreadable" };
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
return new Map(wanted.map((file, at) => [file, results[at]]));
|
|
713
|
+
}
|
|
714
|
+
async function detectAnchorDrift(records, options = {}) {
|
|
715
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
716
|
+
const resolver = options.resolver ?? regexResolver;
|
|
717
|
+
const origin = new LazyOrigin(repoRoot);
|
|
718
|
+
const planned = /* @__PURE__ */ new Map();
|
|
719
|
+
let declaresRepo = false;
|
|
720
|
+
for (const record of records) {
|
|
721
|
+
const anchors = (record.frontmatter.strauss_anchors ?? []).filter(
|
|
722
|
+
(anchor) => anchor.hash
|
|
723
|
+
);
|
|
724
|
+
if (!anchors.length) continue;
|
|
725
|
+
if (anchors.some((anchor) => anchor.repo)) declaresRepo = true;
|
|
726
|
+
planned.set(
|
|
727
|
+
record.conceptId,
|
|
728
|
+
anchors.map((anchor) => ({ anchor, foreign: false }))
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
if (declaresRepo) {
|
|
732
|
+
await origin.prime();
|
|
733
|
+
for (const entries of planned.values()) {
|
|
734
|
+
for (const entry of entries)
|
|
735
|
+
entry.foreign = origin.isForeign(entry.anchor);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
const files = [];
|
|
739
|
+
for (const entries of planned.values()) {
|
|
740
|
+
for (const entry of entries) {
|
|
741
|
+
if (!entry.foreign) files.push(entry.anchor.file);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
const reads = await readAnchorFiles(
|
|
745
|
+
files,
|
|
746
|
+
options.reader ?? anchorFileReader(repoRoot),
|
|
747
|
+
options.concurrency ?? DEFAULT_IO_CONCURRENCY
|
|
748
|
+
);
|
|
749
|
+
const drift = /* @__PURE__ */ new Map();
|
|
750
|
+
for (const record of records) {
|
|
751
|
+
const entries = [];
|
|
752
|
+
for (const { anchor, foreign } of planned.get(record.conceptId) ?? []) {
|
|
753
|
+
const base = {
|
|
754
|
+
file: anchor.file,
|
|
755
|
+
...anchor.symbol ? { symbol: anchor.symbol } : {},
|
|
756
|
+
storedHash: anchor.hash
|
|
757
|
+
};
|
|
758
|
+
if (foreign) {
|
|
759
|
+
entries.push({
|
|
760
|
+
...base,
|
|
761
|
+
state: "unresolved",
|
|
762
|
+
diffSize: null,
|
|
763
|
+
reason: "foreign-repo"
|
|
764
|
+
});
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
const read = reads.get(anchor.file);
|
|
768
|
+
if (!read.ok) {
|
|
769
|
+
entries.push({
|
|
770
|
+
...base,
|
|
771
|
+
state: "unresolved",
|
|
772
|
+
diffSize: null,
|
|
773
|
+
reason: read.reason
|
|
774
|
+
});
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
const resolved = resolveAnchor(read.source, anchor, resolver);
|
|
778
|
+
if (!resolved) {
|
|
779
|
+
entries.push({
|
|
780
|
+
...base,
|
|
781
|
+
state: "unresolved",
|
|
782
|
+
diffSize: null,
|
|
783
|
+
reason: "symbol-not-found"
|
|
784
|
+
});
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
const currentHash = hashAnchorText(resolved.text);
|
|
788
|
+
const currentLines = resolved.endLine - resolved.startLine + 1;
|
|
789
|
+
entries.push({
|
|
790
|
+
...base,
|
|
791
|
+
state: currentHash === anchor.hash ? "match" : "drifted",
|
|
792
|
+
currentHash,
|
|
793
|
+
diffSize: anchor.lines === void 0 ? null : Math.abs(currentLines - anchor.lines)
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
if (entries.length) drift.set(record.conceptId, entries);
|
|
797
|
+
}
|
|
798
|
+
return drift;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// src/errors.ts
|
|
802
|
+
var BaseError = class extends Error {
|
|
803
|
+
code;
|
|
804
|
+
errorType;
|
|
805
|
+
fault;
|
|
806
|
+
retriable;
|
|
807
|
+
reportToUser;
|
|
808
|
+
details;
|
|
809
|
+
constructor(props) {
|
|
810
|
+
super(props.message);
|
|
811
|
+
this.name = props.name ?? this.constructor.name;
|
|
812
|
+
this.code = props.code ?? 500;
|
|
813
|
+
this.errorType = props.errorType;
|
|
814
|
+
this.fault = props.fault;
|
|
815
|
+
this.retriable = props.retriable ?? true;
|
|
816
|
+
this.reportToUser = props.reportToUser ?? false;
|
|
817
|
+
this.details = props.details;
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
|
|
821
|
+
// src/kb-errors.ts
|
|
822
|
+
var KbRecordAlreadyExistsError = class extends BaseError {
|
|
823
|
+
constructor(conceptId2) {
|
|
824
|
+
super({
|
|
825
|
+
message: `kb: ${conceptId2} already exists \u2014 choose a more specific slug, or write with overwrite`,
|
|
826
|
+
errorType: "KbRecordAlreadyExists" /* KbRecordAlreadyExists */,
|
|
827
|
+
code: 409,
|
|
828
|
+
fault: "User" /* User */,
|
|
829
|
+
retriable: false,
|
|
830
|
+
reportToUser: true,
|
|
831
|
+
details: { conceptId: conceptId2, action: "refused" }
|
|
832
|
+
});
|
|
833
|
+
this.conceptId = conceptId2;
|
|
834
|
+
}
|
|
835
|
+
conceptId;
|
|
836
|
+
};
|
|
837
|
+
var KbRecordNotFoundError = class extends BaseError {
|
|
838
|
+
constructor(conceptId2) {
|
|
839
|
+
super({
|
|
840
|
+
message: `kb: ${conceptId2} does not exist`,
|
|
841
|
+
errorType: "KbRecordNotFound" /* KbRecordNotFound */,
|
|
842
|
+
code: 404,
|
|
843
|
+
fault: "User" /* User */,
|
|
844
|
+
retriable: false,
|
|
845
|
+
reportToUser: true,
|
|
846
|
+
details: { conceptId: conceptId2 }
|
|
847
|
+
});
|
|
848
|
+
this.conceptId = conceptId2;
|
|
849
|
+
}
|
|
850
|
+
conceptId;
|
|
851
|
+
};
|
|
852
|
+
var KbWriteConflictError = class extends BaseError {
|
|
853
|
+
constructor(conceptId2) {
|
|
854
|
+
super({
|
|
855
|
+
message: `kb: ${conceptId2} changed while it was being updated \u2014 re-read and retry`,
|
|
856
|
+
errorType: "KbWriteConflict" /* KbWriteConflict */,
|
|
857
|
+
code: 409,
|
|
858
|
+
fault: "System" /* System */,
|
|
859
|
+
retriable: true,
|
|
860
|
+
reportToUser: true,
|
|
861
|
+
details: { conceptId: conceptId2 }
|
|
862
|
+
});
|
|
863
|
+
this.conceptId = conceptId2;
|
|
864
|
+
}
|
|
865
|
+
conceptId;
|
|
866
|
+
};
|
|
867
|
+
var KbSelfVerificationError = class extends BaseError {
|
|
868
|
+
constructor(conceptId2, actor, generatedBy) {
|
|
869
|
+
super({
|
|
870
|
+
message: `kb: ${conceptId2} was generated by ${generatedBy}, and a record's generator cannot verify it \u2014 only a human or a different actor can`,
|
|
871
|
+
errorType: "KbSelfVerification" /* KbSelfVerification */,
|
|
872
|
+
code: 400,
|
|
873
|
+
fault: "User" /* User */,
|
|
874
|
+
retriable: false,
|
|
875
|
+
reportToUser: true,
|
|
876
|
+
details: { conceptId: conceptId2, actor, generatedBy, action: "refused" }
|
|
877
|
+
});
|
|
878
|
+
this.conceptId = conceptId2;
|
|
879
|
+
this.actor = actor;
|
|
880
|
+
this.generatedBy = generatedBy;
|
|
881
|
+
}
|
|
882
|
+
conceptId;
|
|
883
|
+
actor;
|
|
884
|
+
generatedBy;
|
|
885
|
+
};
|
|
886
|
+
var KbPackBudgetExceededError = class extends BaseError {
|
|
887
|
+
constructor(recordCount, approxTokens2, budgetTokens, excluded) {
|
|
888
|
+
super({
|
|
889
|
+
message: `kb: a pack of ${recordCount} records is ~${approxTokens2} tokens against a budget of ${budgetTokens} \u2014 lower hops or maxNodes, or raise the budget`,
|
|
890
|
+
errorType: "KbPackBudgetExceeded" /* KbPackBudgetExceeded */,
|
|
891
|
+
code: 400,
|
|
892
|
+
fault: "User" /* User */,
|
|
893
|
+
retriable: false,
|
|
894
|
+
reportToUser: true,
|
|
895
|
+
details: { recordCount, approxTokens: approxTokens2, budgetTokens, excluded }
|
|
896
|
+
});
|
|
897
|
+
this.recordCount = recordCount;
|
|
898
|
+
this.approxTokens = approxTokens2;
|
|
899
|
+
this.budgetTokens = budgetTokens;
|
|
900
|
+
this.excluded = excluded;
|
|
901
|
+
}
|
|
902
|
+
recordCount;
|
|
903
|
+
approxTokens;
|
|
904
|
+
budgetTokens;
|
|
905
|
+
excluded;
|
|
906
|
+
};
|
|
907
|
+
var KbMissingFlagValueError = class extends BaseError {
|
|
908
|
+
constructor(flag) {
|
|
909
|
+
super({
|
|
910
|
+
message: `kb: ${flag} needs a value \u2014 pass ${flag} <value> or ${flag}=<value>`,
|
|
911
|
+
errorType: "KbMissingFlagValue" /* KbMissingFlagValue */,
|
|
912
|
+
code: 400,
|
|
913
|
+
fault: "User" /* User */,
|
|
914
|
+
retriable: false,
|
|
915
|
+
reportToUser: true,
|
|
916
|
+
details: { flag }
|
|
917
|
+
});
|
|
918
|
+
this.flag = flag;
|
|
919
|
+
}
|
|
920
|
+
flag;
|
|
921
|
+
};
|
|
922
|
+
var KbInvalidConceptIdError = class extends BaseError {
|
|
923
|
+
constructor(message, details) {
|
|
924
|
+
super({
|
|
925
|
+
message: `kb: ${message}`,
|
|
926
|
+
errorType: "KbInvalidConceptId" /* KbInvalidConceptId */,
|
|
927
|
+
code: 400,
|
|
928
|
+
fault: "User" /* User */,
|
|
929
|
+
retriable: false,
|
|
930
|
+
reportToUser: true,
|
|
931
|
+
details
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
|
|
325
936
|
// src/kb-pins/budgets.ts
|
|
326
937
|
function asBudgets(value) {
|
|
327
938
|
if (value === null || typeof value !== "object") return {};
|
|
@@ -371,18 +982,18 @@ var KbBaseFrozenError = class extends Error {
|
|
|
371
982
|
};
|
|
372
983
|
|
|
373
984
|
// src/kb-pins/frozen.ts
|
|
374
|
-
var
|
|
985
|
+
var import_node_path4 = require("path");
|
|
375
986
|
|
|
376
987
|
// src/kb-pins/layers.ts
|
|
377
|
-
var
|
|
988
|
+
var import_promises2 = require("fs/promises");
|
|
378
989
|
var import_node_os = require("os");
|
|
379
|
-
var
|
|
990
|
+
var import_node_path3 = require("path");
|
|
380
991
|
|
|
381
992
|
// src/kb-pins/model.ts
|
|
382
|
-
var
|
|
993
|
+
var import_node_path2 = require("path");
|
|
383
994
|
var import_zod4 = require("zod");
|
|
384
|
-
var PINS_FILE = (0,
|
|
385
|
-
var PINS_LOCAL_FILE = (0,
|
|
995
|
+
var PINS_FILE = (0, import_node_path2.join)(".strauss", "kb-pins.json");
|
|
996
|
+
var PINS_LOCAL_FILE = (0, import_node_path2.join)(".strauss", "kb-pins.local.json");
|
|
386
997
|
var PIN_LAYERS = ["project", "local", "user"];
|
|
387
998
|
var pinSchema = import_zod4.z.object({
|
|
388
999
|
/** Relative to the manifest's root, so the file is committable. */
|
|
@@ -432,10 +1043,10 @@ function userRoot() {
|
|
|
432
1043
|
return process.env.STRAUSS_KB_USER_ROOT || (0, import_node_os.homedir)();
|
|
433
1044
|
}
|
|
434
1045
|
function layerRoot(workspaceDir, layer) {
|
|
435
|
-
return layer === "user" ? userRoot() : (0,
|
|
1046
|
+
return layer === "user" ? userRoot() : (0, import_node_path3.resolve)(workspaceDir);
|
|
436
1047
|
}
|
|
437
1048
|
function layerFile(workspaceDir, layer) {
|
|
438
|
-
return (0,
|
|
1049
|
+
return (0, import_node_path3.join)(
|
|
439
1050
|
layerRoot(workspaceDir, layer),
|
|
440
1051
|
layer === "local" ? PINS_LOCAL_FILE : PINS_FILE
|
|
441
1052
|
);
|
|
@@ -444,7 +1055,7 @@ async function readPinsLayer(workspaceDir, layer) {
|
|
|
444
1055
|
const file = layerFile(workspaceDir, layer);
|
|
445
1056
|
let raw;
|
|
446
1057
|
try {
|
|
447
|
-
raw = await (0,
|
|
1058
|
+
raw = await (0, import_promises2.readFile)(file, "utf8");
|
|
448
1059
|
} catch {
|
|
449
1060
|
return { pins: [] };
|
|
450
1061
|
}
|
|
@@ -468,16 +1079,16 @@ async function readPinsLayer(workspaceDir, layer) {
|
|
|
468
1079
|
}
|
|
469
1080
|
async function writePinsLayer(workspaceDir, layer, manifest) {
|
|
470
1081
|
const file = layerFile(workspaceDir, layer);
|
|
471
|
-
await (0,
|
|
472
|
-
await (0,
|
|
1082
|
+
await (0, import_promises2.mkdir)((0, import_node_path3.dirname)(file), { recursive: true });
|
|
1083
|
+
await (0, import_promises2.writeFile)(file, `${JSON.stringify(manifest, null, 2)}
|
|
473
1084
|
`, "utf8");
|
|
474
1085
|
}
|
|
475
1086
|
function resolvePinPath(rootDir, path) {
|
|
476
|
-
return (0,
|
|
1087
|
+
return (0, import_node_path3.isAbsolute)(path) ? (0, import_node_path3.resolve)(path) : (0, import_node_path3.resolve)(rootDir, path.split("/").join(import_node_path3.sep));
|
|
477
1088
|
}
|
|
478
1089
|
function storablePath(rootDir, bundlePath2) {
|
|
479
|
-
const rel = (0,
|
|
480
|
-
return (rel === "" ? "." : rel).split(
|
|
1090
|
+
const rel = (0, import_node_path3.relative)((0, import_node_path3.resolve)(rootDir), (0, import_node_path3.resolve)(bundlePath2));
|
|
1091
|
+
return (rel === "" ? "." : rel).split(import_node_path3.sep).join("/");
|
|
481
1092
|
}
|
|
482
1093
|
async function readMergedPins(workspaceDir) {
|
|
483
1094
|
const manifests = {};
|
|
@@ -505,7 +1116,7 @@ async function readMergedPins(workspaceDir) {
|
|
|
505
1116
|
// src/kb-pins/frozen.ts
|
|
506
1117
|
async function assertBaseNotFrozen(workspaceDir, bundlePath2) {
|
|
507
1118
|
const merged = await readMergedPins(workspaceDir);
|
|
508
|
-
const absolute = (0,
|
|
1119
|
+
const absolute = (0, import_node_path4.resolve)(bundlePath2);
|
|
509
1120
|
const pin = merged.pins.find((entry) => entry.absolutePath === absolute);
|
|
510
1121
|
if (pin?.frozen === true) {
|
|
511
1122
|
throw new KbBaseFrozenError(pin.path, pin.layer);
|
|
@@ -590,7 +1201,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
|
|
|
590
1201
|
}
|
|
591
1202
|
|
|
592
1203
|
// src/kb-pins/unpin.ts
|
|
593
|
-
var
|
|
1204
|
+
var import_node_path5 = require("path");
|
|
594
1205
|
async function unpinBase(workspaceDir, bundlePath2) {
|
|
595
1206
|
const layers = [];
|
|
596
1207
|
for (const layer of PIN_LAYERS) {
|
|
@@ -611,7 +1222,7 @@ async function unpinBase(workspaceDir, bundlePath2) {
|
|
|
611
1222
|
}
|
|
612
1223
|
}
|
|
613
1224
|
return {
|
|
614
|
-
path: storablePath((0,
|
|
1225
|
+
path: storablePath((0, import_node_path5.resolve)(workspaceDir), bundlePath2),
|
|
615
1226
|
removed: layers.length > 0,
|
|
616
1227
|
layers
|
|
617
1228
|
};
|
|
@@ -621,21 +1232,197 @@ async function unpinBase(workspaceDir, bundlePath2) {
|
|
|
621
1232
|
var import_zod5 = require("zod");
|
|
622
1233
|
var bundlePath = import_zod5.z.string().min(1).describe("Absolute path to the knowledge base directory.");
|
|
623
1234
|
var conceptId = import_zod5.z.string().min(1).describe("e.g. decision.cursor-v2");
|
|
1235
|
+
var REPO_ROOT = import_zod5.z.string().min(1).optional().describe(
|
|
1236
|
+
"Where the anchored source lives, for the drift check. Defaults to the working directory."
|
|
1237
|
+
);
|
|
624
1238
|
function define(command) {
|
|
625
1239
|
return command;
|
|
626
1240
|
}
|
|
627
1241
|
function argvFlag(argv, name) {
|
|
1242
|
+
const joined = argv.find((arg) => arg.startsWith(`${name}=`));
|
|
1243
|
+
if (joined !== void 0) {
|
|
1244
|
+
const value2 = joined.slice(name.length + 1);
|
|
1245
|
+
if (!value2) throw new KbMissingFlagValueError(name);
|
|
1246
|
+
return value2;
|
|
1247
|
+
}
|
|
628
1248
|
const at = argv.indexOf(name);
|
|
629
|
-
|
|
1249
|
+
if (at === -1) return void 0;
|
|
1250
|
+
const value = argv[at + 1];
|
|
1251
|
+
if (value === void 0 || value.startsWith("--")) {
|
|
1252
|
+
throw new KbMissingFlagValueError(name);
|
|
1253
|
+
}
|
|
1254
|
+
return value;
|
|
630
1255
|
}
|
|
631
1256
|
|
|
1257
|
+
// src/commands/anchor-resolve.ts
|
|
1258
|
+
var anchorResolveCommand = define({
|
|
1259
|
+
name: "anchor-resolve",
|
|
1260
|
+
tool: "kb_anchor_resolve",
|
|
1261
|
+
usage: "anchor-resolve <concept-id> [--repo-root <path>] [--rebaseline] [--restamp]",
|
|
1262
|
+
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.",
|
|
1263
|
+
input: import_zod6.z.object({
|
|
1264
|
+
bundlePath,
|
|
1265
|
+
conceptId,
|
|
1266
|
+
repoRoot: import_zod6.z.string().min(1).optional(),
|
|
1267
|
+
rebaseline: import_zod6.z.boolean().optional().describe(
|
|
1268
|
+
"Accept the current code as the new baseline for anchors that drifted."
|
|
1269
|
+
),
|
|
1270
|
+
restamp: import_zod6.z.boolean().optional().describe(
|
|
1271
|
+
"Refresh `resolved_at` on anchors that already match. Off by default, so a green run writes nothing."
|
|
1272
|
+
)
|
|
1273
|
+
}),
|
|
1274
|
+
fromArgv: (argv, path) => ({
|
|
1275
|
+
bundlePath: path,
|
|
1276
|
+
conceptId: argv[1],
|
|
1277
|
+
repoRoot: argvFlag(argv, "--repo-root"),
|
|
1278
|
+
rebaseline: argv.includes("--rebaseline"),
|
|
1279
|
+
restamp: argv.includes("--restamp")
|
|
1280
|
+
}),
|
|
1281
|
+
run: async ({ store, actor, now }, { bundlePath: path, conceptId: id, repoRoot, rebaseline, restamp }) => {
|
|
1282
|
+
const root = repoRoot ?? process.cwd();
|
|
1283
|
+
const record = await store.read(path, id);
|
|
1284
|
+
if (!record) throw new KbRecordNotFoundError(id);
|
|
1285
|
+
const anchors = record.frontmatter.strauss_anchors ?? [];
|
|
1286
|
+
if (!anchors.length) {
|
|
1287
|
+
return {
|
|
1288
|
+
conceptId: id,
|
|
1289
|
+
results: [],
|
|
1290
|
+
verified: false,
|
|
1291
|
+
note: "record has no anchors"
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
const results = [];
|
|
1295
|
+
const updated = [];
|
|
1296
|
+
const origin = new LazyOrigin(root);
|
|
1297
|
+
let dirty = false;
|
|
1298
|
+
if (anchors.some((anchor) => anchor.repo)) await origin.prime();
|
|
1299
|
+
const foreign = new Map(
|
|
1300
|
+
anchors.map((anchor) => [anchor, origin.isForeign(anchor)])
|
|
1301
|
+
);
|
|
1302
|
+
const reads = await readAnchorFiles(
|
|
1303
|
+
anchors.filter((anchor) => !foreign.get(anchor)).map((anchor) => anchor.file),
|
|
1304
|
+
anchorFileReader(root)
|
|
1305
|
+
);
|
|
1306
|
+
for (const anchor of anchors) {
|
|
1307
|
+
const base = {
|
|
1308
|
+
file: anchor.file,
|
|
1309
|
+
...anchor.symbol ? { symbol: anchor.symbol } : {},
|
|
1310
|
+
// Carried onto unresolved findings too: an anchor that once hashed
|
|
1311
|
+
// and now resolves to nothing is a broken anchor, and the exit code
|
|
1312
|
+
// has to be able to tell it from one nobody ever stamped.
|
|
1313
|
+
...anchor.hash ? { storedHash: anchor.hash } : {}
|
|
1314
|
+
};
|
|
1315
|
+
if (foreign.get(anchor)) {
|
|
1316
|
+
results.push({ ...base, state: "unresolved", reason: "foreign-repo" });
|
|
1317
|
+
updated.push(anchor);
|
|
1318
|
+
continue;
|
|
1319
|
+
}
|
|
1320
|
+
const fileRead = reads.get(anchor.file);
|
|
1321
|
+
if (!fileRead.ok) {
|
|
1322
|
+
results.push({ ...base, state: "unresolved", reason: fileRead.reason });
|
|
1323
|
+
updated.push(anchor);
|
|
1324
|
+
continue;
|
|
1325
|
+
}
|
|
1326
|
+
const resolved = resolveAnchor(fileRead.source, anchor);
|
|
1327
|
+
if (!resolved) {
|
|
1328
|
+
results.push({
|
|
1329
|
+
...base,
|
|
1330
|
+
state: "unresolved",
|
|
1331
|
+
reason: "symbol-not-found"
|
|
1332
|
+
});
|
|
1333
|
+
updated.push(anchor);
|
|
1334
|
+
continue;
|
|
1335
|
+
}
|
|
1336
|
+
const currentHash = hashAnchorText(resolved.text);
|
|
1337
|
+
const currentLines = resolved.endLine - resolved.startLine + 1;
|
|
1338
|
+
const stamped = {
|
|
1339
|
+
...anchor,
|
|
1340
|
+
hash: currentHash,
|
|
1341
|
+
lines: currentLines,
|
|
1342
|
+
resolved_at: now()
|
|
1343
|
+
};
|
|
1344
|
+
if (!anchor.hash) {
|
|
1345
|
+
results.push({ ...base, state: "stamped", currentHash });
|
|
1346
|
+
updated.push(stamped);
|
|
1347
|
+
dirty = true;
|
|
1348
|
+
} else if (anchor.hash === currentHash) {
|
|
1349
|
+
results.push({
|
|
1350
|
+
...base,
|
|
1351
|
+
state: "match",
|
|
1352
|
+
currentHash
|
|
1353
|
+
});
|
|
1354
|
+
const refresh = restamp || anchor.resolved_at === void 0;
|
|
1355
|
+
updated.push(refresh ? { ...anchor, resolved_at: now() } : anchor);
|
|
1356
|
+
if (refresh) dirty = true;
|
|
1357
|
+
} else {
|
|
1358
|
+
results.push({
|
|
1359
|
+
...base,
|
|
1360
|
+
state: "drifted",
|
|
1361
|
+
currentHash,
|
|
1362
|
+
diffSize: anchor.lines === void 0 ? null : Math.abs(currentLines - anchor.lines),
|
|
1363
|
+
...rebaseline ? { rebaselined: true } : {}
|
|
1364
|
+
});
|
|
1365
|
+
updated.push(rebaseline ? stamped : anchor);
|
|
1366
|
+
if (rebaseline) dirty = true;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
let frozen = false;
|
|
1370
|
+
if (dirty) {
|
|
1371
|
+
try {
|
|
1372
|
+
await assertBaseNotFrozen(process.cwd(), path);
|
|
1373
|
+
} catch (error) {
|
|
1374
|
+
if (!(error instanceof KbBaseFrozenError)) throw error;
|
|
1375
|
+
frozen = true;
|
|
1376
|
+
}
|
|
1377
|
+
if (!frozen) await store.updateAnchors(path, id, updated, actor);
|
|
1378
|
+
}
|
|
1379
|
+
const frozenNote = frozen ? { frozen: true, note: "base is frozen: nothing was stamped" } : {};
|
|
1380
|
+
const checked = results.filter((entry) => entry.reason !== "foreign-repo");
|
|
1381
|
+
const skipped = results.length - checked.length;
|
|
1382
|
+
const matches2 = checked.filter((entry) => entry.state === "match").length;
|
|
1383
|
+
const clean = checked.length > 0 && checked.every((entry) => entry.state === "match");
|
|
1384
|
+
if (clean) {
|
|
1385
|
+
try {
|
|
1386
|
+
await store.verify(
|
|
1387
|
+
path,
|
|
1388
|
+
id,
|
|
1389
|
+
`anchor-resolve: ${matches2}/${checked.length} anchors match${skipped ? `, ${skipped} in another repo` : ""} (regex resolver)`,
|
|
1390
|
+
actor,
|
|
1391
|
+
now()
|
|
1392
|
+
);
|
|
1393
|
+
} catch (error) {
|
|
1394
|
+
if (!(error instanceof KbSelfVerificationError)) throw error;
|
|
1395
|
+
return {
|
|
1396
|
+
conceptId: id,
|
|
1397
|
+
results,
|
|
1398
|
+
verified: false,
|
|
1399
|
+
verifyRefused: "self-verification",
|
|
1400
|
+
...frozenNote
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
return { conceptId: id, results, verified: true, ...frozenNote };
|
|
1404
|
+
}
|
|
1405
|
+
return { conceptId: id, results, verified: false, ...frozenNote };
|
|
1406
|
+
},
|
|
1407
|
+
// A stored hash that no longer resolves is a broken anchor, not an absence:
|
|
1408
|
+
// the file was deleted or the symbol renamed, and exiting zero on it would
|
|
1409
|
+
// let the one edit that destroys an anchor pass the gate that exists to
|
|
1410
|
+
// catch it. An anchor nobody ever stamped is still just unstamped, and one
|
|
1411
|
+
// belonging to another repository was never this run's to check — failing CI
|
|
1412
|
+
// on either would gate on work this command did not do.
|
|
1413
|
+
failsWhen: (result) => result.results.some(
|
|
1414
|
+
(entry) => entry.state === "drifted" || entry.state === "unresolved" && entry.storedHash !== void 0 && entry.reason !== "foreign-repo"
|
|
1415
|
+
)
|
|
1416
|
+
});
|
|
1417
|
+
|
|
632
1418
|
// src/commands/answer.ts
|
|
1419
|
+
var import_zod7 = require("zod");
|
|
633
1420
|
var answerCommand = define({
|
|
634
1421
|
name: "answer",
|
|
635
1422
|
tool: "kb_answer",
|
|
636
1423
|
usage: "answer <concept-id> <answer...>",
|
|
637
1424
|
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.",
|
|
638
|
-
input:
|
|
1425
|
+
input: import_zod7.z.object({ bundlePath, conceptId, answer: import_zod7.z.string().min(1) }),
|
|
639
1426
|
fromArgv: (argv, path) => ({
|
|
640
1427
|
bundlePath: path,
|
|
641
1428
|
conceptId: argv[1],
|
|
@@ -648,11 +1435,8 @@ var answerCommand = define({
|
|
|
648
1435
|
}
|
|
649
1436
|
});
|
|
650
1437
|
|
|
651
|
-
// src/commands/
|
|
652
|
-
var
|
|
653
|
-
|
|
654
|
-
// src/kb-context.ts
|
|
655
|
-
var import_promises2 = require("fs/promises");
|
|
1438
|
+
// src/commands/catalog.ts
|
|
1439
|
+
var import_zod8 = require("zod");
|
|
656
1440
|
|
|
657
1441
|
// src/adjudicate.ts
|
|
658
1442
|
var STANDING = {
|
|
@@ -664,7 +1448,7 @@ var STANDING = {
|
|
|
664
1448
|
rejected: "rejected",
|
|
665
1449
|
superseded: "superseded"
|
|
666
1450
|
};
|
|
667
|
-
function adjudicate(hits, bundle, now = /* @__PURE__ */ new Date()) {
|
|
1451
|
+
function adjudicate(hits, bundle, now = /* @__PURE__ */ new Date(), anchorDrift) {
|
|
668
1452
|
const byId = new Map(bundle.map((record) => [record.conceptId, record]));
|
|
669
1453
|
return hits.map((record) => {
|
|
670
1454
|
const status = record.frontmatter.strauss_status;
|
|
@@ -694,6 +1478,20 @@ function adjudicate(hits, bundle, now = /* @__PURE__ */ new Date()) {
|
|
|
694
1478
|
if (!record.frontmatter.verified?.length) {
|
|
695
1479
|
warnings.push({ kind: "unverified" });
|
|
696
1480
|
}
|
|
1481
|
+
const moved = (anchorDrift?.get(record.conceptId) ?? []).filter(
|
|
1482
|
+
(entry) => entry.state !== "match" && entry.reason !== "foreign-repo"
|
|
1483
|
+
);
|
|
1484
|
+
if (moved.length) {
|
|
1485
|
+
warnings.push({
|
|
1486
|
+
kind: "drifted",
|
|
1487
|
+
anchors: moved.map(({ file, symbol, diffSize, reason }) => ({
|
|
1488
|
+
file,
|
|
1489
|
+
...symbol !== void 0 ? { symbol } : {},
|
|
1490
|
+
diffSize,
|
|
1491
|
+
...reason !== void 0 ? { reason } : {}
|
|
1492
|
+
}))
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
697
1495
|
return { record, standing: STANDING[status], heads, warnings };
|
|
698
1496
|
});
|
|
699
1497
|
}
|
|
@@ -743,8 +1541,122 @@ function successors(record, byId) {
|
|
|
743
1541
|
if (found) records.push(found);
|
|
744
1542
|
else missing.push(id);
|
|
745
1543
|
}
|
|
746
|
-
return { records, missing };
|
|
1544
|
+
return { records, missing };
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// src/catalog.ts
|
|
1548
|
+
var EMPTY_STANDINGS = {
|
|
1549
|
+
current: 0,
|
|
1550
|
+
superseded: 0,
|
|
1551
|
+
rejected: 0,
|
|
1552
|
+
unsettled: 0,
|
|
1553
|
+
open: 0
|
|
1554
|
+
};
|
|
1555
|
+
function catalog(bundle, options = {}) {
|
|
1556
|
+
const wanted = options.type ? bundle.filter((record) => record.frontmatter.type === options.type) : bundle;
|
|
1557
|
+
const entries = adjudicate(wanted, bundle, options.now ?? /* @__PURE__ */ new Date()).map((hit) => ({
|
|
1558
|
+
conceptId: hit.record.conceptId,
|
|
1559
|
+
type: hit.record.frontmatter.type,
|
|
1560
|
+
title: hit.record.frontmatter.title ?? null,
|
|
1561
|
+
standing: hit.standing,
|
|
1562
|
+
supersededBy: hit.heads.map((head) => head.conceptId),
|
|
1563
|
+
stale: hit.warnings.some((warning) => warning.kind === "stale")
|
|
1564
|
+
})).sort(byTypeThenTitle);
|
|
1565
|
+
const standings = { ...EMPTY_STANDINGS };
|
|
1566
|
+
for (const entry of entries) standings[entry.standing] += 1;
|
|
1567
|
+
return {
|
|
1568
|
+
entries,
|
|
1569
|
+
recordCount: entries.length,
|
|
1570
|
+
standings,
|
|
1571
|
+
currentCount: standings.current,
|
|
1572
|
+
supersededCount: standings.superseded,
|
|
1573
|
+
staleCount: entries.filter((entry) => entry.stale).length
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
function byTypeThenTitle(left, right) {
|
|
1577
|
+
return byCodeUnit(left.type, right.type) || byCodeUnit(left.title ?? "", right.title ?? "") || byCodeUnit(left.conceptId, right.conceptId);
|
|
1578
|
+
}
|
|
1579
|
+
function byCodeUnit(left, right) {
|
|
1580
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
1581
|
+
}
|
|
1582
|
+
function renderCatalogLine(entry) {
|
|
1583
|
+
const parts = [
|
|
1584
|
+
entry.conceptId,
|
|
1585
|
+
entry.type,
|
|
1586
|
+
entry.title ?? "(untitled)",
|
|
1587
|
+
entry.standing === "superseded" ? `superseded \u2192 ${entry.supersededBy.join(", ") || "(no surviving head)"}` : entry.standing
|
|
1588
|
+
];
|
|
1589
|
+
if (entry.stale) parts.push("stale");
|
|
1590
|
+
return `- ${parts.join(" \xB7 ")}`;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
// src/commands/catalog.ts
|
|
1594
|
+
var catalogCommand = define({
|
|
1595
|
+
name: "catalog",
|
|
1596
|
+
tool: "kb_catalog",
|
|
1597
|
+
usage: "catalog [type]",
|
|
1598
|
+
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.",
|
|
1599
|
+
input: import_zod8.z.object({
|
|
1600
|
+
bundlePath,
|
|
1601
|
+
type: import_zod8.z.enum(KB_RECORD_TYPES).optional()
|
|
1602
|
+
}),
|
|
1603
|
+
fromArgv: (argv, path) => ({
|
|
1604
|
+
bundlePath: path,
|
|
1605
|
+
...argv[1] && !argv[1].startsWith("--") ? { type: argv[1] } : {}
|
|
1606
|
+
}),
|
|
1607
|
+
run: async ({ store }, { bundlePath: path, type }) => render(
|
|
1608
|
+
await store.catalog(path, { ...type ? { type } : {} }),
|
|
1609
|
+
path,
|
|
1610
|
+
type
|
|
1611
|
+
)
|
|
1612
|
+
});
|
|
1613
|
+
function render(result, bundle, type) {
|
|
1614
|
+
const lines = [
|
|
1615
|
+
`# KB Catalog${type ? ` \u2014 ${type}` : ""}`,
|
|
1616
|
+
`bundle: ${bundle}`,
|
|
1617
|
+
`${count(result.recordCount, "record")}: ${standingCounts(result)}`
|
|
1618
|
+
];
|
|
1619
|
+
if (result.staleCount) {
|
|
1620
|
+
lines.push(
|
|
1621
|
+
`${result.staleCount} stale \u2014 a flag over the standings above, not one of them`
|
|
1622
|
+
);
|
|
1623
|
+
}
|
|
1624
|
+
lines.push("");
|
|
1625
|
+
if (!result.entries.length) {
|
|
1626
|
+
lines.push(
|
|
1627
|
+
type ? `(no records of type ${type})` : "(no records \u2014 this base is empty)"
|
|
1628
|
+
);
|
|
1629
|
+
} else {
|
|
1630
|
+
for (const entry of result.entries) lines.push(renderCatalogLine(entry));
|
|
1631
|
+
}
|
|
1632
|
+
lines.push(
|
|
1633
|
+
"",
|
|
1634
|
+
"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."
|
|
1635
|
+
);
|
|
1636
|
+
return lines.join("\n");
|
|
747
1637
|
}
|
|
1638
|
+
function standingCounts(result) {
|
|
1639
|
+
const ORDER = [
|
|
1640
|
+
"current",
|
|
1641
|
+
"open",
|
|
1642
|
+
"unsettled",
|
|
1643
|
+
"rejected",
|
|
1644
|
+
"superseded"
|
|
1645
|
+
];
|
|
1646
|
+
const parts = ORDER.filter((standing) => result.standings[standing]).map(
|
|
1647
|
+
(standing) => `${result.standings[standing]} ${standing}`
|
|
1648
|
+
);
|
|
1649
|
+
return parts.length ? parts.join(" \xB7 ") : "none";
|
|
1650
|
+
}
|
|
1651
|
+
function count(value, noun) {
|
|
1652
|
+
return `${value} ${value === 1 ? noun : `${noun}s`}`;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
// src/commands/context.ts
|
|
1656
|
+
var import_zod9 = require("zod");
|
|
1657
|
+
|
|
1658
|
+
// src/kb-context.ts
|
|
1659
|
+
var import_promises3 = require("fs/promises");
|
|
748
1660
|
|
|
749
1661
|
// src/kb-index.ts
|
|
750
1662
|
var INDEX_FILE = "INDEX.md";
|
|
@@ -971,13 +1883,13 @@ function toHookJson(block, event) {
|
|
|
971
1883
|
var CONTEXT_BEGIN = "<!-- strauss-kb:begin -->";
|
|
972
1884
|
var CONTEXT_END = "<!-- strauss-kb:end -->";
|
|
973
1885
|
async function syncInstructions(file, block) {
|
|
974
|
-
const existing = await (0,
|
|
1886
|
+
const existing = await (0, import_promises3.readFile)(file, "utf8").catch(() => null);
|
|
975
1887
|
const region = block ? `${CONTEXT_BEGIN}
|
|
976
1888
|
${block.trim()}
|
|
977
1889
|
${CONTEXT_END}` : null;
|
|
978
1890
|
if (existing === null) {
|
|
979
1891
|
if (!region) return { file, action: "unchanged" };
|
|
980
|
-
await (0,
|
|
1892
|
+
await (0, import_promises3.writeFile)(file, `${region}
|
|
981
1893
|
`, "utf8");
|
|
982
1894
|
return { file, action: "created" };
|
|
983
1895
|
}
|
|
@@ -988,11 +1900,11 @@ ${CONTEXT_END}` : null;
|
|
|
988
1900
|
const after = existing.slice(end + CONTEXT_END.length);
|
|
989
1901
|
const next = region ? `${before}${region}${after}` : `${before.replace(/\n+$/, "\n")}${after.replace(/^\n+/, "\n")}`;
|
|
990
1902
|
if (next === existing) return { file, action: "unchanged" };
|
|
991
|
-
await (0,
|
|
1903
|
+
await (0, import_promises3.writeFile)(file, next, "utf8");
|
|
992
1904
|
return { file, action: region ? "replaced" : "removed" };
|
|
993
1905
|
}
|
|
994
1906
|
if (!region) return { file, action: "unchanged" };
|
|
995
|
-
await (0,
|
|
1907
|
+
await (0, import_promises3.writeFile)(
|
|
996
1908
|
file,
|
|
997
1909
|
`${existing.replace(/\n*$/, "\n\n")}${region}
|
|
998
1910
|
`,
|
|
@@ -1007,20 +1919,20 @@ var contextCommand = define({
|
|
|
1007
1919
|
tool: "kb_context",
|
|
1008
1920
|
usage: "context [--profile NAME] [--budget N] [--full-under N] [--format json] [--event NAME]",
|
|
1009
1921
|
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.",
|
|
1010
|
-
input:
|
|
1011
|
-
budgetTokens:
|
|
1922
|
+
input: import_zod9.z.object({
|
|
1923
|
+
budgetTokens: import_zod9.z.number().int().positive().optional().describe(
|
|
1012
1924
|
"Ceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000."
|
|
1013
1925
|
),
|
|
1014
|
-
fullUnderTokens:
|
|
1926
|
+
fullUnderTokens: import_zod9.z.number().int().positive().optional().describe(
|
|
1015
1927
|
"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."
|
|
1016
1928
|
),
|
|
1017
|
-
profile:
|
|
1929
|
+
profile: import_zod9.z.string().optional().describe(
|
|
1018
1930
|
"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."
|
|
1019
1931
|
),
|
|
1020
|
-
format:
|
|
1932
|
+
format: import_zod9.z.enum(["markdown", "json"]).optional().describe(
|
|
1021
1933
|
"CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
|
|
1022
1934
|
),
|
|
1023
|
-
event:
|
|
1935
|
+
event: import_zod9.z.string().optional().describe(
|
|
1024
1936
|
"hookEventName stamped into the JSON envelope. Only meaningful with format=json."
|
|
1025
1937
|
)
|
|
1026
1938
|
}),
|
|
@@ -1056,7 +1968,7 @@ var contextCommand = define({
|
|
|
1056
1968
|
});
|
|
1057
1969
|
|
|
1058
1970
|
// src/commands/doctor.ts
|
|
1059
|
-
var
|
|
1971
|
+
var import_zod10 = require("zod");
|
|
1060
1972
|
|
|
1061
1973
|
// src/kb-edges.ts
|
|
1062
1974
|
var KB_EDGE_KINDS = [
|
|
@@ -1180,7 +2092,8 @@ var CHECK_HEADLINES = {
|
|
|
1180
2092
|
aging: "still open or still proposed long after it was written",
|
|
1181
2093
|
orphaned: "no other record links to it",
|
|
1182
2094
|
"broken-supersession": "the supersession pointers do not resolve",
|
|
1183
|
-
"superseded-but-cited": "a live record's body links to one that no longer holds"
|
|
2095
|
+
"superseded-but-cited": "a live record's body links to one that no longer holds",
|
|
2096
|
+
drifted: "the code an anchor points at moved out from under its hash"
|
|
1184
2097
|
};
|
|
1185
2098
|
var DAY_MS = 864e5;
|
|
1186
2099
|
function doctor(bundle, options = {}) {
|
|
@@ -1190,7 +2103,7 @@ function doctor(bundle, options = {}) {
|
|
|
1190
2103
|
agingDays: options.agingDays ?? DEFAULT_AGING_DAYS
|
|
1191
2104
|
};
|
|
1192
2105
|
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
1193
|
-
const adjudicated = adjudicate(bundle, bundle, now);
|
|
2106
|
+
const adjudicated = adjudicate(bundle, bundle, now, options.anchorDrift);
|
|
1194
2107
|
const standings = new Map(
|
|
1195
2108
|
adjudicated.map((hit) => [hit.record.conceptId, hit.standing])
|
|
1196
2109
|
);
|
|
@@ -1204,7 +2117,8 @@ function doctor(bundle, options = {}) {
|
|
|
1204
2117
|
group("aging", aging(inForce, now, thresholds.agingDays)),
|
|
1205
2118
|
group("orphaned", orphaned(bundle)),
|
|
1206
2119
|
group("broken-supersession", brokenSupersession(bundle, adjudicated)),
|
|
1207
|
-
group("superseded-but-cited", supersededButCited(bundle, standings))
|
|
2120
|
+
group("superseded-but-cited", supersededButCited(bundle, standings)),
|
|
2121
|
+
group("drifted", drifted(inForce))
|
|
1208
2122
|
];
|
|
1209
2123
|
const counts = Object.fromEntries(
|
|
1210
2124
|
groups.map((entry) => [entry.check, entry.count])
|
|
@@ -1390,6 +2304,29 @@ function supersededButCited(bundle, standings) {
|
|
|
1390
2304
|
}
|
|
1391
2305
|
return findings;
|
|
1392
2306
|
}
|
|
2307
|
+
function drifted(hits) {
|
|
2308
|
+
const findings = [];
|
|
2309
|
+
for (const hit of hits) {
|
|
2310
|
+
const warning = hit.warnings.find((entry) => entry.kind === "drifted");
|
|
2311
|
+
if (!warning) continue;
|
|
2312
|
+
findings.push(
|
|
2313
|
+
finding(
|
|
2314
|
+
hit.record,
|
|
2315
|
+
`${warning.anchors.length} ${warning.anchors.length === 1 ? "anchor no longer matches" : "anchors no longer match"}: ${warning.anchors.map((anchor) => {
|
|
2316
|
+
const at = anchor.symbol ? `${anchor.file}:${anchor.symbol}` : anchor.file;
|
|
2317
|
+
if (anchor.reason) return `${at} (${anchor.reason})`;
|
|
2318
|
+
if (anchor.diffSize === null) {
|
|
2319
|
+
return `${at} (changed, size unrecorded)`;
|
|
2320
|
+
}
|
|
2321
|
+
return anchor.diffSize === 0 ? `${at} (content changed, same line count)` : `${at} (${anchor.diffSize} line${anchor.diffSize === 1 ? "" : "s"} apart)`;
|
|
2322
|
+
}).join(", ")}`
|
|
2323
|
+
)
|
|
2324
|
+
);
|
|
2325
|
+
}
|
|
2326
|
+
return findings.sort(
|
|
2327
|
+
(left, right) => left.conceptId.localeCompare(right.conceptId)
|
|
2328
|
+
);
|
|
2329
|
+
}
|
|
1393
2330
|
function replaces(later, earlier) {
|
|
1394
2331
|
return (later.frontmatter.strauss_supersedes ?? []).includes(earlier.conceptId) || earlier.frontmatter.strauss_superseded_by === later.conceptId;
|
|
1395
2332
|
}
|
|
@@ -1413,14 +2350,15 @@ function ageInDays(record, now) {
|
|
|
1413
2350
|
}
|
|
1414
2351
|
|
|
1415
2352
|
// src/commands/doctor.ts
|
|
1416
|
-
var days = (what, fallback) =>
|
|
2353
|
+
var days = (what, fallback) => import_zod10.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
|
|
1417
2354
|
var doctorCommand = define({
|
|
1418
2355
|
name: "doctor",
|
|
1419
2356
|
tool: "kb_doctor",
|
|
1420
|
-
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--strict]",
|
|
1421
|
-
description: "
|
|
1422
|
-
input:
|
|
2357
|
+
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--strict]",
|
|
2358
|
+
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.",
|
|
2359
|
+
input: import_zod10.z.object({
|
|
1423
2360
|
bundlePath,
|
|
2361
|
+
repoRoot: REPO_ROOT,
|
|
1424
2362
|
expiringDays: days(
|
|
1425
2363
|
"How far ahead `expiring` looks, in days.",
|
|
1426
2364
|
DEFAULT_EXPIRING_DAYS
|
|
@@ -1433,7 +2371,7 @@ var doctorCommand = define({
|
|
|
1433
2371
|
"How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
|
|
1434
2372
|
DEFAULT_AGING_DAYS
|
|
1435
2373
|
),
|
|
1436
|
-
strict:
|
|
2374
|
+
strict: import_zod10.z.boolean().optional().describe(
|
|
1437
2375
|
"Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
|
|
1438
2376
|
)
|
|
1439
2377
|
}),
|
|
@@ -1445,32 +2383,39 @@ var doctorCommand = define({
|
|
|
1445
2383
|
const expiring2 = argvFlag(argv, "--expiring-days");
|
|
1446
2384
|
const unverified2 = argvFlag(argv, "--unverified-days");
|
|
1447
2385
|
const agingDays = argvFlag(argv, "--aging-days");
|
|
2386
|
+
const repoRoot = argvFlag(argv, "--repo-root");
|
|
1448
2387
|
return {
|
|
1449
2388
|
bundlePath: path,
|
|
2389
|
+
...repoRoot !== void 0 ? { repoRoot } : {},
|
|
1450
2390
|
...expiring2 !== void 0 ? { expiringDays: Number(expiring2) } : {},
|
|
1451
2391
|
...unverified2 !== void 0 ? { unverifiedDays: Number(unverified2) } : {},
|
|
1452
2392
|
...agingDays !== void 0 ? { agingDays: Number(agingDays) } : {},
|
|
1453
2393
|
...argv.includes("--strict") ? { strict: true } : {}
|
|
1454
2394
|
};
|
|
1455
2395
|
},
|
|
1456
|
-
run: async ({ store, now }, { bundlePath: path, expiringDays, unverifiedDays, agingDays }) => {
|
|
2396
|
+
run: async ({ store, now }, { bundlePath: path, expiringDays, unverifiedDays, agingDays, repoRoot }) => {
|
|
1457
2397
|
const checkedAt = now();
|
|
1458
|
-
const
|
|
2398
|
+
const records = await store.list(path);
|
|
2399
|
+
const anchorDrift = await store.detectDrift(records, repoRoot);
|
|
2400
|
+
const report = doctor(records, {
|
|
1459
2401
|
...expiringDays !== void 0 ? { expiringDays } : {},
|
|
1460
2402
|
...unverifiedDays !== void 0 ? { unverifiedDays } : {},
|
|
1461
2403
|
...agingDays !== void 0 ? { agingDays } : {},
|
|
2404
|
+
...anchorDrift !== void 0 ? { anchorDrift } : {},
|
|
1462
2405
|
now: new Date(checkedAt)
|
|
1463
2406
|
});
|
|
1464
2407
|
return { bundlePath: path, checkedAt, ...report };
|
|
1465
2408
|
},
|
|
1466
|
-
render: (result) =>
|
|
1467
|
-
// Only expiry, and only under --strict. The other
|
|
2409
|
+
render: (result) => render2(result),
|
|
2410
|
+
// Only expiry, and only under --strict. The other seven checks report debt a
|
|
1468
2411
|
// reader decides about; an expired record is the base asserting something it
|
|
1469
2412
|
// already said it would stop standing behind, which is the one finding a
|
|
1470
|
-
// pipeline can act on without a judgment call.
|
|
2413
|
+
// pipeline can act on without a judgment call. Drift has its own gate —
|
|
2414
|
+
// `anchor-resolve` exits non-zero on it, against a repo root the caller
|
|
2415
|
+
// named, which is the run a CI pipeline should be making anyway.
|
|
1471
2416
|
failsWhen: (result, input) => input.strict === true && result.counts.expired > 0
|
|
1472
2417
|
});
|
|
1473
|
-
function
|
|
2418
|
+
function render2(result) {
|
|
1474
2419
|
const { thresholds } = result;
|
|
1475
2420
|
const lines = [
|
|
1476
2421
|
`# KB Doctor \u2014 ${result.bundlePath}`,
|
|
@@ -1502,13 +2447,13 @@ function render(result) {
|
|
|
1502
2447
|
}
|
|
1503
2448
|
|
|
1504
2449
|
// src/commands/list.ts
|
|
1505
|
-
var
|
|
2450
|
+
var import_zod11 = require("zod");
|
|
1506
2451
|
var listCommand = define({
|
|
1507
2452
|
name: "list",
|
|
1508
2453
|
tool: "kb_list",
|
|
1509
2454
|
usage: "list [type]",
|
|
1510
2455
|
description: "Every record, optionally narrowed to one type. Use kb_query when you have a question; this is for enumerating.",
|
|
1511
|
-
input:
|
|
2456
|
+
input: import_zod11.z.object({ bundlePath, type: import_zod11.z.enum(KB_RECORD_TYPES).optional() }),
|
|
1512
2457
|
fromArgv: (argv, path) => ({ bundlePath: path, type: argv[1] }),
|
|
1513
2458
|
run: async ({ store }, { bundlePath: path, type }) => (await store.list(path, type)).map((record) => ({
|
|
1514
2459
|
conceptId: record.conceptId,
|
|
@@ -1520,36 +2465,40 @@ var listCommand = define({
|
|
|
1520
2465
|
});
|
|
1521
2466
|
|
|
1522
2467
|
// src/commands/load.ts
|
|
1523
|
-
var
|
|
2468
|
+
var import_zod12 = require("zod");
|
|
1524
2469
|
var loadCommand = define({
|
|
1525
2470
|
name: "load",
|
|
1526
2471
|
tool: "kb_load",
|
|
1527
|
-
usage: "load [type] [--budget N | --all]",
|
|
1528
|
-
description: "
|
|
1529
|
-
input:
|
|
2472
|
+
usage: "load [type] [--budget N | --all] [--repo-root PATH]",
|
|
2473
|
+
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.",
|
|
2474
|
+
input: import_zod12.z.object({
|
|
1530
2475
|
bundlePath,
|
|
1531
|
-
type:
|
|
1532
|
-
budgetTokens:
|
|
1533
|
-
all:
|
|
1534
|
-
"
|
|
1535
|
-
)
|
|
2476
|
+
type: import_zod12.z.enum(KB_RECORD_TYPES).optional(),
|
|
2477
|
+
budgetTokens: import_zod12.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
|
|
2478
|
+
all: import_zod12.z.boolean().optional().describe(
|
|
2479
|
+
"Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
|
|
2480
|
+
),
|
|
2481
|
+
repoRoot: REPO_ROOT
|
|
1536
2482
|
}).refine((value) => !(value.all && value.budgetTokens !== void 0), {
|
|
1537
|
-
message: "all
|
|
2483
|
+
message: "all is mutually exclusive with budgetTokens: pass a ceiling or none, not both."
|
|
1538
2484
|
}),
|
|
1539
2485
|
fromArgv: (argv, path) => {
|
|
1540
2486
|
const budget = argvFlag(argv, "--budget");
|
|
2487
|
+
const repoRoot = argvFlag(argv, "--repo-root");
|
|
1541
2488
|
return {
|
|
1542
2489
|
bundlePath: path,
|
|
1543
2490
|
...argv[1] && !argv[1].startsWith("--") ? { type: argv[1] } : {},
|
|
1544
2491
|
...budget ? { budgetTokens: Number(budget) } : {},
|
|
1545
|
-
...argv.includes("--all") ? { all: true } : {}
|
|
2492
|
+
...argv.includes("--all") ? { all: true } : {},
|
|
2493
|
+
...repoRoot !== void 0 ? { repoRoot } : {}
|
|
1546
2494
|
};
|
|
1547
2495
|
},
|
|
1548
|
-
run: async ({ store }, { bundlePath: path, type, budgetTokens, all }) => {
|
|
2496
|
+
run: async ({ store }, { bundlePath: path, type, budgetTokens, all, repoRoot }) => {
|
|
1549
2497
|
const result = await store.load(path, {
|
|
1550
2498
|
...type ? { type } : {},
|
|
1551
2499
|
...budgetTokens ? { budgetTokens } : {},
|
|
1552
|
-
...all ? { all } : {}
|
|
2500
|
+
...all ? { all } : {},
|
|
2501
|
+
...repoRoot !== void 0 ? { repoRoot } : {}
|
|
1553
2502
|
});
|
|
1554
2503
|
if (!result.loaded) return result;
|
|
1555
2504
|
return {
|
|
@@ -1568,25 +2517,25 @@ var loadCommand = define({
|
|
|
1568
2517
|
});
|
|
1569
2518
|
|
|
1570
2519
|
// src/commands/log.ts
|
|
1571
|
-
var
|
|
2520
|
+
var import_zod13 = require("zod");
|
|
1572
2521
|
var logCommand = define({
|
|
1573
2522
|
name: "log",
|
|
1574
2523
|
tool: "kb_log",
|
|
1575
2524
|
usage: "log",
|
|
1576
2525
|
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.",
|
|
1577
|
-
input:
|
|
2526
|
+
input: import_zod13.z.object({ bundlePath }),
|
|
1578
2527
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
1579
2528
|
run: ({ store }, { bundlePath: path }) => store.readLog(path)
|
|
1580
2529
|
});
|
|
1581
2530
|
|
|
1582
2531
|
// src/commands/no-decision.ts
|
|
1583
|
-
var
|
|
2532
|
+
var import_zod14 = require("zod");
|
|
1584
2533
|
var noDecisionCommand = define({
|
|
1585
2534
|
name: "no-decision",
|
|
1586
2535
|
tool: "kb_no_decision",
|
|
1587
2536
|
usage: "no-decision <reason...>",
|
|
1588
2537
|
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.',
|
|
1589
|
-
input:
|
|
2538
|
+
input: import_zod14.z.object({ bundlePath, reason: import_zod14.z.string().min(1) }),
|
|
1590
2539
|
fromArgv: (argv, path) => ({
|
|
1591
2540
|
bundlePath: path,
|
|
1592
2541
|
reason: argv.slice(1).join(" ").trim()
|
|
@@ -1603,20 +2552,20 @@ var noDecisionCommand = define({
|
|
|
1603
2552
|
});
|
|
1604
2553
|
|
|
1605
2554
|
// src/commands/pack.ts
|
|
1606
|
-
var
|
|
2555
|
+
var import_zod15 = require("zod");
|
|
1607
2556
|
var packCommand = define({
|
|
1608
2557
|
name: "pack",
|
|
1609
2558
|
tool: "kb_pack",
|
|
1610
2559
|
usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
|
|
1611
2560
|
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.",
|
|
1612
|
-
input:
|
|
2561
|
+
input: import_zod15.z.object({
|
|
1613
2562
|
bundlePath,
|
|
1614
2563
|
conceptId,
|
|
1615
|
-
hops:
|
|
1616
|
-
maxNodes:
|
|
2564
|
+
hops: import_zod15.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
|
|
2565
|
+
maxNodes: import_zod15.z.number().int().positive().optional().describe(
|
|
1617
2566
|
"How many records the pack may hold, root included. Defaults to 20."
|
|
1618
2567
|
),
|
|
1619
|
-
budgetTokens:
|
|
2568
|
+
budgetTokens: import_zod15.z.number().int().positive().optional().describe(
|
|
1620
2569
|
"Approximate token ceiling over what is actually emitted. Defaults to 25000."
|
|
1621
2570
|
)
|
|
1622
2571
|
}),
|
|
@@ -1638,10 +2587,10 @@ var packCommand = define({
|
|
|
1638
2587
|
...maxNodes !== void 0 ? { maxNodes } : {},
|
|
1639
2588
|
...budgetTokens !== void 0 ? { budgetTokens } : {}
|
|
1640
2589
|
});
|
|
1641
|
-
return
|
|
2590
|
+
return render3(result, path, now());
|
|
1642
2591
|
}
|
|
1643
2592
|
});
|
|
1644
|
-
function
|
|
2593
|
+
function render3(result, bundle, at) {
|
|
1645
2594
|
const lines = [
|
|
1646
2595
|
`# KB Pack \u2014 ${result.root}`,
|
|
1647
2596
|
`bundle: ${bundle}`,
|
|
@@ -1703,22 +2652,22 @@ function warningLabel(warning) {
|
|
|
1703
2652
|
}
|
|
1704
2653
|
|
|
1705
2654
|
// src/commands/pin.ts
|
|
1706
|
-
var
|
|
2655
|
+
var import_zod16 = require("zod");
|
|
1707
2656
|
var pinCommand = define({
|
|
1708
2657
|
name: "pin",
|
|
1709
2658
|
tool: "kb_pin",
|
|
1710
2659
|
usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
|
|
1711
2660
|
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.",
|
|
1712
|
-
input:
|
|
2661
|
+
input: import_zod16.z.object({
|
|
1713
2662
|
bundlePath,
|
|
1714
|
-
mode:
|
|
2663
|
+
mode: import_zod16.z.enum(["full", "index"]).optional().describe(
|
|
1715
2664
|
"full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
|
|
1716
2665
|
),
|
|
1717
|
-
profiles:
|
|
1718
|
-
layer:
|
|
2666
|
+
profiles: import_zod16.z.array(import_zod16.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
|
|
2667
|
+
layer: import_zod16.z.enum(["project", "local", "user"]).optional().describe(
|
|
1719
2668
|
"Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
|
|
1720
2669
|
),
|
|
1721
|
-
frozen:
|
|
2670
|
+
frozen: import_zod16.z.boolean().optional().describe(
|
|
1722
2671
|
"true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
|
|
1723
2672
|
)
|
|
1724
2673
|
}),
|
|
@@ -1747,38 +2696,48 @@ var pinCommand = define({
|
|
|
1747
2696
|
});
|
|
1748
2697
|
|
|
1749
2698
|
// src/commands/pins.ts
|
|
1750
|
-
var
|
|
2699
|
+
var import_zod17 = require("zod");
|
|
1751
2700
|
var pinsCommand = define({
|
|
1752
2701
|
name: "pins",
|
|
1753
2702
|
tool: "kb_pins",
|
|
1754
2703
|
usage: "pins",
|
|
1755
2704
|
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.",
|
|
1756
|
-
input:
|
|
2705
|
+
input: import_zod17.z.object({}),
|
|
1757
2706
|
fromArgv: () => ({}),
|
|
1758
2707
|
run: ({ store }) => listPins(store, process.cwd())
|
|
1759
2708
|
});
|
|
1760
2709
|
|
|
1761
2710
|
// src/commands/query.ts
|
|
1762
|
-
var
|
|
2711
|
+
var import_zod18 = require("zod");
|
|
1763
2712
|
var queryCommand = define({
|
|
1764
2713
|
name: "query",
|
|
1765
2714
|
tool: "kb_query",
|
|
1766
|
-
usage: "query <text...>",
|
|
1767
|
-
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.
|
|
1768
|
-
input:
|
|
2715
|
+
usage: "query <text...> [--repo-root PATH]",
|
|
2716
|
+
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.",
|
|
2717
|
+
input: import_zod18.z.object({
|
|
1769
2718
|
bundlePath,
|
|
1770
|
-
text:
|
|
1771
|
-
type:
|
|
1772
|
-
includeNonCurrent:
|
|
2719
|
+
text: import_zod18.z.string().optional(),
|
|
2720
|
+
type: import_zod18.z.enum(KB_RECORD_TYPES).optional(),
|
|
2721
|
+
includeNonCurrent: import_zod18.z.boolean().optional(),
|
|
2722
|
+
repoRoot: REPO_ROOT
|
|
1773
2723
|
}),
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
2724
|
+
// `--repo-root` is a flag, so its value must not fall into the search text.
|
|
2725
|
+
fromArgv: (argv, path) => {
|
|
2726
|
+
const repoRoot = argvFlag(argv, "--repo-root");
|
|
2727
|
+
const words = argv.slice(1);
|
|
2728
|
+
const flag = words.indexOf("--repo-root");
|
|
2729
|
+
if (flag !== -1) words.splice(flag, 2);
|
|
2730
|
+
return {
|
|
2731
|
+
bundlePath: path,
|
|
2732
|
+
text: words.join(" ").trim(),
|
|
2733
|
+
includeNonCurrent: true,
|
|
2734
|
+
...repoRoot !== void 0 ? { repoRoot } : {}
|
|
2735
|
+
};
|
|
2736
|
+
},
|
|
2737
|
+
run: async ({ store }, { bundlePath: path, text, type, includeNonCurrent, repoRoot }) => (await store.query(path, text ?? "", {
|
|
1780
2738
|
...type ? { type } : {},
|
|
1781
|
-
includeNonCurrent: includeNonCurrent === true
|
|
2739
|
+
includeNonCurrent: includeNonCurrent === true,
|
|
2740
|
+
...repoRoot !== void 0 ? { repoRoot } : {}
|
|
1782
2741
|
})).map((hit) => ({
|
|
1783
2742
|
conceptId: hit.record.conceptId,
|
|
1784
2743
|
title: hit.record.frontmatter.title ?? null,
|
|
@@ -1791,27 +2750,27 @@ var queryCommand = define({
|
|
|
1791
2750
|
});
|
|
1792
2751
|
|
|
1793
2752
|
// src/commands/read-index.ts
|
|
1794
|
-
var
|
|
2753
|
+
var import_zod19 = require("zod");
|
|
1795
2754
|
var readIndexCommand = define({
|
|
1796
2755
|
name: "index",
|
|
1797
2756
|
tool: "kb_index",
|
|
1798
2757
|
usage: "index",
|
|
1799
2758
|
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.",
|
|
1800
|
-
input:
|
|
2759
|
+
input: import_zod19.z.object({ bundlePath }),
|
|
1801
2760
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
1802
2761
|
run: ({ store }, { bundlePath: path }) => store.readIndex(path)
|
|
1803
2762
|
});
|
|
1804
2763
|
|
|
1805
2764
|
// src/commands/schema.ts
|
|
1806
|
-
var
|
|
2765
|
+
var import_zod22 = require("zod");
|
|
1807
2766
|
|
|
1808
2767
|
// src/json-schema.ts
|
|
1809
|
-
var
|
|
2768
|
+
var import_zod21 = require("zod");
|
|
1810
2769
|
|
|
1811
2770
|
// src/kb-log.ts
|
|
1812
|
-
var
|
|
2771
|
+
var import_zod20 = require("zod");
|
|
1813
2772
|
var LOG_FILE = "log.jsonl";
|
|
1814
|
-
var kbLogEntrySchema =
|
|
2773
|
+
var kbLogEntrySchema = import_zod20.z.object({
|
|
1815
2774
|
// Validated, not just `min(1)`: `at` is a sort key (see `parseLog`
|
|
1816
2775
|
// below), and a value that isn't actually chronological — a Unix
|
|
1817
2776
|
// timestamp, a human-typed date, garbage — would sort wrong without
|
|
@@ -1820,12 +2779,12 @@ var kbLogEntrySchema = import_zod18.z.object({
|
|
|
1820
2779
|
// and rejects everything else, including a non-`Z` offset — so a
|
|
1821
2780
|
// malformed `at` is reported the same way a malformed line already is,
|
|
1822
2781
|
// rather than silently sorting into the wrong place.
|
|
1823
|
-
at:
|
|
1824
|
-
by:
|
|
1825
|
-
operation:
|
|
1826
|
-
conceptId:
|
|
2782
|
+
at: import_zod20.z.iso.datetime(),
|
|
2783
|
+
by: import_zod20.z.string().min(1),
|
|
2784
|
+
operation: import_zod20.z.string().min(1),
|
|
2785
|
+
conceptId: import_zod20.z.string().min(1),
|
|
1827
2786
|
/** Second concept id, where the operation relates two — supersession. */
|
|
1828
|
-
target:
|
|
2787
|
+
target: import_zod20.z.string().min(1).optional()
|
|
1829
2788
|
}).strict();
|
|
1830
2789
|
function renderLogEntry(entry) {
|
|
1831
2790
|
return `${JSON.stringify(kbLogEntrySchema.parse(entry))}
|
|
@@ -1863,11 +2822,11 @@ function parseLog(raw) {
|
|
|
1863
2822
|
// src/json-schema.ts
|
|
1864
2823
|
function kbJsonSchemas() {
|
|
1865
2824
|
return {
|
|
1866
|
-
recordFrontmatter:
|
|
2825
|
+
recordFrontmatter: import_zod21.z.toJSONSchema(kbRecordFrontmatterSchema, {
|
|
1867
2826
|
io: "input"
|
|
1868
2827
|
}),
|
|
1869
|
-
composeInput:
|
|
1870
|
-
logEntry:
|
|
2828
|
+
composeInput: import_zod21.z.toJSONSchema(composeInputSchema, { io: "input" }),
|
|
2829
|
+
logEntry: import_zod21.z.toJSONSchema(kbLogEntrySchema, { io: "input" })
|
|
1871
2830
|
};
|
|
1872
2831
|
}
|
|
1873
2832
|
|
|
@@ -1877,22 +2836,22 @@ var schemaCommand = define({
|
|
|
1877
2836
|
tool: "kb_schema",
|
|
1878
2837
|
usage: "schema",
|
|
1879
2838
|
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.",
|
|
1880
|
-
input:
|
|
2839
|
+
input: import_zod22.z.object({}),
|
|
1881
2840
|
fromArgv: () => ({}),
|
|
1882
2841
|
run: () => Promise.resolve(kbJsonSchemas())
|
|
1883
2842
|
});
|
|
1884
2843
|
|
|
1885
2844
|
// src/commands/status.ts
|
|
1886
|
-
var
|
|
2845
|
+
var import_zod23 = require("zod");
|
|
1887
2846
|
var statusCommand = define({
|
|
1888
2847
|
name: "status",
|
|
1889
2848
|
tool: "kb_status",
|
|
1890
2849
|
usage: "status <concept-id> <status>",
|
|
1891
2850
|
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.",
|
|
1892
|
-
input:
|
|
2851
|
+
input: import_zod23.z.object({
|
|
1893
2852
|
bundlePath,
|
|
1894
2853
|
conceptId,
|
|
1895
|
-
status:
|
|
2854
|
+
status: import_zod23.z.enum(KB_RECORD_STATUSES)
|
|
1896
2855
|
}),
|
|
1897
2856
|
fromArgv: (argv, path) => ({
|
|
1898
2857
|
bundlePath: path,
|
|
@@ -1907,13 +2866,13 @@ var statusCommand = define({
|
|
|
1907
2866
|
});
|
|
1908
2867
|
|
|
1909
2868
|
// src/commands/supersede.ts
|
|
1910
|
-
var
|
|
2869
|
+
var import_zod24 = require("zod");
|
|
1911
2870
|
var supersedeCommand = define({
|
|
1912
2871
|
name: "supersede",
|
|
1913
2872
|
tool: "kb_supersede",
|
|
1914
2873
|
usage: "supersede <concept-id> <replacement-id>",
|
|
1915
2874
|
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.",
|
|
1916
|
-
input:
|
|
2875
|
+
input: import_zod24.z.object({ bundlePath, conceptId, replacementId: conceptId }),
|
|
1917
2876
|
fromArgv: (argv, path) => ({
|
|
1918
2877
|
bundlePath: path,
|
|
1919
2878
|
conceptId: argv[1],
|
|
@@ -1927,16 +2886,16 @@ var supersedeCommand = define({
|
|
|
1927
2886
|
});
|
|
1928
2887
|
|
|
1929
2888
|
// src/commands/sync-instructions.ts
|
|
1930
|
-
var
|
|
2889
|
+
var import_zod25 = require("zod");
|
|
1931
2890
|
var syncInstructionsCommand = define({
|
|
1932
2891
|
name: "sync-instructions",
|
|
1933
2892
|
usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
|
|
1934
2893
|
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.",
|
|
1935
|
-
input:
|
|
1936
|
-
file:
|
|
1937
|
-
budgetTokens:
|
|
1938
|
-
fullUnderTokens:
|
|
1939
|
-
profile:
|
|
2894
|
+
input: import_zod25.z.object({
|
|
2895
|
+
file: import_zod25.z.string().min(1).describe("The instruction file to edit in place."),
|
|
2896
|
+
budgetTokens: import_zod25.z.number().int().positive().optional(),
|
|
2897
|
+
fullUnderTokens: import_zod25.z.number().int().positive().optional(),
|
|
2898
|
+
profile: import_zod25.z.string().optional()
|
|
1940
2899
|
}),
|
|
1941
2900
|
fromArgv: (argv) => {
|
|
1942
2901
|
const budget = argvFlag(argv, "--budget");
|
|
@@ -1962,7 +2921,7 @@ var syncInstructionsCommand = define({
|
|
|
1962
2921
|
});
|
|
1963
2922
|
|
|
1964
2923
|
// src/commands/trace.ts
|
|
1965
|
-
var
|
|
2924
|
+
var import_zod26 = require("zod");
|
|
1966
2925
|
|
|
1967
2926
|
// src/trace.ts
|
|
1968
2927
|
var TRACE_EDGES = ["supersession", "anchor", "source"];
|
|
@@ -2008,11 +2967,11 @@ var traceCommand = define({
|
|
|
2008
2967
|
tool: "kb_trace",
|
|
2009
2968
|
usage: "trace <concept-id> [edges...]",
|
|
2010
2969
|
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.',
|
|
2011
|
-
input:
|
|
2970
|
+
input: import_zod26.z.object({
|
|
2012
2971
|
bundlePath,
|
|
2013
2972
|
conceptId,
|
|
2014
|
-
edges:
|
|
2015
|
-
depth:
|
|
2973
|
+
edges: import_zod26.z.array(import_zod26.z.enum(TRACE_EDGES)).optional(),
|
|
2974
|
+
depth: import_zod26.z.number().int().positive().optional()
|
|
2016
2975
|
}),
|
|
2017
2976
|
fromArgv: (argv, path) => ({
|
|
2018
2977
|
bundlePath: path,
|
|
@@ -2034,53 +2993,53 @@ var traceCommand = define({
|
|
|
2034
2993
|
});
|
|
2035
2994
|
|
|
2036
2995
|
// src/commands/types.ts
|
|
2037
|
-
var
|
|
2996
|
+
var import_zod27 = require("zod");
|
|
2038
2997
|
var typesCommand = define({
|
|
2039
2998
|
name: "types",
|
|
2040
2999
|
tool: "kb_types",
|
|
2041
3000
|
usage: "types",
|
|
2042
3001
|
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.",
|
|
2043
|
-
input:
|
|
3002
|
+
input: import_zod27.z.object({}),
|
|
2044
3003
|
fromArgv: () => ({}),
|
|
2045
3004
|
run: () => Promise.resolve(RECORD_TYPES)
|
|
2046
3005
|
});
|
|
2047
3006
|
|
|
2048
3007
|
// src/commands/unpin.ts
|
|
2049
|
-
var
|
|
3008
|
+
var import_zod28 = require("zod");
|
|
2050
3009
|
var unpinCommand = define({
|
|
2051
3010
|
name: "unpin",
|
|
2052
3011
|
tool: "kb_unpin",
|
|
2053
3012
|
usage: "unpin [bundle-path]",
|
|
2054
3013
|
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.",
|
|
2055
|
-
input:
|
|
3014
|
+
input: import_zod28.z.object({ bundlePath }),
|
|
2056
3015
|
fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
|
|
2057
3016
|
run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
|
|
2058
3017
|
});
|
|
2059
3018
|
|
|
2060
3019
|
// src/commands/validate.ts
|
|
2061
|
-
var
|
|
3020
|
+
var import_zod29 = require("zod");
|
|
2062
3021
|
var validateCommand = define({
|
|
2063
3022
|
name: "validate",
|
|
2064
3023
|
tool: "kb_validate",
|
|
2065
3024
|
usage: "validate",
|
|
2066
3025
|
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.",
|
|
2067
|
-
input:
|
|
3026
|
+
input: import_zod29.z.object({ bundlePath }),
|
|
2068
3027
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
2069
3028
|
run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
|
|
2070
3029
|
failsWhen: (result) => Array.isArray(result) && result.length > 0
|
|
2071
3030
|
});
|
|
2072
3031
|
|
|
2073
3032
|
// src/commands/verify.ts
|
|
2074
|
-
var
|
|
3033
|
+
var import_zod30 = require("zod");
|
|
2075
3034
|
var verifyCommand = define({
|
|
2076
3035
|
name: "verify",
|
|
2077
3036
|
tool: "kb_verify",
|
|
2078
3037
|
usage: "verify <concept-id> --note <text>",
|
|
2079
3038
|
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.",
|
|
2080
|
-
input:
|
|
3039
|
+
input: import_zod30.z.object({
|
|
2081
3040
|
bundlePath,
|
|
2082
3041
|
conceptId,
|
|
2083
|
-
note:
|
|
3042
|
+
note: import_zod30.z.string().refine((s) => s.trim().length > 0, {
|
|
2084
3043
|
message: "note must say what the check found"
|
|
2085
3044
|
})
|
|
2086
3045
|
}),
|
|
@@ -2100,7 +3059,7 @@ var verifyCommand = define({
|
|
|
2100
3059
|
});
|
|
2101
3060
|
|
|
2102
3061
|
// src/commands/write.ts
|
|
2103
|
-
var
|
|
3062
|
+
var import_zod31 = require("zod");
|
|
2104
3063
|
var writeCommand = define({
|
|
2105
3064
|
name: "write",
|
|
2106
3065
|
tool: "kb_write",
|
|
@@ -2114,9 +3073,9 @@ var writeCommand = define({
|
|
|
2114
3073
|
"- Prefer a new record over overloading an existing one, and keep each short. A record nobody finishes reading is not durable memory.",
|
|
2115
3074
|
"- Records are never deleted; supersede instead, so the earlier reasoning stays inspectable."
|
|
2116
3075
|
].join("\n"),
|
|
2117
|
-
input:
|
|
3076
|
+
input: import_zod31.z.object({
|
|
2118
3077
|
bundlePath,
|
|
2119
|
-
type:
|
|
3078
|
+
type: import_zod31.z.enum(KB_RECORD_TYPES),
|
|
2120
3079
|
input: composeInputSchema
|
|
2121
3080
|
}),
|
|
2122
3081
|
fromArgv: async (argv, path, stdin) => ({
|
|
@@ -2140,7 +3099,7 @@ var writeCommand = define({
|
|
|
2140
3099
|
});
|
|
2141
3100
|
|
|
2142
3101
|
// src/commands/write-decision.ts
|
|
2143
|
-
var
|
|
3102
|
+
var import_zod32 = require("zod");
|
|
2144
3103
|
var writeDecisionCommand = define({
|
|
2145
3104
|
name: "write-decision",
|
|
2146
3105
|
tool: "kb_write_decision",
|
|
@@ -2153,7 +3112,7 @@ var writeDecisionCommand = define({
|
|
|
2153
3112
|
"- `alternative` is what you turned down and why, not a list of everything considered.",
|
|
2154
3113
|
"- A reference to material you read goes in `sources`; a reference to code goes in `anchors`; a reference to another record goes in `relatedConceptIds`."
|
|
2155
3114
|
].join("\n"),
|
|
2156
|
-
input:
|
|
3115
|
+
input: import_zod32.z.object({ bundlePath, input: decisionInputSchema }),
|
|
2157
3116
|
fromArgv: async (_argv, path, stdin) => ({
|
|
2158
3117
|
bundlePath: path,
|
|
2159
3118
|
input: JSON.parse(await stdin())
|
|
@@ -2182,7 +3141,9 @@ var KB_COMMANDS = [
|
|
|
2182
3141
|
supersedeCommand,
|
|
2183
3142
|
answerCommand,
|
|
2184
3143
|
verifyCommand,
|
|
3144
|
+
anchorResolveCommand,
|
|
2185
3145
|
loadCommand,
|
|
3146
|
+
catalogCommand,
|
|
2186
3147
|
packCommand,
|
|
2187
3148
|
queryCommand,
|
|
2188
3149
|
traceCommand,
|
|
@@ -2204,9 +3165,9 @@ var KB_COMMANDS_BY_NAME = new Map(
|
|
|
2204
3165
|
);
|
|
2205
3166
|
|
|
2206
3167
|
// src/kb-store.ts
|
|
2207
|
-
var
|
|
2208
|
-
var
|
|
2209
|
-
var
|
|
3168
|
+
var import_node_crypto2 = require("crypto");
|
|
3169
|
+
var import_promises5 = require("fs/promises");
|
|
3170
|
+
var import_node_path7 = require("path");
|
|
2210
3171
|
|
|
2211
3172
|
// src/markdown.ts
|
|
2212
3173
|
var import_gray_matter = __toESM(require("gray-matter"), 1);
|
|
@@ -2233,129 +3194,9 @@ function parseMarkdownWithFrontmatter(text, schema) {
|
|
|
2233
3194
|
};
|
|
2234
3195
|
}
|
|
2235
3196
|
|
|
2236
|
-
// src/errors.ts
|
|
2237
|
-
var BaseError = class extends Error {
|
|
2238
|
-
code;
|
|
2239
|
-
errorType;
|
|
2240
|
-
fault;
|
|
2241
|
-
retriable;
|
|
2242
|
-
reportToUser;
|
|
2243
|
-
details;
|
|
2244
|
-
constructor(props) {
|
|
2245
|
-
super(props.message);
|
|
2246
|
-
this.name = props.name ?? this.constructor.name;
|
|
2247
|
-
this.code = props.code ?? 500;
|
|
2248
|
-
this.errorType = props.errorType;
|
|
2249
|
-
this.fault = props.fault;
|
|
2250
|
-
this.retriable = props.retriable ?? true;
|
|
2251
|
-
this.reportToUser = props.reportToUser ?? false;
|
|
2252
|
-
this.details = props.details;
|
|
2253
|
-
}
|
|
2254
|
-
};
|
|
2255
|
-
|
|
2256
|
-
// src/kb-errors.ts
|
|
2257
|
-
var KbRecordAlreadyExistsError = class extends BaseError {
|
|
2258
|
-
constructor(conceptId2) {
|
|
2259
|
-
super({
|
|
2260
|
-
message: `kb: ${conceptId2} already exists \u2014 choose a more specific slug, or write with overwrite`,
|
|
2261
|
-
errorType: "KbRecordAlreadyExists" /* KbRecordAlreadyExists */,
|
|
2262
|
-
code: 409,
|
|
2263
|
-
fault: "User" /* User */,
|
|
2264
|
-
retriable: false,
|
|
2265
|
-
reportToUser: true,
|
|
2266
|
-
details: { conceptId: conceptId2, action: "refused" }
|
|
2267
|
-
});
|
|
2268
|
-
this.conceptId = conceptId2;
|
|
2269
|
-
}
|
|
2270
|
-
conceptId;
|
|
2271
|
-
};
|
|
2272
|
-
var KbRecordNotFoundError = class extends BaseError {
|
|
2273
|
-
constructor(conceptId2) {
|
|
2274
|
-
super({
|
|
2275
|
-
message: `kb: ${conceptId2} does not exist`,
|
|
2276
|
-
errorType: "KbRecordNotFound" /* KbRecordNotFound */,
|
|
2277
|
-
code: 404,
|
|
2278
|
-
fault: "User" /* User */,
|
|
2279
|
-
retriable: false,
|
|
2280
|
-
reportToUser: true,
|
|
2281
|
-
details: { conceptId: conceptId2 }
|
|
2282
|
-
});
|
|
2283
|
-
this.conceptId = conceptId2;
|
|
2284
|
-
}
|
|
2285
|
-
conceptId;
|
|
2286
|
-
};
|
|
2287
|
-
var KbWriteConflictError = class extends BaseError {
|
|
2288
|
-
constructor(conceptId2) {
|
|
2289
|
-
super({
|
|
2290
|
-
message: `kb: ${conceptId2} changed while it was being updated \u2014 re-read and retry`,
|
|
2291
|
-
errorType: "KbWriteConflict" /* KbWriteConflict */,
|
|
2292
|
-
code: 409,
|
|
2293
|
-
fault: "System" /* System */,
|
|
2294
|
-
retriable: true,
|
|
2295
|
-
reportToUser: true,
|
|
2296
|
-
details: { conceptId: conceptId2 }
|
|
2297
|
-
});
|
|
2298
|
-
this.conceptId = conceptId2;
|
|
2299
|
-
}
|
|
2300
|
-
conceptId;
|
|
2301
|
-
};
|
|
2302
|
-
var KbSelfVerificationError = class extends BaseError {
|
|
2303
|
-
constructor(conceptId2, actor, generatedBy) {
|
|
2304
|
-
super({
|
|
2305
|
-
message: `kb: ${conceptId2} was generated by ${generatedBy}, and a record's generator cannot verify it \u2014 only a human or a different actor can`,
|
|
2306
|
-
errorType: "KbSelfVerification" /* KbSelfVerification */,
|
|
2307
|
-
code: 400,
|
|
2308
|
-
fault: "User" /* User */,
|
|
2309
|
-
retriable: false,
|
|
2310
|
-
reportToUser: true,
|
|
2311
|
-
details: { conceptId: conceptId2, actor, generatedBy, action: "refused" }
|
|
2312
|
-
});
|
|
2313
|
-
this.conceptId = conceptId2;
|
|
2314
|
-
this.actor = actor;
|
|
2315
|
-
this.generatedBy = generatedBy;
|
|
2316
|
-
}
|
|
2317
|
-
conceptId;
|
|
2318
|
-
actor;
|
|
2319
|
-
generatedBy;
|
|
2320
|
-
};
|
|
2321
|
-
var KbPackBudgetExceededError = class extends BaseError {
|
|
2322
|
-
constructor(recordCount, approxTokens2, budgetTokens, excluded) {
|
|
2323
|
-
super({
|
|
2324
|
-
message: `kb: a pack of ${recordCount} records is ~${approxTokens2} tokens against a budget of ${budgetTokens} \u2014 lower hops or maxNodes, or raise the budget`,
|
|
2325
|
-
errorType: "KbPackBudgetExceeded" /* KbPackBudgetExceeded */,
|
|
2326
|
-
code: 400,
|
|
2327
|
-
fault: "User" /* User */,
|
|
2328
|
-
retriable: false,
|
|
2329
|
-
reportToUser: true,
|
|
2330
|
-
details: { recordCount, approxTokens: approxTokens2, budgetTokens, excluded }
|
|
2331
|
-
});
|
|
2332
|
-
this.recordCount = recordCount;
|
|
2333
|
-
this.approxTokens = approxTokens2;
|
|
2334
|
-
this.budgetTokens = budgetTokens;
|
|
2335
|
-
this.excluded = excluded;
|
|
2336
|
-
}
|
|
2337
|
-
recordCount;
|
|
2338
|
-
approxTokens;
|
|
2339
|
-
budgetTokens;
|
|
2340
|
-
excluded;
|
|
2341
|
-
};
|
|
2342
|
-
var KbInvalidConceptIdError = class extends BaseError {
|
|
2343
|
-
constructor(message, details) {
|
|
2344
|
-
super({
|
|
2345
|
-
message: `kb: ${message}`,
|
|
2346
|
-
errorType: "KbInvalidConceptId" /* KbInvalidConceptId */,
|
|
2347
|
-
code: 400,
|
|
2348
|
-
fault: "User" /* User */,
|
|
2349
|
-
retriable: false,
|
|
2350
|
-
reportToUser: true,
|
|
2351
|
-
details
|
|
2352
|
-
});
|
|
2353
|
-
}
|
|
2354
|
-
};
|
|
2355
|
-
|
|
2356
3197
|
// src/search-index.ts
|
|
2357
|
-
var
|
|
2358
|
-
var
|
|
3198
|
+
var import_promises4 = require("fs/promises");
|
|
3199
|
+
var import_node_path6 = require("path");
|
|
2359
3200
|
var SEARCH_INDEX_FILE = ".index.sqlite";
|
|
2360
3201
|
var COLLECTION = "kb";
|
|
2361
3202
|
async function searchBase(bundlePath2, query, options = {}) {
|
|
@@ -2364,7 +3205,7 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
2364
3205
|
let store = null;
|
|
2365
3206
|
try {
|
|
2366
3207
|
store = await qmd.createStore({
|
|
2367
|
-
dbPath: (0,
|
|
3208
|
+
dbPath: (0, import_node_path6.join)(bundlePath2, SEARCH_INDEX_FILE),
|
|
2368
3209
|
config: {
|
|
2369
3210
|
collections: {
|
|
2370
3211
|
[COLLECTION]: {
|
|
@@ -2399,16 +3240,19 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
2399
3240
|
}
|
|
2400
3241
|
}
|
|
2401
3242
|
async function isStale(bundlePath2) {
|
|
2402
|
-
const indexAt = await (0,
|
|
3243
|
+
const indexAt = await (0, import_promises4.stat)((0, import_node_path6.join)(bundlePath2, SEARCH_INDEX_FILE)).then((s) => s.mtimeMs).catch(() => 0);
|
|
2403
3244
|
if (!indexAt) return true;
|
|
2404
3245
|
const { readdir: readdir2 } = await import("fs/promises");
|
|
2405
|
-
const names = await readdir2(bundlePath2).catch(() => [])
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
3246
|
+
const names = (await readdir2(bundlePath2).catch(() => [])).filter(
|
|
3247
|
+
(name) => name.endsWith(".md") && name !== INDEX_FILE
|
|
3248
|
+
);
|
|
3249
|
+
let stale = false;
|
|
3250
|
+
await mapLimit(names, DEFAULT_IO_CONCURRENCY, async (name) => {
|
|
3251
|
+
if (stale) return;
|
|
3252
|
+
const at = await (0, import_promises4.stat)((0, import_node_path6.join)(bundlePath2, name)).then((s) => s.mtimeMs).catch(() => 0);
|
|
3253
|
+
if (at > indexAt) stale = true;
|
|
3254
|
+
});
|
|
3255
|
+
return stale;
|
|
2412
3256
|
}
|
|
2413
3257
|
function resolveHits(hits, records) {
|
|
2414
3258
|
const byName = /* @__PURE__ */ new Map();
|
|
@@ -2548,7 +3392,7 @@ function appendUnionMergeLine(contents) {
|
|
|
2548
3392
|
}
|
|
2549
3393
|
|
|
2550
3394
|
// src/kb-store.ts
|
|
2551
|
-
var KB_DIR = (0,
|
|
3395
|
+
var KB_DIR = (0, import_node_path7.join)(".strauss", "kb");
|
|
2552
3396
|
var STORE_OWNED = /* @__PURE__ */ new Set([INDEX_FILE, LOG_FILE, SEARCH_INDEX_FILE]);
|
|
2553
3397
|
var DEFAULT_LOAD_BUDGET = 25e3;
|
|
2554
3398
|
var KbStore = class {
|
|
@@ -2579,7 +3423,7 @@ var KbStore = class {
|
|
|
2579
3423
|
const conceptId2 = `${input.type}.${input.slug}`;
|
|
2580
3424
|
const root = this.root(bundlePath2);
|
|
2581
3425
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
2582
|
-
await (0,
|
|
3426
|
+
await (0, import_promises5.mkdir)(root, { recursive: true });
|
|
2583
3427
|
await this.publish(
|
|
2584
3428
|
target,
|
|
2585
3429
|
stringifyMarkdownWithFrontmatter(input.body, frontmatter),
|
|
@@ -2618,7 +3462,7 @@ var KbStore = class {
|
|
|
2618
3462
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
2619
3463
|
let raw;
|
|
2620
3464
|
try {
|
|
2621
|
-
raw = await (0,
|
|
3465
|
+
raw = await (0, import_promises5.readFile)(target, "utf8");
|
|
2622
3466
|
} catch {
|
|
2623
3467
|
return null;
|
|
2624
3468
|
}
|
|
@@ -2635,15 +3479,15 @@ var KbStore = class {
|
|
|
2635
3479
|
const root = this.root(bundlePath2);
|
|
2636
3480
|
let names;
|
|
2637
3481
|
try {
|
|
2638
|
-
names = await (0,
|
|
3482
|
+
names = await (0, import_promises5.readdir)(root);
|
|
2639
3483
|
} catch {
|
|
2640
3484
|
return [];
|
|
2641
3485
|
}
|
|
2642
3486
|
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}.`));
|
|
2643
|
-
const records = await
|
|
2644
|
-
wanted
|
|
2645
|
-
|
|
2646
|
-
)
|
|
3487
|
+
const records = await mapLimit(
|
|
3488
|
+
wanted,
|
|
3489
|
+
DEFAULT_IO_CONCURRENCY,
|
|
3490
|
+
async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await (0, import_promises5.readFile)((0, import_node_path7.join)(root, name), "utf8"))
|
|
2647
3491
|
);
|
|
2648
3492
|
return records.filter((record) => record !== null);
|
|
2649
3493
|
}
|
|
@@ -2665,6 +3509,21 @@ var KbStore = class {
|
|
|
2665
3509
|
{ operation: `status:${status}`, by: actor }
|
|
2666
3510
|
);
|
|
2667
3511
|
}
|
|
3512
|
+
/**
|
|
3513
|
+
* Replaces a record's anchors wholesale, preserving everything else.
|
|
3514
|
+
*
|
|
3515
|
+
* Wholesale rather than merged: the caller just resolved the anchors it is
|
|
3516
|
+
* writing, so it holds the complete current set, and a merge would keep
|
|
3517
|
+
* stale entries the resolution pass deliberately dropped.
|
|
3518
|
+
*/
|
|
3519
|
+
async updateAnchors(bundlePath2, conceptId2, anchors, actor = "unknown") {
|
|
3520
|
+
return this.mutate(
|
|
3521
|
+
bundlePath2,
|
|
3522
|
+
conceptId2,
|
|
3523
|
+
(frontmatter) => ({ ...frontmatter, strauss_anchors: anchors }),
|
|
3524
|
+
{ operation: "anchor-resolve", by: actor }
|
|
3525
|
+
);
|
|
3526
|
+
}
|
|
2668
3527
|
/**
|
|
2669
3528
|
* Appends one `verified[]` event: who checked the record, when, and what the
|
|
2670
3529
|
* check found. Append-only — prior events are history, and are spread into
|
|
@@ -2763,9 +3622,12 @@ ${answer}
|
|
|
2763
3622
|
const bundle = await this.list(bundlePath2);
|
|
2764
3623
|
const needle = text.trim();
|
|
2765
3624
|
const hits = needle ? await this.rank(bundlePath2, needle, bundle) : bundle;
|
|
3625
|
+
const narrowed = options.type ? hits.filter((r) => r.frontmatter.type === options.type) : hits;
|
|
2766
3626
|
const adjudicated = adjudicate(
|
|
2767
|
-
|
|
2768
|
-
bundle
|
|
3627
|
+
narrowed,
|
|
3628
|
+
bundle,
|
|
3629
|
+
/* @__PURE__ */ new Date(),
|
|
3630
|
+
await this.detectDrift(narrowed, options.repoRoot)
|
|
2769
3631
|
);
|
|
2770
3632
|
if (options.includeNonCurrent) return adjudicated;
|
|
2771
3633
|
const present = new Set(adjudicated.map((hit) => hit.record.conceptId));
|
|
@@ -2784,6 +3646,50 @@ ${answer}
|
|
|
2784
3646
|
const lowered = needle.toLowerCase();
|
|
2785
3647
|
return bundle.filter((record) => matches(record, lowered));
|
|
2786
3648
|
}
|
|
3649
|
+
/**
|
|
3650
|
+
* Anchor drift over the records about to be handed back. Like the search
|
|
3651
|
+
* index, this is an enrichment: a filesystem failure degrades to "no drift
|
|
3652
|
+
* reported" rather than failing the read. Anchors without a stored hash are
|
|
3653
|
+
* skipped inside `detectAnchorDrift`, so a base nobody has stamped pays no
|
|
3654
|
+
* fs cost here. `repoRoot` defaults to the working directory — the CLI runs
|
|
3655
|
+
* at the repo root, and the MCP server's cwd is the workspace.
|
|
3656
|
+
*
|
|
3657
|
+
* Public because `doctor` needs the same map with the same degradation: a
|
|
3658
|
+
* sweep that failed to read the tree should report no drift, not fail.
|
|
3659
|
+
*
|
|
3660
|
+
* When no root was given and not one anchored file was found, the finding is
|
|
3661
|
+
* discarded. A base read from somewhere other than the tree it describes
|
|
3662
|
+
* misses every file at once, and that shape is far likelier to be a wrong
|
|
3663
|
+
* default root than a repository where every anchored file was deleted on
|
|
3664
|
+
* the same day. Reporting it would put a drift warning on every record in
|
|
3665
|
+
* the base, which teaches a reader to ignore the warning — the one outcome
|
|
3666
|
+
* worse than not having it. One file found anywhere makes the root
|
|
3667
|
+
* plausible, and the misses become findings again; an explicit `repoRoot` is
|
|
3668
|
+
* taken at its word either way.
|
|
3669
|
+
*/
|
|
3670
|
+
async detectDrift(records, repoRoot) {
|
|
3671
|
+
try {
|
|
3672
|
+
const drift = await detectAnchorDrift(records, {
|
|
3673
|
+
repoRoot: repoRoot ?? process.cwd()
|
|
3674
|
+
});
|
|
3675
|
+
if (repoRoot === void 0 && looksLikeWrongRepoRoot(drift)) {
|
|
3676
|
+
this.logger.warn?.({
|
|
3677
|
+
operation: "kb.anchor-drift",
|
|
3678
|
+
outcome: "skipped",
|
|
3679
|
+
reason: "no anchored file found under the default repo root"
|
|
3680
|
+
});
|
|
3681
|
+
return void 0;
|
|
3682
|
+
}
|
|
3683
|
+
return drift;
|
|
3684
|
+
} catch (error) {
|
|
3685
|
+
this.logger.warn?.({
|
|
3686
|
+
operation: "kb.anchor-drift",
|
|
3687
|
+
outcome: "skipped",
|
|
3688
|
+
error: error instanceof Error ? error.message : "unknown"
|
|
3689
|
+
});
|
|
3690
|
+
return void 0;
|
|
3691
|
+
}
|
|
3692
|
+
}
|
|
2787
3693
|
/**
|
|
2788
3694
|
* The whole base, adjudicated, when it is small enough to hand over.
|
|
2789
3695
|
*
|
|
@@ -2803,15 +3709,27 @@ ${answer}
|
|
|
2803
3709
|
* is indistinguishable from a complete one, so a caller would answer "that
|
|
2804
3710
|
* was never decided" from a slice it did not know was a slice.
|
|
2805
3711
|
*
|
|
2806
|
-
*
|
|
2807
|
-
*
|
|
2808
|
-
*
|
|
3712
|
+
* A token budget decides that, measured over what is actually handed back.
|
|
3713
|
+
* The refusal names the estimate and the budget, because a caller told only
|
|
3714
|
+
* "too big" cannot tell whether to narrow the type filter, raise the budget,
|
|
3715
|
+
* or stop loading the base whole altogether. Past the budget the answer is
|
|
3716
|
+
* the catalog and then a pack, which is what the refusal says.
|
|
3717
|
+
*
|
|
3718
|
+
* That refusal is the default guardrail. `all` bypasses the budget outright
|
|
3719
|
+
* and always hands back the whole bundle: an explicit, never-accidental
|
|
3720
|
+
* escape hatch for an operator who has the budget to spend, not a wider
|
|
3721
|
+
* default.
|
|
2809
3722
|
*/
|
|
2810
3723
|
async load(bundlePath2, options = {}) {
|
|
2811
3724
|
const budgetTokens = options.budgetTokens ?? DEFAULT_LOAD_BUDGET;
|
|
2812
3725
|
const bundle = await this.list(bundlePath2);
|
|
2813
3726
|
const wanted = options.type ? bundle.filter((record) => record.frontmatter.type === options.type) : bundle;
|
|
2814
|
-
const adjudicated = adjudicate(
|
|
3727
|
+
const adjudicated = adjudicate(
|
|
3728
|
+
wanted,
|
|
3729
|
+
bundle,
|
|
3730
|
+
/* @__PURE__ */ new Date(),
|
|
3731
|
+
await this.detectDrift(wanted, options.repoRoot)
|
|
3732
|
+
);
|
|
2815
3733
|
const records = adjudicated.filter((hit) => hit.standing !== "superseded");
|
|
2816
3734
|
const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(stub);
|
|
2817
3735
|
const approxTokens2 = records.reduce((total, hit) => total + estimateTokens(hit.record), 0) + superseded.reduce((total, entry) => total + estimateStubTokens(entry), 0);
|
|
@@ -2820,7 +3738,12 @@ ${answer}
|
|
|
2820
3738
|
loaded: false,
|
|
2821
3739
|
recordCount: wanted.length,
|
|
2822
3740
|
approxTokens: approxTokens2,
|
|
2823
|
-
budgetTokens
|
|
3741
|
+
budgetTokens,
|
|
3742
|
+
message: refusalMessage({
|
|
3743
|
+
approxTokens: approxTokens2,
|
|
3744
|
+
budgetTokens,
|
|
3745
|
+
type: options.type
|
|
3746
|
+
})
|
|
2824
3747
|
};
|
|
2825
3748
|
}
|
|
2826
3749
|
return {
|
|
@@ -2836,6 +3759,10 @@ ${answer}
|
|
|
2836
3759
|
async trace(bundlePath2, seedId, options = {}) {
|
|
2837
3760
|
return trace(seedId, await this.list(bundlePath2), options);
|
|
2838
3761
|
}
|
|
3762
|
+
/** Every record named in one line each. See `catalog.ts`. */
|
|
3763
|
+
async catalog(bundlePath2, options = {}) {
|
|
3764
|
+
return catalog(await this.list(bundlePath2), options);
|
|
3765
|
+
}
|
|
2839
3766
|
/** A bounded neighbourhood around one record. See `pack.ts`. */
|
|
2840
3767
|
async pack(bundlePath2, rootId, options = {}) {
|
|
2841
3768
|
return pack(await this.list(bundlePath2), rootId, options);
|
|
@@ -2850,11 +3777,11 @@ ${answer}
|
|
|
2850
3777
|
async readIndex(bundlePath2) {
|
|
2851
3778
|
const root = this.root(bundlePath2);
|
|
2852
3779
|
const expected = renderIndex(await this.list(bundlePath2));
|
|
2853
|
-
const stored = await (0,
|
|
3780
|
+
const stored = await (0, import_promises5.readFile)((0, import_node_path7.join)(root, INDEX_FILE), "utf8").catch(
|
|
2854
3781
|
() => null
|
|
2855
3782
|
);
|
|
2856
3783
|
if (indexIsStale(stored, expected)) {
|
|
2857
|
-
await this.publish((0,
|
|
3784
|
+
await this.publish((0, import_node_path7.join)(root, INDEX_FILE), expected, true, INDEX_FILE);
|
|
2858
3785
|
this.logger.info?.({
|
|
2859
3786
|
operation: "kb.index.repair",
|
|
2860
3787
|
bundlePath: root,
|
|
@@ -2871,8 +3798,8 @@ ${answer}
|
|
|
2871
3798
|
* knows which agent touched what. So a bad line is surfaced and left alone.
|
|
2872
3799
|
*/
|
|
2873
3800
|
async readLog(bundlePath2) {
|
|
2874
|
-
const raw = await (0,
|
|
2875
|
-
(0,
|
|
3801
|
+
const raw = await (0, import_promises5.readFile)(
|
|
3802
|
+
(0, import_node_path7.join)(this.root(bundlePath2), LOG_FILE),
|
|
2876
3803
|
"utf8"
|
|
2877
3804
|
).catch(() => "");
|
|
2878
3805
|
const result = parseLog(raw);
|
|
@@ -2923,14 +3850,14 @@ ${answer}
|
|
|
2923
3850
|
}
|
|
2924
3851
|
async mutate(bundlePath2, conceptId2, change, entry, changeBody = (body) => body) {
|
|
2925
3852
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
2926
|
-
const before = await (0,
|
|
3853
|
+
const before = await (0, import_promises5.readFile)(target, "utf8").catch(() => null);
|
|
2927
3854
|
if (before === null) throw new KbRecordNotFoundError(conceptId2);
|
|
2928
3855
|
const parsed = this.parse(conceptId2, before);
|
|
2929
3856
|
if (!parsed) throw new KbRecordNotFoundError(conceptId2);
|
|
2930
3857
|
const frontmatter = change(parsed.frontmatter);
|
|
2931
3858
|
const body = changeBody(parsed.body);
|
|
2932
3859
|
const contents = stringifyMarkdownWithFrontmatter(body, frontmatter);
|
|
2933
|
-
const witness = await (0,
|
|
3860
|
+
const witness = await (0, import_promises5.readFile)(target, "utf8").catch(() => null);
|
|
2934
3861
|
if (witness === null || digest(witness) !== digest(before)) {
|
|
2935
3862
|
throw new KbWriteConflictError(conceptId2);
|
|
2936
3863
|
}
|
|
@@ -2956,20 +3883,20 @@ ${answer}
|
|
|
2956
3883
|
*/
|
|
2957
3884
|
async publish(target, contents, overwrite, conceptId2) {
|
|
2958
3885
|
const staging = `${target}.${process.pid}.tmp`;
|
|
2959
|
-
await (0,
|
|
3886
|
+
await (0, import_promises5.writeFile)(staging, contents, "utf8");
|
|
2960
3887
|
try {
|
|
2961
3888
|
if (overwrite) {
|
|
2962
|
-
await (0,
|
|
3889
|
+
await (0, import_promises5.rename)(staging, target);
|
|
2963
3890
|
return;
|
|
2964
3891
|
}
|
|
2965
|
-
await (0,
|
|
3892
|
+
await (0, import_promises5.link)(staging, target);
|
|
2966
3893
|
} catch (error) {
|
|
2967
3894
|
if (error.code === "EEXIST") {
|
|
2968
3895
|
throw new KbRecordAlreadyExistsError(conceptId2);
|
|
2969
3896
|
}
|
|
2970
3897
|
throw error;
|
|
2971
3898
|
} finally {
|
|
2972
|
-
await (0,
|
|
3899
|
+
await (0, import_promises5.unlink)(staging).catch(() => void 0);
|
|
2973
3900
|
}
|
|
2974
3901
|
}
|
|
2975
3902
|
/**
|
|
@@ -3013,20 +3940,30 @@ ${answer}
|
|
|
3013
3940
|
* file must not fail the mutation it guards.
|
|
3014
3941
|
*/
|
|
3015
3942
|
async ensureGitattributes(root) {
|
|
3016
|
-
const target = (0,
|
|
3943
|
+
const target = (0, import_node_path7.join)(root, GITATTRIBUTES_FILE);
|
|
3017
3944
|
try {
|
|
3018
3945
|
let existing;
|
|
3019
3946
|
try {
|
|
3020
|
-
existing = await (0,
|
|
3947
|
+
existing = await (0, import_promises5.readFile)(target, "utf8");
|
|
3021
3948
|
} catch (error) {
|
|
3022
3949
|
if (error.code !== "ENOENT") throw error;
|
|
3023
3950
|
existing = null;
|
|
3024
3951
|
}
|
|
3025
3952
|
if (existing === null) {
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3953
|
+
try {
|
|
3954
|
+
await (0, import_promises5.writeFile)(target, appendUnionMergeLine(""), {
|
|
3955
|
+
encoding: "utf8",
|
|
3956
|
+
flag: "wx"
|
|
3957
|
+
});
|
|
3958
|
+
} catch (error) {
|
|
3959
|
+
if (error.code !== "EEXIST") throw error;
|
|
3960
|
+
this.logger.info?.({
|
|
3961
|
+
operation: "kb.gitattributes.ensure",
|
|
3962
|
+
bundlePath: root,
|
|
3963
|
+
outcome: "exists"
|
|
3964
|
+
});
|
|
3965
|
+
return;
|
|
3966
|
+
}
|
|
3030
3967
|
this.logger.info?.({
|
|
3031
3968
|
operation: "kb.gitattributes.ensure",
|
|
3032
3969
|
bundlePath: root,
|
|
@@ -3035,7 +3972,7 @@ ${answer}
|
|
|
3035
3972
|
return;
|
|
3036
3973
|
}
|
|
3037
3974
|
if (!hasMergeDeclaration(existing)) {
|
|
3038
|
-
await (0,
|
|
3975
|
+
await (0, import_promises5.appendFile)(target, appendUnionMergeLine(existing), "utf8");
|
|
3039
3976
|
this.logger.info?.({
|
|
3040
3977
|
operation: "kb.gitattributes.ensure",
|
|
3041
3978
|
bundlePath: root,
|
|
@@ -3054,7 +3991,7 @@ ${answer}
|
|
|
3054
3991
|
async record(root, entry) {
|
|
3055
3992
|
await this.ensureGitattributes(root);
|
|
3056
3993
|
const line = renderLogEntry({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
|
|
3057
|
-
await (0,
|
|
3994
|
+
await (0, import_promises5.appendFile)((0, import_node_path7.join)(root, LOG_FILE), line, "utf8").catch((error) => {
|
|
3058
3995
|
this.logger.warn?.({
|
|
3059
3996
|
operation: "kb.log.append",
|
|
3060
3997
|
outcome: "failed",
|
|
@@ -3080,18 +4017,18 @@ ${answer}
|
|
|
3080
4017
|
};
|
|
3081
4018
|
}
|
|
3082
4019
|
root(bundlePath2) {
|
|
3083
|
-
return (0,
|
|
4020
|
+
return (0, import_node_path7.resolve)(bundlePath2);
|
|
3084
4021
|
}
|
|
3085
4022
|
// Concept ids are `<type>.<slug>` and map to a single file directly under the
|
|
3086
4023
|
// bundle root; anything carrying a separator would escape it.
|
|
3087
4024
|
recordPath(bundlePath2, conceptId2) {
|
|
3088
|
-
if (conceptId2.includes(
|
|
4025
|
+
if (conceptId2.includes(import_node_path7.sep) || conceptId2.includes("/")) {
|
|
3089
4026
|
throw new KbInvalidConceptIdError(
|
|
3090
4027
|
"concept id must not contain a path separator",
|
|
3091
4028
|
{ conceptId: conceptId2 }
|
|
3092
4029
|
);
|
|
3093
4030
|
}
|
|
3094
|
-
return (0,
|
|
4031
|
+
return (0, import_node_path7.join)(this.root(bundlePath2), `${conceptId2}.md`);
|
|
3095
4032
|
}
|
|
3096
4033
|
};
|
|
3097
4034
|
function estimateTokens(record) {
|
|
@@ -3102,6 +4039,14 @@ function estimateTokens(record) {
|
|
|
3102
4039
|
function estimateStubTokens(entry) {
|
|
3103
4040
|
return Math.ceil(JSON.stringify(entry).length / 4);
|
|
3104
4041
|
}
|
|
4042
|
+
function refusalMessage(refusal) {
|
|
4043
|
+
const scope = refusal.type ? ` of type ${refusal.type}` : "";
|
|
4044
|
+
return [
|
|
4045
|
+
`Refusing to load this base whole: ~${refusal.approxTokens} tokens is past the ${refusal.budgetTokens}-token budget.`,
|
|
4046
|
+
`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.`,
|
|
4047
|
+
`To load anyway: raise budgetTokens (currently ${refusal.budgetTokens}), or all=true to bypass the budget.`
|
|
4048
|
+
].join(" ");
|
|
4049
|
+
}
|
|
3105
4050
|
function stub(hit) {
|
|
3106
4051
|
return {
|
|
3107
4052
|
conceptId: hit.record.conceptId,
|
|
@@ -3122,11 +4067,11 @@ function normalizeActor(id) {
|
|
|
3122
4067
|
return id.slice(0, colon + 1).toLowerCase() + id.slice(colon + 1);
|
|
3123
4068
|
}
|
|
3124
4069
|
function digest(contents) {
|
|
3125
|
-
return (0,
|
|
4070
|
+
return (0, import_node_crypto2.createHash)("sha256").update(contents).digest("hex");
|
|
3126
4071
|
}
|
|
3127
4072
|
|
|
3128
4073
|
// src/version.ts
|
|
3129
|
-
var VERSION = true ? "0.1.
|
|
4074
|
+
var VERSION = true ? "0.1.11" : "0.0.0-dev";
|
|
3130
4075
|
|
|
3131
4076
|
// src/cli.ts
|
|
3132
4077
|
async function runKbCli(argv) {
|
|
@@ -3185,18 +4130,18 @@ function takeLiteral(argv) {
|
|
|
3185
4130
|
function takeBundle(argv) {
|
|
3186
4131
|
const at = argv.indexOf("--bundle");
|
|
3187
4132
|
if (at === -1) {
|
|
3188
|
-
return { bundle: (0,
|
|
4133
|
+
return { bundle: (0, import_node_path8.join)(process.cwd(), KB_DIR), rest: argv };
|
|
3189
4134
|
}
|
|
3190
4135
|
const bundle = argv[at + 1];
|
|
3191
4136
|
if (!bundle) die("--bundle requires a path");
|
|
3192
4137
|
return { bundle, rest: [...argv.slice(0, at), ...argv.slice(at + 2)] };
|
|
3193
4138
|
}
|
|
3194
4139
|
function readStdin() {
|
|
3195
|
-
return new Promise((
|
|
4140
|
+
return new Promise((resolve6, reject) => {
|
|
3196
4141
|
let text = "";
|
|
3197
4142
|
process.stdin.setEncoding("utf8");
|
|
3198
4143
|
process.stdin.on("data", (chunk) => text += chunk);
|
|
3199
|
-
process.stdin.on("end", () =>
|
|
4144
|
+
process.stdin.on("end", () => resolve6(text));
|
|
3200
4145
|
process.stdin.on("error", reject);
|
|
3201
4146
|
});
|
|
3202
4147
|
}
|