@saasontools/strauss-kb 0.1.10 → 0.1.12
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 +96 -27
- package/dist/{chunk-EJQPZWN5.js → chunk-33ZCBEUV.js} +1183 -375
- package/dist/chunk-33ZCBEUV.js.map +1 -0
- package/dist/{chunk-NMTP7V7E.js → chunk-EXKK2KUN.js} +2 -2
- package/dist/{chunk-RGK3K6LN.js → chunk-F2U2YLWV.js} +2 -2
- package/dist/cli-main.cjs +1195 -395
- package/dist/cli-main.cjs.map +1 -1
- package/dist/cli-main.js +2 -2
- package/dist/index.cjs +991 -180
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +204 -10
- package/dist/index.d.ts +204 -10
- package/dist/index.js +13 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp-main.cjs +1190 -390
- package/dist/mcp-main.cjs.map +1 -1
- package/dist/mcp-main.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-EJQPZWN5.js.map +0 -1
- /package/dist/{chunk-NMTP7V7E.js.map → chunk-EXKK2KUN.js.map} +0 -0
- /package/dist/{chunk-RGK3K6LN.js.map → chunk-F2U2YLWV.js.map} +0 -0
|
@@ -18,7 +18,31 @@ var kbVerifiedEventSchema = kbActorStampSchema.extend({
|
|
|
18
18
|
});
|
|
19
19
|
var kbAnchorSchema = z.object({
|
|
20
20
|
file: z.string().min(1),
|
|
21
|
-
symbol: z.string().min(1).optional()
|
|
21
|
+
symbol: z.string().min(1).optional(),
|
|
22
|
+
/**
|
|
23
|
+
* Which repository the file lives in — a remote URL
|
|
24
|
+
* (`https://github.com/org/name`) or a short name. Absent means the base's
|
|
25
|
+
* own repository, which is what nearly every anchor means.
|
|
26
|
+
*
|
|
27
|
+
* Unvalidated beyond not-blank: one repository has many spellings.
|
|
28
|
+
* Matched after normalisation; see ARCHITECTURE.
|
|
29
|
+
*/
|
|
30
|
+
repo: z.string().trim().min(1).optional(),
|
|
31
|
+
/**
|
|
32
|
+
* The git rev the evidence was taken at. Prefer a commit SHA: a branch
|
|
33
|
+
* name is a moving pointer, so an anchor pinned to one says the evidence
|
|
34
|
+
* came from wherever that branch happens to be now, which is not a
|
|
35
|
+
* baseline. Recorded and preserved in v1; ref-pinned reads land with
|
|
36
|
+
* SAA-709.
|
|
37
|
+
*/
|
|
38
|
+
ref: z.string().trim().min(1).optional(),
|
|
39
|
+
hash: z.string().regex(/^sha256:[0-9a-f]{64}$/, {
|
|
40
|
+
message: "hash must be sha256:<64 hex chars>"
|
|
41
|
+
}).optional(),
|
|
42
|
+
/** ISO 8601 timestamp of the last successful resolution. */
|
|
43
|
+
resolved_at: z.string().min(1).optional(),
|
|
44
|
+
/** Line count of the text the hash was taken over. */
|
|
45
|
+
lines: z.number().int().positive().optional()
|
|
22
46
|
}).strict();
|
|
23
47
|
var KB_RECORD_TYPES = [
|
|
24
48
|
"fact",
|
|
@@ -295,6 +319,609 @@ function selectDecisions(records) {
|
|
|
295
319
|
);
|
|
296
320
|
}
|
|
297
321
|
|
|
322
|
+
// src/anchor-resolver.ts
|
|
323
|
+
import { execFile } from "child_process";
|
|
324
|
+
import { createHash } from "crypto";
|
|
325
|
+
import { readFile, realpath, stat } from "fs/promises";
|
|
326
|
+
import { isAbsolute, relative, resolve, sep } from "path";
|
|
327
|
+
import { promisify } from "util";
|
|
328
|
+
|
|
329
|
+
// src/concurrency.ts
|
|
330
|
+
var DEFAULT_IO_CONCURRENCY = 16;
|
|
331
|
+
async function mapLimit(items, limit, fn) {
|
|
332
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
333
|
+
throw new RangeError(
|
|
334
|
+
`mapLimit: "limit" must be a positive integer, got ${limit}`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
const out = new Array(items.length);
|
|
338
|
+
let next = 0;
|
|
339
|
+
let failed = false;
|
|
340
|
+
const runners = Array.from(
|
|
341
|
+
{ length: Math.min(limit, items.length) },
|
|
342
|
+
async () => {
|
|
343
|
+
while (!failed && next < items.length) {
|
|
344
|
+
const at = next++;
|
|
345
|
+
try {
|
|
346
|
+
out[at] = await fn(items[at], at);
|
|
347
|
+
} catch (error) {
|
|
348
|
+
failed = true;
|
|
349
|
+
throw error;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
);
|
|
354
|
+
await Promise.all(runners);
|
|
355
|
+
return out;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/anchor-resolver.ts
|
|
359
|
+
var execFileAsync = promisify(execFile);
|
|
360
|
+
var MAX_ANCHOR_FILE_BYTES = 1048576;
|
|
361
|
+
var PARENT_SCOPE_LINES = 50;
|
|
362
|
+
var CLEAN_STATE = { blockComment: false, template: false };
|
|
363
|
+
function stripLine(line, state) {
|
|
364
|
+
let out = "";
|
|
365
|
+
let index = 0;
|
|
366
|
+
let { blockComment, template } = state;
|
|
367
|
+
while (index < line.length) {
|
|
368
|
+
const char = line[index];
|
|
369
|
+
const next = line[index + 1];
|
|
370
|
+
if (blockComment) {
|
|
371
|
+
if (char === "*" && next === "/") {
|
|
372
|
+
blockComment = false;
|
|
373
|
+
index += 2;
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
index += 1;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (template) {
|
|
380
|
+
if (char === "\\") {
|
|
381
|
+
index += 2;
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (char === "`") template = false;
|
|
385
|
+
index += 1;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (char === "/" && next === "*") {
|
|
389
|
+
blockComment = true;
|
|
390
|
+
index += 2;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
if (char === "/" && next === "/") break;
|
|
394
|
+
if (char === "`") {
|
|
395
|
+
template = true;
|
|
396
|
+
index += 1;
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (char === "'" || char === '"') {
|
|
400
|
+
const quote = char;
|
|
401
|
+
index += 1;
|
|
402
|
+
while (index < line.length) {
|
|
403
|
+
if (line[index] === "\\") {
|
|
404
|
+
index += 2;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
if (line[index] === quote) {
|
|
408
|
+
index += 1;
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
index += 1;
|
|
412
|
+
}
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
out += char;
|
|
416
|
+
index += 1;
|
|
417
|
+
}
|
|
418
|
+
return { code: out, state: { blockComment, template } };
|
|
419
|
+
}
|
|
420
|
+
function span(lines, from, to) {
|
|
421
|
+
return {
|
|
422
|
+
text: lines.slice(from, to + 1).join("\n"),
|
|
423
|
+
startLine: from + 1,
|
|
424
|
+
endLine: to + 1
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
function captureBraceBlock(lines, matchLine) {
|
|
428
|
+
let depth = 0;
|
|
429
|
+
let opened = false;
|
|
430
|
+
let state = CLEAN_STATE;
|
|
431
|
+
for (let index = matchLine; index < lines.length; index++) {
|
|
432
|
+
const stripped = stripLine(lines[index] ?? "", state);
|
|
433
|
+
state = stripped.state;
|
|
434
|
+
for (const char of stripped.code) {
|
|
435
|
+
if (char === "{") {
|
|
436
|
+
depth += 1;
|
|
437
|
+
opened = true;
|
|
438
|
+
} else if (char === "}") {
|
|
439
|
+
depth = Math.max(0, depth - 1);
|
|
440
|
+
} else if (char === ";" && !opened) {
|
|
441
|
+
return span(lines, matchLine, index);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (opened && depth === 0) return span(lines, matchLine, index);
|
|
445
|
+
}
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
var PYTHON_HEADER = /^\s*(?:async\s+)?(?:def|class)\s+[A-Za-z_]\w*\s*[(:]/;
|
|
449
|
+
function captureIndentedBlock(lines, matchLine) {
|
|
450
|
+
const header = lines[matchLine] ?? "";
|
|
451
|
+
const indent = header.length - header.trimStart().length;
|
|
452
|
+
let headerEnd = -1;
|
|
453
|
+
for (let index = matchLine; index < lines.length && index <= matchLine + 20; index++) {
|
|
454
|
+
const code = stripLine(lines[index] ?? "", CLEAN_STATE).code.trimEnd();
|
|
455
|
+
if (code.endsWith(":")) {
|
|
456
|
+
headerEnd = index;
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
if (code.includes(":")) return span(lines, matchLine, index);
|
|
460
|
+
}
|
|
461
|
+
if (headerEnd === -1) return null;
|
|
462
|
+
let end = headerEnd;
|
|
463
|
+
for (let index = headerEnd + 1; index < lines.length; index++) {
|
|
464
|
+
const line = lines[index] ?? "";
|
|
465
|
+
if (line.trim() === "") continue;
|
|
466
|
+
const lineIndent = line.length - line.trimStart().length;
|
|
467
|
+
if (lineIndent <= indent) break;
|
|
468
|
+
end = index;
|
|
469
|
+
}
|
|
470
|
+
return end === headerEnd ? null : span(lines, matchLine, end);
|
|
471
|
+
}
|
|
472
|
+
var TIERS = [
|
|
473
|
+
(name) => new RegExp(
|
|
474
|
+
`(?:function|class|interface|type|enum|const|let|var|def)\\s+${name}\\b`
|
|
475
|
+
),
|
|
476
|
+
(name) => new RegExp(`\\b${name}\\s*[:=]`),
|
|
477
|
+
(name) => new RegExp(`\\b${name}\\s*\\(`),
|
|
478
|
+
(name) => new RegExp(`\\b${name}\\b`)
|
|
479
|
+
];
|
|
480
|
+
var regexResolver = {
|
|
481
|
+
name: "regex",
|
|
482
|
+
resolve(source, symbol) {
|
|
483
|
+
const segments = symbol.split(".");
|
|
484
|
+
const name = segments[segments.length - 1];
|
|
485
|
+
if (!name) return null;
|
|
486
|
+
const parent = segments.length > 1 ? segments[segments.length - 2] : void 0;
|
|
487
|
+
const escaped = escapeRegExp(name);
|
|
488
|
+
const parentPattern = parent ? new RegExp(`\\b${escapeRegExp(parent)}\\b`) : null;
|
|
489
|
+
const lines = source.split("\n");
|
|
490
|
+
for (const tier of TIERS) {
|
|
491
|
+
const pattern = tier(escaped);
|
|
492
|
+
let candidates = lines.map((line, index) => ({ line, index })).filter((entry) => pattern.test(entry.line)).map((entry) => entry.index);
|
|
493
|
+
if (!candidates.length) continue;
|
|
494
|
+
if (parentPattern && candidates.length > 1) {
|
|
495
|
+
const distances = candidates.map(
|
|
496
|
+
(index) => distanceToParent(lines, index, parentPattern)
|
|
497
|
+
);
|
|
498
|
+
const nearest = Math.min(...distances);
|
|
499
|
+
if (Number.isFinite(nearest)) {
|
|
500
|
+
candidates = candidates.filter((_, at) => distances[at] === nearest);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (candidates.length !== 1) return null;
|
|
504
|
+
const matchLine = candidates[0];
|
|
505
|
+
return PYTHON_HEADER.test(lines[matchLine] ?? "") ? captureIndentedBlock(lines, matchLine) : captureBraceBlock(lines, matchLine);
|
|
506
|
+
}
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
function escapeRegExp(value) {
|
|
511
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
512
|
+
}
|
|
513
|
+
function distanceToParent(lines, index, parent) {
|
|
514
|
+
const floor = Math.max(0, index - PARENT_SCOPE_LINES);
|
|
515
|
+
for (let at = index; at >= floor; at--) {
|
|
516
|
+
if (parent.test(lines[at] ?? "")) return index - at;
|
|
517
|
+
}
|
|
518
|
+
return Number.POSITIVE_INFINITY;
|
|
519
|
+
}
|
|
520
|
+
function hashAnchorText(text) {
|
|
521
|
+
return `sha256:${createHash("sha256").update(text.replace(/\r\n/g, "\n")).digest("hex")}`;
|
|
522
|
+
}
|
|
523
|
+
function resolveAnchor(source, anchor, resolver = regexResolver) {
|
|
524
|
+
const normalized = source.replace(/\r\n/g, "\n");
|
|
525
|
+
if (!anchor.symbol) {
|
|
526
|
+
const lines = normalized.split("\n");
|
|
527
|
+
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
528
|
+
return {
|
|
529
|
+
text: normalized,
|
|
530
|
+
startLine: 1,
|
|
531
|
+
endLine: Math.max(1, lines.length)
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
return resolver.resolve(normalized, anchor.symbol);
|
|
535
|
+
}
|
|
536
|
+
function anchorFilePath(repoRoot, file) {
|
|
537
|
+
const path = resolve(repoRoot, file.replace(/^\.\//, ""));
|
|
538
|
+
const rel = relative(resolve(repoRoot), path);
|
|
539
|
+
if (rel === "" || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
return path;
|
|
543
|
+
}
|
|
544
|
+
function contains(root, path) {
|
|
545
|
+
const rel = relative(root, path);
|
|
546
|
+
return rel !== "" && rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
547
|
+
}
|
|
548
|
+
function normalizeRepoUrl(value) {
|
|
549
|
+
let url = value.trim().replace(/^git\+/, "");
|
|
550
|
+
const scp = /^[\w.-]+@([\w.-]+):(.+)$/.exec(url);
|
|
551
|
+
if (scp) url = `https://${scp[1]}/${scp[2]}`;
|
|
552
|
+
url = url.replace(/^ssh:\/\/(?:[^@/]+@)?/, "https://");
|
|
553
|
+
url = trimTrailingSlashes(url);
|
|
554
|
+
if (url.endsWith(".git")) url = url.slice(0, -4);
|
|
555
|
+
return trimTrailingSlashes(url).toLowerCase();
|
|
556
|
+
}
|
|
557
|
+
function trimTrailingSlashes(value) {
|
|
558
|
+
let end = value.length;
|
|
559
|
+
while (end > 0 && value[end - 1] === "/") end -= 1;
|
|
560
|
+
return value.slice(0, end);
|
|
561
|
+
}
|
|
562
|
+
function repoPath(normalized) {
|
|
563
|
+
const withoutScheme = normalized.replace(/^[a-z0-9+.-]+:\/\//, "");
|
|
564
|
+
const segments = withoutScheme.split("/").filter(Boolean);
|
|
565
|
+
return segments.length > 1 ? segments.slice(1).join("/") : "";
|
|
566
|
+
}
|
|
567
|
+
function repoIdentifies(declared, originUrl) {
|
|
568
|
+
if (!originUrl) return false;
|
|
569
|
+
const origin = normalizeRepoUrl(originUrl);
|
|
570
|
+
const want = normalizeRepoUrl(declared);
|
|
571
|
+
if (!want || !origin) return false;
|
|
572
|
+
if (want === origin) return true;
|
|
573
|
+
const path = repoPath(origin);
|
|
574
|
+
if (!path) return false;
|
|
575
|
+
return want === path || want === (path.split("/").pop() ?? "");
|
|
576
|
+
}
|
|
577
|
+
async function repoOriginUrl(repoRoot) {
|
|
578
|
+
try {
|
|
579
|
+
const { stdout } = await execFileAsync(
|
|
580
|
+
"git",
|
|
581
|
+
["-C", repoRoot, "config", "--get", "remote.origin.url"],
|
|
582
|
+
{ timeout: 5e3 }
|
|
583
|
+
);
|
|
584
|
+
return stdout.trim() || null;
|
|
585
|
+
} catch {
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
var LazyOrigin = class {
|
|
590
|
+
constructor(repoRoot) {
|
|
591
|
+
this.repoRoot = repoRoot;
|
|
592
|
+
}
|
|
593
|
+
repoRoot;
|
|
594
|
+
url = null;
|
|
595
|
+
asked = false;
|
|
596
|
+
/** Asks git once, so later `isForeign` calls need no await. */
|
|
597
|
+
async prime() {
|
|
598
|
+
if (this.asked) return;
|
|
599
|
+
this.url = await repoOriginUrl(this.repoRoot);
|
|
600
|
+
this.asked = true;
|
|
601
|
+
}
|
|
602
|
+
/** Only meaningful after `prime`; an unprimed origin identifies nothing. */
|
|
603
|
+
isForeign(anchor) {
|
|
604
|
+
if (!anchor.repo) return false;
|
|
605
|
+
return !repoIdentifies(anchor.repo, this.url);
|
|
606
|
+
}
|
|
607
|
+
async foreign(anchor) {
|
|
608
|
+
if (!anchor.repo) return false;
|
|
609
|
+
await this.prime();
|
|
610
|
+
return this.isForeign(anchor);
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
function errorCode(error) {
|
|
614
|
+
return typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0;
|
|
615
|
+
}
|
|
616
|
+
function anchorFileReader(repoRoot) {
|
|
617
|
+
let rootOnce;
|
|
618
|
+
const realRoot = () => {
|
|
619
|
+
rootOnce ??= realpath(resolve(repoRoot)).catch((error) => {
|
|
620
|
+
rootOnce = void 0;
|
|
621
|
+
throw error;
|
|
622
|
+
});
|
|
623
|
+
return rootOnce;
|
|
624
|
+
};
|
|
625
|
+
return (file) => readAnchorFileWithRoot(repoRoot, file, realRoot);
|
|
626
|
+
}
|
|
627
|
+
async function readAnchorFileWithRoot(repoRoot, file, realRoot) {
|
|
628
|
+
const lexical = anchorFilePath(repoRoot, file);
|
|
629
|
+
if (lexical === null) return { ok: false, reason: "outside-repo" };
|
|
630
|
+
let root;
|
|
631
|
+
let path;
|
|
632
|
+
try {
|
|
633
|
+
root = await realRoot();
|
|
634
|
+
path = await realpath(lexical);
|
|
635
|
+
} catch (error) {
|
|
636
|
+
const code = errorCode(error);
|
|
637
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
638
|
+
return { ok: false, reason: "file-missing" };
|
|
639
|
+
}
|
|
640
|
+
return { ok: false, reason: "file-unreadable" };
|
|
641
|
+
}
|
|
642
|
+
if (!contains(root, path)) return { ok: false, reason: "outside-repo" };
|
|
643
|
+
try {
|
|
644
|
+
const stats = await stat(path);
|
|
645
|
+
if (!stats.isFile()) return { ok: false, reason: "file-unreadable" };
|
|
646
|
+
if (stats.size > MAX_ANCHOR_FILE_BYTES) {
|
|
647
|
+
return { ok: false, reason: "file-too-large" };
|
|
648
|
+
}
|
|
649
|
+
return { ok: true, source: await readFile(path, "utf8") };
|
|
650
|
+
} catch (error) {
|
|
651
|
+
const code = errorCode(error);
|
|
652
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
653
|
+
return { ok: false, reason: "file-missing" };
|
|
654
|
+
}
|
|
655
|
+
return { ok: false, reason: "file-unreadable" };
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
function looksLikeWrongRepoRoot(drift) {
|
|
659
|
+
let checked = 0;
|
|
660
|
+
for (const entries of drift.values()) {
|
|
661
|
+
for (const entry of entries) {
|
|
662
|
+
if (entry.reason === "foreign-repo") continue;
|
|
663
|
+
checked += 1;
|
|
664
|
+
if (entry.state !== "unresolved" || entry.reason !== "file-missing") {
|
|
665
|
+
return false;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return checked > 0;
|
|
670
|
+
}
|
|
671
|
+
async function readAnchorFiles(files, read, concurrency = DEFAULT_IO_CONCURRENCY) {
|
|
672
|
+
if (!Number.isInteger(concurrency) || concurrency < 1) {
|
|
673
|
+
throw new RangeError(
|
|
674
|
+
`readAnchorFiles: option "concurrency" must be a positive integer, got ${concurrency}`
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
const wanted = [...new Set(files)];
|
|
678
|
+
const results = await mapLimit(wanted, concurrency, async (file) => {
|
|
679
|
+
try {
|
|
680
|
+
return await read(file);
|
|
681
|
+
} catch {
|
|
682
|
+
return { ok: false, reason: "file-unreadable" };
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
return new Map(wanted.map((file, at) => [file, results[at]]));
|
|
686
|
+
}
|
|
687
|
+
async function detectAnchorDrift(records, options = {}) {
|
|
688
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
689
|
+
const resolver = options.resolver ?? regexResolver;
|
|
690
|
+
const origin = new LazyOrigin(repoRoot);
|
|
691
|
+
const planned = /* @__PURE__ */ new Map();
|
|
692
|
+
let declaresRepo = false;
|
|
693
|
+
for (const record of records) {
|
|
694
|
+
const anchors = (record.frontmatter.strauss_anchors ?? []).filter(
|
|
695
|
+
(anchor) => anchor.hash
|
|
696
|
+
);
|
|
697
|
+
if (!anchors.length) continue;
|
|
698
|
+
if (anchors.some((anchor) => anchor.repo)) declaresRepo = true;
|
|
699
|
+
planned.set(
|
|
700
|
+
record.conceptId,
|
|
701
|
+
anchors.map((anchor) => ({ anchor, foreign: false }))
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
if (declaresRepo) {
|
|
705
|
+
await origin.prime();
|
|
706
|
+
for (const entries of planned.values()) {
|
|
707
|
+
for (const entry of entries)
|
|
708
|
+
entry.foreign = origin.isForeign(entry.anchor);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
const files = [];
|
|
712
|
+
for (const entries of planned.values()) {
|
|
713
|
+
for (const entry of entries) {
|
|
714
|
+
if (!entry.foreign) files.push(entry.anchor.file);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
const reads = await readAnchorFiles(
|
|
718
|
+
files,
|
|
719
|
+
options.reader ?? anchorFileReader(repoRoot),
|
|
720
|
+
options.concurrency ?? DEFAULT_IO_CONCURRENCY
|
|
721
|
+
);
|
|
722
|
+
const drift = /* @__PURE__ */ new Map();
|
|
723
|
+
for (const record of records) {
|
|
724
|
+
const entries = [];
|
|
725
|
+
for (const { anchor, foreign } of planned.get(record.conceptId) ?? []) {
|
|
726
|
+
const base = {
|
|
727
|
+
file: anchor.file,
|
|
728
|
+
...anchor.symbol ? { symbol: anchor.symbol } : {},
|
|
729
|
+
storedHash: anchor.hash
|
|
730
|
+
};
|
|
731
|
+
if (foreign) {
|
|
732
|
+
entries.push({
|
|
733
|
+
...base,
|
|
734
|
+
state: "unresolved",
|
|
735
|
+
diffSize: null,
|
|
736
|
+
reason: "foreign-repo"
|
|
737
|
+
});
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
const read = reads.get(anchor.file);
|
|
741
|
+
if (!read.ok) {
|
|
742
|
+
entries.push({
|
|
743
|
+
...base,
|
|
744
|
+
state: "unresolved",
|
|
745
|
+
diffSize: null,
|
|
746
|
+
reason: read.reason
|
|
747
|
+
});
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
const resolved = resolveAnchor(read.source, anchor, resolver);
|
|
751
|
+
if (!resolved) {
|
|
752
|
+
entries.push({
|
|
753
|
+
...base,
|
|
754
|
+
state: "unresolved",
|
|
755
|
+
diffSize: null,
|
|
756
|
+
reason: "symbol-not-found"
|
|
757
|
+
});
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
const currentHash = hashAnchorText(resolved.text);
|
|
761
|
+
const currentLines = resolved.endLine - resolved.startLine + 1;
|
|
762
|
+
entries.push({
|
|
763
|
+
...base,
|
|
764
|
+
state: currentHash === anchor.hash ? "match" : "drifted",
|
|
765
|
+
currentHash,
|
|
766
|
+
diffSize: anchor.lines === void 0 ? null : Math.abs(currentLines - anchor.lines)
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
if (entries.length) drift.set(record.conceptId, entries);
|
|
770
|
+
}
|
|
771
|
+
return drift;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// src/errors.ts
|
|
775
|
+
var Fault = /* @__PURE__ */ ((Fault2) => {
|
|
776
|
+
Fault2["Configuration"] = "Configuration";
|
|
777
|
+
Fault2["System"] = "System";
|
|
778
|
+
Fault2["User"] = "User";
|
|
779
|
+
return Fault2;
|
|
780
|
+
})(Fault || {});
|
|
781
|
+
var ErrorTypes = /* @__PURE__ */ ((ErrorTypes2) => {
|
|
782
|
+
ErrorTypes2["KbRecordAlreadyExists"] = "KbRecordAlreadyExists";
|
|
783
|
+
ErrorTypes2["KbInvalidConceptId"] = "KbInvalidConceptId";
|
|
784
|
+
ErrorTypes2["KbMissingFlagValue"] = "KbMissingFlagValue";
|
|
785
|
+
ErrorTypes2["KbPackBudgetExceeded"] = "KbPackBudgetExceeded";
|
|
786
|
+
ErrorTypes2["KbRecordNotFound"] = "KbRecordNotFound";
|
|
787
|
+
ErrorTypes2["KbSelfVerification"] = "KbSelfVerification";
|
|
788
|
+
ErrorTypes2["KbWriteConflict"] = "KbWriteConflict";
|
|
789
|
+
return ErrorTypes2;
|
|
790
|
+
})(ErrorTypes || {});
|
|
791
|
+
var BaseError = class extends Error {
|
|
792
|
+
code;
|
|
793
|
+
errorType;
|
|
794
|
+
fault;
|
|
795
|
+
retriable;
|
|
796
|
+
reportToUser;
|
|
797
|
+
details;
|
|
798
|
+
constructor(props) {
|
|
799
|
+
super(props.message);
|
|
800
|
+
this.name = props.name ?? this.constructor.name;
|
|
801
|
+
this.code = props.code ?? 500;
|
|
802
|
+
this.errorType = props.errorType;
|
|
803
|
+
this.fault = props.fault;
|
|
804
|
+
this.retriable = props.retriable ?? true;
|
|
805
|
+
this.reportToUser = props.reportToUser ?? false;
|
|
806
|
+
this.details = props.details;
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
// src/kb-errors.ts
|
|
811
|
+
var KbRecordAlreadyExistsError = class extends BaseError {
|
|
812
|
+
constructor(conceptId2) {
|
|
813
|
+
super({
|
|
814
|
+
message: `kb: ${conceptId2} already exists \u2014 choose a more specific slug, or write with overwrite`,
|
|
815
|
+
errorType: "KbRecordAlreadyExists" /* KbRecordAlreadyExists */,
|
|
816
|
+
code: 409,
|
|
817
|
+
fault: "User" /* User */,
|
|
818
|
+
retriable: false,
|
|
819
|
+
reportToUser: true,
|
|
820
|
+
details: { conceptId: conceptId2, action: "refused" }
|
|
821
|
+
});
|
|
822
|
+
this.conceptId = conceptId2;
|
|
823
|
+
}
|
|
824
|
+
conceptId;
|
|
825
|
+
};
|
|
826
|
+
var KbRecordNotFoundError = class extends BaseError {
|
|
827
|
+
constructor(conceptId2) {
|
|
828
|
+
super({
|
|
829
|
+
message: `kb: ${conceptId2} does not exist`,
|
|
830
|
+
errorType: "KbRecordNotFound" /* KbRecordNotFound */,
|
|
831
|
+
code: 404,
|
|
832
|
+
fault: "User" /* User */,
|
|
833
|
+
retriable: false,
|
|
834
|
+
reportToUser: true,
|
|
835
|
+
details: { conceptId: conceptId2 }
|
|
836
|
+
});
|
|
837
|
+
this.conceptId = conceptId2;
|
|
838
|
+
}
|
|
839
|
+
conceptId;
|
|
840
|
+
};
|
|
841
|
+
var KbWriteConflictError = class extends BaseError {
|
|
842
|
+
constructor(conceptId2) {
|
|
843
|
+
super({
|
|
844
|
+
message: `kb: ${conceptId2} changed while it was being updated \u2014 re-read and retry`,
|
|
845
|
+
errorType: "KbWriteConflict" /* KbWriteConflict */,
|
|
846
|
+
code: 409,
|
|
847
|
+
fault: "System" /* System */,
|
|
848
|
+
retriable: true,
|
|
849
|
+
reportToUser: true,
|
|
850
|
+
details: { conceptId: conceptId2 }
|
|
851
|
+
});
|
|
852
|
+
this.conceptId = conceptId2;
|
|
853
|
+
}
|
|
854
|
+
conceptId;
|
|
855
|
+
};
|
|
856
|
+
var KbSelfVerificationError = class extends BaseError {
|
|
857
|
+
constructor(conceptId2, actor, generatedBy) {
|
|
858
|
+
super({
|
|
859
|
+
message: `kb: ${conceptId2} was generated by ${generatedBy}, and a record's generator cannot verify it \u2014 only a human or a different actor can`,
|
|
860
|
+
errorType: "KbSelfVerification" /* KbSelfVerification */,
|
|
861
|
+
code: 400,
|
|
862
|
+
fault: "User" /* User */,
|
|
863
|
+
retriable: false,
|
|
864
|
+
reportToUser: true,
|
|
865
|
+
details: { conceptId: conceptId2, actor, generatedBy, action: "refused" }
|
|
866
|
+
});
|
|
867
|
+
this.conceptId = conceptId2;
|
|
868
|
+
this.actor = actor;
|
|
869
|
+
this.generatedBy = generatedBy;
|
|
870
|
+
}
|
|
871
|
+
conceptId;
|
|
872
|
+
actor;
|
|
873
|
+
generatedBy;
|
|
874
|
+
};
|
|
875
|
+
var KbPackBudgetExceededError = class extends BaseError {
|
|
876
|
+
constructor(recordCount, approxTokens2, budgetTokens, excluded) {
|
|
877
|
+
super({
|
|
878
|
+
message: `kb: a pack of ${recordCount} records is ~${approxTokens2} tokens against a budget of ${budgetTokens} \u2014 lower hops or maxNodes, or raise the budget`,
|
|
879
|
+
errorType: "KbPackBudgetExceeded" /* KbPackBudgetExceeded */,
|
|
880
|
+
code: 400,
|
|
881
|
+
fault: "User" /* User */,
|
|
882
|
+
retriable: false,
|
|
883
|
+
reportToUser: true,
|
|
884
|
+
details: { recordCount, approxTokens: approxTokens2, budgetTokens, excluded }
|
|
885
|
+
});
|
|
886
|
+
this.recordCount = recordCount;
|
|
887
|
+
this.approxTokens = approxTokens2;
|
|
888
|
+
this.budgetTokens = budgetTokens;
|
|
889
|
+
this.excluded = excluded;
|
|
890
|
+
}
|
|
891
|
+
recordCount;
|
|
892
|
+
approxTokens;
|
|
893
|
+
budgetTokens;
|
|
894
|
+
excluded;
|
|
895
|
+
};
|
|
896
|
+
var KbMissingFlagValueError = class extends BaseError {
|
|
897
|
+
constructor(flag) {
|
|
898
|
+
super({
|
|
899
|
+
message: `kb: ${flag} needs a value \u2014 pass ${flag} <value> or ${flag}=<value>`,
|
|
900
|
+
errorType: "KbMissingFlagValue" /* KbMissingFlagValue */,
|
|
901
|
+
code: 400,
|
|
902
|
+
fault: "User" /* User */,
|
|
903
|
+
retriable: false,
|
|
904
|
+
reportToUser: true,
|
|
905
|
+
details: { flag }
|
|
906
|
+
});
|
|
907
|
+
this.flag = flag;
|
|
908
|
+
}
|
|
909
|
+
flag;
|
|
910
|
+
};
|
|
911
|
+
var KbInvalidConceptIdError = class extends BaseError {
|
|
912
|
+
constructor(message, details) {
|
|
913
|
+
super({
|
|
914
|
+
message: `kb: ${message}`,
|
|
915
|
+
errorType: "KbInvalidConceptId" /* KbInvalidConceptId */,
|
|
916
|
+
code: 400,
|
|
917
|
+
fault: "User" /* User */,
|
|
918
|
+
retriable: false,
|
|
919
|
+
reportToUser: true,
|
|
920
|
+
details
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
|
|
298
925
|
// src/kb-pins/budgets.ts
|
|
299
926
|
function asBudgets(value) {
|
|
300
927
|
if (value === null || typeof value !== "object") return {};
|
|
@@ -393,14 +1020,14 @@ var pinsManifestSchema = z4.object({
|
|
|
393
1020
|
}).passthrough();
|
|
394
1021
|
|
|
395
1022
|
// src/kb-pins/layers.ts
|
|
396
|
-
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
1023
|
+
import { mkdir, readFile as readFile2, writeFile } from "fs/promises";
|
|
397
1024
|
import { homedir } from "os";
|
|
398
|
-
import { dirname, isAbsolute, join as join2, relative, resolve, sep } from "path";
|
|
1025
|
+
import { dirname, isAbsolute as isAbsolute2, join as join2, relative as relative2, resolve as resolve2, sep as sep2 } from "path";
|
|
399
1026
|
function userRoot() {
|
|
400
1027
|
return process.env.STRAUSS_KB_USER_ROOT || homedir();
|
|
401
1028
|
}
|
|
402
1029
|
function layerRoot(workspaceDir, layer) {
|
|
403
|
-
return layer === "user" ? userRoot() :
|
|
1030
|
+
return layer === "user" ? userRoot() : resolve2(workspaceDir);
|
|
404
1031
|
}
|
|
405
1032
|
function layerFile(workspaceDir, layer) {
|
|
406
1033
|
return join2(
|
|
@@ -412,7 +1039,7 @@ async function readPinsLayer(workspaceDir, layer) {
|
|
|
412
1039
|
const file = layerFile(workspaceDir, layer);
|
|
413
1040
|
let raw;
|
|
414
1041
|
try {
|
|
415
|
-
raw = await
|
|
1042
|
+
raw = await readFile2(file, "utf8");
|
|
416
1043
|
} catch {
|
|
417
1044
|
return { pins: [] };
|
|
418
1045
|
}
|
|
@@ -441,11 +1068,11 @@ async function writePinsLayer(workspaceDir, layer, manifest) {
|
|
|
441
1068
|
`, "utf8");
|
|
442
1069
|
}
|
|
443
1070
|
function resolvePinPath(rootDir, path) {
|
|
444
|
-
return
|
|
1071
|
+
return isAbsolute2(path) ? resolve2(path) : resolve2(rootDir, path.split("/").join(sep2));
|
|
445
1072
|
}
|
|
446
1073
|
function storablePath(rootDir, bundlePath2) {
|
|
447
|
-
const rel =
|
|
448
|
-
return (rel === "" ? "." : rel).split(
|
|
1074
|
+
const rel = relative2(resolve2(rootDir), resolve2(bundlePath2));
|
|
1075
|
+
return (rel === "" ? "." : rel).split(sep2).join("/");
|
|
449
1076
|
}
|
|
450
1077
|
async function readMergedPins(workspaceDir) {
|
|
451
1078
|
const manifests = {};
|
|
@@ -471,10 +1098,10 @@ async function readMergedPins(workspaceDir) {
|
|
|
471
1098
|
}
|
|
472
1099
|
|
|
473
1100
|
// src/kb-pins/frozen.ts
|
|
474
|
-
import { resolve as
|
|
1101
|
+
import { resolve as resolve3 } from "path";
|
|
475
1102
|
async function assertBaseNotFrozen(workspaceDir, bundlePath2) {
|
|
476
1103
|
const merged = await readMergedPins(workspaceDir);
|
|
477
|
-
const absolute =
|
|
1104
|
+
const absolute = resolve3(bundlePath2);
|
|
478
1105
|
const pin = merged.pins.find((entry) => entry.absolutePath === absolute);
|
|
479
1106
|
if (pin?.frozen === true) {
|
|
480
1107
|
throw new KbBaseFrozenError(pin.path, pin.layer);
|
|
@@ -521,221 +1148,70 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
|
|
|
521
1148
|
if (existing) {
|
|
522
1149
|
const updated = { ...existing, ...fields };
|
|
523
1150
|
if (Object.keys(fields).length) {
|
|
524
|
-
await writePinsLayer(workspaceDir, layer, {
|
|
525
|
-
...manifest,
|
|
526
|
-
pins: manifest.pins.map(
|
|
527
|
-
(entry2) => entry2 === existing ? updated : entry2
|
|
528
|
-
)
|
|
529
|
-
});
|
|
530
|
-
}
|
|
531
|
-
return {
|
|
532
|
-
path: existing.path,
|
|
533
|
-
layer,
|
|
534
|
-
pinnedAt: existing.pinnedAt ?? at,
|
|
535
|
-
alreadyPinned: true,
|
|
536
|
-
...updated.mode ? { mode: updated.mode } : {},
|
|
537
|
-
...updated.profiles ? { profiles: updated.profiles } : {},
|
|
538
|
-
...updated.frozen !== void 0 ? { frozen: updated.frozen } : {},
|
|
539
|
-
...warning ? { warning } : {}
|
|
540
|
-
};
|
|
541
|
-
}
|
|
542
|
-
const entry = {
|
|
543
|
-
path: storablePath(root, bundlePath2),
|
|
544
|
-
pinnedAt: at,
|
|
545
|
-
...fields
|
|
546
|
-
};
|
|
547
|
-
await writePinsLayer(workspaceDir, layer, {
|
|
548
|
-
...manifest,
|
|
549
|
-
pins: [...manifest.pins, entry]
|
|
550
|
-
});
|
|
551
|
-
return {
|
|
552
|
-
path: entry.path,
|
|
553
|
-
layer,
|
|
554
|
-
pinnedAt: at,
|
|
555
|
-
alreadyPinned: false,
|
|
556
|
-
...fields,
|
|
557
|
-
...warning ? { warning } : {}
|
|
558
|
-
};
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
// src/kb-pins/unpin.ts
|
|
562
|
-
import { resolve as resolve3 } from "path";
|
|
563
|
-
async function unpinBase(workspaceDir, bundlePath2) {
|
|
564
|
-
const layers = [];
|
|
565
|
-
for (const layer of PIN_LAYERS) {
|
|
566
|
-
const root = layerRoot(workspaceDir, layer);
|
|
567
|
-
let manifest;
|
|
568
|
-
try {
|
|
569
|
-
manifest = await readPinsLayer(workspaceDir, layer);
|
|
570
|
-
} catch {
|
|
571
|
-
continue;
|
|
572
|
-
}
|
|
573
|
-
const absolute = resolvePinPath(root, storablePath(root, bundlePath2));
|
|
574
|
-
const kept = manifest.pins.filter(
|
|
575
|
-
(entry) => resolvePinPath(root, entry.path) !== absolute
|
|
576
|
-
);
|
|
577
|
-
if (kept.length !== manifest.pins.length) {
|
|
578
|
-
await writePinsLayer(workspaceDir, layer, { ...manifest, pins: kept });
|
|
579
|
-
layers.push(layer);
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
return {
|
|
583
|
-
path: storablePath(resolve3(workspaceDir), bundlePath2),
|
|
584
|
-
removed: layers.length > 0,
|
|
585
|
-
layers
|
|
586
|
-
};
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
// src/errors.ts
|
|
590
|
-
var Fault = /* @__PURE__ */ ((Fault2) => {
|
|
591
|
-
Fault2["Configuration"] = "Configuration";
|
|
592
|
-
Fault2["System"] = "System";
|
|
593
|
-
Fault2["User"] = "User";
|
|
594
|
-
return Fault2;
|
|
595
|
-
})(Fault || {});
|
|
596
|
-
var ErrorTypes = /* @__PURE__ */ ((ErrorTypes2) => {
|
|
597
|
-
ErrorTypes2["KbRecordAlreadyExists"] = "KbRecordAlreadyExists";
|
|
598
|
-
ErrorTypes2["KbInvalidConceptId"] = "KbInvalidConceptId";
|
|
599
|
-
ErrorTypes2["KbMissingFlagValue"] = "KbMissingFlagValue";
|
|
600
|
-
ErrorTypes2["KbPackBudgetExceeded"] = "KbPackBudgetExceeded";
|
|
601
|
-
ErrorTypes2["KbRecordNotFound"] = "KbRecordNotFound";
|
|
602
|
-
ErrorTypes2["KbSelfVerification"] = "KbSelfVerification";
|
|
603
|
-
ErrorTypes2["KbWriteConflict"] = "KbWriteConflict";
|
|
604
|
-
return ErrorTypes2;
|
|
605
|
-
})(ErrorTypes || {});
|
|
606
|
-
var BaseError = class extends Error {
|
|
607
|
-
code;
|
|
608
|
-
errorType;
|
|
609
|
-
fault;
|
|
610
|
-
retriable;
|
|
611
|
-
reportToUser;
|
|
612
|
-
details;
|
|
613
|
-
constructor(props) {
|
|
614
|
-
super(props.message);
|
|
615
|
-
this.name = props.name ?? this.constructor.name;
|
|
616
|
-
this.code = props.code ?? 500;
|
|
617
|
-
this.errorType = props.errorType;
|
|
618
|
-
this.fault = props.fault;
|
|
619
|
-
this.retriable = props.retriable ?? true;
|
|
620
|
-
this.reportToUser = props.reportToUser ?? false;
|
|
621
|
-
this.details = props.details;
|
|
622
|
-
}
|
|
623
|
-
};
|
|
624
|
-
|
|
625
|
-
// src/kb-errors.ts
|
|
626
|
-
var KbRecordAlreadyExistsError = class extends BaseError {
|
|
627
|
-
constructor(conceptId2) {
|
|
628
|
-
super({
|
|
629
|
-
message: `kb: ${conceptId2} already exists \u2014 choose a more specific slug, or write with overwrite`,
|
|
630
|
-
errorType: "KbRecordAlreadyExists" /* KbRecordAlreadyExists */,
|
|
631
|
-
code: 409,
|
|
632
|
-
fault: "User" /* User */,
|
|
633
|
-
retriable: false,
|
|
634
|
-
reportToUser: true,
|
|
635
|
-
details: { conceptId: conceptId2, action: "refused" }
|
|
636
|
-
});
|
|
637
|
-
this.conceptId = conceptId2;
|
|
638
|
-
}
|
|
639
|
-
conceptId;
|
|
640
|
-
};
|
|
641
|
-
var KbRecordNotFoundError = class extends BaseError {
|
|
642
|
-
constructor(conceptId2) {
|
|
643
|
-
super({
|
|
644
|
-
message: `kb: ${conceptId2} does not exist`,
|
|
645
|
-
errorType: "KbRecordNotFound" /* KbRecordNotFound */,
|
|
646
|
-
code: 404,
|
|
647
|
-
fault: "User" /* User */,
|
|
648
|
-
retriable: false,
|
|
649
|
-
reportToUser: true,
|
|
650
|
-
details: { conceptId: conceptId2 }
|
|
651
|
-
});
|
|
652
|
-
this.conceptId = conceptId2;
|
|
653
|
-
}
|
|
654
|
-
conceptId;
|
|
655
|
-
};
|
|
656
|
-
var KbWriteConflictError = class extends BaseError {
|
|
657
|
-
constructor(conceptId2) {
|
|
658
|
-
super({
|
|
659
|
-
message: `kb: ${conceptId2} changed while it was being updated \u2014 re-read and retry`,
|
|
660
|
-
errorType: "KbWriteConflict" /* KbWriteConflict */,
|
|
661
|
-
code: 409,
|
|
662
|
-
fault: "System" /* System */,
|
|
663
|
-
retriable: true,
|
|
664
|
-
reportToUser: true,
|
|
665
|
-
details: { conceptId: conceptId2 }
|
|
666
|
-
});
|
|
667
|
-
this.conceptId = conceptId2;
|
|
668
|
-
}
|
|
669
|
-
conceptId;
|
|
670
|
-
};
|
|
671
|
-
var KbSelfVerificationError = class extends BaseError {
|
|
672
|
-
constructor(conceptId2, actor, generatedBy) {
|
|
673
|
-
super({
|
|
674
|
-
message: `kb: ${conceptId2} was generated by ${generatedBy}, and a record's generator cannot verify it \u2014 only a human or a different actor can`,
|
|
675
|
-
errorType: "KbSelfVerification" /* KbSelfVerification */,
|
|
676
|
-
code: 400,
|
|
677
|
-
fault: "User" /* User */,
|
|
678
|
-
retriable: false,
|
|
679
|
-
reportToUser: true,
|
|
680
|
-
details: { conceptId: conceptId2, actor, generatedBy, action: "refused" }
|
|
681
|
-
});
|
|
682
|
-
this.conceptId = conceptId2;
|
|
683
|
-
this.actor = actor;
|
|
684
|
-
this.generatedBy = generatedBy;
|
|
685
|
-
}
|
|
686
|
-
conceptId;
|
|
687
|
-
actor;
|
|
688
|
-
generatedBy;
|
|
689
|
-
};
|
|
690
|
-
var KbPackBudgetExceededError = class extends BaseError {
|
|
691
|
-
constructor(recordCount, approxTokens2, budgetTokens, excluded) {
|
|
692
|
-
super({
|
|
693
|
-
message: `kb: a pack of ${recordCount} records is ~${approxTokens2} tokens against a budget of ${budgetTokens} \u2014 lower hops or maxNodes, or raise the budget`,
|
|
694
|
-
errorType: "KbPackBudgetExceeded" /* KbPackBudgetExceeded */,
|
|
695
|
-
code: 400,
|
|
696
|
-
fault: "User" /* User */,
|
|
697
|
-
retriable: false,
|
|
698
|
-
reportToUser: true,
|
|
699
|
-
details: { recordCount, approxTokens: approxTokens2, budgetTokens, excluded }
|
|
700
|
-
});
|
|
701
|
-
this.recordCount = recordCount;
|
|
702
|
-
this.approxTokens = approxTokens2;
|
|
703
|
-
this.budgetTokens = budgetTokens;
|
|
704
|
-
this.excluded = excluded;
|
|
705
|
-
}
|
|
706
|
-
recordCount;
|
|
707
|
-
approxTokens;
|
|
708
|
-
budgetTokens;
|
|
709
|
-
excluded;
|
|
710
|
-
};
|
|
711
|
-
var KbMissingFlagValueError = class extends BaseError {
|
|
712
|
-
constructor(flag) {
|
|
713
|
-
super({
|
|
714
|
-
message: `kb: ${flag} needs a value \u2014 pass ${flag} <value> or ${flag}=<value>`,
|
|
715
|
-
errorType: "KbMissingFlagValue" /* KbMissingFlagValue */,
|
|
716
|
-
code: 400,
|
|
717
|
-
fault: "User" /* User */,
|
|
718
|
-
retriable: false,
|
|
719
|
-
reportToUser: true,
|
|
720
|
-
details: { flag }
|
|
721
|
-
});
|
|
722
|
-
this.flag = flag;
|
|
1151
|
+
await writePinsLayer(workspaceDir, layer, {
|
|
1152
|
+
...manifest,
|
|
1153
|
+
pins: manifest.pins.map(
|
|
1154
|
+
(entry2) => entry2 === existing ? updated : entry2
|
|
1155
|
+
)
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
return {
|
|
1159
|
+
path: existing.path,
|
|
1160
|
+
layer,
|
|
1161
|
+
pinnedAt: existing.pinnedAt ?? at,
|
|
1162
|
+
alreadyPinned: true,
|
|
1163
|
+
...updated.mode ? { mode: updated.mode } : {},
|
|
1164
|
+
...updated.profiles ? { profiles: updated.profiles } : {},
|
|
1165
|
+
...updated.frozen !== void 0 ? { frozen: updated.frozen } : {},
|
|
1166
|
+
...warning ? { warning } : {}
|
|
1167
|
+
};
|
|
723
1168
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
1169
|
+
const entry = {
|
|
1170
|
+
path: storablePath(root, bundlePath2),
|
|
1171
|
+
pinnedAt: at,
|
|
1172
|
+
...fields
|
|
1173
|
+
};
|
|
1174
|
+
await writePinsLayer(workspaceDir, layer, {
|
|
1175
|
+
...manifest,
|
|
1176
|
+
pins: [...manifest.pins, entry]
|
|
1177
|
+
});
|
|
1178
|
+
return {
|
|
1179
|
+
path: entry.path,
|
|
1180
|
+
layer,
|
|
1181
|
+
pinnedAt: at,
|
|
1182
|
+
alreadyPinned: false,
|
|
1183
|
+
...fields,
|
|
1184
|
+
...warning ? { warning } : {}
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// src/kb-pins/unpin.ts
|
|
1189
|
+
import { resolve as resolve4 } from "path";
|
|
1190
|
+
async function unpinBase(workspaceDir, bundlePath2) {
|
|
1191
|
+
const layers = [];
|
|
1192
|
+
for (const layer of PIN_LAYERS) {
|
|
1193
|
+
const root = layerRoot(workspaceDir, layer);
|
|
1194
|
+
let manifest;
|
|
1195
|
+
try {
|
|
1196
|
+
manifest = await readPinsLayer(workspaceDir, layer);
|
|
1197
|
+
} catch {
|
|
1198
|
+
continue;
|
|
1199
|
+
}
|
|
1200
|
+
const absolute = resolvePinPath(root, storablePath(root, bundlePath2));
|
|
1201
|
+
const kept = manifest.pins.filter(
|
|
1202
|
+
(entry) => resolvePinPath(root, entry.path) !== absolute
|
|
1203
|
+
);
|
|
1204
|
+
if (kept.length !== manifest.pins.length) {
|
|
1205
|
+
await writePinsLayer(workspaceDir, layer, { ...manifest, pins: kept });
|
|
1206
|
+
layers.push(layer);
|
|
1207
|
+
}
|
|
737
1208
|
}
|
|
738
|
-
|
|
1209
|
+
return {
|
|
1210
|
+
path: storablePath(resolve4(workspaceDir), bundlePath2),
|
|
1211
|
+
removed: layers.length > 0,
|
|
1212
|
+
layers
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
739
1215
|
|
|
740
1216
|
// src/adjudicate.ts
|
|
741
1217
|
var STANDING = {
|
|
@@ -747,7 +1223,7 @@ var STANDING = {
|
|
|
747
1223
|
rejected: "rejected",
|
|
748
1224
|
superseded: "superseded"
|
|
749
1225
|
};
|
|
750
|
-
function adjudicate(hits, bundle, now = /* @__PURE__ */ new Date()) {
|
|
1226
|
+
function adjudicate(hits, bundle, now = /* @__PURE__ */ new Date(), anchorDrift) {
|
|
751
1227
|
const byId = new Map(bundle.map((record) => [record.conceptId, record]));
|
|
752
1228
|
return hits.map((record) => {
|
|
753
1229
|
const status = record.frontmatter.strauss_status;
|
|
@@ -777,6 +1253,20 @@ function adjudicate(hits, bundle, now = /* @__PURE__ */ new Date()) {
|
|
|
777
1253
|
if (!record.frontmatter.verified?.length) {
|
|
778
1254
|
warnings.push({ kind: "unverified" });
|
|
779
1255
|
}
|
|
1256
|
+
const moved = (anchorDrift?.get(record.conceptId) ?? []).filter(
|
|
1257
|
+
(entry) => entry.state !== "match" && entry.reason !== "foreign-repo"
|
|
1258
|
+
);
|
|
1259
|
+
if (moved.length) {
|
|
1260
|
+
warnings.push({
|
|
1261
|
+
kind: "drifted",
|
|
1262
|
+
anchors: moved.map(({ file, symbol, diffSize, reason }) => ({
|
|
1263
|
+
file,
|
|
1264
|
+
...symbol !== void 0 ? { symbol } : {},
|
|
1265
|
+
diffSize,
|
|
1266
|
+
...reason !== void 0 ? { reason } : {}
|
|
1267
|
+
}))
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
780
1270
|
return { record, standing: STANDING[status], heads, warnings };
|
|
781
1271
|
});
|
|
782
1272
|
}
|
|
@@ -897,7 +1387,7 @@ function indexIsStale(stored, expected) {
|
|
|
897
1387
|
}
|
|
898
1388
|
|
|
899
1389
|
// src/kb-context.ts
|
|
900
|
-
import { readFile as
|
|
1390
|
+
import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
901
1391
|
var HEADING2 = "## Knowledge bases (pinned)";
|
|
902
1392
|
var DEFAULT_CONTEXT_BUDGET = 4e3;
|
|
903
1393
|
var CONTEXT_PROFILES = {
|
|
@@ -1101,7 +1591,7 @@ function toHookJson(block, event) {
|
|
|
1101
1591
|
var CONTEXT_BEGIN = "<!-- strauss-kb:begin -->";
|
|
1102
1592
|
var CONTEXT_END = "<!-- strauss-kb:end -->";
|
|
1103
1593
|
async function syncInstructions(file, block) {
|
|
1104
|
-
const existing = await
|
|
1594
|
+
const existing = await readFile3(file, "utf8").catch(() => null);
|
|
1105
1595
|
const region = block ? `${CONTEXT_BEGIN}
|
|
1106
1596
|
${block.trim()}
|
|
1107
1597
|
${CONTEXT_END}` : null;
|
|
@@ -1253,7 +1743,8 @@ var KB_DOCTOR_CHECKS = [
|
|
|
1253
1743
|
"aging",
|
|
1254
1744
|
"orphaned",
|
|
1255
1745
|
"broken-supersession",
|
|
1256
|
-
"superseded-but-cited"
|
|
1746
|
+
"superseded-but-cited",
|
|
1747
|
+
"drifted"
|
|
1257
1748
|
];
|
|
1258
1749
|
var CHECK_HEADLINES = {
|
|
1259
1750
|
expired: "past its stale_after date",
|
|
@@ -1262,7 +1753,8 @@ var CHECK_HEADLINES = {
|
|
|
1262
1753
|
aging: "still open or still proposed long after it was written",
|
|
1263
1754
|
orphaned: "no other record links to it",
|
|
1264
1755
|
"broken-supersession": "the supersession pointers do not resolve",
|
|
1265
|
-
"superseded-but-cited": "a live record's body links to one that no longer holds"
|
|
1756
|
+
"superseded-but-cited": "a live record's body links to one that no longer holds",
|
|
1757
|
+
drifted: "the code an anchor points at moved out from under its hash"
|
|
1266
1758
|
};
|
|
1267
1759
|
var DAY_MS = 864e5;
|
|
1268
1760
|
function doctor(bundle, options = {}) {
|
|
@@ -1272,7 +1764,7 @@ function doctor(bundle, options = {}) {
|
|
|
1272
1764
|
agingDays: options.agingDays ?? DEFAULT_AGING_DAYS
|
|
1273
1765
|
};
|
|
1274
1766
|
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
1275
|
-
const adjudicated = adjudicate(bundle, bundle, now);
|
|
1767
|
+
const adjudicated = adjudicate(bundle, bundle, now, options.anchorDrift);
|
|
1276
1768
|
const standings = new Map(
|
|
1277
1769
|
adjudicated.map((hit) => [hit.record.conceptId, hit.standing])
|
|
1278
1770
|
);
|
|
@@ -1286,7 +1778,8 @@ function doctor(bundle, options = {}) {
|
|
|
1286
1778
|
group("aging", aging(inForce, now, thresholds.agingDays)),
|
|
1287
1779
|
group("orphaned", orphaned(bundle)),
|
|
1288
1780
|
group("broken-supersession", brokenSupersession(bundle, adjudicated)),
|
|
1289
|
-
group("superseded-but-cited", supersededButCited(bundle, standings))
|
|
1781
|
+
group("superseded-but-cited", supersededButCited(bundle, standings)),
|
|
1782
|
+
group("drifted", drifted(inForce))
|
|
1290
1783
|
];
|
|
1291
1784
|
const counts = Object.fromEntries(
|
|
1292
1785
|
groups.map((entry) => [entry.check, entry.count])
|
|
@@ -1472,6 +1965,29 @@ function supersededButCited(bundle, standings) {
|
|
|
1472
1965
|
}
|
|
1473
1966
|
return findings;
|
|
1474
1967
|
}
|
|
1968
|
+
function drifted(hits) {
|
|
1969
|
+
const findings = [];
|
|
1970
|
+
for (const hit of hits) {
|
|
1971
|
+
const warning = hit.warnings.find((entry) => entry.kind === "drifted");
|
|
1972
|
+
if (!warning) continue;
|
|
1973
|
+
findings.push(
|
|
1974
|
+
finding(
|
|
1975
|
+
hit.record,
|
|
1976
|
+
`${warning.anchors.length} ${warning.anchors.length === 1 ? "anchor no longer matches" : "anchors no longer match"}: ${warning.anchors.map((anchor) => {
|
|
1977
|
+
const at = anchor.symbol ? `${anchor.file}:${anchor.symbol}` : anchor.file;
|
|
1978
|
+
if (anchor.reason) return `${at} (${anchor.reason})`;
|
|
1979
|
+
if (anchor.diffSize === null) {
|
|
1980
|
+
return `${at} (changed, size unrecorded)`;
|
|
1981
|
+
}
|
|
1982
|
+
return anchor.diffSize === 0 ? `${at} (content changed, same line count)` : `${at} (${anchor.diffSize} line${anchor.diffSize === 1 ? "" : "s"} apart)`;
|
|
1983
|
+
}).join(", ")}`
|
|
1984
|
+
)
|
|
1985
|
+
);
|
|
1986
|
+
}
|
|
1987
|
+
return findings.sort(
|
|
1988
|
+
(left, right) => left.conceptId.localeCompare(right.conceptId)
|
|
1989
|
+
);
|
|
1990
|
+
}
|
|
1475
1991
|
function replaces(later, earlier) {
|
|
1476
1992
|
return (later.frontmatter.strauss_supersedes ?? []).includes(earlier.conceptId) || earlier.frontmatter.strauss_superseded_by === later.conceptId;
|
|
1477
1993
|
}
|
|
@@ -1596,13 +2112,16 @@ function byGeneratedAt(left, right) {
|
|
|
1596
2112
|
return at(left).localeCompare(at(right)) || left.depth - right.depth;
|
|
1597
2113
|
}
|
|
1598
2114
|
|
|
1599
|
-
// src/commands/
|
|
2115
|
+
// src/commands/anchor-resolve.ts
|
|
1600
2116
|
import { z as z8 } from "zod";
|
|
1601
2117
|
|
|
1602
2118
|
// src/commands/model.ts
|
|
1603
2119
|
import { z as z7 } from "zod";
|
|
1604
2120
|
var bundlePath = z7.string().min(1).describe("Absolute path to the knowledge base directory.");
|
|
1605
2121
|
var conceptId = z7.string().min(1).describe("e.g. decision.cursor-v2");
|
|
2122
|
+
var REPO_ROOT = z7.string().min(1).optional().describe(
|
|
2123
|
+
"Where the anchored source lives, for the drift check. Defaults to the working directory."
|
|
2124
|
+
);
|
|
1606
2125
|
function define(command) {
|
|
1607
2126
|
return command;
|
|
1608
2127
|
}
|
|
@@ -1622,13 +2141,175 @@ function argvFlag(argv, name) {
|
|
|
1622
2141
|
return value;
|
|
1623
2142
|
}
|
|
1624
2143
|
|
|
2144
|
+
// src/commands/anchor-resolve.ts
|
|
2145
|
+
var anchorResolveCommand = define({
|
|
2146
|
+
name: "anchor-resolve",
|
|
2147
|
+
tool: "kb_anchor_resolve",
|
|
2148
|
+
usage: "anchor-resolve <concept-id> [--repo-root <path>] [--rebaseline] [--restamp]",
|
|
2149
|
+
description: "Resolve a record's anchors against the working tree: stamp a hash onto anchors that lack one, report drift where the code moved. kb_verify's mechanical counterpart \u2014 reach for it when the question is whether the code still is what it was, not whether the claim still holds. Anchors naming another repository are skipped. Exits non-zero on drift.",
|
|
2150
|
+
input: z8.object({
|
|
2151
|
+
bundlePath,
|
|
2152
|
+
conceptId,
|
|
2153
|
+
repoRoot: z8.string().min(1).optional(),
|
|
2154
|
+
rebaseline: z8.boolean().optional().describe(
|
|
2155
|
+
"Accept the current code as the new baseline for anchors that drifted."
|
|
2156
|
+
),
|
|
2157
|
+
restamp: z8.boolean().optional().describe(
|
|
2158
|
+
"Refresh `resolved_at` on anchors that already match. Off by default, so a green run writes nothing."
|
|
2159
|
+
)
|
|
2160
|
+
}),
|
|
2161
|
+
fromArgv: (argv, path) => ({
|
|
2162
|
+
bundlePath: path,
|
|
2163
|
+
conceptId: argv[1],
|
|
2164
|
+
repoRoot: argvFlag(argv, "--repo-root"),
|
|
2165
|
+
rebaseline: argv.includes("--rebaseline"),
|
|
2166
|
+
restamp: argv.includes("--restamp")
|
|
2167
|
+
}),
|
|
2168
|
+
run: async ({ store, actor, now }, { bundlePath: path, conceptId: id, repoRoot, rebaseline, restamp }) => {
|
|
2169
|
+
const root = repoRoot ?? process.cwd();
|
|
2170
|
+
const record = await store.read(path, id);
|
|
2171
|
+
if (!record) throw new KbRecordNotFoundError(id);
|
|
2172
|
+
const anchors = record.frontmatter.strauss_anchors ?? [];
|
|
2173
|
+
if (!anchors.length) {
|
|
2174
|
+
return {
|
|
2175
|
+
conceptId: id,
|
|
2176
|
+
results: [],
|
|
2177
|
+
verified: false,
|
|
2178
|
+
note: "record has no anchors"
|
|
2179
|
+
};
|
|
2180
|
+
}
|
|
2181
|
+
const results = [];
|
|
2182
|
+
const updated = [];
|
|
2183
|
+
const origin = new LazyOrigin(root);
|
|
2184
|
+
let dirty = false;
|
|
2185
|
+
if (anchors.some((anchor) => anchor.repo)) await origin.prime();
|
|
2186
|
+
const foreign = new Map(
|
|
2187
|
+
anchors.map((anchor) => [anchor, origin.isForeign(anchor)])
|
|
2188
|
+
);
|
|
2189
|
+
const reads = await readAnchorFiles(
|
|
2190
|
+
anchors.filter((anchor) => !foreign.get(anchor)).map((anchor) => anchor.file),
|
|
2191
|
+
anchorFileReader(root)
|
|
2192
|
+
);
|
|
2193
|
+
for (const anchor of anchors) {
|
|
2194
|
+
const base = {
|
|
2195
|
+
file: anchor.file,
|
|
2196
|
+
...anchor.symbol ? { symbol: anchor.symbol } : {},
|
|
2197
|
+
// Carried onto unresolved findings too: an anchor that once hashed
|
|
2198
|
+
// and now resolves to nothing is a broken anchor, and the exit code
|
|
2199
|
+
// has to be able to tell it from one nobody ever stamped.
|
|
2200
|
+
...anchor.hash ? { storedHash: anchor.hash } : {}
|
|
2201
|
+
};
|
|
2202
|
+
if (foreign.get(anchor)) {
|
|
2203
|
+
results.push({ ...base, state: "unresolved", reason: "foreign-repo" });
|
|
2204
|
+
updated.push(anchor);
|
|
2205
|
+
continue;
|
|
2206
|
+
}
|
|
2207
|
+
const fileRead = reads.get(anchor.file);
|
|
2208
|
+
if (!fileRead.ok) {
|
|
2209
|
+
results.push({ ...base, state: "unresolved", reason: fileRead.reason });
|
|
2210
|
+
updated.push(anchor);
|
|
2211
|
+
continue;
|
|
2212
|
+
}
|
|
2213
|
+
const resolved = resolveAnchor(fileRead.source, anchor);
|
|
2214
|
+
if (!resolved) {
|
|
2215
|
+
results.push({
|
|
2216
|
+
...base,
|
|
2217
|
+
state: "unresolved",
|
|
2218
|
+
reason: "symbol-not-found"
|
|
2219
|
+
});
|
|
2220
|
+
updated.push(anchor);
|
|
2221
|
+
continue;
|
|
2222
|
+
}
|
|
2223
|
+
const currentHash = hashAnchorText(resolved.text);
|
|
2224
|
+
const currentLines = resolved.endLine - resolved.startLine + 1;
|
|
2225
|
+
const stamped = {
|
|
2226
|
+
...anchor,
|
|
2227
|
+
hash: currentHash,
|
|
2228
|
+
lines: currentLines,
|
|
2229
|
+
resolved_at: now()
|
|
2230
|
+
};
|
|
2231
|
+
if (!anchor.hash) {
|
|
2232
|
+
results.push({ ...base, state: "stamped", currentHash });
|
|
2233
|
+
updated.push(stamped);
|
|
2234
|
+
dirty = true;
|
|
2235
|
+
} else if (anchor.hash === currentHash) {
|
|
2236
|
+
results.push({
|
|
2237
|
+
...base,
|
|
2238
|
+
state: "match",
|
|
2239
|
+
currentHash
|
|
2240
|
+
});
|
|
2241
|
+
const refresh = restamp || anchor.resolved_at === void 0;
|
|
2242
|
+
updated.push(refresh ? { ...anchor, resolved_at: now() } : anchor);
|
|
2243
|
+
if (refresh) dirty = true;
|
|
2244
|
+
} else {
|
|
2245
|
+
results.push({
|
|
2246
|
+
...base,
|
|
2247
|
+
state: "drifted",
|
|
2248
|
+
currentHash,
|
|
2249
|
+
diffSize: anchor.lines === void 0 ? null : Math.abs(currentLines - anchor.lines),
|
|
2250
|
+
...rebaseline ? { rebaselined: true } : {}
|
|
2251
|
+
});
|
|
2252
|
+
updated.push(rebaseline ? stamped : anchor);
|
|
2253
|
+
if (rebaseline) dirty = true;
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
let frozen = false;
|
|
2257
|
+
if (dirty) {
|
|
2258
|
+
try {
|
|
2259
|
+
await assertBaseNotFrozen(process.cwd(), path);
|
|
2260
|
+
} catch (error) {
|
|
2261
|
+
if (!(error instanceof KbBaseFrozenError)) throw error;
|
|
2262
|
+
frozen = true;
|
|
2263
|
+
}
|
|
2264
|
+
if (!frozen) await store.updateAnchors(path, id, updated, actor);
|
|
2265
|
+
}
|
|
2266
|
+
const frozenNote = frozen ? { frozen: true, note: "base is frozen: nothing was stamped" } : {};
|
|
2267
|
+
const checked = results.filter((entry) => entry.reason !== "foreign-repo");
|
|
2268
|
+
const skipped = results.length - checked.length;
|
|
2269
|
+
const matches2 = checked.filter((entry) => entry.state === "match").length;
|
|
2270
|
+
const clean = checked.length > 0 && checked.every((entry) => entry.state === "match");
|
|
2271
|
+
if (clean) {
|
|
2272
|
+
try {
|
|
2273
|
+
await store.verify(
|
|
2274
|
+
path,
|
|
2275
|
+
id,
|
|
2276
|
+
`anchor-resolve: ${matches2}/${checked.length} anchors match${skipped ? `, ${skipped} in another repo` : ""} (regex resolver)`,
|
|
2277
|
+
actor,
|
|
2278
|
+
now()
|
|
2279
|
+
);
|
|
2280
|
+
} catch (error) {
|
|
2281
|
+
if (!(error instanceof KbSelfVerificationError)) throw error;
|
|
2282
|
+
return {
|
|
2283
|
+
conceptId: id,
|
|
2284
|
+
results,
|
|
2285
|
+
verified: false,
|
|
2286
|
+
verifyRefused: "self-verification",
|
|
2287
|
+
...frozenNote
|
|
2288
|
+
};
|
|
2289
|
+
}
|
|
2290
|
+
return { conceptId: id, results, verified: true, ...frozenNote };
|
|
2291
|
+
}
|
|
2292
|
+
return { conceptId: id, results, verified: false, ...frozenNote };
|
|
2293
|
+
},
|
|
2294
|
+
// A stored hash that no longer resolves is a broken anchor, not an absence:
|
|
2295
|
+
// the file was deleted or the symbol renamed, and exiting zero on it would
|
|
2296
|
+
// let the one edit that destroys an anchor pass the gate that exists to
|
|
2297
|
+
// catch it. An anchor nobody ever stamped is still just unstamped, and one
|
|
2298
|
+
// belonging to another repository was never this run's to check — failing CI
|
|
2299
|
+
// on either would gate on work this command did not do.
|
|
2300
|
+
failsWhen: (result) => result.results.some(
|
|
2301
|
+
(entry) => entry.state === "drifted" || entry.state === "unresolved" && entry.storedHash !== void 0 && entry.reason !== "foreign-repo"
|
|
2302
|
+
)
|
|
2303
|
+
});
|
|
2304
|
+
|
|
1625
2305
|
// src/commands/answer.ts
|
|
2306
|
+
import { z as z9 } from "zod";
|
|
1626
2307
|
var answerCommand = define({
|
|
1627
2308
|
name: "answer",
|
|
1628
2309
|
tool: "kb_answer",
|
|
1629
2310
|
usage: "answer <concept-id> <answer...>",
|
|
1630
2311
|
description: "Resolve an open question: sets the status, stamps who answered and when, and appends an Answer section. If the answer overturns an assumption or a decision, that is a supersession \u2014 do it explicitly.",
|
|
1631
|
-
input:
|
|
2312
|
+
input: z9.object({ bundlePath, conceptId, answer: z9.string().min(1) }),
|
|
1632
2313
|
fromArgv: (argv, path) => ({
|
|
1633
2314
|
bundlePath: path,
|
|
1634
2315
|
conceptId: argv[1],
|
|
@@ -1642,15 +2323,15 @@ var answerCommand = define({
|
|
|
1642
2323
|
});
|
|
1643
2324
|
|
|
1644
2325
|
// src/commands/catalog.ts
|
|
1645
|
-
import { z as
|
|
2326
|
+
import { z as z10 } from "zod";
|
|
1646
2327
|
var catalogCommand = define({
|
|
1647
2328
|
name: "catalog",
|
|
1648
2329
|
tool: "kb_catalog",
|
|
1649
2330
|
usage: "catalog [type]",
|
|
1650
2331
|
description: "Lists every record as one line \u2014 concept id, type, title, standing, and a stale flag \u2014 at roughly thirty tokens each. Pick this over kb_load once kb_load refuses: kb_catalog never refuses. Superseded records show only their replacement; fetch bodies with kb_load, kb_pack, kb_query, or kb_trace.",
|
|
1651
|
-
input:
|
|
2332
|
+
input: z10.object({
|
|
1652
2333
|
bundlePath,
|
|
1653
|
-
type:
|
|
2334
|
+
type: z10.enum(KB_RECORD_TYPES).optional()
|
|
1654
2335
|
}),
|
|
1655
2336
|
fromArgv: (argv, path) => ({
|
|
1656
2337
|
bundlePath: path,
|
|
@@ -1705,26 +2386,26 @@ function count(value, noun) {
|
|
|
1705
2386
|
}
|
|
1706
2387
|
|
|
1707
2388
|
// src/commands/context.ts
|
|
1708
|
-
import { z as
|
|
2389
|
+
import { z as z11 } from "zod";
|
|
1709
2390
|
var contextCommand = define({
|
|
1710
2391
|
name: "context",
|
|
1711
2392
|
tool: "kb_context",
|
|
1712
2393
|
usage: "context [--profile NAME] [--budget N] [--full-under N] [--format json] [--event NAME]",
|
|
1713
2394
|
description: "The pinned-base index block, for injection at every context birth \u2014 startup, clear, resume, and after compaction. An index, not the content: concept ids, titles and standing, with the bodies left behind kb_load at the point of use. Emits nothing when nothing is pinned. Refuses with the list of bases and their sizes rather than truncating past its budget. Budgets resolve most-specific-first: explicit flags, then the workspace manifests' `context` tables (per profile, over their `default`), then the built-in profile (session-start, compact, turn), then package defaults \u2014 so a repo tunes its own numbers in .strauss/kb-pins.json without touching hook commands. Like kb_schema and kb_types this takes no bundlePath \u2014 it reads the workspace pin manifests, because which bases a session should see is workspace state, not a property of one base.",
|
|
1714
|
-
input:
|
|
1715
|
-
budgetTokens:
|
|
2395
|
+
input: z11.object({
|
|
2396
|
+
budgetTokens: z11.number().int().positive().optional().describe(
|
|
1716
2397
|
"Ceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000."
|
|
1717
2398
|
),
|
|
1718
|
-
fullUnderTokens:
|
|
2399
|
+
fullUnderTokens: z11.number().int().positive().optional().describe(
|
|
1719
2400
|
"Per-base rendering threshold, applied before the budget: a base whose complete load fits under this arrives as full records instead of index lines, and the whole block still answers to budgetTokens. Off by default \u2014 index-only is the safe default at a context birth, because injected bodies outlive the qualifiers on them; the session-start profile opts tiny bases in at 1500."
|
|
1720
2401
|
),
|
|
1721
|
-
profile:
|
|
2402
|
+
profile: z11.string().optional().describe(
|
|
1722
2403
|
"Named budget set: built-ins are session-start (full-under 1500), compact and turn (budget 2500); the manifests' `context` tables override per repo. Unknown names fall through to defaults rather than failing."
|
|
1723
2404
|
),
|
|
1724
|
-
format:
|
|
2405
|
+
format: z11.enum(["markdown", "json"]).optional().describe(
|
|
1725
2406
|
"CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
|
|
1726
2407
|
),
|
|
1727
|
-
event:
|
|
2408
|
+
event: z11.string().optional().describe(
|
|
1728
2409
|
"hookEventName stamped into the JSON envelope. Only meaningful with format=json."
|
|
1729
2410
|
)
|
|
1730
2411
|
}),
|
|
@@ -1760,15 +2441,16 @@ var contextCommand = define({
|
|
|
1760
2441
|
});
|
|
1761
2442
|
|
|
1762
2443
|
// src/commands/doctor.ts
|
|
1763
|
-
import { z as
|
|
1764
|
-
var days = (what, fallback) =>
|
|
2444
|
+
import { z as z12 } from "zod";
|
|
2445
|
+
var days = (what, fallback) => z12.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
|
|
1765
2446
|
var doctorCommand = define({
|
|
1766
2447
|
name: "doctor",
|
|
1767
2448
|
tool: "kb_doctor",
|
|
1768
|
-
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--strict]",
|
|
1769
|
-
description: "
|
|
1770
|
-
input:
|
|
2449
|
+
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--strict]",
|
|
2450
|
+
description: "Read-only health sweep: expired, expiring, unverified, aging, orphaned, broken-supersession, superseded-but-cited, drifted anchors. Every group is reported even when empty; nothing is written or re-stamped. Use it when picking up a base you have not touched in a while; kb_validate only checks that pointers between records agree.",
|
|
2451
|
+
input: z12.object({
|
|
1771
2452
|
bundlePath,
|
|
2453
|
+
repoRoot: REPO_ROOT,
|
|
1772
2454
|
expiringDays: days(
|
|
1773
2455
|
"How far ahead `expiring` looks, in days.",
|
|
1774
2456
|
DEFAULT_EXPIRING_DAYS
|
|
@@ -1781,7 +2463,7 @@ var doctorCommand = define({
|
|
|
1781
2463
|
"How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
|
|
1782
2464
|
DEFAULT_AGING_DAYS
|
|
1783
2465
|
),
|
|
1784
|
-
strict:
|
|
2466
|
+
strict: z12.boolean().optional().describe(
|
|
1785
2467
|
"Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
|
|
1786
2468
|
)
|
|
1787
2469
|
}),
|
|
@@ -1793,29 +2475,36 @@ var doctorCommand = define({
|
|
|
1793
2475
|
const expiring2 = argvFlag(argv, "--expiring-days");
|
|
1794
2476
|
const unverified2 = argvFlag(argv, "--unverified-days");
|
|
1795
2477
|
const agingDays = argvFlag(argv, "--aging-days");
|
|
2478
|
+
const repoRoot = argvFlag(argv, "--repo-root");
|
|
1796
2479
|
return {
|
|
1797
2480
|
bundlePath: path,
|
|
2481
|
+
...repoRoot !== void 0 ? { repoRoot } : {},
|
|
1798
2482
|
...expiring2 !== void 0 ? { expiringDays: Number(expiring2) } : {},
|
|
1799
2483
|
...unverified2 !== void 0 ? { unverifiedDays: Number(unverified2) } : {},
|
|
1800
2484
|
...agingDays !== void 0 ? { agingDays: Number(agingDays) } : {},
|
|
1801
2485
|
...argv.includes("--strict") ? { strict: true } : {}
|
|
1802
2486
|
};
|
|
1803
2487
|
},
|
|
1804
|
-
run: async ({ store, now }, { bundlePath: path, expiringDays, unverifiedDays, agingDays }) => {
|
|
2488
|
+
run: async ({ store, now }, { bundlePath: path, expiringDays, unverifiedDays, agingDays, repoRoot }) => {
|
|
1805
2489
|
const checkedAt = now();
|
|
1806
|
-
const
|
|
2490
|
+
const records = await store.list(path);
|
|
2491
|
+
const anchorDrift = await store.detectDrift(records, repoRoot);
|
|
2492
|
+
const report = doctor(records, {
|
|
1807
2493
|
...expiringDays !== void 0 ? { expiringDays } : {},
|
|
1808
2494
|
...unverifiedDays !== void 0 ? { unverifiedDays } : {},
|
|
1809
2495
|
...agingDays !== void 0 ? { agingDays } : {},
|
|
2496
|
+
...anchorDrift !== void 0 ? { anchorDrift } : {},
|
|
1810
2497
|
now: new Date(checkedAt)
|
|
1811
2498
|
});
|
|
1812
2499
|
return { bundlePath: path, checkedAt, ...report };
|
|
1813
2500
|
},
|
|
1814
2501
|
render: (result) => render2(result),
|
|
1815
|
-
// Only expiry, and only under --strict. The other
|
|
2502
|
+
// Only expiry, and only under --strict. The other seven checks report debt a
|
|
1816
2503
|
// reader decides about; an expired record is the base asserting something it
|
|
1817
2504
|
// already said it would stop standing behind, which is the one finding a
|
|
1818
|
-
// pipeline can act on without a judgment call.
|
|
2505
|
+
// pipeline can act on without a judgment call. Drift has its own gate —
|
|
2506
|
+
// `anchor-resolve` exits non-zero on it, against a repo root the caller
|
|
2507
|
+
// named, which is the run a CI pipeline should be making anyway.
|
|
1819
2508
|
failsWhen: (result, input) => input.strict === true && result.counts.expired > 0
|
|
1820
2509
|
});
|
|
1821
2510
|
function render2(result) {
|
|
@@ -1850,13 +2539,13 @@ function render2(result) {
|
|
|
1850
2539
|
}
|
|
1851
2540
|
|
|
1852
2541
|
// src/commands/list.ts
|
|
1853
|
-
import { z as
|
|
2542
|
+
import { z as z13 } from "zod";
|
|
1854
2543
|
var listCommand = define({
|
|
1855
2544
|
name: "list",
|
|
1856
2545
|
tool: "kb_list",
|
|
1857
2546
|
usage: "list [type]",
|
|
1858
2547
|
description: "Every record, optionally narrowed to one type. Use kb_query when you have a question; this is for enumerating.",
|
|
1859
|
-
input:
|
|
2548
|
+
input: z13.object({ bundlePath, type: z13.enum(KB_RECORD_TYPES).optional() }),
|
|
1860
2549
|
fromArgv: (argv, path) => ({ bundlePath: path, type: argv[1] }),
|
|
1861
2550
|
run: async ({ store }, { bundlePath: path, type }) => (await store.list(path, type)).map((record) => ({
|
|
1862
2551
|
conceptId: record.conceptId,
|
|
@@ -1868,36 +2557,40 @@ var listCommand = define({
|
|
|
1868
2557
|
});
|
|
1869
2558
|
|
|
1870
2559
|
// src/commands/load.ts
|
|
1871
|
-
import { z as
|
|
2560
|
+
import { z as z14 } from "zod";
|
|
1872
2561
|
var loadCommand = define({
|
|
1873
2562
|
name: "load",
|
|
1874
2563
|
tool: "kb_load",
|
|
1875
|
-
usage: "load [type] [--budget N] [--
|
|
1876
|
-
description: "Loads the whole knowledge base at once, each record with its standing. Superseded records arrive as stubs
|
|
1877
|
-
input:
|
|
2564
|
+
usage: "load [type] [--budget N | --all] [--repo-root PATH]",
|
|
2565
|
+
description: "Loads the whole knowledge base at once, each record with its standing. Superseded records arrive as stubs; rejected and open records arrive whole. Refuses past the token budget \u2014 call kb_catalog, kb_pack on it; `all` bypasses the budget. Never read record files directly. Cache-stable; `digest` is the base's content stamp \u2014 hooks use it to tell you when to reload.",
|
|
2566
|
+
input: z14.object({
|
|
1878
2567
|
bundlePath,
|
|
1879
|
-
type:
|
|
1880
|
-
budgetTokens:
|
|
1881
|
-
all:
|
|
2568
|
+
type: z14.enum(KB_RECORD_TYPES).optional(),
|
|
2569
|
+
budgetTokens: z14.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
|
|
2570
|
+
all: z14.boolean().optional().describe(
|
|
1882
2571
|
"Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
|
|
1883
|
-
)
|
|
2572
|
+
),
|
|
2573
|
+
repoRoot: REPO_ROOT
|
|
1884
2574
|
}).refine((value) => !(value.all && value.budgetTokens !== void 0), {
|
|
1885
2575
|
message: "all is mutually exclusive with budgetTokens: pass a ceiling or none, not both."
|
|
1886
2576
|
}),
|
|
1887
2577
|
fromArgv: (argv, path) => {
|
|
1888
2578
|
const budget = argvFlag(argv, "--budget");
|
|
2579
|
+
const repoRoot = argvFlag(argv, "--repo-root");
|
|
1889
2580
|
return {
|
|
1890
2581
|
bundlePath: path,
|
|
1891
2582
|
...argv[1] && !argv[1].startsWith("--") ? { type: argv[1] } : {},
|
|
1892
2583
|
...budget ? { budgetTokens: Number(budget) } : {},
|
|
1893
|
-
...argv.includes("--all") ? { all: true } : {}
|
|
2584
|
+
...argv.includes("--all") ? { all: true } : {},
|
|
2585
|
+
...repoRoot !== void 0 ? { repoRoot } : {}
|
|
1894
2586
|
};
|
|
1895
2587
|
},
|
|
1896
|
-
run: async ({ store }, { bundlePath: path, type, budgetTokens, all }) => {
|
|
2588
|
+
run: async ({ store }, { bundlePath: path, type, budgetTokens, all, repoRoot }) => {
|
|
1897
2589
|
const result = await store.load(path, {
|
|
1898
2590
|
...type ? { type } : {},
|
|
1899
2591
|
...budgetTokens ? { budgetTokens } : {},
|
|
1900
|
-
...all ? { all } : {}
|
|
2592
|
+
...all ? { all } : {},
|
|
2593
|
+
...repoRoot !== void 0 ? { repoRoot } : {}
|
|
1901
2594
|
});
|
|
1902
2595
|
if (!result.loaded) return result;
|
|
1903
2596
|
return {
|
|
@@ -1916,25 +2609,25 @@ var loadCommand = define({
|
|
|
1916
2609
|
});
|
|
1917
2610
|
|
|
1918
2611
|
// src/commands/log.ts
|
|
1919
|
-
import { z as
|
|
2612
|
+
import { z as z15 } from "zod";
|
|
1920
2613
|
var logCommand = define({
|
|
1921
2614
|
name: "log",
|
|
1922
2615
|
tool: "kb_log",
|
|
1923
2616
|
usage: "log",
|
|
1924
2617
|
description: "What touched what, and when. The only artifact here that cannot be reconstructed from the records, so malformed lines are reported rather than repaired.",
|
|
1925
|
-
input:
|
|
2618
|
+
input: z15.object({ bundlePath }),
|
|
1926
2619
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
1927
2620
|
run: ({ store }, { bundlePath: path }) => store.readLog(path)
|
|
1928
2621
|
});
|
|
1929
2622
|
|
|
1930
2623
|
// src/commands/no-decision.ts
|
|
1931
|
-
import { z as
|
|
2624
|
+
import { z as z16 } from "zod";
|
|
1932
2625
|
var noDecisionCommand = define({
|
|
1933
2626
|
name: "no-decision",
|
|
1934
2627
|
tool: "kb_no_decision",
|
|
1935
2628
|
usage: "no-decision <reason...>",
|
|
1936
2629
|
description: 'Claim in one sentence that there was nothing to decide. Gating on "did you write a decision?" rewards writing a junk one; gating on "did you answer?" does not, so silence has to be expressible. Idempotent \u2014 restating it is not a collision.',
|
|
1937
|
-
input:
|
|
2630
|
+
input: z16.object({ bundlePath, reason: z16.string().min(1) }),
|
|
1938
2631
|
fromArgv: (argv, path) => ({
|
|
1939
2632
|
bundlePath: path,
|
|
1940
2633
|
reason: argv.slice(1).join(" ").trim()
|
|
@@ -1951,20 +2644,20 @@ var noDecisionCommand = define({
|
|
|
1951
2644
|
});
|
|
1952
2645
|
|
|
1953
2646
|
// src/commands/pack.ts
|
|
1954
|
-
import { z as
|
|
2647
|
+
import { z as z17 } from "zod";
|
|
1955
2648
|
var packCommand = define({
|
|
1956
2649
|
name: "pack",
|
|
1957
2650
|
tool: "kb_pack",
|
|
1958
2651
|
usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
|
|
1959
2652
|
description: "The bounded neighbourhood around one record: everything within `hops` of the root, ranked and cut to `maxNodes`, with every cut record named under Excluded \u2014 a named gap is knowable, a silent one is not. Prefer this over kb_load when the base is too large to hold whole and the work centres on one record; prefer it over kb_query when the question needs the governed neighbourhood \u2014 what was settled and what binds near this record \u2014 rather than a lookup by wording. Superseded records arrive as name, replacement and date stubs exactly as kb_load emits them: their bodies no longer hold, and kb_trace has the history. Refuses outright rather than truncating when the pack would exceed its token budget \u2014 a partial pack is indistinguishable from a complete one \u2014 reporting the record count and every already-cut id so the caller can lower hops or maxNodes, or raise the budget. The header carries the bundle, root, budget and a timestamp; everything below the header is byte-identical across runs over an unchanged base, so two packs can be diffed and a changed byte means changed knowledge. This tool (with kb_load, kb_query and kb_trace) is the only supported way to read a base; a raw file read bypasses supersession resolution and returns replaced records as if current.",
|
|
1960
|
-
input:
|
|
2653
|
+
input: z17.object({
|
|
1961
2654
|
bundlePath,
|
|
1962
2655
|
conceptId,
|
|
1963
|
-
hops:
|
|
1964
|
-
maxNodes:
|
|
2656
|
+
hops: z17.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
|
|
2657
|
+
maxNodes: z17.number().int().positive().optional().describe(
|
|
1965
2658
|
"How many records the pack may hold, root included. Defaults to 20."
|
|
1966
2659
|
),
|
|
1967
|
-
budgetTokens:
|
|
2660
|
+
budgetTokens: z17.number().int().positive().optional().describe(
|
|
1968
2661
|
"Approximate token ceiling over what is actually emitted. Defaults to 25000."
|
|
1969
2662
|
)
|
|
1970
2663
|
}),
|
|
@@ -2051,22 +2744,22 @@ function warningLabel(warning) {
|
|
|
2051
2744
|
}
|
|
2052
2745
|
|
|
2053
2746
|
// src/commands/pin.ts
|
|
2054
|
-
import { z as
|
|
2747
|
+
import { z as z18 } from "zod";
|
|
2055
2748
|
var pinCommand = define({
|
|
2056
2749
|
name: "pin",
|
|
2057
2750
|
tool: "kb_pin",
|
|
2058
2751
|
usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
|
|
2059
2752
|
description: "Pin a base into a workspace pin manifest, so `context` surfaces it at every context birth. Three layers, nearest wins: the committed project manifest (.strauss/kb-pins.json, the default), `--local` (.strauss/kb-pins.local.json, personal and gitignored), and `--user` (~/.strauss/kb-pins.json, every workspace). Idempotent \u2014 re-pinning changes nothing unless --mode, --profiles, or --frozen/--unfreeze are given, which update just those fields. `--mode full` preloads the whole base into the block regardless of the full-under threshold; `--mode index` never upgrades. `--profiles` scopes the pin to named context profiles. `--frozen` marks the base concluded: write commands against it refuse and `context` labels it read-only. A path with no records yet succeeds with a warning; bases are routinely pinned before they are populated. Pins are workspace state: the pinned base itself is never touched.",
|
|
2060
|
-
input:
|
|
2753
|
+
input: z18.object({
|
|
2061
2754
|
bundlePath,
|
|
2062
|
-
mode:
|
|
2755
|
+
mode: z18.enum(["full", "index"]).optional().describe(
|
|
2063
2756
|
"full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
|
|
2064
2757
|
),
|
|
2065
|
-
profiles:
|
|
2066
|
-
layer:
|
|
2758
|
+
profiles: z18.array(z18.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
|
|
2759
|
+
layer: z18.enum(["project", "local", "user"]).optional().describe(
|
|
2067
2760
|
"Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
|
|
2068
2761
|
),
|
|
2069
|
-
frozen:
|
|
2762
|
+
frozen: z18.boolean().optional().describe(
|
|
2070
2763
|
"true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
|
|
2071
2764
|
)
|
|
2072
2765
|
}),
|
|
@@ -2095,38 +2788,48 @@ var pinCommand = define({
|
|
|
2095
2788
|
});
|
|
2096
2789
|
|
|
2097
2790
|
// src/commands/pins.ts
|
|
2098
|
-
import { z as
|
|
2791
|
+
import { z as z19 } from "zod";
|
|
2099
2792
|
var pinsCommand = define({
|
|
2100
2793
|
name: "pins",
|
|
2101
2794
|
tool: "kb_pins",
|
|
2102
2795
|
usage: "pins",
|
|
2103
2796
|
description: "Every pinned base across the manifest layers, each with its layer and whether it currently resolves to readable records. Reads the workspace manifests rather than any one base, like kb_context.",
|
|
2104
|
-
input:
|
|
2797
|
+
input: z19.object({}),
|
|
2105
2798
|
fromArgv: () => ({}),
|
|
2106
2799
|
run: ({ store }) => listPins(store, process.cwd())
|
|
2107
2800
|
});
|
|
2108
2801
|
|
|
2109
2802
|
// src/commands/query.ts
|
|
2110
|
-
import { z as
|
|
2803
|
+
import { z as z20 } from "zod";
|
|
2111
2804
|
var queryCommand = define({
|
|
2112
2805
|
name: "query",
|
|
2113
2806
|
tool: "kb_query",
|
|
2114
|
-
usage: "query <text...>",
|
|
2115
|
-
description: "Search
|
|
2116
|
-
input:
|
|
2807
|
+
usage: "query <text...> [--repo-root PATH]",
|
|
2808
|
+
description: "Search; every hit carries its standing. Flagged, never filtered: a superseded hit returns with its replacement, a rejected one is marked. Prefer kb_load when the base fits its budget \u2014 a full read beats search. Results are volatile: place them at the tail, not the cached prefix. Never read record files directly.",
|
|
2809
|
+
input: z20.object({
|
|
2117
2810
|
bundlePath,
|
|
2118
|
-
text:
|
|
2119
|
-
type:
|
|
2120
|
-
includeNonCurrent:
|
|
2121
|
-
|
|
2122
|
-
fromArgv: (argv, path) => ({
|
|
2123
|
-
bundlePath: path,
|
|
2124
|
-
text: argv.slice(1).join(" ").trim(),
|
|
2125
|
-
includeNonCurrent: true
|
|
2811
|
+
text: z20.string().optional(),
|
|
2812
|
+
type: z20.enum(KB_RECORD_TYPES).optional(),
|
|
2813
|
+
includeNonCurrent: z20.boolean().optional(),
|
|
2814
|
+
repoRoot: REPO_ROOT
|
|
2126
2815
|
}),
|
|
2127
|
-
|
|
2816
|
+
// `--repo-root` is a flag, so its value must not fall into the search text.
|
|
2817
|
+
fromArgv: (argv, path) => {
|
|
2818
|
+
const repoRoot = argvFlag(argv, "--repo-root");
|
|
2819
|
+
const words = argv.slice(1);
|
|
2820
|
+
const flag = words.indexOf("--repo-root");
|
|
2821
|
+
if (flag !== -1) words.splice(flag, 2);
|
|
2822
|
+
return {
|
|
2823
|
+
bundlePath: path,
|
|
2824
|
+
text: words.join(" ").trim(),
|
|
2825
|
+
includeNonCurrent: true,
|
|
2826
|
+
...repoRoot !== void 0 ? { repoRoot } : {}
|
|
2827
|
+
};
|
|
2828
|
+
},
|
|
2829
|
+
run: async ({ store }, { bundlePath: path, text, type, includeNonCurrent, repoRoot }) => (await store.query(path, text ?? "", {
|
|
2128
2830
|
...type ? { type } : {},
|
|
2129
|
-
includeNonCurrent: includeNonCurrent === true
|
|
2831
|
+
includeNonCurrent: includeNonCurrent === true,
|
|
2832
|
+
...repoRoot !== void 0 ? { repoRoot } : {}
|
|
2130
2833
|
})).map((hit) => ({
|
|
2131
2834
|
conceptId: hit.record.conceptId,
|
|
2132
2835
|
title: hit.record.frontmatter.title ?? null,
|
|
@@ -2139,40 +2842,40 @@ var queryCommand = define({
|
|
|
2139
2842
|
});
|
|
2140
2843
|
|
|
2141
2844
|
// src/commands/read-index.ts
|
|
2142
|
-
import { z as
|
|
2845
|
+
import { z as z21 } from "zod";
|
|
2143
2846
|
var readIndexCommand = define({
|
|
2144
2847
|
name: "index",
|
|
2145
2848
|
tool: "kb_index",
|
|
2146
2849
|
usage: "index",
|
|
2147
2850
|
description: "The index, rebuilt if it disagrees with the records. One call gives the whole shape of the base: title, type, status, and description per record. The cheap re-orientation call after compaction or deep in a long session \u2014 a few hundred tokens; call it (or kb_context, when bases are pinned) first, then kb_load or fetch by concept id.",
|
|
2148
|
-
input:
|
|
2851
|
+
input: z21.object({ bundlePath }),
|
|
2149
2852
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
2150
2853
|
run: ({ store }, { bundlePath: path }) => store.readIndex(path)
|
|
2151
2854
|
});
|
|
2152
2855
|
|
|
2153
2856
|
// src/commands/schema.ts
|
|
2154
|
-
import { z as
|
|
2857
|
+
import { z as z22 } from "zod";
|
|
2155
2858
|
var schemaCommand = define({
|
|
2156
2859
|
name: "schema",
|
|
2157
2860
|
tool: "kb_schema",
|
|
2158
2861
|
usage: "schema",
|
|
2159
2862
|
description: "JSON Schema for the frontmatter, the write input, and log entries \u2014 generated from the code that enforces them, so it cannot drift from what a write will accept.",
|
|
2160
|
-
input:
|
|
2863
|
+
input: z22.object({}),
|
|
2161
2864
|
fromArgv: () => ({}),
|
|
2162
2865
|
run: () => Promise.resolve(kbJsonSchemas())
|
|
2163
2866
|
});
|
|
2164
2867
|
|
|
2165
2868
|
// src/commands/status.ts
|
|
2166
|
-
import { z as
|
|
2869
|
+
import { z as z23 } from "zod";
|
|
2167
2870
|
var statusCommand = define({
|
|
2168
2871
|
name: "status",
|
|
2169
2872
|
tool: "kb_status",
|
|
2170
2873
|
usage: "status <concept-id> <status>",
|
|
2171
2874
|
description: "Move a record's status, leaving everything else alone. Uses a compare-and-swap, so a concurrent change fails loudly rather than being overwritten.",
|
|
2172
|
-
input:
|
|
2875
|
+
input: z23.object({
|
|
2173
2876
|
bundlePath,
|
|
2174
2877
|
conceptId,
|
|
2175
|
-
status:
|
|
2878
|
+
status: z23.enum(KB_RECORD_STATUSES)
|
|
2176
2879
|
}),
|
|
2177
2880
|
fromArgv: (argv, path) => ({
|
|
2178
2881
|
bundlePath: path,
|
|
@@ -2187,13 +2890,13 @@ var statusCommand = define({
|
|
|
2187
2890
|
});
|
|
2188
2891
|
|
|
2189
2892
|
// src/commands/supersede.ts
|
|
2190
|
-
import { z as
|
|
2893
|
+
import { z as z24 } from "zod";
|
|
2191
2894
|
var supersedeCommand = define({
|
|
2192
2895
|
name: "supersede",
|
|
2193
2896
|
tool: "kb_supersede",
|
|
2194
2897
|
usage: "supersede <concept-id> <replacement-id>",
|
|
2195
2898
|
description: "Mark a record superseded by another, linking both directions. Use this rather than editing a record whose meaning changed \u2014 a record that quietly becomes something else invalidates every reference to it, and the earlier understanding is what a later trace needs.",
|
|
2196
|
-
input:
|
|
2899
|
+
input: z24.object({ bundlePath, conceptId, replacementId: conceptId }),
|
|
2197
2900
|
fromArgv: (argv, path) => ({
|
|
2198
2901
|
bundlePath: path,
|
|
2199
2902
|
conceptId: argv[1],
|
|
@@ -2207,16 +2910,16 @@ var supersedeCommand = define({
|
|
|
2207
2910
|
});
|
|
2208
2911
|
|
|
2209
2912
|
// src/commands/sync-instructions.ts
|
|
2210
|
-
import { z as
|
|
2913
|
+
import { z as z25 } from "zod";
|
|
2211
2914
|
var syncInstructionsCommand = define({
|
|
2212
2915
|
name: "sync-instructions",
|
|
2213
2916
|
usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
|
|
2214
2917
|
description: "Idempotently plant the `context` block between sentinel comments in an instruction file (AGENTS.md, CLAUDE.md), creating the block when absent and leaving everything outside the sentinels alone. CLI-only: this is file plumbing for runtimes whose instruction files are re-read where their conversations are not, not an agent capability \u2014 the capability is kb_context.",
|
|
2215
|
-
input:
|
|
2216
|
-
file:
|
|
2217
|
-
budgetTokens:
|
|
2218
|
-
fullUnderTokens:
|
|
2219
|
-
profile:
|
|
2918
|
+
input: z25.object({
|
|
2919
|
+
file: z25.string().min(1).describe("The instruction file to edit in place."),
|
|
2920
|
+
budgetTokens: z25.number().int().positive().optional(),
|
|
2921
|
+
fullUnderTokens: z25.number().int().positive().optional(),
|
|
2922
|
+
profile: z25.string().optional()
|
|
2220
2923
|
}),
|
|
2221
2924
|
fromArgv: (argv) => {
|
|
2222
2925
|
const budget = argvFlag(argv, "--budget");
|
|
@@ -2242,17 +2945,17 @@ var syncInstructionsCommand = define({
|
|
|
2242
2945
|
});
|
|
2243
2946
|
|
|
2244
2947
|
// src/commands/trace.ts
|
|
2245
|
-
import { z as
|
|
2948
|
+
import { z as z26 } from "zod";
|
|
2246
2949
|
var traceCommand = define({
|
|
2247
2950
|
name: "trace",
|
|
2248
2951
|
tool: "kb_trace",
|
|
2249
2952
|
usage: "trace <concept-id> [edges...]",
|
|
2250
2953
|
description: 'How a position was arrived at, as a timeline ordered by when each record was written. Deliberately includes rejected, draft, and superseded records \u2014 in a history those are the content, not noise. Follows supersession, shared code anchors, and shared sources. Use when the question is "why is this the way it is" rather than "what do we hold now". This tool (with kb_load and kb_query) is the only supported way to read a base; a raw file read bypasses supersession resolution and returns replaced records as if current.',
|
|
2251
|
-
input:
|
|
2954
|
+
input: z26.object({
|
|
2252
2955
|
bundlePath,
|
|
2253
2956
|
conceptId,
|
|
2254
|
-
edges:
|
|
2255
|
-
depth:
|
|
2957
|
+
edges: z26.array(z26.enum(TRACE_EDGES)).optional(),
|
|
2958
|
+
depth: z26.number().int().positive().optional()
|
|
2256
2959
|
}),
|
|
2257
2960
|
fromArgv: (argv, path) => ({
|
|
2258
2961
|
bundlePath: path,
|
|
@@ -2274,53 +2977,53 @@ var traceCommand = define({
|
|
|
2274
2977
|
});
|
|
2275
2978
|
|
|
2276
2979
|
// src/commands/types.ts
|
|
2277
|
-
import { z as
|
|
2980
|
+
import { z as z27 } from "zod";
|
|
2278
2981
|
var typesCommand = define({
|
|
2279
2982
|
name: "types",
|
|
2280
2983
|
tool: "kb_types",
|
|
2281
2984
|
usage: "types",
|
|
2282
2985
|
description: "The twelve record types with their purpose, body sections, and starting status. Read this before writing rather than guessing headings \u2014 a section the type does not define is rejected.",
|
|
2283
|
-
input:
|
|
2986
|
+
input: z27.object({}),
|
|
2284
2987
|
fromArgv: () => ({}),
|
|
2285
2988
|
run: () => Promise.resolve(RECORD_TYPES)
|
|
2286
2989
|
});
|
|
2287
2990
|
|
|
2288
2991
|
// src/commands/unpin.ts
|
|
2289
|
-
import { z as
|
|
2992
|
+
import { z as z28 } from "zod";
|
|
2290
2993
|
var unpinCommand = define({
|
|
2291
2994
|
name: "unpin",
|
|
2292
2995
|
tool: "kb_unpin",
|
|
2293
2996
|
usage: "unpin [bundle-path]",
|
|
2294
2997
|
description: "Remove a base from every pin manifest layer that holds it \u2014 project, local, and user \u2014 because unpinned means gone, not still injected from another file. Reports which layers were touched.",
|
|
2295
|
-
input:
|
|
2998
|
+
input: z28.object({ bundlePath }),
|
|
2296
2999
|
fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
|
|
2297
3000
|
run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
|
|
2298
3001
|
});
|
|
2299
3002
|
|
|
2300
3003
|
// src/commands/validate.ts
|
|
2301
|
-
import { z as
|
|
3004
|
+
import { z as z29 } from "zod";
|
|
2302
3005
|
var validateCommand = define({
|
|
2303
3006
|
name: "validate",
|
|
2304
3007
|
tool: "kb_validate",
|
|
2305
3008
|
usage: "validate",
|
|
2306
3009
|
description: "Check pointers no single record can see: supersession links that disagree between the two records, and assumptions that cite sources. Per-record shape is enforced on every read, so a problem here means someone edited a file by hand.",
|
|
2307
|
-
input:
|
|
3010
|
+
input: z29.object({ bundlePath }),
|
|
2308
3011
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
2309
3012
|
run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
|
|
2310
3013
|
failsWhen: (result) => Array.isArray(result) && result.length > 0
|
|
2311
3014
|
});
|
|
2312
3015
|
|
|
2313
3016
|
// src/commands/verify.ts
|
|
2314
|
-
import { z as
|
|
3017
|
+
import { z as z30 } from "zod";
|
|
2315
3018
|
var verifyCommand = define({
|
|
2316
3019
|
name: "verify",
|
|
2317
3020
|
tool: "kb_verify",
|
|
2318
3021
|
usage: "verify <concept-id> --note <text>",
|
|
2319
3022
|
description: "Append one verified[] event \u2014 who checked the record, when, and what the check found. Appends only; prior events are never rewritten. A record's own generator is refused unless the actor is human: re-reading your own output is not an independent check.",
|
|
2320
|
-
input:
|
|
3023
|
+
input: z30.object({
|
|
2321
3024
|
bundlePath,
|
|
2322
3025
|
conceptId,
|
|
2323
|
-
note:
|
|
3026
|
+
note: z30.string().refine((s) => s.trim().length > 0, {
|
|
2324
3027
|
message: "note must say what the check found"
|
|
2325
3028
|
})
|
|
2326
3029
|
}),
|
|
@@ -2340,7 +3043,7 @@ var verifyCommand = define({
|
|
|
2340
3043
|
});
|
|
2341
3044
|
|
|
2342
3045
|
// src/commands/write.ts
|
|
2343
|
-
import { z as
|
|
3046
|
+
import { z as z31 } from "zod";
|
|
2344
3047
|
var writeCommand = define({
|
|
2345
3048
|
name: "write",
|
|
2346
3049
|
tool: "kb_write",
|
|
@@ -2354,9 +3057,9 @@ var writeCommand = define({
|
|
|
2354
3057
|
"- Prefer a new record over overloading an existing one, and keep each short. A record nobody finishes reading is not durable memory.",
|
|
2355
3058
|
"- Records are never deleted; supersede instead, so the earlier reasoning stays inspectable."
|
|
2356
3059
|
].join("\n"),
|
|
2357
|
-
input:
|
|
3060
|
+
input: z31.object({
|
|
2358
3061
|
bundlePath,
|
|
2359
|
-
type:
|
|
3062
|
+
type: z31.enum(KB_RECORD_TYPES),
|
|
2360
3063
|
input: composeInputSchema
|
|
2361
3064
|
}),
|
|
2362
3065
|
fromArgv: async (argv, path, stdin) => ({
|
|
@@ -2380,7 +3083,7 @@ var writeCommand = define({
|
|
|
2380
3083
|
});
|
|
2381
3084
|
|
|
2382
3085
|
// src/commands/write-decision.ts
|
|
2383
|
-
import { z as
|
|
3086
|
+
import { z as z32 } from "zod";
|
|
2384
3087
|
var writeDecisionCommand = define({
|
|
2385
3088
|
name: "write-decision",
|
|
2386
3089
|
tool: "kb_write_decision",
|
|
@@ -2393,7 +3096,7 @@ var writeDecisionCommand = define({
|
|
|
2393
3096
|
"- `alternative` is what you turned down and why, not a list of everything considered.",
|
|
2394
3097
|
"- A reference to material you read goes in `sources`; a reference to code goes in `anchors`; a reference to another record goes in `relatedConceptIds`."
|
|
2395
3098
|
].join("\n"),
|
|
2396
|
-
input:
|
|
3099
|
+
input: z32.object({ bundlePath, input: decisionInputSchema }),
|
|
2397
3100
|
fromArgv: async (_argv, path, stdin) => ({
|
|
2398
3101
|
bundlePath: path,
|
|
2399
3102
|
input: JSON.parse(await stdin())
|
|
@@ -2422,6 +3125,7 @@ var KB_COMMANDS = [
|
|
|
2422
3125
|
supersedeCommand,
|
|
2423
3126
|
answerCommand,
|
|
2424
3127
|
verifyCommand,
|
|
3128
|
+
anchorResolveCommand,
|
|
2425
3129
|
loadCommand,
|
|
2426
3130
|
catalogCommand,
|
|
2427
3131
|
packCommand,
|
|
@@ -2470,7 +3174,7 @@ function parseMarkdownWithFrontmatter(text, schema) {
|
|
|
2470
3174
|
}
|
|
2471
3175
|
|
|
2472
3176
|
// src/search-index.ts
|
|
2473
|
-
import { stat } from "fs/promises";
|
|
3177
|
+
import { stat as stat2 } from "fs/promises";
|
|
2474
3178
|
import { join as join3 } from "path";
|
|
2475
3179
|
var SEARCH_INDEX_FILE = ".index.sqlite";
|
|
2476
3180
|
var COLLECTION = "kb";
|
|
@@ -2515,16 +3219,19 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
2515
3219
|
}
|
|
2516
3220
|
}
|
|
2517
3221
|
async function isStale(bundlePath2) {
|
|
2518
|
-
const indexAt = await
|
|
3222
|
+
const indexAt = await stat2(join3(bundlePath2, SEARCH_INDEX_FILE)).then((s) => s.mtimeMs).catch(() => 0);
|
|
2519
3223
|
if (!indexAt) return true;
|
|
2520
3224
|
const { readdir: readdir2 } = await import("fs/promises");
|
|
2521
|
-
const names = await readdir2(bundlePath2).catch(() => [])
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
3225
|
+
const names = (await readdir2(bundlePath2).catch(() => [])).filter(
|
|
3226
|
+
(name) => name.endsWith(".md") && name !== INDEX_FILE
|
|
3227
|
+
);
|
|
3228
|
+
let stale = false;
|
|
3229
|
+
await mapLimit(names, DEFAULT_IO_CONCURRENCY, async (name) => {
|
|
3230
|
+
if (stale) return;
|
|
3231
|
+
const at = await stat2(join3(bundlePath2, name)).then((s) => s.mtimeMs).catch(() => 0);
|
|
3232
|
+
if (at > indexAt) stale = true;
|
|
3233
|
+
});
|
|
3234
|
+
return stale;
|
|
2528
3235
|
}
|
|
2529
3236
|
function resolveHits(hits, records) {
|
|
2530
3237
|
const byName = /* @__PURE__ */ new Map();
|
|
@@ -2556,18 +3263,18 @@ async function loadQmd(logger) {
|
|
|
2556
3263
|
}
|
|
2557
3264
|
|
|
2558
3265
|
// src/kb-store.ts
|
|
2559
|
-
import { createHash } from "crypto";
|
|
3266
|
+
import { createHash as createHash2 } from "crypto";
|
|
2560
3267
|
import {
|
|
2561
3268
|
appendFile,
|
|
2562
3269
|
link,
|
|
2563
3270
|
mkdir as mkdir2,
|
|
2564
3271
|
readdir,
|
|
2565
|
-
readFile as
|
|
3272
|
+
readFile as readFile4,
|
|
2566
3273
|
rename,
|
|
2567
3274
|
unlink,
|
|
2568
3275
|
writeFile as writeFile3
|
|
2569
3276
|
} from "fs/promises";
|
|
2570
|
-
import { join as join4, resolve as
|
|
3277
|
+
import { join as join4, resolve as resolve5, sep as sep3 } from "path";
|
|
2571
3278
|
|
|
2572
3279
|
// src/kb-gitattributes.ts
|
|
2573
3280
|
var GITATTRIBUTES_FILE = ".gitattributes";
|
|
@@ -2664,7 +3371,7 @@ var KbStore = class {
|
|
|
2664
3371
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
2665
3372
|
let raw;
|
|
2666
3373
|
try {
|
|
2667
|
-
raw = await
|
|
3374
|
+
raw = await readFile4(target, "utf8");
|
|
2668
3375
|
} catch {
|
|
2669
3376
|
return null;
|
|
2670
3377
|
}
|
|
@@ -2686,10 +3393,10 @@ var KbStore = class {
|
|
|
2686
3393
|
return [];
|
|
2687
3394
|
}
|
|
2688
3395
|
const wanted = names.sort().filter((name) => name.endsWith(".md") && !STORE_OWNED.has(name)).map((name) => ({ name, conceptId: name.slice(0, -".md".length) })).filter(({ conceptId: conceptId2 }) => !type || conceptId2.startsWith(`${type}.`));
|
|
2689
|
-
const records = await
|
|
2690
|
-
wanted
|
|
2691
|
-
|
|
2692
|
-
)
|
|
3396
|
+
const records = await mapLimit(
|
|
3397
|
+
wanted,
|
|
3398
|
+
DEFAULT_IO_CONCURRENCY,
|
|
3399
|
+
async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await readFile4(join4(root, name), "utf8"))
|
|
2693
3400
|
);
|
|
2694
3401
|
return records.filter((record) => record !== null);
|
|
2695
3402
|
}
|
|
@@ -2711,6 +3418,21 @@ var KbStore = class {
|
|
|
2711
3418
|
{ operation: `status:${status}`, by: actor }
|
|
2712
3419
|
);
|
|
2713
3420
|
}
|
|
3421
|
+
/**
|
|
3422
|
+
* Replaces a record's anchors wholesale, preserving everything else.
|
|
3423
|
+
*
|
|
3424
|
+
* Wholesale rather than merged: the caller just resolved the anchors it is
|
|
3425
|
+
* writing, so it holds the complete current set, and a merge would keep
|
|
3426
|
+
* stale entries the resolution pass deliberately dropped.
|
|
3427
|
+
*/
|
|
3428
|
+
async updateAnchors(bundlePath2, conceptId2, anchors, actor = "unknown") {
|
|
3429
|
+
return this.mutate(
|
|
3430
|
+
bundlePath2,
|
|
3431
|
+
conceptId2,
|
|
3432
|
+
(frontmatter) => ({ ...frontmatter, strauss_anchors: anchors }),
|
|
3433
|
+
{ operation: "anchor-resolve", by: actor }
|
|
3434
|
+
);
|
|
3435
|
+
}
|
|
2714
3436
|
/**
|
|
2715
3437
|
* Appends one `verified[]` event: who checked the record, when, and what the
|
|
2716
3438
|
* check found. Append-only — prior events are history, and are spread into
|
|
@@ -2809,9 +3531,12 @@ ${answer}
|
|
|
2809
3531
|
const bundle = await this.list(bundlePath2);
|
|
2810
3532
|
const needle = text.trim();
|
|
2811
3533
|
const hits = needle ? await this.rank(bundlePath2, needle, bundle) : bundle;
|
|
3534
|
+
const narrowed = options.type ? hits.filter((r) => r.frontmatter.type === options.type) : hits;
|
|
2812
3535
|
const adjudicated = adjudicate(
|
|
2813
|
-
|
|
2814
|
-
bundle
|
|
3536
|
+
narrowed,
|
|
3537
|
+
bundle,
|
|
3538
|
+
/* @__PURE__ */ new Date(),
|
|
3539
|
+
await this.detectDrift(narrowed, options.repoRoot)
|
|
2815
3540
|
);
|
|
2816
3541
|
if (options.includeNonCurrent) return adjudicated;
|
|
2817
3542
|
const present = new Set(adjudicated.map((hit) => hit.record.conceptId));
|
|
@@ -2830,6 +3555,50 @@ ${answer}
|
|
|
2830
3555
|
const lowered = needle.toLowerCase();
|
|
2831
3556
|
return bundle.filter((record) => matches(record, lowered));
|
|
2832
3557
|
}
|
|
3558
|
+
/**
|
|
3559
|
+
* Anchor drift over the records about to be handed back. Like the search
|
|
3560
|
+
* index, this is an enrichment: a filesystem failure degrades to "no drift
|
|
3561
|
+
* reported" rather than failing the read. Anchors without a stored hash are
|
|
3562
|
+
* skipped inside `detectAnchorDrift`, so a base nobody has stamped pays no
|
|
3563
|
+
* fs cost here. `repoRoot` defaults to the working directory — the CLI runs
|
|
3564
|
+
* at the repo root, and the MCP server's cwd is the workspace.
|
|
3565
|
+
*
|
|
3566
|
+
* Public because `doctor` needs the same map with the same degradation: a
|
|
3567
|
+
* sweep that failed to read the tree should report no drift, not fail.
|
|
3568
|
+
*
|
|
3569
|
+
* When no root was given and not one anchored file was found, the finding is
|
|
3570
|
+
* discarded. A base read from somewhere other than the tree it describes
|
|
3571
|
+
* misses every file at once, and that shape is far likelier to be a wrong
|
|
3572
|
+
* default root than a repository where every anchored file was deleted on
|
|
3573
|
+
* the same day. Reporting it would put a drift warning on every record in
|
|
3574
|
+
* the base, which teaches a reader to ignore the warning — the one outcome
|
|
3575
|
+
* worse than not having it. One file found anywhere makes the root
|
|
3576
|
+
* plausible, and the misses become findings again; an explicit `repoRoot` is
|
|
3577
|
+
* taken at its word either way.
|
|
3578
|
+
*/
|
|
3579
|
+
async detectDrift(records, repoRoot) {
|
|
3580
|
+
try {
|
|
3581
|
+
const drift = await detectAnchorDrift(records, {
|
|
3582
|
+
repoRoot: repoRoot ?? process.cwd()
|
|
3583
|
+
});
|
|
3584
|
+
if (repoRoot === void 0 && looksLikeWrongRepoRoot(drift)) {
|
|
3585
|
+
this.logger.warn?.({
|
|
3586
|
+
operation: "kb.anchor-drift",
|
|
3587
|
+
outcome: "skipped",
|
|
3588
|
+
reason: "no anchored file found under the default repo root"
|
|
3589
|
+
});
|
|
3590
|
+
return void 0;
|
|
3591
|
+
}
|
|
3592
|
+
return drift;
|
|
3593
|
+
} catch (error) {
|
|
3594
|
+
this.logger.warn?.({
|
|
3595
|
+
operation: "kb.anchor-drift",
|
|
3596
|
+
outcome: "skipped",
|
|
3597
|
+
error: error instanceof Error ? error.message : "unknown"
|
|
3598
|
+
});
|
|
3599
|
+
return void 0;
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
2833
3602
|
/**
|
|
2834
3603
|
* The whole base, adjudicated, when it is small enough to hand over.
|
|
2835
3604
|
*
|
|
@@ -2864,10 +3633,16 @@ ${answer}
|
|
|
2864
3633
|
const budgetTokens = options.budgetTokens ?? DEFAULT_LOAD_BUDGET;
|
|
2865
3634
|
const bundle = await this.list(bundlePath2);
|
|
2866
3635
|
const wanted = options.type ? bundle.filter((record) => record.frontmatter.type === options.type) : bundle;
|
|
2867
|
-
const adjudicated = adjudicate(
|
|
3636
|
+
const adjudicated = adjudicate(
|
|
3637
|
+
wanted,
|
|
3638
|
+
bundle,
|
|
3639
|
+
/* @__PURE__ */ new Date(),
|
|
3640
|
+
await this.detectDrift(wanted, options.repoRoot)
|
|
3641
|
+
);
|
|
2868
3642
|
const records = adjudicated.filter((hit) => hit.standing !== "superseded");
|
|
2869
3643
|
const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(stub);
|
|
2870
3644
|
const approxTokens2 = records.reduce((total, hit) => total + estimateTokens(hit.record), 0) + superseded.reduce((total, entry) => total + estimateStubTokens(entry), 0);
|
|
3645
|
+
const bundleDigestValue = bundleDigest(records, superseded);
|
|
2871
3646
|
if (!options.all && approxTokens2 > budgetTokens) {
|
|
2872
3647
|
return {
|
|
2873
3648
|
loaded: false,
|
|
@@ -2878,7 +3653,8 @@ ${answer}
|
|
|
2878
3653
|
approxTokens: approxTokens2,
|
|
2879
3654
|
budgetTokens,
|
|
2880
3655
|
type: options.type
|
|
2881
|
-
})
|
|
3656
|
+
}),
|
|
3657
|
+
digest: bundleDigestValue
|
|
2882
3658
|
};
|
|
2883
3659
|
}
|
|
2884
3660
|
return {
|
|
@@ -2887,7 +3663,8 @@ ${answer}
|
|
|
2887
3663
|
tokensLoaded: approxTokens2,
|
|
2888
3664
|
budgetTokens: options.all ? null : budgetTokens,
|
|
2889
3665
|
records,
|
|
2890
|
-
superseded
|
|
3666
|
+
superseded,
|
|
3667
|
+
digest: bundleDigestValue
|
|
2891
3668
|
};
|
|
2892
3669
|
}
|
|
2893
3670
|
/** How a position was arrived at, as a timeline. See `trace.ts`. */
|
|
@@ -2912,7 +3689,7 @@ ${answer}
|
|
|
2912
3689
|
async readIndex(bundlePath2) {
|
|
2913
3690
|
const root = this.root(bundlePath2);
|
|
2914
3691
|
const expected = renderIndex(await this.list(bundlePath2));
|
|
2915
|
-
const stored = await
|
|
3692
|
+
const stored = await readFile4(join4(root, INDEX_FILE), "utf8").catch(
|
|
2916
3693
|
() => null
|
|
2917
3694
|
);
|
|
2918
3695
|
if (indexIsStale(stored, expected)) {
|
|
@@ -2933,7 +3710,7 @@ ${answer}
|
|
|
2933
3710
|
* knows which agent touched what. So a bad line is surfaced and left alone.
|
|
2934
3711
|
*/
|
|
2935
3712
|
async readLog(bundlePath2) {
|
|
2936
|
-
const raw = await
|
|
3713
|
+
const raw = await readFile4(
|
|
2937
3714
|
join4(this.root(bundlePath2), LOG_FILE),
|
|
2938
3715
|
"utf8"
|
|
2939
3716
|
).catch(() => "");
|
|
@@ -2985,14 +3762,14 @@ ${answer}
|
|
|
2985
3762
|
}
|
|
2986
3763
|
async mutate(bundlePath2, conceptId2, change, entry, changeBody = (body) => body) {
|
|
2987
3764
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
2988
|
-
const before = await
|
|
3765
|
+
const before = await readFile4(target, "utf8").catch(() => null);
|
|
2989
3766
|
if (before === null) throw new KbRecordNotFoundError(conceptId2);
|
|
2990
3767
|
const parsed = this.parse(conceptId2, before);
|
|
2991
3768
|
if (!parsed) throw new KbRecordNotFoundError(conceptId2);
|
|
2992
3769
|
const frontmatter = change(parsed.frontmatter);
|
|
2993
3770
|
const body = changeBody(parsed.body);
|
|
2994
3771
|
const contents = stringifyMarkdownWithFrontmatter(body, frontmatter);
|
|
2995
|
-
const witness = await
|
|
3772
|
+
const witness = await readFile4(target, "utf8").catch(() => null);
|
|
2996
3773
|
if (witness === null || digest(witness) !== digest(before)) {
|
|
2997
3774
|
throw new KbWriteConflictError(conceptId2);
|
|
2998
3775
|
}
|
|
@@ -3079,16 +3856,26 @@ ${answer}
|
|
|
3079
3856
|
try {
|
|
3080
3857
|
let existing;
|
|
3081
3858
|
try {
|
|
3082
|
-
existing = await
|
|
3859
|
+
existing = await readFile4(target, "utf8");
|
|
3083
3860
|
} catch (error) {
|
|
3084
3861
|
if (error.code !== "ENOENT") throw error;
|
|
3085
3862
|
existing = null;
|
|
3086
3863
|
}
|
|
3087
3864
|
if (existing === null) {
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3865
|
+
try {
|
|
3866
|
+
await writeFile3(target, appendUnionMergeLine(""), {
|
|
3867
|
+
encoding: "utf8",
|
|
3868
|
+
flag: "wx"
|
|
3869
|
+
});
|
|
3870
|
+
} catch (error) {
|
|
3871
|
+
if (error.code !== "EEXIST") throw error;
|
|
3872
|
+
this.logger.info?.({
|
|
3873
|
+
operation: "kb.gitattributes.ensure",
|
|
3874
|
+
bundlePath: root,
|
|
3875
|
+
outcome: "exists"
|
|
3876
|
+
});
|
|
3877
|
+
return;
|
|
3878
|
+
}
|
|
3092
3879
|
this.logger.info?.({
|
|
3093
3880
|
operation: "kb.gitattributes.ensure",
|
|
3094
3881
|
bundlePath: root,
|
|
@@ -3142,12 +3929,12 @@ ${answer}
|
|
|
3142
3929
|
};
|
|
3143
3930
|
}
|
|
3144
3931
|
root(bundlePath2) {
|
|
3145
|
-
return
|
|
3932
|
+
return resolve5(bundlePath2);
|
|
3146
3933
|
}
|
|
3147
3934
|
// Concept ids are `<type>.<slug>` and map to a single file directly under the
|
|
3148
3935
|
// bundle root; anything carrying a separator would escape it.
|
|
3149
3936
|
recordPath(bundlePath2, conceptId2) {
|
|
3150
|
-
if (conceptId2.includes(
|
|
3937
|
+
if (conceptId2.includes(sep3) || conceptId2.includes("/")) {
|
|
3151
3938
|
throw new KbInvalidConceptIdError(
|
|
3152
3939
|
"concept id must not contain a path separator",
|
|
3153
3940
|
{ conceptId: conceptId2 }
|
|
@@ -3192,7 +3979,23 @@ function normalizeActor(id) {
|
|
|
3192
3979
|
return id.slice(0, colon + 1).toLowerCase() + id.slice(colon + 1);
|
|
3193
3980
|
}
|
|
3194
3981
|
function digest(contents) {
|
|
3195
|
-
return
|
|
3982
|
+
return createHash2("sha256").update(contents).digest("hex");
|
|
3983
|
+
}
|
|
3984
|
+
function bundleDigest(records, superseded) {
|
|
3985
|
+
const entries = [
|
|
3986
|
+
...records.map(
|
|
3987
|
+
(hit) => `${hit.record.conceptId}:current:${digest(
|
|
3988
|
+
stringifyMarkdownWithFrontmatter(
|
|
3989
|
+
hit.record.body,
|
|
3990
|
+
hit.record.frontmatter
|
|
3991
|
+
)
|
|
3992
|
+
)}`
|
|
3993
|
+
),
|
|
3994
|
+
...superseded.map(
|
|
3995
|
+
(entry) => `${entry.conceptId}:superseded:${digest(JSON.stringify(entry))}`
|
|
3996
|
+
)
|
|
3997
|
+
].sort();
|
|
3998
|
+
return digest(entries.join("\n"));
|
|
3196
3999
|
}
|
|
3197
4000
|
|
|
3198
4001
|
// src/pack.ts
|
|
@@ -3280,7 +4083,7 @@ function typeRank(record) {
|
|
|
3280
4083
|
}
|
|
3281
4084
|
|
|
3282
4085
|
// src/version.ts
|
|
3283
|
-
var VERSION = true ? "0.1.
|
|
4086
|
+
var VERSION = true ? "0.1.12" : "0.0.0-dev";
|
|
3284
4087
|
|
|
3285
4088
|
export {
|
|
3286
4089
|
kbSourceSchema,
|
|
@@ -3306,6 +4109,21 @@ export {
|
|
|
3306
4109
|
composeNoDecisionRecord,
|
|
3307
4110
|
isNoDecisionRecord,
|
|
3308
4111
|
selectDecisions,
|
|
4112
|
+
regexResolver,
|
|
4113
|
+
hashAnchorText,
|
|
4114
|
+
resolveAnchor,
|
|
4115
|
+
anchorFilePath,
|
|
4116
|
+
detectAnchorDrift,
|
|
4117
|
+
Fault,
|
|
4118
|
+
ErrorTypes,
|
|
4119
|
+
BaseError,
|
|
4120
|
+
KbRecordAlreadyExistsError,
|
|
4121
|
+
KbRecordNotFoundError,
|
|
4122
|
+
KbWriteConflictError,
|
|
4123
|
+
KbSelfVerificationError,
|
|
4124
|
+
KbPackBudgetExceededError,
|
|
4125
|
+
KbMissingFlagValueError,
|
|
4126
|
+
KbInvalidConceptIdError,
|
|
3309
4127
|
contextProfileBudgets,
|
|
3310
4128
|
mergedContextBudgets,
|
|
3311
4129
|
KbPinsMalformedError,
|
|
@@ -3320,16 +4138,6 @@ export {
|
|
|
3320
4138
|
listPins,
|
|
3321
4139
|
pinBase,
|
|
3322
4140
|
unpinBase,
|
|
3323
|
-
Fault,
|
|
3324
|
-
ErrorTypes,
|
|
3325
|
-
BaseError,
|
|
3326
|
-
KbRecordAlreadyExistsError,
|
|
3327
|
-
KbRecordNotFoundError,
|
|
3328
|
-
KbWriteConflictError,
|
|
3329
|
-
KbSelfVerificationError,
|
|
3330
|
-
KbPackBudgetExceededError,
|
|
3331
|
-
KbMissingFlagValueError,
|
|
3332
|
-
KbInvalidConceptIdError,
|
|
3333
4141
|
adjudicate,
|
|
3334
4142
|
resolveHeads,
|
|
3335
4143
|
catalog,
|
|
@@ -3377,4 +4185,4 @@ export {
|
|
|
3377
4185
|
KbStore,
|
|
3378
4186
|
VERSION
|
|
3379
4187
|
};
|
|
3380
|
-
//# sourceMappingURL=chunk-
|
|
4188
|
+
//# sourceMappingURL=chunk-33ZCBEUV.js.map
|