@saasontools/strauss-kb 0.1.14 → 0.1.16
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 +20 -2
- package/README.md +44 -28
- package/dist/{chunk-43KALLFU.js → chunk-H5W53NVU.js} +1090 -388
- package/dist/chunk-H5W53NVU.js.map +1 -0
- package/dist/{chunk-PYA5E7FL.js → chunk-MEZCF646.js} +2 -2
- package/dist/{chunk-MBXNCZ4V.js → chunk-RINBOAQZ.js} +15 -6
- package/dist/chunk-RINBOAQZ.js.map +1 -0
- package/dist/cli-main.cjs +1111 -404
- package/dist/cli-main.cjs.map +1 -1
- package/dist/cli-main.js +2 -2
- package/dist/index.cjs +1152 -434
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +174 -69
- package/dist/index.d.ts +174 -69
- package/dist/index.js +9 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp-main.cjs +1100 -402
- package/dist/mcp-main.cjs.map +1 -1
- package/dist/mcp-main.js +2 -2
- package/package.json +2 -2
- package/dist/chunk-43KALLFU.js.map +0 -1
- package/dist/chunk-MBXNCZ4V.js.map +0 -1
- /package/dist/{chunk-PYA5E7FL.js.map → chunk-MEZCF646.js.map} +0 -0
|
@@ -24,16 +24,17 @@ var kbAnchorSchema = z.object({
|
|
|
24
24
|
* (`https://github.com/org/name`) or a short name. Absent means the base's
|
|
25
25
|
* own repository, which is what nearly every anchor means.
|
|
26
26
|
*
|
|
27
|
-
* Unvalidated beyond not-blank: one repository has many spellings
|
|
28
|
-
*
|
|
27
|
+
* Unvalidated beyond not-blank: one repository has many spellings, matched
|
|
28
|
+
* after normalisation. Only a full URL can be fetched from, so `validate`
|
|
29
|
+
* warns on a short one; see ARCHITECTURE.
|
|
29
30
|
*/
|
|
30
31
|
repo: z.string().trim().min(1).optional(),
|
|
31
32
|
/**
|
|
32
33
|
* The git rev the evidence was taken at. Prefer a commit SHA: a branch
|
|
33
34
|
* name is a moving pointer, so an anchor pinned to one says the evidence
|
|
34
35
|
* came from wherever that branch happens to be now, which is not a
|
|
35
|
-
* baseline.
|
|
36
|
-
*
|
|
36
|
+
* baseline. A foreign anchor is checked at this rev, and compared against
|
|
37
|
+
* the remote's default branch on top of it.
|
|
37
38
|
*/
|
|
38
39
|
ref: z.string().trim().min(1).optional(),
|
|
39
40
|
hash: z.string().regex(/^sha256:[0-9a-f]{64}$/, {
|
|
@@ -415,12 +416,116 @@ function selectDecisions(records) {
|
|
|
415
416
|
);
|
|
416
417
|
}
|
|
417
418
|
|
|
418
|
-
// src/anchor-resolver.ts
|
|
419
|
+
// src/anchor-resolver/repo-identity.ts
|
|
419
420
|
import { execFile } from "child_process";
|
|
420
|
-
import { createHash } from "crypto";
|
|
421
|
-
import { readFile, realpath, stat } from "fs/promises";
|
|
422
|
-
import { isAbsolute, relative, resolve, sep } from "path";
|
|
423
421
|
import { promisify } from "util";
|
|
422
|
+
var execFileAsync = promisify(execFile);
|
|
423
|
+
function normalizeRepoUrl(value) {
|
|
424
|
+
let url = value.trim().replace(/^git\+/, "");
|
|
425
|
+
const scp = /^[\w.-]+@([\w.-]+):(.+)$/.exec(url);
|
|
426
|
+
if (scp) url = `https://${scp[1]}/${scp[2]}`;
|
|
427
|
+
url = url.replace(/^ssh:\/\/(?:[^@/]+@)?/, "https://");
|
|
428
|
+
url = trimTrailingSlashes(url);
|
|
429
|
+
if (url.endsWith(".git")) url = url.slice(0, -4);
|
|
430
|
+
return trimTrailingSlashes(url).toLowerCase();
|
|
431
|
+
}
|
|
432
|
+
function trimTrailingSlashes(value) {
|
|
433
|
+
let end = value.length;
|
|
434
|
+
while (end > 0 && value[end - 1] === "/") end -= 1;
|
|
435
|
+
return value.slice(0, end);
|
|
436
|
+
}
|
|
437
|
+
function isCanonicalRepoUrl(value) {
|
|
438
|
+
return /^[a-z0-9+.-]+:\/\//.test(normalizeRepoUrl(value));
|
|
439
|
+
}
|
|
440
|
+
function repoPath(normalized) {
|
|
441
|
+
const withoutScheme = normalized.replace(/^[a-z0-9+.-]+:\/\//, "");
|
|
442
|
+
const segments = withoutScheme.split("/").filter(Boolean);
|
|
443
|
+
return segments.length > 1 ? segments.slice(1).join("/") : "";
|
|
444
|
+
}
|
|
445
|
+
function repoIdentifies(declared, originUrl) {
|
|
446
|
+
if (!originUrl) return false;
|
|
447
|
+
const origin = normalizeRepoUrl(originUrl);
|
|
448
|
+
const want = normalizeRepoUrl(declared);
|
|
449
|
+
if (!want || !origin) return false;
|
|
450
|
+
if (want === origin) return true;
|
|
451
|
+
const path = repoPath(origin);
|
|
452
|
+
if (!path) return false;
|
|
453
|
+
return want === path || want === (path.split("/").pop() ?? "");
|
|
454
|
+
}
|
|
455
|
+
async function repoOriginUrl(repoRoot) {
|
|
456
|
+
try {
|
|
457
|
+
const { stdout } = await execFileAsync(
|
|
458
|
+
"git",
|
|
459
|
+
["-C", repoRoot, "config", "--get", "remote.origin.url"],
|
|
460
|
+
{ timeout: 5e3 }
|
|
461
|
+
);
|
|
462
|
+
return stdout.trim() || null;
|
|
463
|
+
} catch {
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
var LazyOrigin = class {
|
|
468
|
+
constructor(repoRoot) {
|
|
469
|
+
this.repoRoot = repoRoot;
|
|
470
|
+
}
|
|
471
|
+
repoRoot;
|
|
472
|
+
url = null;
|
|
473
|
+
asked = false;
|
|
474
|
+
/** Asks git once, so later `isForeign` calls need no await. */
|
|
475
|
+
async prime() {
|
|
476
|
+
if (this.asked) return;
|
|
477
|
+
this.url = await repoOriginUrl(this.repoRoot);
|
|
478
|
+
this.asked = true;
|
|
479
|
+
}
|
|
480
|
+
/** Only meaningful after `prime`; an unprimed origin identifies nothing. */
|
|
481
|
+
isForeign(anchor) {
|
|
482
|
+
if (!anchor.repo) return false;
|
|
483
|
+
return !repoIdentifies(anchor.repo, this.url);
|
|
484
|
+
}
|
|
485
|
+
async foreign(anchor) {
|
|
486
|
+
if (!anchor.repo) return false;
|
|
487
|
+
await this.prime();
|
|
488
|
+
return this.isForeign(anchor);
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
// src/remote-repo/cache.ts
|
|
493
|
+
import { homedir } from "os";
|
|
494
|
+
import { join } from "path";
|
|
495
|
+
function repoCacheDir(override) {
|
|
496
|
+
return override ?? process.env["STRAUSS_KB_REPO_CACHE"] ?? join(homedir(), ".strauss", "repo-cache");
|
|
497
|
+
}
|
|
498
|
+
var DEFAULT_FETCH_TIMEOUT_MS = 3e4;
|
|
499
|
+
function fetchTimeoutMs(override) {
|
|
500
|
+
if (override !== void 0) return override;
|
|
501
|
+
const fromEnv = Number(process.env["STRAUSS_KB_FETCH_TIMEOUT_MS"]);
|
|
502
|
+
return Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv : DEFAULT_FETCH_TIMEOUT_MS;
|
|
503
|
+
}
|
|
504
|
+
function cachePathFor(repo, cacheDir) {
|
|
505
|
+
const normalized = normalizeRepoUrl(repo);
|
|
506
|
+
const scheme = /^[a-z0-9+.-]+:\/\//.exec(normalized);
|
|
507
|
+
if (!scheme) return null;
|
|
508
|
+
const segments = normalized.slice(scheme[0].length).split("/").filter(Boolean).map((segment) => safeSegment(segment));
|
|
509
|
+
if (segments.length < 2 || segments.some((segment) => segment === null)) {
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
const path = segments;
|
|
513
|
+
return join(cacheDir, ...path.slice(0, -1), `${path[path.length - 1]}.git`);
|
|
514
|
+
}
|
|
515
|
+
function safeSegment(value) {
|
|
516
|
+
return value === "." || value === ".." || value.includes("\0") ? null : value.replace(/[/\\:]/g, "-");
|
|
517
|
+
}
|
|
518
|
+
function revRef(rev) {
|
|
519
|
+
const safe = rev.replace(/[^A-Za-z0-9_-]/g, "-").slice(0, 64);
|
|
520
|
+
let hash = 5381;
|
|
521
|
+
for (let at = 0; at < rev.length; at++) {
|
|
522
|
+
hash = (hash * 33 ^ rev.charCodeAt(at)) >>> 0;
|
|
523
|
+
}
|
|
524
|
+
return `refs/strauss/${safe}-${hash.toString(16)}`;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// src/remote-repo/read.ts
|
|
528
|
+
import { mkdir } from "fs/promises";
|
|
424
529
|
|
|
425
530
|
// src/concurrency.ts
|
|
426
531
|
var DEFAULT_IO_CONCURRENCY = 16;
|
|
@@ -451,9 +556,415 @@ async function mapLimit(items, limit, fn) {
|
|
|
451
556
|
return out;
|
|
452
557
|
}
|
|
453
558
|
|
|
454
|
-
// src/
|
|
455
|
-
|
|
559
|
+
// src/remote-repo/git.ts
|
|
560
|
+
import { execFile as execFile2 } from "child_process";
|
|
561
|
+
import { promisify as promisify2 } from "util";
|
|
562
|
+
|
|
563
|
+
// src/anchor-resolver/model.ts
|
|
456
564
|
var MAX_ANCHOR_FILE_BYTES = 1048576;
|
|
565
|
+
|
|
566
|
+
// src/remote-repo/git.ts
|
|
567
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
568
|
+
function childEnv() {
|
|
569
|
+
const env = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
|
|
570
|
+
for (const name of ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE"]) {
|
|
571
|
+
delete env[name];
|
|
572
|
+
}
|
|
573
|
+
return env;
|
|
574
|
+
}
|
|
575
|
+
async function git(args, options = {}) {
|
|
576
|
+
try {
|
|
577
|
+
const { stdout, stderr } = await execFileAsync2("git", args, {
|
|
578
|
+
...options.cwd ? { cwd: options.cwd } : {},
|
|
579
|
+
timeout: options.timeoutMs ?? 3e4,
|
|
580
|
+
maxBuffer: options.maxBytes ?? MAX_ANCHOR_FILE_BYTES,
|
|
581
|
+
encoding: "utf8",
|
|
582
|
+
windowsHide: true,
|
|
583
|
+
env: childEnv()
|
|
584
|
+
});
|
|
585
|
+
return { ok: true, stdout, stderr, overflowed: false };
|
|
586
|
+
} catch (error) {
|
|
587
|
+
const failure = error;
|
|
588
|
+
return {
|
|
589
|
+
ok: false,
|
|
590
|
+
stdout: failure.stdout ?? "",
|
|
591
|
+
stderr: failure.stderr ?? "",
|
|
592
|
+
overflowed: failure.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER"
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
function transportReason(stderr) {
|
|
597
|
+
const text = stderr.toLowerCase();
|
|
598
|
+
if (text.includes("authentication failed") || text.includes("permission denied") || text.includes("could not read username") || text.includes("403 forbidden") || text.includes("access denied")) {
|
|
599
|
+
return "repo-unauthorized";
|
|
600
|
+
}
|
|
601
|
+
if (text.includes("couldn't find remote ref") || text.includes("unadvertised object") || text.includes("not our ref")) {
|
|
602
|
+
return "ref-not-found";
|
|
603
|
+
}
|
|
604
|
+
return "remote-unreachable";
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// src/remote-repo/model.ts
|
|
608
|
+
var UNCHECKED_REASONS = [
|
|
609
|
+
"remote-unreachable",
|
|
610
|
+
"repo-unauthorized",
|
|
611
|
+
"default-branch-unknown"
|
|
612
|
+
];
|
|
613
|
+
function isUncheckedReason(reason) {
|
|
614
|
+
return reason !== void 0 && UNCHECKED_REASONS.includes(reason);
|
|
615
|
+
}
|
|
616
|
+
function wantKey(repo, ref, file) {
|
|
617
|
+
return `${repo}\0${ref ?? ""}\0${file}`;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// src/remote-repo/validate.ts
|
|
621
|
+
var MAX_REF_LENGTH = 200;
|
|
622
|
+
var REF_SHAPE = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
623
|
+
function refShapeIsSafe(ref) {
|
|
624
|
+
if (!ref || ref.length > MAX_REF_LENGTH) return false;
|
|
625
|
+
if (ref.includes("..")) return false;
|
|
626
|
+
return REF_SHAPE.test(ref);
|
|
627
|
+
}
|
|
628
|
+
async function refIsWellFormed(ref) {
|
|
629
|
+
if (!refShapeIsSafe(ref)) return false;
|
|
630
|
+
const checked = await git(["check-ref-format", "--allow-onelevel", ref]);
|
|
631
|
+
return checked.ok;
|
|
632
|
+
}
|
|
633
|
+
function filePathIsSafe(file) {
|
|
634
|
+
const path = file.replace(/^\.\//, "");
|
|
635
|
+
if (!path || path.startsWith("-") || path.includes("\0")) return false;
|
|
636
|
+
return !path.split("/").includes("..");
|
|
637
|
+
}
|
|
638
|
+
var DEFAULT_PROTOCOLS = ["https", "ssh", "git"];
|
|
639
|
+
function allowedProtocols() {
|
|
640
|
+
const raw = process.env["STRAUSS_KB_REPO_PROTOCOLS"];
|
|
641
|
+
if (raw === void 0) return [...DEFAULT_PROTOCOLS];
|
|
642
|
+
const listed = raw.split(",").map((entry) => entry.trim().toLowerCase()).filter(Boolean);
|
|
643
|
+
return listed.length ? listed : [...DEFAULT_PROTOCOLS];
|
|
644
|
+
}
|
|
645
|
+
function isShortRepoName(repo) {
|
|
646
|
+
return /^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(repo.trim());
|
|
647
|
+
}
|
|
648
|
+
var SCP_LIKE = /^[\w.-]+@[\w.-]+:(?!\/)\S+$/;
|
|
649
|
+
var URL_SCHEME = /^([A-Za-z0-9+.-]+):\/\//;
|
|
650
|
+
var CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
|
|
651
|
+
function repoUrlIsSafe(repo) {
|
|
652
|
+
const url = repo.trim();
|
|
653
|
+
if (!url || url.startsWith("-") || CONTROL_CHARS.test(url)) return false;
|
|
654
|
+
const allowed = allowedProtocols();
|
|
655
|
+
if (SCP_LIKE.test(url)) return allowed.includes("ssh");
|
|
656
|
+
const scheme = URL_SCHEME.exec(url);
|
|
657
|
+
if (!scheme?.[1]) return false;
|
|
658
|
+
if (!allowed.includes(scheme[1].toLowerCase())) return false;
|
|
659
|
+
const authority = url.slice(scheme[0].length).split("/")[0] ?? "";
|
|
660
|
+
const at = authority.lastIndexOf("@");
|
|
661
|
+
return at < 0 || !authority.slice(0, at).includes(":");
|
|
662
|
+
}
|
|
663
|
+
function protocolArgs() {
|
|
664
|
+
const allowed = allowedProtocols();
|
|
665
|
+
return [
|
|
666
|
+
"-c",
|
|
667
|
+
"protocol.ext.allow=never",
|
|
668
|
+
"-c",
|
|
669
|
+
`protocol.file.allow=${allowed.includes("file") ? "user" : "never"}`
|
|
670
|
+
];
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// src/remote-repo/read.ts
|
|
674
|
+
var DEFAULT_REPO_CONCURRENCY = 4;
|
|
675
|
+
var IMMUTABLE_REV = /^[0-9a-f]{40}$/;
|
|
676
|
+
async function readRemoteAnchors(wants, options = {}) {
|
|
677
|
+
const out = /* @__PURE__ */ new Map();
|
|
678
|
+
if (!wants.length) return out;
|
|
679
|
+
const cacheDir = repoCacheDir(options.cacheDir);
|
|
680
|
+
const timeoutMs = fetchTimeoutMs(options.fetchTimeoutMs);
|
|
681
|
+
const byRepo = /* @__PURE__ */ new Map();
|
|
682
|
+
for (const want of wants) {
|
|
683
|
+
const key = normalizeRepoUrl(want.repo);
|
|
684
|
+
const group2 = byRepo.get(key) ?? { url: want.repo.trim(), wants: [] };
|
|
685
|
+
group2.wants.push(want);
|
|
686
|
+
byRepo.set(key, group2);
|
|
687
|
+
}
|
|
688
|
+
const groups = [...byRepo.entries()];
|
|
689
|
+
const results = await mapLimit(
|
|
690
|
+
groups,
|
|
691
|
+
Math.max(1, options.concurrency ?? DEFAULT_REPO_CONCURRENCY),
|
|
692
|
+
([repo, group2]) => readOneRepo(repo, group2.url, group2.wants, {
|
|
693
|
+
cacheDir,
|
|
694
|
+
timeoutMs,
|
|
695
|
+
offline: options.offline === true
|
|
696
|
+
})
|
|
697
|
+
);
|
|
698
|
+
for (const result of results) {
|
|
699
|
+
for (const [key, read] of result) out.set(key, read);
|
|
700
|
+
}
|
|
701
|
+
return out;
|
|
702
|
+
}
|
|
703
|
+
async function readOneRepo(repo, url, declared, context) {
|
|
704
|
+
let wants = declared;
|
|
705
|
+
const all = (read) => new Map(wants.map((want) => [wantKey(repo, want.ref, want.file), read]));
|
|
706
|
+
if (isShortRepoName(url)) {
|
|
707
|
+
return all({ ok: false, reason: "remote-unreachable" });
|
|
708
|
+
}
|
|
709
|
+
if (!repoUrlIsSafe(url)) return all({ ok: false, reason: "repo-invalid" });
|
|
710
|
+
const cache = cachePathFor(repo, context.cacheDir);
|
|
711
|
+
if (!cache) return all({ ok: false, reason: "remote-unreachable" });
|
|
712
|
+
const rejected = /* @__PURE__ */ new Map();
|
|
713
|
+
const usable = [];
|
|
714
|
+
for (const want of wants) {
|
|
715
|
+
const reason = wantReason(want);
|
|
716
|
+
if (reason)
|
|
717
|
+
rejected.set(wantKey(repo, want.ref, want.file), { ok: false, reason });
|
|
718
|
+
else usable.push(want);
|
|
719
|
+
}
|
|
720
|
+
if (!usable.length) return rejected;
|
|
721
|
+
wants = usable;
|
|
722
|
+
const opened = await openCache(cache, url, context);
|
|
723
|
+
if (opened) return new Map([...rejected, ...all(opened)]);
|
|
724
|
+
const wantsDefault = wants.some((want) => want.ref === void 0);
|
|
725
|
+
const branch = wantsDefault ? await defaultBranch(cache, context) : {};
|
|
726
|
+
const revs = /* @__PURE__ */ new Map();
|
|
727
|
+
for (const rev of distinctRevs(wants, branch.name)) {
|
|
728
|
+
revs.set(
|
|
729
|
+
rev,
|
|
730
|
+
await refIsWellFormed(rev) ? await ensureRev(cache, rev, context) : { ok: false, reason: "ref-invalid" }
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
const reads = await mapLimit(
|
|
734
|
+
wants,
|
|
735
|
+
DEFAULT_IO_CONCURRENCY,
|
|
736
|
+
async (want) => {
|
|
737
|
+
const rev = want.ref ?? branch.name;
|
|
738
|
+
if (rev === void 0) {
|
|
739
|
+
return {
|
|
740
|
+
ok: false,
|
|
741
|
+
reason: branch.reason ?? "default-branch-unknown"
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
const failed = revs.get(rev);
|
|
745
|
+
if (failed) return failed;
|
|
746
|
+
return readBlob(cache, rev, want.file, context);
|
|
747
|
+
}
|
|
748
|
+
);
|
|
749
|
+
return new Map([
|
|
750
|
+
...rejected,
|
|
751
|
+
...wants.map(
|
|
752
|
+
(want, at) => [wantKey(repo, want.ref, want.file), reads[at]]
|
|
753
|
+
)
|
|
754
|
+
]);
|
|
755
|
+
}
|
|
756
|
+
function wantReason(want) {
|
|
757
|
+
if (want.ref !== void 0 && !refShapeIsSafe(want.ref)) return "ref-invalid";
|
|
758
|
+
return filePathIsSafe(want.file) ? void 0 : "outside-repo";
|
|
759
|
+
}
|
|
760
|
+
function distinctRevs(wants, branch) {
|
|
761
|
+
const revs = /* @__PURE__ */ new Set();
|
|
762
|
+
for (const want of wants) {
|
|
763
|
+
if (want.ref !== void 0) revs.add(want.ref);
|
|
764
|
+
else if (branch) revs.add(branch);
|
|
765
|
+
}
|
|
766
|
+
return [...revs];
|
|
767
|
+
}
|
|
768
|
+
async function openCache(cache, url, context) {
|
|
769
|
+
try {
|
|
770
|
+
await mkdir(cache, { recursive: true });
|
|
771
|
+
} catch {
|
|
772
|
+
return { ok: false, reason: "remote-unreachable" };
|
|
773
|
+
}
|
|
774
|
+
const init = await git(["init", "--bare", "--quiet", cache], {
|
|
775
|
+
timeoutMs: context.timeoutMs
|
|
776
|
+
});
|
|
777
|
+
if (!init.ok) return { ok: false, reason: "remote-unreachable" };
|
|
778
|
+
const remote = await git(["config", "remote.origin.url", url], {
|
|
779
|
+
cwd: cache,
|
|
780
|
+
timeoutMs: context.timeoutMs
|
|
781
|
+
});
|
|
782
|
+
return remote.ok ? void 0 : { ok: false, reason: "remote-unreachable" };
|
|
783
|
+
}
|
|
784
|
+
async function defaultBranch(cache, context) {
|
|
785
|
+
if (!context.offline) {
|
|
786
|
+
const listed = await git(
|
|
787
|
+
[...protocolArgs(), "ls-remote", "--symref", "origin", "HEAD"],
|
|
788
|
+
{
|
|
789
|
+
cwd: cache,
|
|
790
|
+
timeoutMs: context.timeoutMs
|
|
791
|
+
}
|
|
792
|
+
);
|
|
793
|
+
const found = /^ref:\s+refs\/heads\/(\S+)\s+HEAD$/m.exec(listed.stdout);
|
|
794
|
+
if (listed.ok && found?.[1]) {
|
|
795
|
+
const name = found[1];
|
|
796
|
+
await git(["config", "strauss.defaultBranch", name], { cwd: cache });
|
|
797
|
+
return { name };
|
|
798
|
+
}
|
|
799
|
+
if (!listed.ok) {
|
|
800
|
+
const reason = transportReason(listed.stderr);
|
|
801
|
+
if (reason !== "ref-not-found") {
|
|
802
|
+
const cached2 = await cachedBranch(cache);
|
|
803
|
+
return cached2 ? { name: cached2 } : { reason };
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
const cached = await cachedBranch(cache);
|
|
808
|
+
if (cached) return { name: cached };
|
|
809
|
+
return {
|
|
810
|
+
reason: context.offline ? "remote-unreachable" : "default-branch-unknown"
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
async function cachedBranch(cache) {
|
|
814
|
+
const stored = await git(["config", "--get", "strauss.defaultBranch"], {
|
|
815
|
+
cwd: cache
|
|
816
|
+
});
|
|
817
|
+
if (stored.ok && stored.stdout.trim()) return stored.stdout.trim();
|
|
818
|
+
const head = await git(
|
|
819
|
+
["symbolic-ref", "--short", "refs/remotes/origin/HEAD"],
|
|
820
|
+
{
|
|
821
|
+
cwd: cache
|
|
822
|
+
}
|
|
823
|
+
);
|
|
824
|
+
const name = head.stdout.trim().replace(/^origin\//, "");
|
|
825
|
+
return head.ok && name ? name : void 0;
|
|
826
|
+
}
|
|
827
|
+
async function ensureRev(cache, rev, context) {
|
|
828
|
+
const ref = revRef(rev);
|
|
829
|
+
const have = await git(
|
|
830
|
+
["rev-parse", "--verify", "--quiet", `${ref}^{commit}`],
|
|
831
|
+
{
|
|
832
|
+
cwd: cache
|
|
833
|
+
}
|
|
834
|
+
);
|
|
835
|
+
const cached = have.ok && have.stdout.trim().length > 0;
|
|
836
|
+
if (cached && (context.offline || IMMUTABLE_REV.test(rev))) return void 0;
|
|
837
|
+
if (context.offline) return { ok: false, reason: "remote-unreachable" };
|
|
838
|
+
const fetched = await git(
|
|
839
|
+
[
|
|
840
|
+
...protocolArgs(),
|
|
841
|
+
"fetch",
|
|
842
|
+
"--depth",
|
|
843
|
+
"1",
|
|
844
|
+
"origin",
|
|
845
|
+
"--end-of-options",
|
|
846
|
+
rev
|
|
847
|
+
],
|
|
848
|
+
{ cwd: cache, timeoutMs: context.timeoutMs }
|
|
849
|
+
);
|
|
850
|
+
if (!fetched.ok) {
|
|
851
|
+
const reason = transportReason(fetched.stderr);
|
|
852
|
+
if (cached && reason !== "ref-not-found") return void 0;
|
|
853
|
+
return { ok: false, reason };
|
|
854
|
+
}
|
|
855
|
+
const head = await git(["rev-parse", "FETCH_HEAD"], { cwd: cache });
|
|
856
|
+
const sha = head.stdout.trim();
|
|
857
|
+
if (!head.ok || !sha) return { ok: false, reason: "remote-unreachable" };
|
|
858
|
+
const updated = await git(["update-ref", ref, sha], { cwd: cache });
|
|
859
|
+
return updated.ok ? void 0 : { ok: false, reason: "remote-unreachable" };
|
|
860
|
+
}
|
|
861
|
+
async function readBlob(cache, rev, file, context) {
|
|
862
|
+
const path = file.replace(/^\.\//, "");
|
|
863
|
+
const blob = await git(
|
|
864
|
+
["cat-file", "blob", "--end-of-options", `${revRef(rev)}:${path}`],
|
|
865
|
+
{
|
|
866
|
+
cwd: cache,
|
|
867
|
+
timeoutMs: context.timeoutMs
|
|
868
|
+
}
|
|
869
|
+
);
|
|
870
|
+
if (blob.ok) return { ok: true, source: blob.stdout };
|
|
871
|
+
if (blob.overflowed) return { ok: false, reason: "file-too-large" };
|
|
872
|
+
const text = blob.stderr.toLowerCase();
|
|
873
|
+
return text.includes("does not exist") || text.includes("not a valid object") ? { ok: false, reason: "file-missing" } : { ok: false, reason: "file-unreadable" };
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// src/anchor-resolver/read.ts
|
|
877
|
+
import { readFile, realpath, stat } from "fs/promises";
|
|
878
|
+
import { isAbsolute, relative, resolve, sep } from "path";
|
|
879
|
+
function anchorFilePath(repoRoot, file) {
|
|
880
|
+
const path = resolve(repoRoot, file.replace(/^\.\//, ""));
|
|
881
|
+
const rel = relative(resolve(repoRoot), path);
|
|
882
|
+
if (rel === "" || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
|
|
883
|
+
return null;
|
|
884
|
+
}
|
|
885
|
+
return path;
|
|
886
|
+
}
|
|
887
|
+
function contains(root, path) {
|
|
888
|
+
const rel = relative(root, path);
|
|
889
|
+
return rel !== "" && rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
890
|
+
}
|
|
891
|
+
function errorCode(error) {
|
|
892
|
+
return typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0;
|
|
893
|
+
}
|
|
894
|
+
function anchorFileReader(repoRoot) {
|
|
895
|
+
let rootOnce;
|
|
896
|
+
const realRoot = () => {
|
|
897
|
+
rootOnce ??= realpath(resolve(repoRoot)).catch((error) => {
|
|
898
|
+
rootOnce = void 0;
|
|
899
|
+
throw error;
|
|
900
|
+
});
|
|
901
|
+
return rootOnce;
|
|
902
|
+
};
|
|
903
|
+
return (file) => readAnchorFileWithRoot(repoRoot, file, realRoot);
|
|
904
|
+
}
|
|
905
|
+
async function readAnchorFileWithRoot(repoRoot, file, realRoot) {
|
|
906
|
+
const lexical = anchorFilePath(repoRoot, file);
|
|
907
|
+
if (lexical === null) return { ok: false, reason: "outside-repo" };
|
|
908
|
+
let root;
|
|
909
|
+
let path;
|
|
910
|
+
try {
|
|
911
|
+
root = await realRoot();
|
|
912
|
+
path = await realpath(lexical);
|
|
913
|
+
} catch (error) {
|
|
914
|
+
const code = errorCode(error);
|
|
915
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
916
|
+
return { ok: false, reason: "file-missing" };
|
|
917
|
+
}
|
|
918
|
+
return { ok: false, reason: "file-unreadable" };
|
|
919
|
+
}
|
|
920
|
+
if (!contains(root, path)) return { ok: false, reason: "outside-repo" };
|
|
921
|
+
try {
|
|
922
|
+
const stats = await stat(path);
|
|
923
|
+
if (!stats.isFile()) return { ok: false, reason: "file-unreadable" };
|
|
924
|
+
if (stats.size > MAX_ANCHOR_FILE_BYTES) {
|
|
925
|
+
return { ok: false, reason: "file-too-large" };
|
|
926
|
+
}
|
|
927
|
+
return { ok: true, source: await readFile(path, "utf8") };
|
|
928
|
+
} catch (error) {
|
|
929
|
+
const code = errorCode(error);
|
|
930
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
931
|
+
return { ok: false, reason: "file-missing" };
|
|
932
|
+
}
|
|
933
|
+
return { ok: false, reason: "file-unreadable" };
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
function looksLikeWrongRepoRoot(drift) {
|
|
937
|
+
let checked = 0;
|
|
938
|
+
for (const entries of drift.values()) {
|
|
939
|
+
for (const entry of entries) {
|
|
940
|
+
if (entry.repo !== void 0) continue;
|
|
941
|
+
checked += 1;
|
|
942
|
+
if (entry.state !== "unresolved" || entry.reason !== "file-missing") {
|
|
943
|
+
return false;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
return checked > 0;
|
|
948
|
+
}
|
|
949
|
+
async function readAnchorFiles(files, read, concurrency = DEFAULT_IO_CONCURRENCY) {
|
|
950
|
+
if (!Number.isInteger(concurrency) || concurrency < 1) {
|
|
951
|
+
throw new RangeError(
|
|
952
|
+
`readAnchorFiles: option "concurrency" must be a positive integer, got ${concurrency}`
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
const wanted = [...new Set(files)];
|
|
956
|
+
const results = await mapLimit(wanted, concurrency, async (file) => {
|
|
957
|
+
try {
|
|
958
|
+
return await read(file);
|
|
959
|
+
} catch {
|
|
960
|
+
return { ok: false, reason: "file-unreadable" };
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
return new Map(wanted.map((file, at) => [file, results[at]]));
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// src/anchor-resolver/resolver.ts
|
|
967
|
+
import { createHash } from "crypto";
|
|
457
968
|
var PARENT_SCOPE_LINES = 50;
|
|
458
969
|
var CLEAN_STATE = { blockComment: false, template: false };
|
|
459
970
|
function stripLine(line, state) {
|
|
@@ -605,181 +1116,32 @@ var regexResolver = {
|
|
|
605
1116
|
};
|
|
606
1117
|
function escapeRegExp(value) {
|
|
607
1118
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
608
|
-
}
|
|
609
|
-
function distanceToParent(lines, index, parent) {
|
|
610
|
-
const floor = Math.max(0, index - PARENT_SCOPE_LINES);
|
|
611
|
-
for (let at = index; at >= floor; at--) {
|
|
612
|
-
if (parent.test(lines[at] ?? "")) return index - at;
|
|
613
|
-
}
|
|
614
|
-
return Number.POSITIVE_INFINITY;
|
|
615
|
-
}
|
|
616
|
-
function hashAnchorText(text) {
|
|
617
|
-
return `sha256:${createHash("sha256").update(text.replace(/\r\n/g, "\n")).digest("hex")}`;
|
|
618
|
-
}
|
|
619
|
-
function resolveAnchor(source, anchor, resolver = regexResolver) {
|
|
620
|
-
const normalized = source.replace(/\r\n/g, "\n");
|
|
621
|
-
if (!anchor.symbol) {
|
|
622
|
-
const lines = normalized.split("\n");
|
|
623
|
-
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
624
|
-
return {
|
|
625
|
-
text: normalized,
|
|
626
|
-
startLine: 1,
|
|
627
|
-
endLine: Math.max(1, lines.length)
|
|
628
|
-
};
|
|
629
|
-
}
|
|
630
|
-
return resolver.resolve(normalized, anchor.symbol);
|
|
631
|
-
}
|
|
632
|
-
function anchorFilePath(repoRoot, file) {
|
|
633
|
-
const path = resolve(repoRoot, file.replace(/^\.\//, ""));
|
|
634
|
-
const rel = relative(resolve(repoRoot), path);
|
|
635
|
-
if (rel === "" || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
|
|
636
|
-
return null;
|
|
637
|
-
}
|
|
638
|
-
return path;
|
|
639
|
-
}
|
|
640
|
-
function contains(root, path) {
|
|
641
|
-
const rel = relative(root, path);
|
|
642
|
-
return rel !== "" && rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
643
|
-
}
|
|
644
|
-
function normalizeRepoUrl(value) {
|
|
645
|
-
let url = value.trim().replace(/^git\+/, "");
|
|
646
|
-
const scp = /^[\w.-]+@([\w.-]+):(.+)$/.exec(url);
|
|
647
|
-
if (scp) url = `https://${scp[1]}/${scp[2]}`;
|
|
648
|
-
url = url.replace(/^ssh:\/\/(?:[^@/]+@)?/, "https://");
|
|
649
|
-
url = trimTrailingSlashes(url);
|
|
650
|
-
if (url.endsWith(".git")) url = url.slice(0, -4);
|
|
651
|
-
return trimTrailingSlashes(url).toLowerCase();
|
|
652
|
-
}
|
|
653
|
-
function trimTrailingSlashes(value) {
|
|
654
|
-
let end = value.length;
|
|
655
|
-
while (end > 0 && value[end - 1] === "/") end -= 1;
|
|
656
|
-
return value.slice(0, end);
|
|
657
|
-
}
|
|
658
|
-
function repoPath(normalized) {
|
|
659
|
-
const withoutScheme = normalized.replace(/^[a-z0-9+.-]+:\/\//, "");
|
|
660
|
-
const segments = withoutScheme.split("/").filter(Boolean);
|
|
661
|
-
return segments.length > 1 ? segments.slice(1).join("/") : "";
|
|
662
|
-
}
|
|
663
|
-
function repoIdentifies(declared, originUrl) {
|
|
664
|
-
if (!originUrl) return false;
|
|
665
|
-
const origin = normalizeRepoUrl(originUrl);
|
|
666
|
-
const want = normalizeRepoUrl(declared);
|
|
667
|
-
if (!want || !origin) return false;
|
|
668
|
-
if (want === origin) return true;
|
|
669
|
-
const path = repoPath(origin);
|
|
670
|
-
if (!path) return false;
|
|
671
|
-
return want === path || want === (path.split("/").pop() ?? "");
|
|
672
|
-
}
|
|
673
|
-
async function repoOriginUrl(repoRoot) {
|
|
674
|
-
try {
|
|
675
|
-
const { stdout } = await execFileAsync(
|
|
676
|
-
"git",
|
|
677
|
-
["-C", repoRoot, "config", "--get", "remote.origin.url"],
|
|
678
|
-
{ timeout: 5e3 }
|
|
679
|
-
);
|
|
680
|
-
return stdout.trim() || null;
|
|
681
|
-
} catch {
|
|
682
|
-
return null;
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
var LazyOrigin = class {
|
|
686
|
-
constructor(repoRoot) {
|
|
687
|
-
this.repoRoot = repoRoot;
|
|
688
|
-
}
|
|
689
|
-
repoRoot;
|
|
690
|
-
url = null;
|
|
691
|
-
asked = false;
|
|
692
|
-
/** Asks git once, so later `isForeign` calls need no await. */
|
|
693
|
-
async prime() {
|
|
694
|
-
if (this.asked) return;
|
|
695
|
-
this.url = await repoOriginUrl(this.repoRoot);
|
|
696
|
-
this.asked = true;
|
|
697
|
-
}
|
|
698
|
-
/** Only meaningful after `prime`; an unprimed origin identifies nothing. */
|
|
699
|
-
isForeign(anchor) {
|
|
700
|
-
if (!anchor.repo) return false;
|
|
701
|
-
return !repoIdentifies(anchor.repo, this.url);
|
|
702
|
-
}
|
|
703
|
-
async foreign(anchor) {
|
|
704
|
-
if (!anchor.repo) return false;
|
|
705
|
-
await this.prime();
|
|
706
|
-
return this.isForeign(anchor);
|
|
707
|
-
}
|
|
708
|
-
};
|
|
709
|
-
function errorCode(error) {
|
|
710
|
-
return typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0;
|
|
711
|
-
}
|
|
712
|
-
function anchorFileReader(repoRoot) {
|
|
713
|
-
let rootOnce;
|
|
714
|
-
const realRoot = () => {
|
|
715
|
-
rootOnce ??= realpath(resolve(repoRoot)).catch((error) => {
|
|
716
|
-
rootOnce = void 0;
|
|
717
|
-
throw error;
|
|
718
|
-
});
|
|
719
|
-
return rootOnce;
|
|
720
|
-
};
|
|
721
|
-
return (file) => readAnchorFileWithRoot(repoRoot, file, realRoot);
|
|
722
|
-
}
|
|
723
|
-
async function readAnchorFileWithRoot(repoRoot, file, realRoot) {
|
|
724
|
-
const lexical = anchorFilePath(repoRoot, file);
|
|
725
|
-
if (lexical === null) return { ok: false, reason: "outside-repo" };
|
|
726
|
-
let root;
|
|
727
|
-
let path;
|
|
728
|
-
try {
|
|
729
|
-
root = await realRoot();
|
|
730
|
-
path = await realpath(lexical);
|
|
731
|
-
} catch (error) {
|
|
732
|
-
const code = errorCode(error);
|
|
733
|
-
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
734
|
-
return { ok: false, reason: "file-missing" };
|
|
735
|
-
}
|
|
736
|
-
return { ok: false, reason: "file-unreadable" };
|
|
737
|
-
}
|
|
738
|
-
if (!contains(root, path)) return { ok: false, reason: "outside-repo" };
|
|
739
|
-
try {
|
|
740
|
-
const stats = await stat(path);
|
|
741
|
-
if (!stats.isFile()) return { ok: false, reason: "file-unreadable" };
|
|
742
|
-
if (stats.size > MAX_ANCHOR_FILE_BYTES) {
|
|
743
|
-
return { ok: false, reason: "file-too-large" };
|
|
744
|
-
}
|
|
745
|
-
return { ok: true, source: await readFile(path, "utf8") };
|
|
746
|
-
} catch (error) {
|
|
747
|
-
const code = errorCode(error);
|
|
748
|
-
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
749
|
-
return { ok: false, reason: "file-missing" };
|
|
750
|
-
}
|
|
751
|
-
return { ok: false, reason: "file-unreadable" };
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
function looksLikeWrongRepoRoot(drift) {
|
|
755
|
-
let checked = 0;
|
|
756
|
-
for (const entries of drift.values()) {
|
|
757
|
-
for (const entry of entries) {
|
|
758
|
-
if (entry.reason === "foreign-repo") continue;
|
|
759
|
-
checked += 1;
|
|
760
|
-
if (entry.state !== "unresolved" || entry.reason !== "file-missing") {
|
|
761
|
-
return false;
|
|
762
|
-
}
|
|
763
|
-
}
|
|
1119
|
+
}
|
|
1120
|
+
function distanceToParent(lines, index, parent) {
|
|
1121
|
+
const floor = Math.max(0, index - PARENT_SCOPE_LINES);
|
|
1122
|
+
for (let at = index; at >= floor; at--) {
|
|
1123
|
+
if (parent.test(lines[at] ?? "")) return index - at;
|
|
764
1124
|
}
|
|
765
|
-
return
|
|
1125
|
+
return Number.POSITIVE_INFINITY;
|
|
766
1126
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
1127
|
+
function hashAnchorText(text) {
|
|
1128
|
+
return `sha256:${createHash("sha256").update(text.replace(/\r\n/g, "\n")).digest("hex")}`;
|
|
1129
|
+
}
|
|
1130
|
+
function resolveAnchor(source, anchor, resolver = regexResolver) {
|
|
1131
|
+
const normalized = source.replace(/\r\n/g, "\n");
|
|
1132
|
+
if (!anchor.symbol) {
|
|
1133
|
+
const lines = normalized.split("\n");
|
|
1134
|
+
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
1135
|
+
return {
|
|
1136
|
+
text: normalized,
|
|
1137
|
+
startLine: 1,
|
|
1138
|
+
endLine: Math.max(1, lines.length)
|
|
1139
|
+
};
|
|
772
1140
|
}
|
|
773
|
-
|
|
774
|
-
const results = await mapLimit(wanted, concurrency, async (file) => {
|
|
775
|
-
try {
|
|
776
|
-
return await read(file);
|
|
777
|
-
} catch {
|
|
778
|
-
return { ok: false, reason: "file-unreadable" };
|
|
779
|
-
}
|
|
780
|
-
});
|
|
781
|
-
return new Map(wanted.map((file, at) => [file, results[at]]));
|
|
1141
|
+
return resolver.resolve(normalized, anchor.symbol);
|
|
782
1142
|
}
|
|
1143
|
+
|
|
1144
|
+
// src/anchor-resolver/drift.ts
|
|
783
1145
|
async function detectAnchorDrift(records, options = {}) {
|
|
784
1146
|
const repoRoot = options.repoRoot ?? process.cwd();
|
|
785
1147
|
const resolver = options.resolver ?? regexResolver;
|
|
@@ -805,67 +1167,97 @@ async function detectAnchorDrift(records, options = {}) {
|
|
|
805
1167
|
}
|
|
806
1168
|
}
|
|
807
1169
|
const files = [];
|
|
1170
|
+
const wants = [];
|
|
808
1171
|
for (const entries of planned.values()) {
|
|
809
|
-
for (const
|
|
810
|
-
if (!
|
|
1172
|
+
for (const { anchor, foreign } of entries) {
|
|
1173
|
+
if (!foreign) files.push(anchor.file);
|
|
1174
|
+
else wants.push(...remoteWants(anchor));
|
|
811
1175
|
}
|
|
812
1176
|
}
|
|
813
|
-
const reads = await
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
1177
|
+
const [reads, remote] = await Promise.all([
|
|
1178
|
+
readAnchorFiles(
|
|
1179
|
+
files,
|
|
1180
|
+
options.reader ?? anchorFileReader(repoRoot),
|
|
1181
|
+
options.concurrency ?? DEFAULT_IO_CONCURRENCY
|
|
1182
|
+
),
|
|
1183
|
+
(options.readRemote ?? readRemoteAnchors)(wants, options.remote ?? {})
|
|
1184
|
+
]);
|
|
818
1185
|
const drift = /* @__PURE__ */ new Map();
|
|
819
1186
|
for (const record of records) {
|
|
820
1187
|
const entries = [];
|
|
821
1188
|
for (const { anchor, foreign } of planned.get(record.conceptId) ?? []) {
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
storedHash: anchor.hash
|
|
826
|
-
};
|
|
827
|
-
if (foreign) {
|
|
828
|
-
entries.push({
|
|
829
|
-
...base,
|
|
830
|
-
state: "unresolved",
|
|
831
|
-
diffSize: null,
|
|
832
|
-
reason: "foreign-repo"
|
|
833
|
-
});
|
|
834
|
-
continue;
|
|
835
|
-
}
|
|
836
|
-
const read = reads.get(anchor.file);
|
|
837
|
-
if (!read.ok) {
|
|
838
|
-
entries.push({
|
|
839
|
-
...base,
|
|
840
|
-
state: "unresolved",
|
|
841
|
-
diffSize: null,
|
|
842
|
-
reason: read.reason
|
|
843
|
-
});
|
|
844
|
-
continue;
|
|
845
|
-
}
|
|
846
|
-
const resolved = resolveAnchor(read.source, anchor, resolver);
|
|
847
|
-
if (!resolved) {
|
|
848
|
-
entries.push({
|
|
849
|
-
...base,
|
|
850
|
-
state: "unresolved",
|
|
851
|
-
diffSize: null,
|
|
852
|
-
reason: "symbol-not-found"
|
|
853
|
-
});
|
|
854
|
-
continue;
|
|
855
|
-
}
|
|
856
|
-
const currentHash = hashAnchorText(resolved.text);
|
|
857
|
-
const currentLines = resolved.endLine - resolved.startLine + 1;
|
|
858
|
-
entries.push({
|
|
859
|
-
...base,
|
|
860
|
-
state: currentHash === anchor.hash ? "match" : "drifted",
|
|
861
|
-
currentHash,
|
|
862
|
-
diffSize: anchor.lines === void 0 ? null : Math.abs(currentLines - anchor.lines)
|
|
863
|
-
});
|
|
1189
|
+
entries.push(
|
|
1190
|
+
foreign ? remoteEntry(anchor, remote, resolver) : localEntry(anchor, reads.get(anchor.file), resolver)
|
|
1191
|
+
);
|
|
864
1192
|
}
|
|
865
1193
|
if (entries.length) drift.set(record.conceptId, entries);
|
|
866
1194
|
}
|
|
867
1195
|
return drift;
|
|
868
1196
|
}
|
|
1197
|
+
function remoteWants(anchor) {
|
|
1198
|
+
const repo = anchor.repo;
|
|
1199
|
+
const wants = [{ repo, file: anchor.file }];
|
|
1200
|
+
if (anchor.ref) wants.unshift({ repo, ref: anchor.ref, file: anchor.file });
|
|
1201
|
+
return wants;
|
|
1202
|
+
}
|
|
1203
|
+
function base(anchor) {
|
|
1204
|
+
return {
|
|
1205
|
+
file: anchor.file,
|
|
1206
|
+
...anchor.symbol ? { symbol: anchor.symbol } : {},
|
|
1207
|
+
storedHash: anchor.hash
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
function unresolved(anchor, reason, repo) {
|
|
1211
|
+
return {
|
|
1212
|
+
...base(anchor),
|
|
1213
|
+
state: "unresolved",
|
|
1214
|
+
diffSize: null,
|
|
1215
|
+
...reason ? { reason } : {},
|
|
1216
|
+
...repo ? { repo } : {}
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
function hashIn(source, anchor, resolver) {
|
|
1220
|
+
const resolved = resolveAnchor(source, anchor, resolver);
|
|
1221
|
+
if (!resolved) return null;
|
|
1222
|
+
return {
|
|
1223
|
+
hash: hashAnchorText(resolved.text),
|
|
1224
|
+
lines: resolved.endLine - resolved.startLine + 1
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
function compared(anchor, current, extra = {}) {
|
|
1228
|
+
return {
|
|
1229
|
+
...base(anchor),
|
|
1230
|
+
state: current.hash === anchor.hash ? "match" : "drifted",
|
|
1231
|
+
currentHash: current.hash,
|
|
1232
|
+
diffSize: anchor.lines === void 0 ? null : Math.abs(current.lines - anchor.lines),
|
|
1233
|
+
...extra
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
function localEntry(anchor, read, resolver) {
|
|
1237
|
+
if (!read.ok) return unresolved(anchor, read.reason);
|
|
1238
|
+
const current = hashIn(read.source, anchor, resolver);
|
|
1239
|
+
return current ? compared(anchor, current) : unresolved(anchor, "symbol-not-found");
|
|
1240
|
+
}
|
|
1241
|
+
function remoteEntry(anchor, remote, resolver) {
|
|
1242
|
+
const repo = anchor.repo;
|
|
1243
|
+
const key = normalizeRepoUrl(repo);
|
|
1244
|
+
const atDefault = remote.get(wantKey(key, void 0, anchor.file));
|
|
1245
|
+
const primary = anchor.ref ? remote.get(wantKey(key, anchor.ref, anchor.file)) : atDefault;
|
|
1246
|
+
if (!primary) return unresolved(anchor, "remote-unreachable", repo);
|
|
1247
|
+
if (!primary.ok) return unresolved(anchor, primary.reason, repo);
|
|
1248
|
+
const current = hashIn(primary.source, anchor, resolver);
|
|
1249
|
+
if (!current) return unresolved(anchor, "symbol-not-found", repo);
|
|
1250
|
+
if (!anchor.ref) return compared(anchor, current, { repo });
|
|
1251
|
+
if (current.hash !== anchor.hash) {
|
|
1252
|
+
return compared(anchor, current, { repo, remoteState: "drifted-from-ref" });
|
|
1253
|
+
}
|
|
1254
|
+
const head = atDefault?.ok ? hashIn(atDefault.source, anchor, resolver) : null;
|
|
1255
|
+
return head && head.hash !== anchor.hash ? {
|
|
1256
|
+
...compared(anchor, head, { repo }),
|
|
1257
|
+
state: "drifted",
|
|
1258
|
+
remoteState: "drifted-on-default"
|
|
1259
|
+
} : compared(anchor, current, { repo, remoteState: "matches-ref" });
|
|
1260
|
+
}
|
|
869
1261
|
|
|
870
1262
|
// src/errors.ts
|
|
871
1263
|
var Fault = /* @__PURE__ */ ((Fault2) => {
|
|
@@ -881,6 +1273,8 @@ var ErrorTypes = /* @__PURE__ */ ((ErrorTypes2) => {
|
|
|
881
1273
|
ErrorTypes2["KbPackBudgetExceeded"] = "KbPackBudgetExceeded";
|
|
882
1274
|
ErrorTypes2["KbRecordNotFound"] = "KbRecordNotFound";
|
|
883
1275
|
ErrorTypes2["KbSelfVerification"] = "KbSelfVerification";
|
|
1276
|
+
ErrorTypes2["KbStampBaselineUnreadable"] = "KbStampBaselineUnreadable";
|
|
1277
|
+
ErrorTypes2["KbStampDigestBaselineAmbiguous"] = "KbStampDigestBaselineAmbiguous";
|
|
884
1278
|
ErrorTypes2["KbUnknownLinkRel"] = "KbUnknownLinkRel";
|
|
885
1279
|
ErrorTypes2["KbWriteConflict"] = "KbWriteConflict";
|
|
886
1280
|
return ErrorTypes2;
|
|
@@ -1035,6 +1429,36 @@ var KbInvalidConceptIdError = class extends BaseError {
|
|
|
1035
1429
|
});
|
|
1036
1430
|
}
|
|
1037
1431
|
};
|
|
1432
|
+
var KbStampBaselineError = class extends BaseError {
|
|
1433
|
+
constructor(since) {
|
|
1434
|
+
super({
|
|
1435
|
+
message: `kb: --since ${since} is neither a 64-character digest nor a readable stamp file`,
|
|
1436
|
+
errorType: "KbStampBaselineUnreadable" /* KbStampBaselineUnreadable */,
|
|
1437
|
+
code: 400,
|
|
1438
|
+
fault: "User" /* User */,
|
|
1439
|
+
retriable: false,
|
|
1440
|
+
reportToUser: true,
|
|
1441
|
+
details: { since }
|
|
1442
|
+
});
|
|
1443
|
+
this.since = since;
|
|
1444
|
+
}
|
|
1445
|
+
since;
|
|
1446
|
+
};
|
|
1447
|
+
var KbStampDigestBaselineError = class extends BaseError {
|
|
1448
|
+
constructor(since) {
|
|
1449
|
+
super({
|
|
1450
|
+
message: `kb: --since ${since} is a digest, which needs --bundle (one base) \u2014 a file baseline works for many`,
|
|
1451
|
+
errorType: "KbStampDigestBaselineAmbiguous" /* KbStampDigestBaselineAmbiguous */,
|
|
1452
|
+
code: 400,
|
|
1453
|
+
fault: "User" /* User */,
|
|
1454
|
+
retriable: false,
|
|
1455
|
+
reportToUser: true,
|
|
1456
|
+
details: { since }
|
|
1457
|
+
});
|
|
1458
|
+
this.since = since;
|
|
1459
|
+
}
|
|
1460
|
+
since;
|
|
1461
|
+
};
|
|
1038
1462
|
|
|
1039
1463
|
// src/kb-pins/budgets.ts
|
|
1040
1464
|
function asBudgets(value) {
|
|
@@ -1085,10 +1509,10 @@ var KbBaseFrozenError = class extends Error {
|
|
|
1085
1509
|
};
|
|
1086
1510
|
|
|
1087
1511
|
// src/kb-pins/model.ts
|
|
1088
|
-
import { join } from "path";
|
|
1512
|
+
import { join as join2 } from "path";
|
|
1089
1513
|
import { z as z4 } from "zod";
|
|
1090
|
-
var PINS_FILE =
|
|
1091
|
-
var PINS_LOCAL_FILE =
|
|
1514
|
+
var PINS_FILE = join2(".strauss", "kb-pins.json");
|
|
1515
|
+
var PINS_LOCAL_FILE = join2(".strauss", "kb-pins.local.json");
|
|
1092
1516
|
var PIN_LAYERS = ["project", "local", "user"];
|
|
1093
1517
|
var pinSchema = z4.object({
|
|
1094
1518
|
/** Relative to the manifest's root, so the file is committable. */
|
|
@@ -1134,17 +1558,17 @@ var pinsManifestSchema = z4.object({
|
|
|
1134
1558
|
}).passthrough();
|
|
1135
1559
|
|
|
1136
1560
|
// src/kb-pins/layers.ts
|
|
1137
|
-
import { mkdir, readFile as readFile2, writeFile } from "fs/promises";
|
|
1138
|
-
import { homedir } from "os";
|
|
1139
|
-
import { dirname, isAbsolute as isAbsolute2, join as
|
|
1561
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile } from "fs/promises";
|
|
1562
|
+
import { homedir as homedir2 } from "os";
|
|
1563
|
+
import { dirname, isAbsolute as isAbsolute2, join as join3, relative as relative2, resolve as resolve2, sep as sep2 } from "path";
|
|
1140
1564
|
function userRoot() {
|
|
1141
|
-
return process.env.STRAUSS_KB_USER_ROOT ||
|
|
1565
|
+
return process.env.STRAUSS_KB_USER_ROOT || homedir2();
|
|
1142
1566
|
}
|
|
1143
1567
|
function layerRoot(workspaceDir, layer) {
|
|
1144
1568
|
return layer === "user" ? userRoot() : resolve2(workspaceDir);
|
|
1145
1569
|
}
|
|
1146
1570
|
function layerFile(workspaceDir, layer) {
|
|
1147
|
-
return
|
|
1571
|
+
return join3(
|
|
1148
1572
|
layerRoot(workspaceDir, layer),
|
|
1149
1573
|
layer === "local" ? PINS_LOCAL_FILE : PINS_FILE
|
|
1150
1574
|
);
|
|
@@ -1177,7 +1601,7 @@ async function readPinsLayer(workspaceDir, layer) {
|
|
|
1177
1601
|
}
|
|
1178
1602
|
async function writePinsLayer(workspaceDir, layer, manifest) {
|
|
1179
1603
|
const file = layerFile(workspaceDir, layer);
|
|
1180
|
-
await
|
|
1604
|
+
await mkdir2(dirname(file), { recursive: true });
|
|
1181
1605
|
await writeFile(file, `${JSON.stringify(manifest, null, 2)}
|
|
1182
1606
|
`, "utf8");
|
|
1183
1607
|
}
|
|
@@ -1367,23 +1791,34 @@ function adjudicate(hits, bundle, now = /* @__PURE__ */ new Date(), anchorDrift)
|
|
|
1367
1791
|
if (!record.frontmatter.verified?.length) {
|
|
1368
1792
|
warnings.push({ kind: "unverified" });
|
|
1369
1793
|
}
|
|
1370
|
-
const
|
|
1371
|
-
(entry) => entry.state !== "match"
|
|
1794
|
+
const found = (anchorDrift?.get(record.conceptId) ?? []).filter(
|
|
1795
|
+
(entry) => entry.state !== "match"
|
|
1372
1796
|
);
|
|
1797
|
+
const unchecked2 = found.filter((entry) => isUncheckedReason(entry.reason));
|
|
1798
|
+
const moved = found.filter((entry) => !isUncheckedReason(entry.reason));
|
|
1373
1799
|
if (moved.length) {
|
|
1800
|
+
warnings.push({ kind: "drifted", anchors: moved.map(warningAnchor) });
|
|
1801
|
+
}
|
|
1802
|
+
if (unchecked2.length) {
|
|
1374
1803
|
warnings.push({
|
|
1375
|
-
kind: "
|
|
1376
|
-
anchors:
|
|
1377
|
-
file,
|
|
1378
|
-
...symbol !== void 0 ? { symbol } : {},
|
|
1379
|
-
diffSize,
|
|
1380
|
-
...reason !== void 0 ? { reason } : {}
|
|
1381
|
-
}))
|
|
1804
|
+
kind: "unchecked",
|
|
1805
|
+
anchors: unchecked2.map(warningAnchor)
|
|
1382
1806
|
});
|
|
1383
1807
|
}
|
|
1384
1808
|
return { record, standing: STANDING[status], heads, warnings };
|
|
1385
1809
|
});
|
|
1386
1810
|
}
|
|
1811
|
+
function warningAnchor(entry) {
|
|
1812
|
+
const { file, symbol, diffSize, reason, repo, remoteState } = entry;
|
|
1813
|
+
return {
|
|
1814
|
+
file,
|
|
1815
|
+
...symbol !== void 0 ? { symbol } : {},
|
|
1816
|
+
diffSize,
|
|
1817
|
+
...reason !== void 0 ? { reason } : {},
|
|
1818
|
+
...repo !== void 0 ? { repo } : {},
|
|
1819
|
+
...remoteState !== void 0 ? { remoteState } : {}
|
|
1820
|
+
};
|
|
1821
|
+
}
|
|
1387
1822
|
function resolveHeads(from, byId) {
|
|
1388
1823
|
const warnings = [];
|
|
1389
1824
|
const heads = /* @__PURE__ */ new Map();
|
|
@@ -1660,7 +2095,7 @@ async function buildContext(store, workspaceDir, options = {}) {
|
|
|
1660
2095
|
operation: "kb.context.refused",
|
|
1661
2096
|
approxTokens: total,
|
|
1662
2097
|
budgetTokens,
|
|
1663
|
-
bases: bases.map((
|
|
2098
|
+
bases: bases.map((base2) => base2.path)
|
|
1664
2099
|
});
|
|
1665
2100
|
const refusal = [
|
|
1666
2101
|
HEADING2,
|
|
@@ -1670,7 +2105,7 @@ async function buildContext(store, workspaceDir, options = {}) {
|
|
|
1670
2105
|
"from a complete one. The pinned bases:",
|
|
1671
2106
|
"",
|
|
1672
2107
|
...bases.map(
|
|
1673
|
-
(
|
|
2108
|
+
(base2) => `- ${base2.path} \u2014 ~${base2.approxTokens} tokens (bundlePath: \`${base2.absolutePath}\`)`
|
|
1674
2109
|
),
|
|
1675
2110
|
"",
|
|
1676
2111
|
"For the question at hand, read what you need now \u2014 `kb_load` a base",
|
|
@@ -1886,6 +2321,16 @@ function validateBundle(records) {
|
|
|
1886
2321
|
);
|
|
1887
2322
|
}
|
|
1888
2323
|
}
|
|
2324
|
+
for (const anchor of fm.strauss_anchors ?? []) {
|
|
2325
|
+
if (anchor.repo && !isCanonicalRepoUrl(anchor.repo)) {
|
|
2326
|
+
report(
|
|
2327
|
+
"anchor_repo",
|
|
2328
|
+
conceptId2,
|
|
2329
|
+
`anchor repo "${anchor.repo}" is not a full remote URL, so it cannot be resolved against a remote`,
|
|
2330
|
+
"warning"
|
|
2331
|
+
);
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
1889
2334
|
if (fm.strauss_assumption && fm.sources?.length) {
|
|
1890
2335
|
report("assumption", conceptId2, "marked an assumption but cites sources");
|
|
1891
2336
|
}
|
|
@@ -1905,7 +2350,8 @@ var KB_DOCTOR_CHECKS = [
|
|
|
1905
2350
|
"orphaned",
|
|
1906
2351
|
"broken-supersession",
|
|
1907
2352
|
"superseded-but-cited",
|
|
1908
|
-
"drifted"
|
|
2353
|
+
"drifted",
|
|
2354
|
+
"unchecked"
|
|
1909
2355
|
];
|
|
1910
2356
|
var CHECK_HEADLINES = {
|
|
1911
2357
|
expired: "past its stale_after date",
|
|
@@ -1915,7 +2361,8 @@ var CHECK_HEADLINES = {
|
|
|
1915
2361
|
orphaned: "no other record links to it",
|
|
1916
2362
|
"broken-supersession": "the supersession pointers do not resolve",
|
|
1917
2363
|
"superseded-but-cited": "a live record's body links to one that no longer holds",
|
|
1918
|
-
drifted: "the code an anchor points at moved out from under its hash"
|
|
2364
|
+
drifted: "the code an anchor points at moved out from under its hash",
|
|
2365
|
+
unchecked: "an anchor in another repository nothing could reach"
|
|
1919
2366
|
};
|
|
1920
2367
|
var DAY_MS = 864e5;
|
|
1921
2368
|
function doctor(bundle, options = {}) {
|
|
@@ -1940,7 +2387,8 @@ function doctor(bundle, options = {}) {
|
|
|
1940
2387
|
group("orphaned", orphaned(bundle)),
|
|
1941
2388
|
group("broken-supersession", brokenSupersession(bundle, adjudicated)),
|
|
1942
2389
|
group("superseded-but-cited", supersededButCited(bundle, standings)),
|
|
1943
|
-
group("drifted", drifted(inForce))
|
|
2390
|
+
group("drifted", drifted(inForce)),
|
|
2391
|
+
group("unchecked", unchecked(inForce))
|
|
1944
2392
|
];
|
|
1945
2393
|
const counts = Object.fromEntries(
|
|
1946
2394
|
groups.map((entry) => [entry.check, entry.count])
|
|
@@ -2127,21 +2575,38 @@ function supersededButCited(bundle, standings) {
|
|
|
2127
2575
|
return findings;
|
|
2128
2576
|
}
|
|
2129
2577
|
function drifted(hits) {
|
|
2578
|
+
return anchorFindings(
|
|
2579
|
+
hits,
|
|
2580
|
+
"drifted",
|
|
2581
|
+
(count2) => count2 === 1 ? "anchor no longer matches" : "anchors no longer match"
|
|
2582
|
+
);
|
|
2583
|
+
}
|
|
2584
|
+
function unchecked(hits) {
|
|
2585
|
+
return anchorFindings(
|
|
2586
|
+
hits,
|
|
2587
|
+
"unchecked",
|
|
2588
|
+
(count2) => count2 === 1 ? "anchor was not checked" : "anchors were not checked"
|
|
2589
|
+
);
|
|
2590
|
+
}
|
|
2591
|
+
function anchorFindings(hits, kind, headline) {
|
|
2130
2592
|
const findings = [];
|
|
2131
2593
|
for (const hit of hits) {
|
|
2132
|
-
const warning = hit.warnings.find(
|
|
2594
|
+
const warning = hit.warnings.find(
|
|
2595
|
+
(entry) => entry.kind === kind
|
|
2596
|
+
);
|
|
2133
2597
|
if (!warning) continue;
|
|
2598
|
+
const byRepo = /* @__PURE__ */ new Map();
|
|
2599
|
+
for (const anchor of warning.anchors) {
|
|
2600
|
+
const repo = anchor.repo ?? "";
|
|
2601
|
+
byRepo.set(repo, [...byRepo.get(repo) ?? [], describeAnchor(anchor)]);
|
|
2602
|
+
}
|
|
2603
|
+
const detail = [...byRepo.entries()].map(
|
|
2604
|
+
([repo, entries]) => repo ? `${repo}: ${entries.join(", ")}` : entries.join(", ")
|
|
2605
|
+
);
|
|
2134
2606
|
findings.push(
|
|
2135
2607
|
finding(
|
|
2136
2608
|
hit.record,
|
|
2137
|
-
`${warning.anchors.length} ${warning.anchors.length
|
|
2138
|
-
const at = anchor.symbol ? `${anchor.file}:${anchor.symbol}` : anchor.file;
|
|
2139
|
-
if (anchor.reason) return `${at} (${anchor.reason})`;
|
|
2140
|
-
if (anchor.diffSize === null) {
|
|
2141
|
-
return `${at} (changed, size unrecorded)`;
|
|
2142
|
-
}
|
|
2143
|
-
return anchor.diffSize === 0 ? `${at} (content changed, same line count)` : `${at} (${anchor.diffSize} line${anchor.diffSize === 1 ? "" : "s"} apart)`;
|
|
2144
|
-
}).join(", ")}`
|
|
2609
|
+
`${warning.anchors.length} ${headline(warning.anchors.length)}: ${detail.join("; ")}`
|
|
2145
2610
|
)
|
|
2146
2611
|
);
|
|
2147
2612
|
}
|
|
@@ -2149,6 +2614,15 @@ function drifted(hits) {
|
|
|
2149
2614
|
(left, right) => left.conceptId.localeCompare(right.conceptId)
|
|
2150
2615
|
);
|
|
2151
2616
|
}
|
|
2617
|
+
function describeAnchor(anchor) {
|
|
2618
|
+
const at = anchor.symbol ? `${anchor.file}:${anchor.symbol}` : anchor.file;
|
|
2619
|
+
if (anchor.reason) return `${at} (${anchor.reason})`;
|
|
2620
|
+
if (anchor.remoteState === "drifted-on-default") {
|
|
2621
|
+
return `${at} (matches ref, moved on the default branch)`;
|
|
2622
|
+
}
|
|
2623
|
+
if (anchor.diffSize === null) return `${at} (changed, size unrecorded)`;
|
|
2624
|
+
return anchor.diffSize === 0 ? `${at} (content changed, same line count)` : `${at} (${anchor.diffSize} line${anchor.diffSize === 1 ? "" : "s"} apart)`;
|
|
2625
|
+
}
|
|
2152
2626
|
function replaces(later, earlier) {
|
|
2153
2627
|
return (later.frontmatter.strauss_supersedes ?? []).includes(earlier.conceptId) || earlier.frontmatter.strauss_superseded_by === later.conceptId;
|
|
2154
2628
|
}
|
|
@@ -2316,12 +2790,15 @@ function argvFlag(argv, name) {
|
|
|
2316
2790
|
var anchorResolveCommand = define({
|
|
2317
2791
|
name: "anchor-resolve",
|
|
2318
2792
|
tool: "kb_anchor_resolve",
|
|
2319
|
-
usage: "anchor-resolve <concept-id> [--repo-root <path>] [--rebaseline] [--restamp]",
|
|
2320
|
-
description: "Resolve a record's anchors
|
|
2793
|
+
usage: "anchor-resolve <concept-id> [--repo-root <path>] [--offline] [--rebaseline] [--restamp]",
|
|
2794
|
+
description: "Resolve a record's anchors: stamp a hash onto anchors that lack one, report drift where the code moved. An anchor naming another repository is read from that remote through a bare cache; --offline uses the cache only. kb_verify's mechanical counterpart \u2014 reach for it when the question is whether the code still is what it was. Exits non-zero on drift.",
|
|
2321
2795
|
input: z8.object({
|
|
2322
2796
|
bundlePath,
|
|
2323
2797
|
conceptId,
|
|
2324
2798
|
repoRoot: z8.string().min(1).optional(),
|
|
2799
|
+
offline: z8.boolean().optional().describe(
|
|
2800
|
+
"Resolve foreign anchors from the local repo cache only, never fetching."
|
|
2801
|
+
),
|
|
2325
2802
|
rebaseline: z8.boolean().optional().describe(
|
|
2326
2803
|
"Accept the current code as the new baseline for anchors that drifted."
|
|
2327
2804
|
),
|
|
@@ -2333,10 +2810,11 @@ var anchorResolveCommand = define({
|
|
|
2333
2810
|
bundlePath: path,
|
|
2334
2811
|
conceptId: argv[1],
|
|
2335
2812
|
repoRoot: argvFlag(argv, "--repo-root"),
|
|
2813
|
+
offline: argv.includes("--offline"),
|
|
2336
2814
|
rebaseline: argv.includes("--rebaseline"),
|
|
2337
2815
|
restamp: argv.includes("--restamp")
|
|
2338
2816
|
}),
|
|
2339
|
-
run: async ({ store, actor, now }, { bundlePath: path, conceptId: id, repoRoot, rebaseline, restamp }) => {
|
|
2817
|
+
run: async ({ store, actor, now }, { bundlePath: path, conceptId: id, repoRoot, offline, rebaseline, restamp }) => {
|
|
2340
2818
|
const root = repoRoot ?? process.cwd();
|
|
2341
2819
|
const record = await store.read(path, id);
|
|
2342
2820
|
if (!record) throw new KbRecordNotFoundError(id);
|
|
@@ -2351,18 +2829,10 @@ var anchorResolveCommand = define({
|
|
|
2351
2829
|
}
|
|
2352
2830
|
const results = [];
|
|
2353
2831
|
const updated = [];
|
|
2354
|
-
const origin = new LazyOrigin(root);
|
|
2355
2832
|
let dirty = false;
|
|
2356
|
-
|
|
2357
|
-
const foreign = new Map(
|
|
2358
|
-
anchors.map((anchor) => [anchor, origin.isForeign(anchor)])
|
|
2359
|
-
);
|
|
2360
|
-
const reads = await readAnchorFiles(
|
|
2361
|
-
anchors.filter((anchor) => !foreign.get(anchor)).map((anchor) => anchor.file),
|
|
2362
|
-
anchorFileReader(root)
|
|
2363
|
-
);
|
|
2833
|
+
const sources = await readSources(anchors, root, offline === true);
|
|
2364
2834
|
for (const anchor of anchors) {
|
|
2365
|
-
const
|
|
2835
|
+
const base2 = {
|
|
2366
2836
|
file: anchor.file,
|
|
2367
2837
|
...anchor.symbol ? { symbol: anchor.symbol } : {},
|
|
2368
2838
|
// Carried onto unresolved findings too: an anchor that once hashed
|
|
@@ -2370,21 +2840,17 @@ var anchorResolveCommand = define({
|
|
|
2370
2840
|
// has to be able to tell it from one nobody ever stamped.
|
|
2371
2841
|
...anchor.hash ? { storedHash: anchor.hash } : {}
|
|
2372
2842
|
};
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
}
|
|
2378
|
-
const fileRead = reads.get(anchor.file);
|
|
2379
|
-
if (!fileRead.ok) {
|
|
2380
|
-
results.push({ ...base, state: "unresolved", reason: fileRead.reason });
|
|
2843
|
+
const source = sources.get(anchor);
|
|
2844
|
+
if (source.repo) base2.repo = source.repo;
|
|
2845
|
+
if (!source.ok) {
|
|
2846
|
+
results.push({ ...base2, state: "unresolved", reason: source.reason });
|
|
2381
2847
|
updated.push(anchor);
|
|
2382
2848
|
continue;
|
|
2383
2849
|
}
|
|
2384
|
-
const resolved = resolveAnchor(
|
|
2850
|
+
const resolved = resolveAnchor(source.source, anchor);
|
|
2385
2851
|
if (!resolved) {
|
|
2386
2852
|
results.push({
|
|
2387
|
-
...
|
|
2853
|
+
...base2,
|
|
2388
2854
|
state: "unresolved",
|
|
2389
2855
|
reason: "symbol-not-found"
|
|
2390
2856
|
});
|
|
@@ -2399,30 +2865,47 @@ var anchorResolveCommand = define({
|
|
|
2399
2865
|
lines: currentLines,
|
|
2400
2866
|
resolved_at: now()
|
|
2401
2867
|
};
|
|
2868
|
+
const pinned = anchor.ref !== void 0 && source.repo !== void 0;
|
|
2402
2869
|
if (!anchor.hash) {
|
|
2403
|
-
results.push({ ...
|
|
2870
|
+
results.push({ ...base2, state: "stamped", currentHash });
|
|
2404
2871
|
updated.push(stamped);
|
|
2405
2872
|
dirty = true;
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
state: "match",
|
|
2410
|
-
currentHash
|
|
2411
|
-
});
|
|
2412
|
-
const refresh = restamp || anchor.resolved_at === void 0;
|
|
2413
|
-
updated.push(refresh ? { ...anchor, resolved_at: now() } : anchor);
|
|
2414
|
-
if (refresh) dirty = true;
|
|
2415
|
-
} else {
|
|
2873
|
+
continue;
|
|
2874
|
+
}
|
|
2875
|
+
if (anchor.hash !== currentHash) {
|
|
2416
2876
|
results.push({
|
|
2417
|
-
...
|
|
2877
|
+
...base2,
|
|
2418
2878
|
state: "drifted",
|
|
2419
2879
|
currentHash,
|
|
2420
|
-
diffSize: anchor
|
|
2880
|
+
diffSize: lineDelta(anchor, currentLines),
|
|
2881
|
+
...pinned ? { remoteState: "drifted-from-ref" } : {},
|
|
2421
2882
|
...rebaseline ? { rebaselined: true } : {}
|
|
2422
2883
|
});
|
|
2423
2884
|
updated.push(rebaseline ? stamped : anchor);
|
|
2424
2885
|
if (rebaseline) dirty = true;
|
|
2886
|
+
continue;
|
|
2887
|
+
}
|
|
2888
|
+
const onDefault = pinned ? headHash(source, anchor) : void 0;
|
|
2889
|
+
if (onDefault && onDefault.hash !== anchor.hash) {
|
|
2890
|
+
results.push({
|
|
2891
|
+
...base2,
|
|
2892
|
+
state: "drifted",
|
|
2893
|
+
currentHash: onDefault.hash,
|
|
2894
|
+
diffSize: lineDelta(anchor, onDefault.lines),
|
|
2895
|
+
remoteState: "drifted-on-default"
|
|
2896
|
+
});
|
|
2897
|
+
updated.push(anchor);
|
|
2898
|
+
continue;
|
|
2425
2899
|
}
|
|
2900
|
+
results.push({
|
|
2901
|
+
...base2,
|
|
2902
|
+
state: "match",
|
|
2903
|
+
currentHash,
|
|
2904
|
+
...pinned ? { remoteState: "matches-ref" } : {}
|
|
2905
|
+
});
|
|
2906
|
+
const refresh = restamp || anchor.resolved_at === void 0;
|
|
2907
|
+
updated.push(refresh ? { ...anchor, resolved_at: now() } : anchor);
|
|
2908
|
+
if (refresh) dirty = true;
|
|
2426
2909
|
}
|
|
2427
2910
|
let frozen = false;
|
|
2428
2911
|
if (dirty) {
|
|
@@ -2435,16 +2918,19 @@ var anchorResolveCommand = define({
|
|
|
2435
2918
|
if (!frozen) await store.updateAnchors(path, id, updated, actor);
|
|
2436
2919
|
}
|
|
2437
2920
|
const frozenNote = frozen ? { frozen: true, note: "base is frozen: nothing was stamped" } : {};
|
|
2438
|
-
const
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
const
|
|
2921
|
+
const unreachable = results.filter(
|
|
2922
|
+
(entry) => isUncheckedReason(entry.reason)
|
|
2923
|
+
).length;
|
|
2924
|
+
const checked = results.length - unreachable;
|
|
2925
|
+
const matches2 = results.filter((entry) => entry.state === "match").length;
|
|
2926
|
+
const note = `${matches2}/${checked} anchors match${unreachable ? `, ${unreachable} unreachable` : ""}`;
|
|
2927
|
+
const clean = checked > 0 && matches2 === checked && unreachable === 0;
|
|
2442
2928
|
if (clean) {
|
|
2443
2929
|
try {
|
|
2444
2930
|
await store.verify(
|
|
2445
2931
|
path,
|
|
2446
2932
|
id,
|
|
2447
|
-
`anchor-resolve: ${
|
|
2933
|
+
`anchor-resolve: ${note} (regex resolver)`,
|
|
2448
2934
|
actor,
|
|
2449
2935
|
now()
|
|
2450
2936
|
);
|
|
@@ -2460,18 +2946,81 @@ var anchorResolveCommand = define({
|
|
|
2460
2946
|
}
|
|
2461
2947
|
return { conceptId: id, results, verified: true, ...frozenNote };
|
|
2462
2948
|
}
|
|
2463
|
-
return {
|
|
2949
|
+
return {
|
|
2950
|
+
conceptId: id,
|
|
2951
|
+
results,
|
|
2952
|
+
verified: false,
|
|
2953
|
+
...unreachable ? { note } : {},
|
|
2954
|
+
...frozenNote
|
|
2955
|
+
};
|
|
2464
2956
|
},
|
|
2465
2957
|
// A stored hash that no longer resolves is a broken anchor, not an absence:
|
|
2466
2958
|
// the file was deleted or the symbol renamed, and exiting zero on it would
|
|
2467
2959
|
// let the one edit that destroys an anchor pass the gate that exists to
|
|
2468
2960
|
// catch it. An anchor nobody ever stamped is still just unstamped, and one
|
|
2469
|
-
//
|
|
2470
|
-
//
|
|
2961
|
+
// whose remote nothing could reach was never checked — failing CI on either
|
|
2962
|
+
// would gate on work this command did not do.
|
|
2471
2963
|
failsWhen: (result) => result.results.some(
|
|
2472
|
-
(entry) => entry.state === "drifted" || entry.state === "unresolved" && entry.storedHash !== void 0 && entry.reason
|
|
2964
|
+
(entry) => entry.state === "drifted" || entry.state === "unresolved" && entry.storedHash !== void 0 && !isUncheckedReason(entry.reason)
|
|
2473
2965
|
)
|
|
2474
2966
|
});
|
|
2967
|
+
function lineDelta(anchor, current) {
|
|
2968
|
+
return anchor.lines === void 0 ? null : Math.abs(current - anchor.lines);
|
|
2969
|
+
}
|
|
2970
|
+
function headHash(source, anchor) {
|
|
2971
|
+
if (source.head === void 0) return void 0;
|
|
2972
|
+
const resolved = resolveAnchor(source.head, anchor);
|
|
2973
|
+
if (!resolved) return void 0;
|
|
2974
|
+
return {
|
|
2975
|
+
hash: hashAnchorText(resolved.text),
|
|
2976
|
+
lines: resolved.endLine - resolved.startLine + 1
|
|
2977
|
+
};
|
|
2978
|
+
}
|
|
2979
|
+
async function readSources(anchors, root, offline) {
|
|
2980
|
+
const origin = new LazyOrigin(root);
|
|
2981
|
+
if (anchors.some((anchor) => anchor.repo)) await origin.prime();
|
|
2982
|
+
const foreign = new Map(
|
|
2983
|
+
anchors.map((anchor) => [anchor, origin.isForeign(anchor)])
|
|
2984
|
+
);
|
|
2985
|
+
const local = anchors.filter((anchor) => !foreign.get(anchor));
|
|
2986
|
+
const remote = anchors.filter((anchor) => foreign.get(anchor));
|
|
2987
|
+
const reads = await readAnchorFiles(
|
|
2988
|
+
local.map((anchor) => anchor.file),
|
|
2989
|
+
anchorFileReader(root)
|
|
2990
|
+
);
|
|
2991
|
+
const blobs = await readRemoteAnchors(remote.flatMap(remoteWants), {
|
|
2992
|
+
offline
|
|
2993
|
+
});
|
|
2994
|
+
const sources = /* @__PURE__ */ new Map();
|
|
2995
|
+
for (const anchor of local) {
|
|
2996
|
+
const read = reads.get(anchor.file);
|
|
2997
|
+
sources.set(
|
|
2998
|
+
anchor,
|
|
2999
|
+
read.ok ? { ok: true, source: read.source } : { ok: false, reason: read.reason }
|
|
3000
|
+
);
|
|
3001
|
+
}
|
|
3002
|
+
for (const anchor of remote) {
|
|
3003
|
+
const repo = anchor.repo;
|
|
3004
|
+
const key = normalizeRepoUrl(repo);
|
|
3005
|
+
const atDefault = blobs.get(wantKey(key, void 0, anchor.file));
|
|
3006
|
+
const primary = anchor.ref ? blobs.get(wantKey(key, anchor.ref, anchor.file)) : atDefault;
|
|
3007
|
+
if (!primary?.ok) {
|
|
3008
|
+
sources.set(anchor, {
|
|
3009
|
+
ok: false,
|
|
3010
|
+
reason: primary?.ok === false ? primary.reason : "remote-unreachable",
|
|
3011
|
+
repo
|
|
3012
|
+
});
|
|
3013
|
+
continue;
|
|
3014
|
+
}
|
|
3015
|
+
sources.set(anchor, {
|
|
3016
|
+
ok: true,
|
|
3017
|
+
source: primary.source,
|
|
3018
|
+
repo,
|
|
3019
|
+
...anchor.ref && atDefault?.ok ? { head: atDefault.source } : {}
|
|
3020
|
+
});
|
|
3021
|
+
}
|
|
3022
|
+
return sources;
|
|
3023
|
+
}
|
|
2475
3024
|
|
|
2476
3025
|
// src/commands/answer.ts
|
|
2477
3026
|
import { z as z9 } from "zod";
|
|
@@ -2629,8 +3178,8 @@ var days = (what, fallback) => z13.number().int().positive().optional().describe
|
|
|
2629
3178
|
var doctorCommand = define({
|
|
2630
3179
|
name: "doctor",
|
|
2631
3180
|
tool: "kb_doctor",
|
|
2632
|
-
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--strict]",
|
|
2633
|
-
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.",
|
|
3181
|
+
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--offline] [--strict]",
|
|
3182
|
+
description: "Read-only health sweep: expired, expiring, unverified, aging, orphaned, broken-supersession, superseded-but-cited, drifted and unchecked 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.",
|
|
2634
3183
|
input: z13.object({
|
|
2635
3184
|
bundlePath,
|
|
2636
3185
|
repoRoot: REPO_ROOT,
|
|
@@ -2646,6 +3195,9 @@ var doctorCommand = define({
|
|
|
2646
3195
|
"How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
|
|
2647
3196
|
DEFAULT_AGING_DAYS
|
|
2648
3197
|
),
|
|
3198
|
+
offline: z13.boolean().optional().describe(
|
|
3199
|
+
"Read foreign anchors from the local repo cache only, never fetching."
|
|
3200
|
+
),
|
|
2649
3201
|
strict: z13.boolean().optional().describe(
|
|
2650
3202
|
"Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
|
|
2651
3203
|
)
|
|
@@ -2665,13 +3217,23 @@ var doctorCommand = define({
|
|
|
2665
3217
|
...expiring2 !== void 0 ? { expiringDays: Number(expiring2) } : {},
|
|
2666
3218
|
...unverified2 !== void 0 ? { unverifiedDays: Number(unverified2) } : {},
|
|
2667
3219
|
...agingDays !== void 0 ? { agingDays: Number(agingDays) } : {},
|
|
3220
|
+
...argv.includes("--offline") ? { offline: true } : {},
|
|
2668
3221
|
...argv.includes("--strict") ? { strict: true } : {}
|
|
2669
3222
|
};
|
|
2670
3223
|
},
|
|
2671
|
-
run: async ({ store, now }, {
|
|
3224
|
+
run: async ({ store, now }, {
|
|
3225
|
+
bundlePath: path,
|
|
3226
|
+
expiringDays,
|
|
3227
|
+
unverifiedDays,
|
|
3228
|
+
agingDays,
|
|
3229
|
+
repoRoot,
|
|
3230
|
+
offline
|
|
3231
|
+
}) => {
|
|
2672
3232
|
const checkedAt = now();
|
|
2673
3233
|
const records = await store.list(path);
|
|
2674
|
-
const anchorDrift = await store.detectDrift(records, repoRoot
|
|
3234
|
+
const anchorDrift = await store.detectDrift(records, repoRoot, {
|
|
3235
|
+
offline: offline === true
|
|
3236
|
+
});
|
|
2675
3237
|
const report = doctor(records, {
|
|
2676
3238
|
...expiringDays !== void 0 ? { expiringDays } : {},
|
|
2677
3239
|
...unverifiedDays !== void 0 ? { unverifiedDays } : {},
|
|
@@ -3081,17 +3643,115 @@ var schemaCommand = define({
|
|
|
3081
3643
|
run: () => Promise.resolve(kbJsonSchemas())
|
|
3082
3644
|
});
|
|
3083
3645
|
|
|
3084
|
-
// src/commands/
|
|
3646
|
+
// src/commands/stamp.ts
|
|
3647
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
3085
3648
|
import { z as z25 } from "zod";
|
|
3649
|
+
var DIGEST = /^[0-9a-f]{64}$/;
|
|
3650
|
+
var stampCommand = define({
|
|
3651
|
+
name: "stamp",
|
|
3652
|
+
tool: "kb_stamp",
|
|
3653
|
+
usage: "stamp [--bundle PATH] [--since DIGEST|FILE]",
|
|
3654
|
+
description: "Content stamp of a base \u2014 `load`'s digest, record counts, per-record digests \u2014 without any bodies. Takes no bundlePath to stamp every pinned base. With `since`, reports only the bases that moved, naming the changed ids when the baseline is a prior stamp; silent when nothing changed. Reads, never writes.",
|
|
3655
|
+
input: z25.object({
|
|
3656
|
+
bundlePath: z25.string().min(1).optional().describe(
|
|
3657
|
+
"Absolute path to one knowledge base. Omit to stamp every pinned base."
|
|
3658
|
+
),
|
|
3659
|
+
since: z25.string().min(1).optional().describe(
|
|
3660
|
+
"Prior digest, or path to a prior `stamp --json`; only moved bases return, with changed ids when the baseline is a file."
|
|
3661
|
+
)
|
|
3662
|
+
}),
|
|
3663
|
+
fromArgv: (argv, path, _stdin, bundleExplicit) => {
|
|
3664
|
+
const since = argvFlag(argv, "--since");
|
|
3665
|
+
return {
|
|
3666
|
+
...bundleExplicit ? { bundlePath: path } : {},
|
|
3667
|
+
...since !== void 0 ? { since } : {}
|
|
3668
|
+
};
|
|
3669
|
+
},
|
|
3670
|
+
run: async ({ store }, { bundlePath: bundlePath2, since }) => {
|
|
3671
|
+
const targets = bundlePath2 ? [bundlePath2] : (await readMergedPins(process.cwd())).pins.map(
|
|
3672
|
+
(pin) => pin.absolutePath
|
|
3673
|
+
);
|
|
3674
|
+
if (since !== void 0 && DIGEST.test(since) && targets.length > 1) {
|
|
3675
|
+
throw new KbStampDigestBaselineError(since);
|
|
3676
|
+
}
|
|
3677
|
+
const stamps = await Promise.all(
|
|
3678
|
+
targets.map((target) => store.stamp(target))
|
|
3679
|
+
);
|
|
3680
|
+
if (since === void 0) {
|
|
3681
|
+
return stamps.map((stamp) => ({ ...stamp, changed: null }));
|
|
3682
|
+
}
|
|
3683
|
+
const baseline = await readBaseline(since);
|
|
3684
|
+
const reports = [];
|
|
3685
|
+
for (const stamp of stamps) {
|
|
3686
|
+
const before = baseline.byPath.get(stamp.path);
|
|
3687
|
+
if (baseline.digest !== null) {
|
|
3688
|
+
if (baseline.digest === stamp.digest) continue;
|
|
3689
|
+
reports.push({ ...stamp, changed: null });
|
|
3690
|
+
continue;
|
|
3691
|
+
}
|
|
3692
|
+
if (before && before.digest === stamp.digest) continue;
|
|
3693
|
+
reports.push({ ...stamp, changed: changedIds(before?.records, stamp) });
|
|
3694
|
+
}
|
|
3695
|
+
return reports;
|
|
3696
|
+
},
|
|
3697
|
+
render: (result) => result.map((report) => {
|
|
3698
|
+
const counts = `${report.recordCount} record(s), ${report.superseded} superseded`;
|
|
3699
|
+
const head = `${report.path} ${report.digest} ${counts}${report.newestAt ? ` newest ${report.newestAt}` : ""}`;
|
|
3700
|
+
return report.changed?.length ? `${head}
|
|
3701
|
+
changed: ${report.changed.join(", ")}` : head;
|
|
3702
|
+
}).join("\n")
|
|
3703
|
+
});
|
|
3704
|
+
function changedIds(before, stamp) {
|
|
3705
|
+
const now = new Map(
|
|
3706
|
+
stamp.records.map((record) => [record.conceptId, record.digest])
|
|
3707
|
+
);
|
|
3708
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3709
|
+
for (const [conceptId2, digest] of now) {
|
|
3710
|
+
if (before?.get(conceptId2) !== digest) ids.add(conceptId2);
|
|
3711
|
+
}
|
|
3712
|
+
for (const conceptId2 of before?.keys() ?? []) {
|
|
3713
|
+
if (!now.has(conceptId2)) ids.add(conceptId2);
|
|
3714
|
+
}
|
|
3715
|
+
return [...ids].sort();
|
|
3716
|
+
}
|
|
3717
|
+
async function readBaseline(since) {
|
|
3718
|
+
if (DIGEST.test(since)) return { digest: since, byPath: /* @__PURE__ */ new Map() };
|
|
3719
|
+
let parsed;
|
|
3720
|
+
try {
|
|
3721
|
+
parsed = JSON.parse(await readFile4(since, "utf8"));
|
|
3722
|
+
} catch {
|
|
3723
|
+
throw new KbStampBaselineError(since);
|
|
3724
|
+
}
|
|
3725
|
+
const entries = Array.isArray(parsed) ? parsed : parsed?.stamps ?? [];
|
|
3726
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
3727
|
+
for (const entry of entries) {
|
|
3728
|
+
if (typeof entry?.path !== "string" || typeof entry?.digest !== "string") {
|
|
3729
|
+
continue;
|
|
3730
|
+
}
|
|
3731
|
+
byPath.set(entry.path, {
|
|
3732
|
+
digest: entry.digest,
|
|
3733
|
+
records: new Map(
|
|
3734
|
+
(entry.records ?? []).map((record) => [
|
|
3735
|
+
record.conceptId,
|
|
3736
|
+
record.digest
|
|
3737
|
+
])
|
|
3738
|
+
)
|
|
3739
|
+
});
|
|
3740
|
+
}
|
|
3741
|
+
return { digest: null, byPath };
|
|
3742
|
+
}
|
|
3743
|
+
|
|
3744
|
+
// src/commands/status.ts
|
|
3745
|
+
import { z as z26 } from "zod";
|
|
3086
3746
|
var statusCommand = define({
|
|
3087
3747
|
name: "status",
|
|
3088
3748
|
tool: "kb_status",
|
|
3089
3749
|
usage: "status <concept-id> <status>",
|
|
3090
3750
|
description: "Move a record's status. Compare-and-swap: a concurrent change fails instead of being overwritten.",
|
|
3091
|
-
input:
|
|
3751
|
+
input: z26.object({
|
|
3092
3752
|
bundlePath,
|
|
3093
3753
|
conceptId,
|
|
3094
|
-
status:
|
|
3754
|
+
status: z26.enum(KB_RECORD_STATUSES)
|
|
3095
3755
|
}),
|
|
3096
3756
|
fromArgv: (argv, path) => ({
|
|
3097
3757
|
bundlePath: path,
|
|
@@ -3106,13 +3766,13 @@ var statusCommand = define({
|
|
|
3106
3766
|
});
|
|
3107
3767
|
|
|
3108
3768
|
// src/commands/supersede.ts
|
|
3109
|
-
import { z as
|
|
3769
|
+
import { z as z27 } from "zod";
|
|
3110
3770
|
var supersedeCommand = define({
|
|
3111
3771
|
name: "supersede",
|
|
3112
3772
|
tool: "kb_supersede",
|
|
3113
3773
|
usage: "supersede <concept-id> <replacement-id>",
|
|
3114
3774
|
description: "Mark a record superseded by another, linked in both directions. Use instead of editing a record whose meaning changed.",
|
|
3115
|
-
input:
|
|
3775
|
+
input: z27.object({ bundlePath, conceptId, replacementId: conceptId }),
|
|
3116
3776
|
fromArgv: (argv, path) => ({
|
|
3117
3777
|
bundlePath: path,
|
|
3118
3778
|
conceptId: argv[1],
|
|
@@ -3126,16 +3786,16 @@ var supersedeCommand = define({
|
|
|
3126
3786
|
});
|
|
3127
3787
|
|
|
3128
3788
|
// src/commands/sync-instructions.ts
|
|
3129
|
-
import { z as
|
|
3789
|
+
import { z as z28 } from "zod";
|
|
3130
3790
|
var syncInstructionsCommand = define({
|
|
3131
3791
|
name: "sync-instructions",
|
|
3132
3792
|
usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
|
|
3133
3793
|
description: "CLI-only: plant the kb_context block between sentinel comments in AGENTS.md or CLAUDE.md, idempotently.",
|
|
3134
|
-
input:
|
|
3135
|
-
file:
|
|
3136
|
-
budgetTokens:
|
|
3137
|
-
fullUnderTokens:
|
|
3138
|
-
profile:
|
|
3794
|
+
input: z28.object({
|
|
3795
|
+
file: z28.string().min(1).describe("The instruction file to edit in place."),
|
|
3796
|
+
budgetTokens: z28.number().int().positive().optional(),
|
|
3797
|
+
fullUnderTokens: z28.number().int().positive().optional(),
|
|
3798
|
+
profile: z28.string().optional()
|
|
3139
3799
|
}),
|
|
3140
3800
|
fromArgv: (argv) => {
|
|
3141
3801
|
const budget = argvFlag(argv, "--budget");
|
|
@@ -3161,17 +3821,17 @@ var syncInstructionsCommand = define({
|
|
|
3161
3821
|
});
|
|
3162
3822
|
|
|
3163
3823
|
// src/commands/trace.ts
|
|
3164
|
-
import { z as
|
|
3824
|
+
import { z as z29 } from "zod";
|
|
3165
3825
|
var traceCommand = define({
|
|
3166
3826
|
name: "trace",
|
|
3167
3827
|
tool: "kb_trace",
|
|
3168
3828
|
usage: "trace <concept-id> [edges...]",
|
|
3169
3829
|
description: 'Timeline of how a position was reached, ordered by write time, following supersession, shared anchors and shared sources. Includes rejected, draft and superseded records \u2014 in a history they are the content. For "why is it like this"; kb_load answers "what holds now".',
|
|
3170
|
-
input:
|
|
3830
|
+
input: z29.object({
|
|
3171
3831
|
bundlePath,
|
|
3172
3832
|
conceptId,
|
|
3173
|
-
edges:
|
|
3174
|
-
depth:
|
|
3833
|
+
edges: z29.array(z29.enum(TRACE_EDGES)).optional(),
|
|
3834
|
+
depth: z29.number().int().positive().optional()
|
|
3175
3835
|
}),
|
|
3176
3836
|
fromArgv: (argv, path) => ({
|
|
3177
3837
|
bundlePath: path,
|
|
@@ -3193,37 +3853,37 @@ var traceCommand = define({
|
|
|
3193
3853
|
});
|
|
3194
3854
|
|
|
3195
3855
|
// src/commands/types.ts
|
|
3196
|
-
import { z as
|
|
3856
|
+
import { z as z30 } from "zod";
|
|
3197
3857
|
var typesCommand = define({
|
|
3198
3858
|
name: "types",
|
|
3199
3859
|
tool: "kb_types",
|
|
3200
3860
|
usage: "types",
|
|
3201
3861
|
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.",
|
|
3202
|
-
input:
|
|
3862
|
+
input: z30.object({}),
|
|
3203
3863
|
fromArgv: () => ({}),
|
|
3204
3864
|
run: () => Promise.resolve(RECORD_TYPES)
|
|
3205
3865
|
});
|
|
3206
3866
|
|
|
3207
3867
|
// src/commands/unpin.ts
|
|
3208
|
-
import { z as
|
|
3868
|
+
import { z as z31 } from "zod";
|
|
3209
3869
|
var unpinCommand = define({
|
|
3210
3870
|
name: "unpin",
|
|
3211
3871
|
tool: "kb_unpin",
|
|
3212
3872
|
usage: "unpin [bundle-path]",
|
|
3213
3873
|
description: "Remove a base from every manifest layer that holds it. Reports the layers touched.",
|
|
3214
|
-
input:
|
|
3874
|
+
input: z31.object({ bundlePath }),
|
|
3215
3875
|
fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
|
|
3216
3876
|
run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
|
|
3217
3877
|
});
|
|
3218
3878
|
|
|
3219
3879
|
// src/commands/validate.ts
|
|
3220
|
-
import { z as
|
|
3880
|
+
import { z as z32 } from "zod";
|
|
3221
3881
|
var validateCommand = define({
|
|
3222
3882
|
name: "validate",
|
|
3223
3883
|
tool: "kb_validate",
|
|
3224
3884
|
usage: "validate",
|
|
3225
3885
|
description: "Check pointers no single record can see: supersession links that disagree between the two records, typed causal links, and assumptions that cite sources. Each finding carries a severity: errors fail the exit code, warnings do not.",
|
|
3226
|
-
input:
|
|
3886
|
+
input: z32.object({ bundlePath }),
|
|
3227
3887
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
3228
3888
|
run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
|
|
3229
3889
|
// Warnings never fail the exit code; every other severity does.
|
|
@@ -3233,16 +3893,16 @@ var validateCommand = define({
|
|
|
3233
3893
|
});
|
|
3234
3894
|
|
|
3235
3895
|
// src/commands/verify.ts
|
|
3236
|
-
import { z as
|
|
3896
|
+
import { z as z33 } from "zod";
|
|
3237
3897
|
var verifyCommand = define({
|
|
3238
3898
|
name: "verify",
|
|
3239
3899
|
tool: "kb_verify",
|
|
3240
3900
|
usage: "verify <concept-id> --note <text>",
|
|
3241
3901
|
description: "Append a verified[] event: who checked, when, and what was found. Append-only. A record's own generator is refused unless the actor is `human:`-prefixed.",
|
|
3242
|
-
input:
|
|
3902
|
+
input: z33.object({
|
|
3243
3903
|
bundlePath,
|
|
3244
3904
|
conceptId,
|
|
3245
|
-
note:
|
|
3905
|
+
note: z33.string().refine((s) => s.trim().length > 0, {
|
|
3246
3906
|
message: "note must say what the check found"
|
|
3247
3907
|
})
|
|
3248
3908
|
}),
|
|
@@ -3262,15 +3922,15 @@ var verifyCommand = define({
|
|
|
3262
3922
|
});
|
|
3263
3923
|
|
|
3264
3924
|
// src/commands/write.ts
|
|
3265
|
-
import { z as
|
|
3925
|
+
import { z as z34 } from "zod";
|
|
3266
3926
|
var writeCommand = define({
|
|
3267
3927
|
name: "write",
|
|
3268
3928
|
tool: "kb_write",
|
|
3269
3929
|
usage: "write <type> < record.json",
|
|
3270
3930
|
description: "Write one record. Search first \u2014 a duplicate concept id is rejected, not overwritten; kb_types lists each type's sections. An unsourced claim is an `assumption` with assumption: true, never a vague `fact`. Conflicting records get a `risk`, `open-question`, or superseding `decision`. Prefer a new short record over overloading one. Never delete; supersede.",
|
|
3271
|
-
input:
|
|
3931
|
+
input: z34.object({
|
|
3272
3932
|
bundlePath,
|
|
3273
|
-
type:
|
|
3933
|
+
type: z34.enum(KB_RECORD_TYPES),
|
|
3274
3934
|
input: composeInputSchema
|
|
3275
3935
|
}),
|
|
3276
3936
|
fromArgv: async (argv, path, stdin) => ({
|
|
@@ -3294,13 +3954,13 @@ var writeCommand = define({
|
|
|
3294
3954
|
});
|
|
3295
3955
|
|
|
3296
3956
|
// src/commands/write-decision.ts
|
|
3297
|
-
import { z as
|
|
3957
|
+
import { z as z35 } from "zod";
|
|
3298
3958
|
var writeDecisionCommand = define({
|
|
3299
3959
|
name: "write-decision",
|
|
3300
3960
|
tool: "kb_write_decision",
|
|
3301
3961
|
usage: "write-decision < decision.json",
|
|
3302
3962
|
description: "Write a decision, with `alternative` (what was rejected and why) and `impact` as fields. Record one when a later reader would otherwise simplify the constraint away; skip when the diff already answers it. `sources` for material read, `anchors` for code, `relatedConceptIds` for records.",
|
|
3303
|
-
input:
|
|
3963
|
+
input: z35.object({ bundlePath, input: decisionInputSchema }),
|
|
3304
3964
|
fromArgv: async (_argv, path, stdin) => ({
|
|
3305
3965
|
bundlePath: path,
|
|
3306
3966
|
input: JSON.parse(await stdin())
|
|
@@ -3340,6 +4000,7 @@ var KB_COMMANDS = [
|
|
|
3340
4000
|
listCommand,
|
|
3341
4001
|
readIndexCommand,
|
|
3342
4002
|
logCommand,
|
|
4003
|
+
stampCommand,
|
|
3343
4004
|
validateCommand,
|
|
3344
4005
|
doctorCommand,
|
|
3345
4006
|
schemaCommand,
|
|
@@ -3381,7 +4042,7 @@ function parseMarkdownWithFrontmatter(text, schema) {
|
|
|
3381
4042
|
|
|
3382
4043
|
// src/search-index.ts
|
|
3383
4044
|
import { stat as stat2 } from "fs/promises";
|
|
3384
|
-
import { join as
|
|
4045
|
+
import { join as join4 } from "path";
|
|
3385
4046
|
var SEARCH_INDEX_FILE = ".index.sqlite";
|
|
3386
4047
|
var COLLECTION = "kb";
|
|
3387
4048
|
async function searchBase(bundlePath2, query, options = {}) {
|
|
@@ -3390,7 +4051,7 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
3390
4051
|
let store = null;
|
|
3391
4052
|
try {
|
|
3392
4053
|
store = await qmd.createStore({
|
|
3393
|
-
dbPath:
|
|
4054
|
+
dbPath: join4(bundlePath2, SEARCH_INDEX_FILE),
|
|
3394
4055
|
config: {
|
|
3395
4056
|
collections: {
|
|
3396
4057
|
[COLLECTION]: {
|
|
@@ -3425,7 +4086,7 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
3425
4086
|
}
|
|
3426
4087
|
}
|
|
3427
4088
|
async function isStale(bundlePath2) {
|
|
3428
|
-
const indexAt = await stat2(
|
|
4089
|
+
const indexAt = await stat2(join4(bundlePath2, SEARCH_INDEX_FILE)).then((s) => s.mtimeMs).catch(() => 0);
|
|
3429
4090
|
if (!indexAt) return true;
|
|
3430
4091
|
const { readdir: readdir2 } = await import("fs/promises");
|
|
3431
4092
|
const names = (await readdir2(bundlePath2).catch(() => [])).filter(
|
|
@@ -3434,7 +4095,7 @@ async function isStale(bundlePath2) {
|
|
|
3434
4095
|
let stale = false;
|
|
3435
4096
|
await mapLimit(names, DEFAULT_IO_CONCURRENCY, async (name) => {
|
|
3436
4097
|
if (stale) return;
|
|
3437
|
-
const at = await stat2(
|
|
4098
|
+
const at = await stat2(join4(bundlePath2, name)).then((s) => s.mtimeMs).catch(() => 0);
|
|
3438
4099
|
if (at > indexAt) stale = true;
|
|
3439
4100
|
});
|
|
3440
4101
|
return stale;
|
|
@@ -3469,18 +4130,49 @@ async function loadQmd(logger) {
|
|
|
3469
4130
|
}
|
|
3470
4131
|
|
|
3471
4132
|
// src/kb-store.ts
|
|
3472
|
-
import { createHash as createHash2 } from "crypto";
|
|
3473
4133
|
import {
|
|
3474
4134
|
appendFile,
|
|
3475
4135
|
link,
|
|
3476
|
-
mkdir as
|
|
4136
|
+
mkdir as mkdir3,
|
|
3477
4137
|
readdir,
|
|
3478
|
-
readFile as
|
|
4138
|
+
readFile as readFile5,
|
|
3479
4139
|
rename,
|
|
3480
4140
|
unlink,
|
|
3481
4141
|
writeFile as writeFile3
|
|
3482
4142
|
} from "fs/promises";
|
|
3483
|
-
import { join as
|
|
4143
|
+
import { join as join5, resolve as resolve5, sep as sep3 } from "path";
|
|
4144
|
+
|
|
4145
|
+
// src/kb-stamp.ts
|
|
4146
|
+
import { createHash as createHash2 } from "crypto";
|
|
4147
|
+
function sha256(contents) {
|
|
4148
|
+
return createHash2("sha256").update(contents).digest("hex");
|
|
4149
|
+
}
|
|
4150
|
+
function bundleStamp(records, superseded) {
|
|
4151
|
+
const entries = [
|
|
4152
|
+
...records.map((hit) => ({
|
|
4153
|
+
conceptId: hit.record.conceptId,
|
|
4154
|
+
digest: `current:${sha256(
|
|
4155
|
+
stringifyMarkdownWithFrontmatter(
|
|
4156
|
+
hit.record.body,
|
|
4157
|
+
hit.record.frontmatter
|
|
4158
|
+
)
|
|
4159
|
+
)}`
|
|
4160
|
+
})),
|
|
4161
|
+
...superseded.map((entry) => ({
|
|
4162
|
+
conceptId: entry.conceptId,
|
|
4163
|
+
digest: `superseded:${sha256(JSON.stringify(entry))}`
|
|
4164
|
+
}))
|
|
4165
|
+
].sort((a, b) => a.conceptId < b.conceptId ? -1 : 1);
|
|
4166
|
+
return {
|
|
4167
|
+
digest: sha256(
|
|
4168
|
+
entries.map((entry) => `${entry.conceptId}:${entry.digest}`).join("\n")
|
|
4169
|
+
),
|
|
4170
|
+
records: entries
|
|
4171
|
+
};
|
|
4172
|
+
}
|
|
4173
|
+
function bundleDigest(records, superseded) {
|
|
4174
|
+
return bundleStamp(records, superseded).digest;
|
|
4175
|
+
}
|
|
3484
4176
|
|
|
3485
4177
|
// src/kb-links/inbound.ts
|
|
3486
4178
|
function inboundIndex(bundle) {
|
|
@@ -3642,7 +4334,7 @@ function appendUnionMergeLine(contents) {
|
|
|
3642
4334
|
}
|
|
3643
4335
|
|
|
3644
4336
|
// src/kb-store.ts
|
|
3645
|
-
var KB_DIR =
|
|
4337
|
+
var KB_DIR = join5(".strauss", "kb");
|
|
3646
4338
|
var STORE_OWNED = /* @__PURE__ */ new Set([INDEX_FILE, LOG_FILE, SEARCH_INDEX_FILE]);
|
|
3647
4339
|
var DEFAULT_LOAD_BUDGET = 25e3;
|
|
3648
4340
|
var KbStore = class {
|
|
@@ -3673,7 +4365,7 @@ var KbStore = class {
|
|
|
3673
4365
|
const conceptId2 = `${input.type}.${input.slug}`;
|
|
3674
4366
|
const root = this.root(bundlePath2);
|
|
3675
4367
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
3676
|
-
await
|
|
4368
|
+
await mkdir3(root, { recursive: true });
|
|
3677
4369
|
await this.publish(
|
|
3678
4370
|
target,
|
|
3679
4371
|
stringifyMarkdownWithFrontmatter(input.body, frontmatter),
|
|
@@ -3712,7 +4404,7 @@ var KbStore = class {
|
|
|
3712
4404
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
3713
4405
|
let raw;
|
|
3714
4406
|
try {
|
|
3715
|
-
raw = await
|
|
4407
|
+
raw = await readFile5(target, "utf8");
|
|
3716
4408
|
} catch {
|
|
3717
4409
|
return null;
|
|
3718
4410
|
}
|
|
@@ -3737,7 +4429,7 @@ var KbStore = class {
|
|
|
3737
4429
|
const records = await mapLimit(
|
|
3738
4430
|
wanted,
|
|
3739
4431
|
DEFAULT_IO_CONCURRENCY,
|
|
3740
|
-
async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await
|
|
4432
|
+
async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await readFile5(join5(root, name), "utf8"))
|
|
3741
4433
|
);
|
|
3742
4434
|
return records.filter((record) => record !== null);
|
|
3743
4435
|
}
|
|
@@ -3905,7 +4597,8 @@ ${answer}
|
|
|
3905
4597
|
* at the repo root, and the MCP server's cwd is the workspace.
|
|
3906
4598
|
*
|
|
3907
4599
|
* Public because `doctor` needs the same map with the same degradation: a
|
|
3908
|
-
* sweep that failed to read the tree should report no drift, not fail
|
|
4600
|
+
* sweep that failed to read the tree should report no drift, not fail — and
|
|
4601
|
+
* `offline: false` there, because a sweep is worth a fetch.
|
|
3909
4602
|
*
|
|
3910
4603
|
* When no root was given and not one anchored file was found, the finding is
|
|
3911
4604
|
* discarded. A base read from somewhere other than the tree it describes
|
|
@@ -3917,10 +4610,13 @@ ${answer}
|
|
|
3917
4610
|
* plausible, and the misses become findings again; an explicit `repoRoot` is
|
|
3918
4611
|
* taken at its word either way.
|
|
3919
4612
|
*/
|
|
3920
|
-
async detectDrift(records, repoRoot) {
|
|
4613
|
+
async detectDrift(records, repoRoot, options = {}) {
|
|
3921
4614
|
try {
|
|
3922
4615
|
const drift = await detectAnchorDrift(records, {
|
|
3923
|
-
repoRoot: repoRoot ?? process.cwd()
|
|
4616
|
+
repoRoot: repoRoot ?? process.cwd(),
|
|
4617
|
+
// Offline by default: a read path must never spend a network fetch per
|
|
4618
|
+
// call. `doctor` and `anchor-resolve` are the verbs that go get it.
|
|
4619
|
+
remote: { offline: options.offline !== false }
|
|
3924
4620
|
});
|
|
3925
4621
|
if (repoRoot === void 0 && looksLikeWrongRepoRoot(drift)) {
|
|
3926
4622
|
this.logger.warn?.({
|
|
@@ -4008,6 +4704,28 @@ ${answer}
|
|
|
4008
4704
|
digest: bundleDigestValue
|
|
4009
4705
|
};
|
|
4010
4706
|
}
|
|
4707
|
+
/**
|
|
4708
|
+
* `load`'s digest without `load`'s bodies — the same records, adjudicated
|
|
4709
|
+
* the same way, handed back as a stamp. Skips the anchor drift pass, which
|
|
4710
|
+
* reads source files and only ever adds warnings: no warning reaches the
|
|
4711
|
+
* digest, so the value is identical to the one `load` returns.
|
|
4712
|
+
*/
|
|
4713
|
+
async stamp(bundlePath2) {
|
|
4714
|
+
const bundle = await this.list(bundlePath2);
|
|
4715
|
+
const adjudicated = adjudicate(bundle, bundle, /* @__PURE__ */ new Date());
|
|
4716
|
+
const current = adjudicated.filter((hit) => hit.standing !== "superseded");
|
|
4717
|
+
const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(stub);
|
|
4718
|
+
const stamped = bundleStamp(current, superseded);
|
|
4719
|
+
const dates = bundle.map((record) => record.frontmatter.generated?.at ?? null).filter((at) => typeof at === "string").sort();
|
|
4720
|
+
return {
|
|
4721
|
+
path: bundlePath2,
|
|
4722
|
+
digest: stamped.digest,
|
|
4723
|
+
recordCount: bundle.length,
|
|
4724
|
+
superseded: superseded.length,
|
|
4725
|
+
newestAt: dates.at(-1) ?? null,
|
|
4726
|
+
records: stamped.records
|
|
4727
|
+
};
|
|
4728
|
+
}
|
|
4011
4729
|
/** How a position was arrived at, as a timeline. See `trace.ts`. */
|
|
4012
4730
|
async trace(bundlePath2, seedId, options = {}) {
|
|
4013
4731
|
return trace(seedId, await this.list(bundlePath2), options);
|
|
@@ -4038,11 +4756,11 @@ ${answer}
|
|
|
4038
4756
|
async readIndex(bundlePath2) {
|
|
4039
4757
|
const root = this.root(bundlePath2);
|
|
4040
4758
|
const expected = renderIndex(await this.list(bundlePath2));
|
|
4041
|
-
const stored = await
|
|
4759
|
+
const stored = await readFile5(join5(root, INDEX_FILE), "utf8").catch(
|
|
4042
4760
|
() => null
|
|
4043
4761
|
);
|
|
4044
4762
|
if (indexIsStale(stored, expected)) {
|
|
4045
|
-
await this.publish(
|
|
4763
|
+
await this.publish(join5(root, INDEX_FILE), expected, true, INDEX_FILE);
|
|
4046
4764
|
this.logger.info?.({
|
|
4047
4765
|
operation: "kb.index.repair",
|
|
4048
4766
|
bundlePath: root,
|
|
@@ -4059,8 +4777,8 @@ ${answer}
|
|
|
4059
4777
|
* knows which agent touched what. So a bad line is surfaced and left alone.
|
|
4060
4778
|
*/
|
|
4061
4779
|
async readLog(bundlePath2) {
|
|
4062
|
-
const raw = await
|
|
4063
|
-
|
|
4780
|
+
const raw = await readFile5(
|
|
4781
|
+
join5(this.root(bundlePath2), LOG_FILE),
|
|
4064
4782
|
"utf8"
|
|
4065
4783
|
).catch(() => "");
|
|
4066
4784
|
const result = parseLog(raw);
|
|
@@ -4111,15 +4829,15 @@ ${answer}
|
|
|
4111
4829
|
}
|
|
4112
4830
|
async mutate(bundlePath2, conceptId2, change, entry, changeBody = (body) => body) {
|
|
4113
4831
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
4114
|
-
const before = await
|
|
4832
|
+
const before = await readFile5(target, "utf8").catch(() => null);
|
|
4115
4833
|
if (before === null) throw new KbRecordNotFoundError(conceptId2);
|
|
4116
4834
|
const parsed = this.parse(conceptId2, before);
|
|
4117
4835
|
if (!parsed) throw new KbRecordNotFoundError(conceptId2);
|
|
4118
4836
|
const frontmatter = change(parsed.frontmatter);
|
|
4119
4837
|
const body = changeBody(parsed.body);
|
|
4120
4838
|
const contents = stringifyMarkdownWithFrontmatter(body, frontmatter);
|
|
4121
|
-
const witness = await
|
|
4122
|
-
if (witness === null ||
|
|
4839
|
+
const witness = await readFile5(target, "utf8").catch(() => null);
|
|
4840
|
+
if (witness === null || sha256(witness) !== sha256(before)) {
|
|
4123
4841
|
throw new KbWriteConflictError(conceptId2);
|
|
4124
4842
|
}
|
|
4125
4843
|
await this.publish(target, contents, true, conceptId2);
|
|
@@ -4201,11 +4919,11 @@ ${answer}
|
|
|
4201
4919
|
* file must not fail the mutation it guards.
|
|
4202
4920
|
*/
|
|
4203
4921
|
async ensureGitattributes(root) {
|
|
4204
|
-
const target =
|
|
4922
|
+
const target = join5(root, GITATTRIBUTES_FILE);
|
|
4205
4923
|
try {
|
|
4206
4924
|
let existing;
|
|
4207
4925
|
try {
|
|
4208
|
-
existing = await
|
|
4926
|
+
existing = await readFile5(target, "utf8");
|
|
4209
4927
|
} catch (error) {
|
|
4210
4928
|
if (error.code !== "ENOENT") throw error;
|
|
4211
4929
|
existing = null;
|
|
@@ -4252,7 +4970,7 @@ ${answer}
|
|
|
4252
4970
|
async record(root, entry) {
|
|
4253
4971
|
await this.ensureGitattributes(root);
|
|
4254
4972
|
const line = renderLogEntry({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
|
|
4255
|
-
await appendFile(
|
|
4973
|
+
await appendFile(join5(root, LOG_FILE), line, "utf8").catch((error) => {
|
|
4256
4974
|
this.logger.warn?.({
|
|
4257
4975
|
operation: "kb.log.append",
|
|
4258
4976
|
outcome: "failed",
|
|
@@ -4289,7 +5007,7 @@ ${answer}
|
|
|
4289
5007
|
{ conceptId: conceptId2 }
|
|
4290
5008
|
);
|
|
4291
5009
|
}
|
|
4292
|
-
return
|
|
5010
|
+
return join5(this.root(bundlePath2), `${conceptId2}.md`);
|
|
4293
5011
|
}
|
|
4294
5012
|
};
|
|
4295
5013
|
function estimateTokens(record) {
|
|
@@ -4327,25 +5045,6 @@ function normalizeActor(id) {
|
|
|
4327
5045
|
if (colon === -1) return id.toLowerCase();
|
|
4328
5046
|
return id.slice(0, colon + 1).toLowerCase() + id.slice(colon + 1);
|
|
4329
5047
|
}
|
|
4330
|
-
function digest(contents) {
|
|
4331
|
-
return createHash2("sha256").update(contents).digest("hex");
|
|
4332
|
-
}
|
|
4333
|
-
function bundleDigest(records, superseded) {
|
|
4334
|
-
const entries = [
|
|
4335
|
-
...records.map(
|
|
4336
|
-
(hit) => `${hit.record.conceptId}:current:${digest(
|
|
4337
|
-
stringifyMarkdownWithFrontmatter(
|
|
4338
|
-
hit.record.body,
|
|
4339
|
-
hit.record.frontmatter
|
|
4340
|
-
)
|
|
4341
|
-
)}`
|
|
4342
|
-
),
|
|
4343
|
-
...superseded.map(
|
|
4344
|
-
(entry) => `${entry.conceptId}:superseded:${digest(JSON.stringify(entry))}`
|
|
4345
|
-
)
|
|
4346
|
-
].sort();
|
|
4347
|
-
return digest(entries.join("\n"));
|
|
4348
|
-
}
|
|
4349
5048
|
|
|
4350
5049
|
// src/pack.ts
|
|
4351
5050
|
var DEFAULT_PACK_HOPS = 2;
|
|
@@ -4432,7 +5131,7 @@ function typeRank(record) {
|
|
|
4432
5131
|
}
|
|
4433
5132
|
|
|
4434
5133
|
// src/version.ts
|
|
4435
|
-
var VERSION = true ? "0.1.
|
|
5134
|
+
var VERSION = true ? "0.1.16" : "0.0.0-dev";
|
|
4436
5135
|
|
|
4437
5136
|
export {
|
|
4438
5137
|
kbSourceSchema,
|
|
@@ -4464,10 +5163,13 @@ export {
|
|
|
4464
5163
|
composeNoDecisionRecord,
|
|
4465
5164
|
isNoDecisionRecord,
|
|
4466
5165
|
selectDecisions,
|
|
5166
|
+
isCanonicalRepoUrl,
|
|
5167
|
+
repoCacheDir,
|
|
5168
|
+
readRemoteAnchors,
|
|
5169
|
+
anchorFilePath,
|
|
4467
5170
|
regexResolver,
|
|
4468
5171
|
hashAnchorText,
|
|
4469
5172
|
resolveAnchor,
|
|
4470
|
-
anchorFilePath,
|
|
4471
5173
|
detectAnchorDrift,
|
|
4472
5174
|
Fault,
|
|
4473
5175
|
ErrorTypes,
|
|
@@ -4545,4 +5247,4 @@ export {
|
|
|
4545
5247
|
KbStore,
|
|
4546
5248
|
VERSION
|
|
4547
5249
|
};
|
|
4548
|
-
//# sourceMappingURL=chunk-
|
|
5250
|
+
//# sourceMappingURL=chunk-H5W53NVU.js.map
|