@saasontools/strauss-kb 0.1.14 → 0.1.15
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 +27 -17
- package/dist/{chunk-43KALLFU.js → chunk-KNIUBCZY.js} +849 -312
- package/dist/chunk-KNIUBCZY.js.map +1 -0
- package/dist/{chunk-PYA5E7FL.js → chunk-LACCRB2Y.js} +2 -2
- package/dist/{chunk-MBXNCZ4V.js → chunk-MK7GU4DX.js} +2 -2
- package/dist/cli-main.cjs +878 -343
- package/dist/cli-main.cjs.map +1 -1
- package/dist/cli-main.js +2 -2
- package/dist/index.cjs +887 -343
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +144 -67
- package/dist/index.d.ts +144 -67
- package/dist/index.js +9 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp-main.cjs +876 -341
- 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-PYA5E7FL.js.map → chunk-LACCRB2Y.js.map} +0 -0
- /package/dist/{chunk-MBXNCZ4V.js.map → chunk-MK7GU4DX.js.map} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -99,6 +99,7 @@ __export(index_exports, {
|
|
|
99
99
|
impact: () => impact,
|
|
100
100
|
inboundIndex: () => inboundIndex,
|
|
101
101
|
indexIsStale: () => indexIsStale,
|
|
102
|
+
isCanonicalRepoUrl: () => isCanonicalRepoUrl,
|
|
102
103
|
isKbLinkRel: () => isKbLinkRel,
|
|
103
104
|
isKbRecordType: () => isKbRecordType,
|
|
104
105
|
isNoDecisionRecord: () => isNoDecisionRecord,
|
|
@@ -122,11 +123,13 @@ __export(index_exports, {
|
|
|
122
123
|
pinBase: () => pinBase,
|
|
123
124
|
readMergedPins: () => readMergedPins,
|
|
124
125
|
readPinsLayer: () => readPinsLayer,
|
|
126
|
+
readRemoteAnchors: () => readRemoteAnchors,
|
|
125
127
|
regexResolver: () => regexResolver,
|
|
126
128
|
renderCatalogLine: () => renderCatalogLine,
|
|
127
129
|
renderIndex: () => renderIndex,
|
|
128
130
|
renderIndexLine: () => renderIndexLine,
|
|
129
131
|
renderLogEntry: () => renderLogEntry,
|
|
132
|
+
repoCacheDir: () => repoCacheDir,
|
|
130
133
|
resolveAnchor: () => resolveAnchor,
|
|
131
134
|
resolveHeads: () => resolveHeads,
|
|
132
135
|
resolveHits: () => resolveHits,
|
|
@@ -147,8 +150,8 @@ module.exports = __toCommonJS(index_exports);
|
|
|
147
150
|
|
|
148
151
|
// src/kb-store.ts
|
|
149
152
|
var import_node_crypto2 = require("crypto");
|
|
150
|
-
var
|
|
151
|
-
var
|
|
153
|
+
var import_promises4 = require("fs/promises");
|
|
154
|
+
var import_node_path4 = require("path");
|
|
152
155
|
|
|
153
156
|
// src/concurrency.ts
|
|
154
157
|
var DEFAULT_IO_CONCURRENCY = 16;
|
|
@@ -230,16 +233,17 @@ var kbAnchorSchema = import_zod.z.object({
|
|
|
230
233
|
* (`https://github.com/org/name`) or a short name. Absent means the base's
|
|
231
234
|
* own repository, which is what nearly every anchor means.
|
|
232
235
|
*
|
|
233
|
-
* Unvalidated beyond not-blank: one repository has many spellings
|
|
234
|
-
*
|
|
236
|
+
* Unvalidated beyond not-blank: one repository has many spellings, matched
|
|
237
|
+
* after normalisation. Only a full URL can be fetched from, so `validate`
|
|
238
|
+
* warns on a short one; see ARCHITECTURE.
|
|
235
239
|
*/
|
|
236
240
|
repo: import_zod.z.string().trim().min(1).optional(),
|
|
237
241
|
/**
|
|
238
242
|
* The git rev the evidence was taken at. Prefer a commit SHA: a branch
|
|
239
243
|
* name is a moving pointer, so an anchor pinned to one says the evidence
|
|
240
244
|
* came from wherever that branch happens to be now, which is not a
|
|
241
|
-
* baseline.
|
|
242
|
-
*
|
|
245
|
+
* baseline. A foreign anchor is checked at this rev, and compared against
|
|
246
|
+
* the remote's default branch on top of it.
|
|
243
247
|
*/
|
|
244
248
|
ref: import_zod.z.string().trim().min(1).optional(),
|
|
245
249
|
hash: import_zod.z.string().regex(/^sha256:[0-9a-f]{64}$/, {
|
|
@@ -517,6 +521,436 @@ function indexIsStale(stored, expected) {
|
|
|
517
521
|
return stored !== expected;
|
|
518
522
|
}
|
|
519
523
|
|
|
524
|
+
// src/remote-repo/cache.ts
|
|
525
|
+
var import_node_os = require("os");
|
|
526
|
+
var import_node_path = require("path");
|
|
527
|
+
|
|
528
|
+
// src/anchor-resolver/repo-identity.ts
|
|
529
|
+
var import_node_child_process = require("child_process");
|
|
530
|
+
var import_node_util = require("util");
|
|
531
|
+
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
532
|
+
function normalizeRepoUrl(value) {
|
|
533
|
+
let url = value.trim().replace(/^git\+/, "");
|
|
534
|
+
const scp = /^[\w.-]+@([\w.-]+):(.+)$/.exec(url);
|
|
535
|
+
if (scp) url = `https://${scp[1]}/${scp[2]}`;
|
|
536
|
+
url = url.replace(/^ssh:\/\/(?:[^@/]+@)?/, "https://");
|
|
537
|
+
url = trimTrailingSlashes(url);
|
|
538
|
+
if (url.endsWith(".git")) url = url.slice(0, -4);
|
|
539
|
+
return trimTrailingSlashes(url).toLowerCase();
|
|
540
|
+
}
|
|
541
|
+
function trimTrailingSlashes(value) {
|
|
542
|
+
let end = value.length;
|
|
543
|
+
while (end > 0 && value[end - 1] === "/") end -= 1;
|
|
544
|
+
return value.slice(0, end);
|
|
545
|
+
}
|
|
546
|
+
function isCanonicalRepoUrl(value) {
|
|
547
|
+
return /^[a-z0-9+.-]+:\/\//.test(normalizeRepoUrl(value));
|
|
548
|
+
}
|
|
549
|
+
function repoPath(normalized) {
|
|
550
|
+
const withoutScheme = normalized.replace(/^[a-z0-9+.-]+:\/\//, "");
|
|
551
|
+
const segments = withoutScheme.split("/").filter(Boolean);
|
|
552
|
+
return segments.length > 1 ? segments.slice(1).join("/") : "";
|
|
553
|
+
}
|
|
554
|
+
function repoIdentifies(declared, originUrl) {
|
|
555
|
+
if (!originUrl) return false;
|
|
556
|
+
const origin = normalizeRepoUrl(originUrl);
|
|
557
|
+
const want = normalizeRepoUrl(declared);
|
|
558
|
+
if (!want || !origin) return false;
|
|
559
|
+
if (want === origin) return true;
|
|
560
|
+
const path = repoPath(origin);
|
|
561
|
+
if (!path) return false;
|
|
562
|
+
return want === path || want === (path.split("/").pop() ?? "");
|
|
563
|
+
}
|
|
564
|
+
async function repoOriginUrl(repoRoot) {
|
|
565
|
+
try {
|
|
566
|
+
const { stdout } = await execFileAsync(
|
|
567
|
+
"git",
|
|
568
|
+
["-C", repoRoot, "config", "--get", "remote.origin.url"],
|
|
569
|
+
{ timeout: 5e3 }
|
|
570
|
+
);
|
|
571
|
+
return stdout.trim() || null;
|
|
572
|
+
} catch {
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
var LazyOrigin = class {
|
|
577
|
+
constructor(repoRoot) {
|
|
578
|
+
this.repoRoot = repoRoot;
|
|
579
|
+
}
|
|
580
|
+
repoRoot;
|
|
581
|
+
url = null;
|
|
582
|
+
asked = false;
|
|
583
|
+
/** Asks git once, so later `isForeign` calls need no await. */
|
|
584
|
+
async prime() {
|
|
585
|
+
if (this.asked) return;
|
|
586
|
+
this.url = await repoOriginUrl(this.repoRoot);
|
|
587
|
+
this.asked = true;
|
|
588
|
+
}
|
|
589
|
+
/** Only meaningful after `prime`; an unprimed origin identifies nothing. */
|
|
590
|
+
isForeign(anchor) {
|
|
591
|
+
if (!anchor.repo) return false;
|
|
592
|
+
return !repoIdentifies(anchor.repo, this.url);
|
|
593
|
+
}
|
|
594
|
+
async foreign(anchor) {
|
|
595
|
+
if (!anchor.repo) return false;
|
|
596
|
+
await this.prime();
|
|
597
|
+
return this.isForeign(anchor);
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
// src/remote-repo/cache.ts
|
|
602
|
+
function repoCacheDir(override) {
|
|
603
|
+
return override ?? process.env["STRAUSS_KB_REPO_CACHE"] ?? (0, import_node_path.join)((0, import_node_os.homedir)(), ".strauss", "repo-cache");
|
|
604
|
+
}
|
|
605
|
+
var DEFAULT_FETCH_TIMEOUT_MS = 3e4;
|
|
606
|
+
function fetchTimeoutMs(override) {
|
|
607
|
+
if (override !== void 0) return override;
|
|
608
|
+
const fromEnv = Number(process.env["STRAUSS_KB_FETCH_TIMEOUT_MS"]);
|
|
609
|
+
return Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv : DEFAULT_FETCH_TIMEOUT_MS;
|
|
610
|
+
}
|
|
611
|
+
function cachePathFor(repo, cacheDir) {
|
|
612
|
+
const normalized = normalizeRepoUrl(repo);
|
|
613
|
+
const scheme = /^[a-z0-9+.-]+:\/\//.exec(normalized);
|
|
614
|
+
if (!scheme) return null;
|
|
615
|
+
const segments = normalized.slice(scheme[0].length).split("/").filter(Boolean).map((segment) => safeSegment(segment));
|
|
616
|
+
if (segments.length < 2 || segments.some((segment) => segment === null)) {
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
const path = segments;
|
|
620
|
+
return (0, import_node_path.join)(cacheDir, ...path.slice(0, -1), `${path[path.length - 1]}.git`);
|
|
621
|
+
}
|
|
622
|
+
function safeSegment(value) {
|
|
623
|
+
return value === "." || value === ".." || value.includes("\0") ? null : value.replace(/[/\\:]/g, "-");
|
|
624
|
+
}
|
|
625
|
+
function revRef(rev) {
|
|
626
|
+
const safe = rev.replace(/[^A-Za-z0-9_-]/g, "-").slice(0, 64);
|
|
627
|
+
let hash = 5381;
|
|
628
|
+
for (let at = 0; at < rev.length; at++) {
|
|
629
|
+
hash = (hash * 33 ^ rev.charCodeAt(at)) >>> 0;
|
|
630
|
+
}
|
|
631
|
+
return `refs/strauss/${safe}-${hash.toString(16)}`;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// src/remote-repo/model.ts
|
|
635
|
+
var UNCHECKED_REASONS = [
|
|
636
|
+
"remote-unreachable",
|
|
637
|
+
"repo-unauthorized",
|
|
638
|
+
"default-branch-unknown"
|
|
639
|
+
];
|
|
640
|
+
function isUncheckedReason(reason) {
|
|
641
|
+
return reason !== void 0 && UNCHECKED_REASONS.includes(reason);
|
|
642
|
+
}
|
|
643
|
+
function wantKey(repo, ref, file) {
|
|
644
|
+
return `${repo}\0${ref ?? ""}\0${file}`;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// src/remote-repo/read.ts
|
|
648
|
+
var import_promises = require("fs/promises");
|
|
649
|
+
|
|
650
|
+
// src/remote-repo/git.ts
|
|
651
|
+
var import_node_child_process2 = require("child_process");
|
|
652
|
+
var import_node_util2 = require("util");
|
|
653
|
+
|
|
654
|
+
// src/anchor-resolver/model.ts
|
|
655
|
+
var MAX_ANCHOR_FILE_BYTES = 1048576;
|
|
656
|
+
|
|
657
|
+
// src/remote-repo/git.ts
|
|
658
|
+
var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
|
|
659
|
+
function childEnv() {
|
|
660
|
+
const env = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
|
|
661
|
+
for (const name of ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE"]) {
|
|
662
|
+
delete env[name];
|
|
663
|
+
}
|
|
664
|
+
return env;
|
|
665
|
+
}
|
|
666
|
+
async function git(args, options = {}) {
|
|
667
|
+
try {
|
|
668
|
+
const { stdout, stderr } = await execFileAsync2("git", args, {
|
|
669
|
+
...options.cwd ? { cwd: options.cwd } : {},
|
|
670
|
+
timeout: options.timeoutMs ?? 3e4,
|
|
671
|
+
maxBuffer: options.maxBytes ?? MAX_ANCHOR_FILE_BYTES,
|
|
672
|
+
encoding: "utf8",
|
|
673
|
+
windowsHide: true,
|
|
674
|
+
env: childEnv()
|
|
675
|
+
});
|
|
676
|
+
return { ok: true, stdout, stderr, overflowed: false };
|
|
677
|
+
} catch (error) {
|
|
678
|
+
const failure = error;
|
|
679
|
+
return {
|
|
680
|
+
ok: false,
|
|
681
|
+
stdout: failure.stdout ?? "",
|
|
682
|
+
stderr: failure.stderr ?? "",
|
|
683
|
+
overflowed: failure.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER"
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
function transportReason(stderr) {
|
|
688
|
+
const text = stderr.toLowerCase();
|
|
689
|
+
if (text.includes("authentication failed") || text.includes("permission denied") || text.includes("could not read username") || text.includes("403 forbidden") || text.includes("access denied")) {
|
|
690
|
+
return "repo-unauthorized";
|
|
691
|
+
}
|
|
692
|
+
if (text.includes("couldn't find remote ref") || text.includes("unadvertised object") || text.includes("not our ref")) {
|
|
693
|
+
return "ref-not-found";
|
|
694
|
+
}
|
|
695
|
+
return "remote-unreachable";
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// src/remote-repo/validate.ts
|
|
699
|
+
var MAX_REF_LENGTH = 200;
|
|
700
|
+
var REF_SHAPE = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
701
|
+
function refShapeIsSafe(ref) {
|
|
702
|
+
if (!ref || ref.length > MAX_REF_LENGTH) return false;
|
|
703
|
+
if (ref.includes("..")) return false;
|
|
704
|
+
return REF_SHAPE.test(ref);
|
|
705
|
+
}
|
|
706
|
+
async function refIsWellFormed(ref) {
|
|
707
|
+
if (!refShapeIsSafe(ref)) return false;
|
|
708
|
+
const checked = await git(["check-ref-format", "--allow-onelevel", ref]);
|
|
709
|
+
return checked.ok;
|
|
710
|
+
}
|
|
711
|
+
function filePathIsSafe(file) {
|
|
712
|
+
const path = file.replace(/^\.\//, "");
|
|
713
|
+
if (!path || path.startsWith("-") || path.includes("\0")) return false;
|
|
714
|
+
return !path.split("/").includes("..");
|
|
715
|
+
}
|
|
716
|
+
var DEFAULT_PROTOCOLS = ["https", "ssh", "git"];
|
|
717
|
+
function allowedProtocols() {
|
|
718
|
+
const raw = process.env["STRAUSS_KB_REPO_PROTOCOLS"];
|
|
719
|
+
if (raw === void 0) return [...DEFAULT_PROTOCOLS];
|
|
720
|
+
const listed = raw.split(",").map((entry) => entry.trim().toLowerCase()).filter(Boolean);
|
|
721
|
+
return listed.length ? listed : [...DEFAULT_PROTOCOLS];
|
|
722
|
+
}
|
|
723
|
+
function isShortRepoName(repo) {
|
|
724
|
+
return /^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(repo.trim());
|
|
725
|
+
}
|
|
726
|
+
var SCP_LIKE = /^[\w.-]+@[\w.-]+:(?!\/)\S+$/;
|
|
727
|
+
var URL_SCHEME = /^([A-Za-z0-9+.-]+):\/\//;
|
|
728
|
+
var CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
|
|
729
|
+
function repoUrlIsSafe(repo) {
|
|
730
|
+
const url = repo.trim();
|
|
731
|
+
if (!url || url.startsWith("-") || CONTROL_CHARS.test(url)) return false;
|
|
732
|
+
const allowed = allowedProtocols();
|
|
733
|
+
if (SCP_LIKE.test(url)) return allowed.includes("ssh");
|
|
734
|
+
const scheme = URL_SCHEME.exec(url);
|
|
735
|
+
if (!scheme?.[1]) return false;
|
|
736
|
+
if (!allowed.includes(scheme[1].toLowerCase())) return false;
|
|
737
|
+
const authority = url.slice(scheme[0].length).split("/")[0] ?? "";
|
|
738
|
+
const at = authority.lastIndexOf("@");
|
|
739
|
+
return at < 0 || !authority.slice(0, at).includes(":");
|
|
740
|
+
}
|
|
741
|
+
function protocolArgs() {
|
|
742
|
+
const allowed = allowedProtocols();
|
|
743
|
+
return [
|
|
744
|
+
"-c",
|
|
745
|
+
"protocol.ext.allow=never",
|
|
746
|
+
"-c",
|
|
747
|
+
`protocol.file.allow=${allowed.includes("file") ? "user" : "never"}`
|
|
748
|
+
];
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// src/remote-repo/read.ts
|
|
752
|
+
var DEFAULT_REPO_CONCURRENCY = 4;
|
|
753
|
+
var IMMUTABLE_REV = /^[0-9a-f]{40}$/;
|
|
754
|
+
async function readRemoteAnchors(wants, options = {}) {
|
|
755
|
+
const out = /* @__PURE__ */ new Map();
|
|
756
|
+
if (!wants.length) return out;
|
|
757
|
+
const cacheDir = repoCacheDir(options.cacheDir);
|
|
758
|
+
const timeoutMs = fetchTimeoutMs(options.fetchTimeoutMs);
|
|
759
|
+
const byRepo = /* @__PURE__ */ new Map();
|
|
760
|
+
for (const want of wants) {
|
|
761
|
+
const key2 = normalizeRepoUrl(want.repo);
|
|
762
|
+
const group2 = byRepo.get(key2) ?? { url: want.repo.trim(), wants: [] };
|
|
763
|
+
group2.wants.push(want);
|
|
764
|
+
byRepo.set(key2, group2);
|
|
765
|
+
}
|
|
766
|
+
const groups = [...byRepo.entries()];
|
|
767
|
+
const results = await mapLimit(
|
|
768
|
+
groups,
|
|
769
|
+
Math.max(1, options.concurrency ?? DEFAULT_REPO_CONCURRENCY),
|
|
770
|
+
([repo, group2]) => readOneRepo(repo, group2.url, group2.wants, {
|
|
771
|
+
cacheDir,
|
|
772
|
+
timeoutMs,
|
|
773
|
+
offline: options.offline === true
|
|
774
|
+
})
|
|
775
|
+
);
|
|
776
|
+
for (const result of results) {
|
|
777
|
+
for (const [key2, read] of result) out.set(key2, read);
|
|
778
|
+
}
|
|
779
|
+
return out;
|
|
780
|
+
}
|
|
781
|
+
async function readOneRepo(repo, url, declared, context) {
|
|
782
|
+
let wants = declared;
|
|
783
|
+
const all = (read) => new Map(wants.map((want) => [wantKey(repo, want.ref, want.file), read]));
|
|
784
|
+
if (isShortRepoName(url)) {
|
|
785
|
+
return all({ ok: false, reason: "remote-unreachable" });
|
|
786
|
+
}
|
|
787
|
+
if (!repoUrlIsSafe(url)) return all({ ok: false, reason: "repo-invalid" });
|
|
788
|
+
const cache = cachePathFor(repo, context.cacheDir);
|
|
789
|
+
if (!cache) return all({ ok: false, reason: "remote-unreachable" });
|
|
790
|
+
const rejected = /* @__PURE__ */ new Map();
|
|
791
|
+
const usable = [];
|
|
792
|
+
for (const want of wants) {
|
|
793
|
+
const reason = wantReason(want);
|
|
794
|
+
if (reason)
|
|
795
|
+
rejected.set(wantKey(repo, want.ref, want.file), { ok: false, reason });
|
|
796
|
+
else usable.push(want);
|
|
797
|
+
}
|
|
798
|
+
if (!usable.length) return rejected;
|
|
799
|
+
wants = usable;
|
|
800
|
+
const opened = await openCache(cache, url, context);
|
|
801
|
+
if (opened) return new Map([...rejected, ...all(opened)]);
|
|
802
|
+
const wantsDefault = wants.some((want) => want.ref === void 0);
|
|
803
|
+
const branch = wantsDefault ? await defaultBranch(cache, context) : {};
|
|
804
|
+
const revs = /* @__PURE__ */ new Map();
|
|
805
|
+
for (const rev of distinctRevs(wants, branch.name)) {
|
|
806
|
+
revs.set(
|
|
807
|
+
rev,
|
|
808
|
+
await refIsWellFormed(rev) ? await ensureRev(cache, rev, context) : { ok: false, reason: "ref-invalid" }
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
const reads = await mapLimit(
|
|
812
|
+
wants,
|
|
813
|
+
DEFAULT_IO_CONCURRENCY,
|
|
814
|
+
async (want) => {
|
|
815
|
+
const rev = want.ref ?? branch.name;
|
|
816
|
+
if (rev === void 0) {
|
|
817
|
+
return {
|
|
818
|
+
ok: false,
|
|
819
|
+
reason: branch.reason ?? "default-branch-unknown"
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
const failed = revs.get(rev);
|
|
823
|
+
if (failed) return failed;
|
|
824
|
+
return readBlob(cache, rev, want.file, context);
|
|
825
|
+
}
|
|
826
|
+
);
|
|
827
|
+
return new Map([
|
|
828
|
+
...rejected,
|
|
829
|
+
...wants.map(
|
|
830
|
+
(want, at) => [wantKey(repo, want.ref, want.file), reads[at]]
|
|
831
|
+
)
|
|
832
|
+
]);
|
|
833
|
+
}
|
|
834
|
+
function wantReason(want) {
|
|
835
|
+
if (want.ref !== void 0 && !refShapeIsSafe(want.ref)) return "ref-invalid";
|
|
836
|
+
return filePathIsSafe(want.file) ? void 0 : "outside-repo";
|
|
837
|
+
}
|
|
838
|
+
function distinctRevs(wants, branch) {
|
|
839
|
+
const revs = /* @__PURE__ */ new Set();
|
|
840
|
+
for (const want of wants) {
|
|
841
|
+
if (want.ref !== void 0) revs.add(want.ref);
|
|
842
|
+
else if (branch) revs.add(branch);
|
|
843
|
+
}
|
|
844
|
+
return [...revs];
|
|
845
|
+
}
|
|
846
|
+
async function openCache(cache, url, context) {
|
|
847
|
+
try {
|
|
848
|
+
await (0, import_promises.mkdir)(cache, { recursive: true });
|
|
849
|
+
} catch {
|
|
850
|
+
return { ok: false, reason: "remote-unreachable" };
|
|
851
|
+
}
|
|
852
|
+
const init = await git(["init", "--bare", "--quiet", cache], {
|
|
853
|
+
timeoutMs: context.timeoutMs
|
|
854
|
+
});
|
|
855
|
+
if (!init.ok) return { ok: false, reason: "remote-unreachable" };
|
|
856
|
+
const remote = await git(["config", "remote.origin.url", url], {
|
|
857
|
+
cwd: cache,
|
|
858
|
+
timeoutMs: context.timeoutMs
|
|
859
|
+
});
|
|
860
|
+
return remote.ok ? void 0 : { ok: false, reason: "remote-unreachable" };
|
|
861
|
+
}
|
|
862
|
+
async function defaultBranch(cache, context) {
|
|
863
|
+
if (!context.offline) {
|
|
864
|
+
const listed = await git(
|
|
865
|
+
[...protocolArgs(), "ls-remote", "--symref", "origin", "HEAD"],
|
|
866
|
+
{
|
|
867
|
+
cwd: cache,
|
|
868
|
+
timeoutMs: context.timeoutMs
|
|
869
|
+
}
|
|
870
|
+
);
|
|
871
|
+
const found = /^ref:\s+refs\/heads\/(\S+)\s+HEAD$/m.exec(listed.stdout);
|
|
872
|
+
if (listed.ok && found?.[1]) {
|
|
873
|
+
const name = found[1];
|
|
874
|
+
await git(["config", "strauss.defaultBranch", name], { cwd: cache });
|
|
875
|
+
return { name };
|
|
876
|
+
}
|
|
877
|
+
if (!listed.ok) {
|
|
878
|
+
const reason = transportReason(listed.stderr);
|
|
879
|
+
if (reason !== "ref-not-found") {
|
|
880
|
+
const cached2 = await cachedBranch(cache);
|
|
881
|
+
return cached2 ? { name: cached2 } : { reason };
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
const cached = await cachedBranch(cache);
|
|
886
|
+
if (cached) return { name: cached };
|
|
887
|
+
return {
|
|
888
|
+
reason: context.offline ? "remote-unreachable" : "default-branch-unknown"
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
async function cachedBranch(cache) {
|
|
892
|
+
const stored = await git(["config", "--get", "strauss.defaultBranch"], {
|
|
893
|
+
cwd: cache
|
|
894
|
+
});
|
|
895
|
+
if (stored.ok && stored.stdout.trim()) return stored.stdout.trim();
|
|
896
|
+
const head = await git(
|
|
897
|
+
["symbolic-ref", "--short", "refs/remotes/origin/HEAD"],
|
|
898
|
+
{
|
|
899
|
+
cwd: cache
|
|
900
|
+
}
|
|
901
|
+
);
|
|
902
|
+
const name = head.stdout.trim().replace(/^origin\//, "");
|
|
903
|
+
return head.ok && name ? name : void 0;
|
|
904
|
+
}
|
|
905
|
+
async function ensureRev(cache, rev, context) {
|
|
906
|
+
const ref = revRef(rev);
|
|
907
|
+
const have = await git(
|
|
908
|
+
["rev-parse", "--verify", "--quiet", `${ref}^{commit}`],
|
|
909
|
+
{
|
|
910
|
+
cwd: cache
|
|
911
|
+
}
|
|
912
|
+
);
|
|
913
|
+
const cached = have.ok && have.stdout.trim().length > 0;
|
|
914
|
+
if (cached && (context.offline || IMMUTABLE_REV.test(rev))) return void 0;
|
|
915
|
+
if (context.offline) return { ok: false, reason: "remote-unreachable" };
|
|
916
|
+
const fetched = await git(
|
|
917
|
+
[
|
|
918
|
+
...protocolArgs(),
|
|
919
|
+
"fetch",
|
|
920
|
+
"--depth",
|
|
921
|
+
"1",
|
|
922
|
+
"origin",
|
|
923
|
+
"--end-of-options",
|
|
924
|
+
rev
|
|
925
|
+
],
|
|
926
|
+
{ cwd: cache, timeoutMs: context.timeoutMs }
|
|
927
|
+
);
|
|
928
|
+
if (!fetched.ok) {
|
|
929
|
+
const reason = transportReason(fetched.stderr);
|
|
930
|
+
if (cached && reason !== "ref-not-found") return void 0;
|
|
931
|
+
return { ok: false, reason };
|
|
932
|
+
}
|
|
933
|
+
const head = await git(["rev-parse", "FETCH_HEAD"], { cwd: cache });
|
|
934
|
+
const sha = head.stdout.trim();
|
|
935
|
+
if (!head.ok || !sha) return { ok: false, reason: "remote-unreachable" };
|
|
936
|
+
const updated = await git(["update-ref", ref, sha], { cwd: cache });
|
|
937
|
+
return updated.ok ? void 0 : { ok: false, reason: "remote-unreachable" };
|
|
938
|
+
}
|
|
939
|
+
async function readBlob(cache, rev, file, context) {
|
|
940
|
+
const path = file.replace(/^\.\//, "");
|
|
941
|
+
const blob = await git(
|
|
942
|
+
["cat-file", "blob", "--end-of-options", `${revRef(rev)}:${path}`],
|
|
943
|
+
{
|
|
944
|
+
cwd: cache,
|
|
945
|
+
timeoutMs: context.timeoutMs
|
|
946
|
+
}
|
|
947
|
+
);
|
|
948
|
+
if (blob.ok) return { ok: true, source: blob.stdout };
|
|
949
|
+
if (blob.overflowed) return { ok: false, reason: "file-too-large" };
|
|
950
|
+
const text = blob.stderr.toLowerCase();
|
|
951
|
+
return text.includes("does not exist") || text.includes("not a valid object") ? { ok: false, reason: "file-missing" } : { ok: false, reason: "file-unreadable" };
|
|
952
|
+
}
|
|
953
|
+
|
|
520
954
|
// src/adjudicate.ts
|
|
521
955
|
var STANDING = {
|
|
522
956
|
accepted: "current",
|
|
@@ -557,23 +991,34 @@ function adjudicate(hits, bundle, now = /* @__PURE__ */ new Date(), anchorDrift)
|
|
|
557
991
|
if (!record.frontmatter.verified?.length) {
|
|
558
992
|
warnings.push({ kind: "unverified" });
|
|
559
993
|
}
|
|
560
|
-
const
|
|
561
|
-
(entry) => entry.state !== "match"
|
|
994
|
+
const found = (anchorDrift?.get(record.conceptId) ?? []).filter(
|
|
995
|
+
(entry) => entry.state !== "match"
|
|
562
996
|
);
|
|
997
|
+
const unchecked2 = found.filter((entry) => isUncheckedReason(entry.reason));
|
|
998
|
+
const moved = found.filter((entry) => !isUncheckedReason(entry.reason));
|
|
563
999
|
if (moved.length) {
|
|
1000
|
+
warnings.push({ kind: "drifted", anchors: moved.map(warningAnchor) });
|
|
1001
|
+
}
|
|
1002
|
+
if (unchecked2.length) {
|
|
564
1003
|
warnings.push({
|
|
565
|
-
kind: "
|
|
566
|
-
anchors:
|
|
567
|
-
file,
|
|
568
|
-
...symbol !== void 0 ? { symbol } : {},
|
|
569
|
-
diffSize,
|
|
570
|
-
...reason !== void 0 ? { reason } : {}
|
|
571
|
-
}))
|
|
1004
|
+
kind: "unchecked",
|
|
1005
|
+
anchors: unchecked2.map(warningAnchor)
|
|
572
1006
|
});
|
|
573
1007
|
}
|
|
574
1008
|
return { record, standing: STANDING[status], heads, warnings };
|
|
575
1009
|
});
|
|
576
1010
|
}
|
|
1011
|
+
function warningAnchor(entry) {
|
|
1012
|
+
const { file, symbol, diffSize, reason, repo, remoteState } = entry;
|
|
1013
|
+
return {
|
|
1014
|
+
file,
|
|
1015
|
+
...symbol !== void 0 ? { symbol } : {},
|
|
1016
|
+
diffSize,
|
|
1017
|
+
...reason !== void 0 ? { reason } : {},
|
|
1018
|
+
...repo !== void 0 ? { repo } : {},
|
|
1019
|
+
...remoteState !== void 0 ? { remoteState } : {}
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
577
1022
|
function resolveHeads(from, byId) {
|
|
578
1023
|
const warnings = [];
|
|
579
1024
|
const heads = /* @__PURE__ */ new Map();
|
|
@@ -623,14 +1068,98 @@ function successors(record, byId) {
|
|
|
623
1068
|
return { records, missing };
|
|
624
1069
|
}
|
|
625
1070
|
|
|
626
|
-
// src/anchor-resolver.ts
|
|
627
|
-
var
|
|
1071
|
+
// src/anchor-resolver/read.ts
|
|
1072
|
+
var import_promises2 = require("fs/promises");
|
|
1073
|
+
var import_node_path2 = require("path");
|
|
1074
|
+
function anchorFilePath(repoRoot, file) {
|
|
1075
|
+
const path = (0, import_node_path2.resolve)(repoRoot, file.replace(/^\.\//, ""));
|
|
1076
|
+
const rel = (0, import_node_path2.relative)((0, import_node_path2.resolve)(repoRoot), path);
|
|
1077
|
+
if (rel === "" || rel === ".." || rel.startsWith(`..${import_node_path2.sep}`) || (0, import_node_path2.isAbsolute)(rel)) {
|
|
1078
|
+
return null;
|
|
1079
|
+
}
|
|
1080
|
+
return path;
|
|
1081
|
+
}
|
|
1082
|
+
function contains(root, path) {
|
|
1083
|
+
const rel = (0, import_node_path2.relative)(root, path);
|
|
1084
|
+
return rel !== "" && rel !== ".." && !rel.startsWith(`..${import_node_path2.sep}`) && !(0, import_node_path2.isAbsolute)(rel);
|
|
1085
|
+
}
|
|
1086
|
+
function errorCode(error) {
|
|
1087
|
+
return typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0;
|
|
1088
|
+
}
|
|
1089
|
+
function anchorFileReader(repoRoot) {
|
|
1090
|
+
let rootOnce;
|
|
1091
|
+
const realRoot = () => {
|
|
1092
|
+
rootOnce ??= (0, import_promises2.realpath)((0, import_node_path2.resolve)(repoRoot)).catch((error) => {
|
|
1093
|
+
rootOnce = void 0;
|
|
1094
|
+
throw error;
|
|
1095
|
+
});
|
|
1096
|
+
return rootOnce;
|
|
1097
|
+
};
|
|
1098
|
+
return (file) => readAnchorFileWithRoot(repoRoot, file, realRoot);
|
|
1099
|
+
}
|
|
1100
|
+
async function readAnchorFileWithRoot(repoRoot, file, realRoot) {
|
|
1101
|
+
const lexical = anchorFilePath(repoRoot, file);
|
|
1102
|
+
if (lexical === null) return { ok: false, reason: "outside-repo" };
|
|
1103
|
+
let root;
|
|
1104
|
+
let path;
|
|
1105
|
+
try {
|
|
1106
|
+
root = await realRoot();
|
|
1107
|
+
path = await (0, import_promises2.realpath)(lexical);
|
|
1108
|
+
} catch (error) {
|
|
1109
|
+
const code = errorCode(error);
|
|
1110
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
1111
|
+
return { ok: false, reason: "file-missing" };
|
|
1112
|
+
}
|
|
1113
|
+
return { ok: false, reason: "file-unreadable" };
|
|
1114
|
+
}
|
|
1115
|
+
if (!contains(root, path)) return { ok: false, reason: "outside-repo" };
|
|
1116
|
+
try {
|
|
1117
|
+
const stats = await (0, import_promises2.stat)(path);
|
|
1118
|
+
if (!stats.isFile()) return { ok: false, reason: "file-unreadable" };
|
|
1119
|
+
if (stats.size > MAX_ANCHOR_FILE_BYTES) {
|
|
1120
|
+
return { ok: false, reason: "file-too-large" };
|
|
1121
|
+
}
|
|
1122
|
+
return { ok: true, source: await (0, import_promises2.readFile)(path, "utf8") };
|
|
1123
|
+
} catch (error) {
|
|
1124
|
+
const code = errorCode(error);
|
|
1125
|
+
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
1126
|
+
return { ok: false, reason: "file-missing" };
|
|
1127
|
+
}
|
|
1128
|
+
return { ok: false, reason: "file-unreadable" };
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
function looksLikeWrongRepoRoot(drift) {
|
|
1132
|
+
let checked = 0;
|
|
1133
|
+
for (const entries of drift.values()) {
|
|
1134
|
+
for (const entry of entries) {
|
|
1135
|
+
if (entry.repo !== void 0) continue;
|
|
1136
|
+
checked += 1;
|
|
1137
|
+
if (entry.state !== "unresolved" || entry.reason !== "file-missing") {
|
|
1138
|
+
return false;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
return checked > 0;
|
|
1143
|
+
}
|
|
1144
|
+
async function readAnchorFiles(files, read, concurrency = DEFAULT_IO_CONCURRENCY) {
|
|
1145
|
+
if (!Number.isInteger(concurrency) || concurrency < 1) {
|
|
1146
|
+
throw new RangeError(
|
|
1147
|
+
`readAnchorFiles: option "concurrency" must be a positive integer, got ${concurrency}`
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
const wanted = [...new Set(files)];
|
|
1151
|
+
const results = await mapLimit(wanted, concurrency, async (file) => {
|
|
1152
|
+
try {
|
|
1153
|
+
return await read(file);
|
|
1154
|
+
} catch {
|
|
1155
|
+
return { ok: false, reason: "file-unreadable" };
|
|
1156
|
+
}
|
|
1157
|
+
});
|
|
1158
|
+
return new Map(wanted.map((file, at) => [file, results[at]]));
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
// src/anchor-resolver/resolver.ts
|
|
628
1162
|
var import_node_crypto = require("crypto");
|
|
629
|
-
var import_promises = require("fs/promises");
|
|
630
|
-
var import_node_path = require("path");
|
|
631
|
-
var import_node_util = require("util");
|
|
632
|
-
var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
633
|
-
var MAX_ANCHOR_FILE_BYTES = 1048576;
|
|
634
1163
|
var PARENT_SCOPE_LINES = 50;
|
|
635
1164
|
var CLEAN_STATE = { blockComment: false, template: false };
|
|
636
1165
|
function stripLine(line, state) {
|
|
@@ -806,157 +1335,8 @@ function resolveAnchor(source, anchor, resolver = regexResolver) {
|
|
|
806
1335
|
}
|
|
807
1336
|
return resolver.resolve(normalized, anchor.symbol);
|
|
808
1337
|
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
const rel = (0, import_node_path.relative)((0, import_node_path.resolve)(repoRoot), path);
|
|
812
|
-
if (rel === "" || rel === ".." || rel.startsWith(`..${import_node_path.sep}`) || (0, import_node_path.isAbsolute)(rel)) {
|
|
813
|
-
return null;
|
|
814
|
-
}
|
|
815
|
-
return path;
|
|
816
|
-
}
|
|
817
|
-
function contains(root, path) {
|
|
818
|
-
const rel = (0, import_node_path.relative)(root, path);
|
|
819
|
-
return rel !== "" && rel !== ".." && !rel.startsWith(`..${import_node_path.sep}`) && !(0, import_node_path.isAbsolute)(rel);
|
|
820
|
-
}
|
|
821
|
-
function normalizeRepoUrl(value) {
|
|
822
|
-
let url = value.trim().replace(/^git\+/, "");
|
|
823
|
-
const scp = /^[\w.-]+@([\w.-]+):(.+)$/.exec(url);
|
|
824
|
-
if (scp) url = `https://${scp[1]}/${scp[2]}`;
|
|
825
|
-
url = url.replace(/^ssh:\/\/(?:[^@/]+@)?/, "https://");
|
|
826
|
-
url = trimTrailingSlashes(url);
|
|
827
|
-
if (url.endsWith(".git")) url = url.slice(0, -4);
|
|
828
|
-
return trimTrailingSlashes(url).toLowerCase();
|
|
829
|
-
}
|
|
830
|
-
function trimTrailingSlashes(value) {
|
|
831
|
-
let end = value.length;
|
|
832
|
-
while (end > 0 && value[end - 1] === "/") end -= 1;
|
|
833
|
-
return value.slice(0, end);
|
|
834
|
-
}
|
|
835
|
-
function repoPath(normalized) {
|
|
836
|
-
const withoutScheme = normalized.replace(/^[a-z0-9+.-]+:\/\//, "");
|
|
837
|
-
const segments = withoutScheme.split("/").filter(Boolean);
|
|
838
|
-
return segments.length > 1 ? segments.slice(1).join("/") : "";
|
|
839
|
-
}
|
|
840
|
-
function repoIdentifies(declared, originUrl) {
|
|
841
|
-
if (!originUrl) return false;
|
|
842
|
-
const origin = normalizeRepoUrl(originUrl);
|
|
843
|
-
const want = normalizeRepoUrl(declared);
|
|
844
|
-
if (!want || !origin) return false;
|
|
845
|
-
if (want === origin) return true;
|
|
846
|
-
const path = repoPath(origin);
|
|
847
|
-
if (!path) return false;
|
|
848
|
-
return want === path || want === (path.split("/").pop() ?? "");
|
|
849
|
-
}
|
|
850
|
-
async function repoOriginUrl(repoRoot) {
|
|
851
|
-
try {
|
|
852
|
-
const { stdout } = await execFileAsync(
|
|
853
|
-
"git",
|
|
854
|
-
["-C", repoRoot, "config", "--get", "remote.origin.url"],
|
|
855
|
-
{ timeout: 5e3 }
|
|
856
|
-
);
|
|
857
|
-
return stdout.trim() || null;
|
|
858
|
-
} catch {
|
|
859
|
-
return null;
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
var LazyOrigin = class {
|
|
863
|
-
constructor(repoRoot) {
|
|
864
|
-
this.repoRoot = repoRoot;
|
|
865
|
-
}
|
|
866
|
-
repoRoot;
|
|
867
|
-
url = null;
|
|
868
|
-
asked = false;
|
|
869
|
-
/** Asks git once, so later `isForeign` calls need no await. */
|
|
870
|
-
async prime() {
|
|
871
|
-
if (this.asked) return;
|
|
872
|
-
this.url = await repoOriginUrl(this.repoRoot);
|
|
873
|
-
this.asked = true;
|
|
874
|
-
}
|
|
875
|
-
/** Only meaningful after `prime`; an unprimed origin identifies nothing. */
|
|
876
|
-
isForeign(anchor) {
|
|
877
|
-
if (!anchor.repo) return false;
|
|
878
|
-
return !repoIdentifies(anchor.repo, this.url);
|
|
879
|
-
}
|
|
880
|
-
async foreign(anchor) {
|
|
881
|
-
if (!anchor.repo) return false;
|
|
882
|
-
await this.prime();
|
|
883
|
-
return this.isForeign(anchor);
|
|
884
|
-
}
|
|
885
|
-
};
|
|
886
|
-
function errorCode(error) {
|
|
887
|
-
return typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0;
|
|
888
|
-
}
|
|
889
|
-
function anchorFileReader(repoRoot) {
|
|
890
|
-
let rootOnce;
|
|
891
|
-
const realRoot = () => {
|
|
892
|
-
rootOnce ??= (0, import_promises.realpath)((0, import_node_path.resolve)(repoRoot)).catch((error) => {
|
|
893
|
-
rootOnce = void 0;
|
|
894
|
-
throw error;
|
|
895
|
-
});
|
|
896
|
-
return rootOnce;
|
|
897
|
-
};
|
|
898
|
-
return (file) => readAnchorFileWithRoot(repoRoot, file, realRoot);
|
|
899
|
-
}
|
|
900
|
-
async function readAnchorFileWithRoot(repoRoot, file, realRoot) {
|
|
901
|
-
const lexical = anchorFilePath(repoRoot, file);
|
|
902
|
-
if (lexical === null) return { ok: false, reason: "outside-repo" };
|
|
903
|
-
let root;
|
|
904
|
-
let path;
|
|
905
|
-
try {
|
|
906
|
-
root = await realRoot();
|
|
907
|
-
path = await (0, import_promises.realpath)(lexical);
|
|
908
|
-
} catch (error) {
|
|
909
|
-
const code = errorCode(error);
|
|
910
|
-
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
911
|
-
return { ok: false, reason: "file-missing" };
|
|
912
|
-
}
|
|
913
|
-
return { ok: false, reason: "file-unreadable" };
|
|
914
|
-
}
|
|
915
|
-
if (!contains(root, path)) return { ok: false, reason: "outside-repo" };
|
|
916
|
-
try {
|
|
917
|
-
const stats = await (0, import_promises.stat)(path);
|
|
918
|
-
if (!stats.isFile()) return { ok: false, reason: "file-unreadable" };
|
|
919
|
-
if (stats.size > MAX_ANCHOR_FILE_BYTES) {
|
|
920
|
-
return { ok: false, reason: "file-too-large" };
|
|
921
|
-
}
|
|
922
|
-
return { ok: true, source: await (0, import_promises.readFile)(path, "utf8") };
|
|
923
|
-
} catch (error) {
|
|
924
|
-
const code = errorCode(error);
|
|
925
|
-
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
926
|
-
return { ok: false, reason: "file-missing" };
|
|
927
|
-
}
|
|
928
|
-
return { ok: false, reason: "file-unreadable" };
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
function looksLikeWrongRepoRoot(drift) {
|
|
932
|
-
let checked = 0;
|
|
933
|
-
for (const entries of drift.values()) {
|
|
934
|
-
for (const entry of entries) {
|
|
935
|
-
if (entry.reason === "foreign-repo") continue;
|
|
936
|
-
checked += 1;
|
|
937
|
-
if (entry.state !== "unresolved" || entry.reason !== "file-missing") {
|
|
938
|
-
return false;
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
return checked > 0;
|
|
943
|
-
}
|
|
944
|
-
async function readAnchorFiles(files, read, concurrency = DEFAULT_IO_CONCURRENCY) {
|
|
945
|
-
if (!Number.isInteger(concurrency) || concurrency < 1) {
|
|
946
|
-
throw new RangeError(
|
|
947
|
-
`readAnchorFiles: option "concurrency" must be a positive integer, got ${concurrency}`
|
|
948
|
-
);
|
|
949
|
-
}
|
|
950
|
-
const wanted = [...new Set(files)];
|
|
951
|
-
const results = await mapLimit(wanted, concurrency, async (file) => {
|
|
952
|
-
try {
|
|
953
|
-
return await read(file);
|
|
954
|
-
} catch {
|
|
955
|
-
return { ok: false, reason: "file-unreadable" };
|
|
956
|
-
}
|
|
957
|
-
});
|
|
958
|
-
return new Map(wanted.map((file, at) => [file, results[at]]));
|
|
959
|
-
}
|
|
1338
|
+
|
|
1339
|
+
// src/anchor-resolver/drift.ts
|
|
960
1340
|
async function detectAnchorDrift(records, options = {}) {
|
|
961
1341
|
const repoRoot = options.repoRoot ?? process.cwd();
|
|
962
1342
|
const resolver = options.resolver ?? regexResolver;
|
|
@@ -982,71 +1362,101 @@ async function detectAnchorDrift(records, options = {}) {
|
|
|
982
1362
|
}
|
|
983
1363
|
}
|
|
984
1364
|
const files = [];
|
|
1365
|
+
const wants = [];
|
|
985
1366
|
for (const entries of planned.values()) {
|
|
986
|
-
for (const
|
|
987
|
-
if (!
|
|
1367
|
+
for (const { anchor, foreign } of entries) {
|
|
1368
|
+
if (!foreign) files.push(anchor.file);
|
|
1369
|
+
else wants.push(...remoteWants(anchor));
|
|
988
1370
|
}
|
|
989
1371
|
}
|
|
990
|
-
const reads = await
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1372
|
+
const [reads, remote] = await Promise.all([
|
|
1373
|
+
readAnchorFiles(
|
|
1374
|
+
files,
|
|
1375
|
+
options.reader ?? anchorFileReader(repoRoot),
|
|
1376
|
+
options.concurrency ?? DEFAULT_IO_CONCURRENCY
|
|
1377
|
+
),
|
|
1378
|
+
(options.readRemote ?? readRemoteAnchors)(wants, options.remote ?? {})
|
|
1379
|
+
]);
|
|
995
1380
|
const drift = /* @__PURE__ */ new Map();
|
|
996
1381
|
for (const record of records) {
|
|
997
1382
|
const entries = [];
|
|
998
1383
|
for (const { anchor, foreign } of planned.get(record.conceptId) ?? []) {
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
storedHash: anchor.hash
|
|
1003
|
-
};
|
|
1004
|
-
if (foreign) {
|
|
1005
|
-
entries.push({
|
|
1006
|
-
...base,
|
|
1007
|
-
state: "unresolved",
|
|
1008
|
-
diffSize: null,
|
|
1009
|
-
reason: "foreign-repo"
|
|
1010
|
-
});
|
|
1011
|
-
continue;
|
|
1012
|
-
}
|
|
1013
|
-
const read = reads.get(anchor.file);
|
|
1014
|
-
if (!read.ok) {
|
|
1015
|
-
entries.push({
|
|
1016
|
-
...base,
|
|
1017
|
-
state: "unresolved",
|
|
1018
|
-
diffSize: null,
|
|
1019
|
-
reason: read.reason
|
|
1020
|
-
});
|
|
1021
|
-
continue;
|
|
1022
|
-
}
|
|
1023
|
-
const resolved = resolveAnchor(read.source, anchor, resolver);
|
|
1024
|
-
if (!resolved) {
|
|
1025
|
-
entries.push({
|
|
1026
|
-
...base,
|
|
1027
|
-
state: "unresolved",
|
|
1028
|
-
diffSize: null,
|
|
1029
|
-
reason: "symbol-not-found"
|
|
1030
|
-
});
|
|
1031
|
-
continue;
|
|
1032
|
-
}
|
|
1033
|
-
const currentHash = hashAnchorText(resolved.text);
|
|
1034
|
-
const currentLines = resolved.endLine - resolved.startLine + 1;
|
|
1035
|
-
entries.push({
|
|
1036
|
-
...base,
|
|
1037
|
-
state: currentHash === anchor.hash ? "match" : "drifted",
|
|
1038
|
-
currentHash,
|
|
1039
|
-
diffSize: anchor.lines === void 0 ? null : Math.abs(currentLines - anchor.lines)
|
|
1040
|
-
});
|
|
1384
|
+
entries.push(
|
|
1385
|
+
foreign ? remoteEntry(anchor, remote, resolver) : localEntry(anchor, reads.get(anchor.file), resolver)
|
|
1386
|
+
);
|
|
1041
1387
|
}
|
|
1042
1388
|
if (entries.length) drift.set(record.conceptId, entries);
|
|
1043
1389
|
}
|
|
1044
1390
|
return drift;
|
|
1045
1391
|
}
|
|
1392
|
+
function remoteWants(anchor) {
|
|
1393
|
+
const repo = anchor.repo;
|
|
1394
|
+
const wants = [{ repo, file: anchor.file }];
|
|
1395
|
+
if (anchor.ref) wants.unshift({ repo, ref: anchor.ref, file: anchor.file });
|
|
1396
|
+
return wants;
|
|
1397
|
+
}
|
|
1398
|
+
function base(anchor) {
|
|
1399
|
+
return {
|
|
1400
|
+
file: anchor.file,
|
|
1401
|
+
...anchor.symbol ? { symbol: anchor.symbol } : {},
|
|
1402
|
+
storedHash: anchor.hash
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
function unresolved(anchor, reason, repo) {
|
|
1406
|
+
return {
|
|
1407
|
+
...base(anchor),
|
|
1408
|
+
state: "unresolved",
|
|
1409
|
+
diffSize: null,
|
|
1410
|
+
...reason ? { reason } : {},
|
|
1411
|
+
...repo ? { repo } : {}
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
function hashIn(source, anchor, resolver) {
|
|
1415
|
+
const resolved = resolveAnchor(source, anchor, resolver);
|
|
1416
|
+
if (!resolved) return null;
|
|
1417
|
+
return {
|
|
1418
|
+
hash: hashAnchorText(resolved.text),
|
|
1419
|
+
lines: resolved.endLine - resolved.startLine + 1
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
function compared(anchor, current, extra = {}) {
|
|
1423
|
+
return {
|
|
1424
|
+
...base(anchor),
|
|
1425
|
+
state: current.hash === anchor.hash ? "match" : "drifted",
|
|
1426
|
+
currentHash: current.hash,
|
|
1427
|
+
diffSize: anchor.lines === void 0 ? null : Math.abs(current.lines - anchor.lines),
|
|
1428
|
+
...extra
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
function localEntry(anchor, read, resolver) {
|
|
1432
|
+
if (!read.ok) return unresolved(anchor, read.reason);
|
|
1433
|
+
const current = hashIn(read.source, anchor, resolver);
|
|
1434
|
+
return current ? compared(anchor, current) : unresolved(anchor, "symbol-not-found");
|
|
1435
|
+
}
|
|
1436
|
+
function remoteEntry(anchor, remote, resolver) {
|
|
1437
|
+
const repo = anchor.repo;
|
|
1438
|
+
const key2 = normalizeRepoUrl(repo);
|
|
1439
|
+
const atDefault = remote.get(wantKey(key2, void 0, anchor.file));
|
|
1440
|
+
const primary = anchor.ref ? remote.get(wantKey(key2, anchor.ref, anchor.file)) : atDefault;
|
|
1441
|
+
if (!primary) return unresolved(anchor, "remote-unreachable", repo);
|
|
1442
|
+
if (!primary.ok) return unresolved(anchor, primary.reason, repo);
|
|
1443
|
+
const current = hashIn(primary.source, anchor, resolver);
|
|
1444
|
+
if (!current) return unresolved(anchor, "symbol-not-found", repo);
|
|
1445
|
+
if (!anchor.ref) return compared(anchor, current, { repo });
|
|
1446
|
+
if (current.hash !== anchor.hash) {
|
|
1447
|
+
return compared(anchor, current, { repo, remoteState: "drifted-from-ref" });
|
|
1448
|
+
}
|
|
1449
|
+
const head = atDefault?.ok ? hashIn(atDefault.source, anchor, resolver) : null;
|
|
1450
|
+
return head && head.hash !== anchor.hash ? {
|
|
1451
|
+
...compared(anchor, head, { repo }),
|
|
1452
|
+
state: "drifted",
|
|
1453
|
+
remoteState: "drifted-on-default"
|
|
1454
|
+
} : compared(anchor, current, { repo, remoteState: "matches-ref" });
|
|
1455
|
+
}
|
|
1046
1456
|
|
|
1047
1457
|
// src/search-index.ts
|
|
1048
|
-
var
|
|
1049
|
-
var
|
|
1458
|
+
var import_promises3 = require("fs/promises");
|
|
1459
|
+
var import_node_path3 = require("path");
|
|
1050
1460
|
|
|
1051
1461
|
// src/kb-log.ts
|
|
1052
1462
|
var import_zod2 = require("zod");
|
|
@@ -1109,7 +1519,7 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
1109
1519
|
let store = null;
|
|
1110
1520
|
try {
|
|
1111
1521
|
store = await qmd.createStore({
|
|
1112
|
-
dbPath: (0,
|
|
1522
|
+
dbPath: (0, import_node_path3.join)(bundlePath2, SEARCH_INDEX_FILE),
|
|
1113
1523
|
config: {
|
|
1114
1524
|
collections: {
|
|
1115
1525
|
[COLLECTION]: {
|
|
@@ -1144,7 +1554,7 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
1144
1554
|
}
|
|
1145
1555
|
}
|
|
1146
1556
|
async function isStale(bundlePath2) {
|
|
1147
|
-
const indexAt = await (0,
|
|
1557
|
+
const indexAt = await (0, import_promises3.stat)((0, import_node_path3.join)(bundlePath2, SEARCH_INDEX_FILE)).then((s) => s.mtimeMs).catch(() => 0);
|
|
1148
1558
|
if (!indexAt) return true;
|
|
1149
1559
|
const { readdir: readdir2 } = await import("fs/promises");
|
|
1150
1560
|
const names = (await readdir2(bundlePath2).catch(() => [])).filter(
|
|
@@ -1153,7 +1563,7 @@ async function isStale(bundlePath2) {
|
|
|
1153
1563
|
let stale = false;
|
|
1154
1564
|
await mapLimit(names, DEFAULT_IO_CONCURRENCY, async (name) => {
|
|
1155
1565
|
if (stale) return;
|
|
1156
|
-
const at = await (0,
|
|
1566
|
+
const at = await (0, import_promises3.stat)((0, import_node_path3.join)(bundlePath2, name)).then((s) => s.mtimeMs).catch(() => 0);
|
|
1157
1567
|
if (at > indexAt) stale = true;
|
|
1158
1568
|
});
|
|
1159
1569
|
return stale;
|
|
@@ -1743,7 +2153,7 @@ function appendUnionMergeLine(contents) {
|
|
|
1743
2153
|
}
|
|
1744
2154
|
|
|
1745
2155
|
// src/kb-store.ts
|
|
1746
|
-
var KB_DIR = (0,
|
|
2156
|
+
var KB_DIR = (0, import_node_path4.join)(".strauss", "kb");
|
|
1747
2157
|
var STORE_OWNED = /* @__PURE__ */ new Set([INDEX_FILE, LOG_FILE, SEARCH_INDEX_FILE]);
|
|
1748
2158
|
var DEFAULT_LOAD_BUDGET = 25e3;
|
|
1749
2159
|
var KbStore = class {
|
|
@@ -1774,7 +2184,7 @@ var KbStore = class {
|
|
|
1774
2184
|
const conceptId2 = `${input.type}.${input.slug}`;
|
|
1775
2185
|
const root = this.root(bundlePath2);
|
|
1776
2186
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
1777
|
-
await (0,
|
|
2187
|
+
await (0, import_promises4.mkdir)(root, { recursive: true });
|
|
1778
2188
|
await this.publish(
|
|
1779
2189
|
target,
|
|
1780
2190
|
stringifyMarkdownWithFrontmatter(input.body, frontmatter),
|
|
@@ -1813,7 +2223,7 @@ var KbStore = class {
|
|
|
1813
2223
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
1814
2224
|
let raw;
|
|
1815
2225
|
try {
|
|
1816
|
-
raw = await (0,
|
|
2226
|
+
raw = await (0, import_promises4.readFile)(target, "utf8");
|
|
1817
2227
|
} catch {
|
|
1818
2228
|
return null;
|
|
1819
2229
|
}
|
|
@@ -1830,7 +2240,7 @@ var KbStore = class {
|
|
|
1830
2240
|
const root = this.root(bundlePath2);
|
|
1831
2241
|
let names;
|
|
1832
2242
|
try {
|
|
1833
|
-
names = await (0,
|
|
2243
|
+
names = await (0, import_promises4.readdir)(root);
|
|
1834
2244
|
} catch {
|
|
1835
2245
|
return [];
|
|
1836
2246
|
}
|
|
@@ -1838,7 +2248,7 @@ var KbStore = class {
|
|
|
1838
2248
|
const records = await mapLimit(
|
|
1839
2249
|
wanted,
|
|
1840
2250
|
DEFAULT_IO_CONCURRENCY,
|
|
1841
|
-
async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await (0,
|
|
2251
|
+
async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await (0, import_promises4.readFile)((0, import_node_path4.join)(root, name), "utf8"))
|
|
1842
2252
|
);
|
|
1843
2253
|
return records.filter((record) => record !== null);
|
|
1844
2254
|
}
|
|
@@ -2006,7 +2416,8 @@ ${answer}
|
|
|
2006
2416
|
* at the repo root, and the MCP server's cwd is the workspace.
|
|
2007
2417
|
*
|
|
2008
2418
|
* Public because `doctor` needs the same map with the same degradation: a
|
|
2009
|
-
* sweep that failed to read the tree should report no drift, not fail
|
|
2419
|
+
* sweep that failed to read the tree should report no drift, not fail — and
|
|
2420
|
+
* `offline: false` there, because a sweep is worth a fetch.
|
|
2010
2421
|
*
|
|
2011
2422
|
* When no root was given and not one anchored file was found, the finding is
|
|
2012
2423
|
* discarded. A base read from somewhere other than the tree it describes
|
|
@@ -2018,10 +2429,13 @@ ${answer}
|
|
|
2018
2429
|
* plausible, and the misses become findings again; an explicit `repoRoot` is
|
|
2019
2430
|
* taken at its word either way.
|
|
2020
2431
|
*/
|
|
2021
|
-
async detectDrift(records, repoRoot) {
|
|
2432
|
+
async detectDrift(records, repoRoot, options = {}) {
|
|
2022
2433
|
try {
|
|
2023
2434
|
const drift = await detectAnchorDrift(records, {
|
|
2024
|
-
repoRoot: repoRoot ?? process.cwd()
|
|
2435
|
+
repoRoot: repoRoot ?? process.cwd(),
|
|
2436
|
+
// Offline by default: a read path must never spend a network fetch per
|
|
2437
|
+
// call. `doctor` and `anchor-resolve` are the verbs that go get it.
|
|
2438
|
+
remote: { offline: options.offline !== false }
|
|
2025
2439
|
});
|
|
2026
2440
|
if (repoRoot === void 0 && looksLikeWrongRepoRoot(drift)) {
|
|
2027
2441
|
this.logger.warn?.({
|
|
@@ -2139,11 +2553,11 @@ ${answer}
|
|
|
2139
2553
|
async readIndex(bundlePath2) {
|
|
2140
2554
|
const root = this.root(bundlePath2);
|
|
2141
2555
|
const expected = renderIndex(await this.list(bundlePath2));
|
|
2142
|
-
const stored = await (0,
|
|
2556
|
+
const stored = await (0, import_promises4.readFile)((0, import_node_path4.join)(root, INDEX_FILE), "utf8").catch(
|
|
2143
2557
|
() => null
|
|
2144
2558
|
);
|
|
2145
2559
|
if (indexIsStale(stored, expected)) {
|
|
2146
|
-
await this.publish((0,
|
|
2560
|
+
await this.publish((0, import_node_path4.join)(root, INDEX_FILE), expected, true, INDEX_FILE);
|
|
2147
2561
|
this.logger.info?.({
|
|
2148
2562
|
operation: "kb.index.repair",
|
|
2149
2563
|
bundlePath: root,
|
|
@@ -2160,8 +2574,8 @@ ${answer}
|
|
|
2160
2574
|
* knows which agent touched what. So a bad line is surfaced and left alone.
|
|
2161
2575
|
*/
|
|
2162
2576
|
async readLog(bundlePath2) {
|
|
2163
|
-
const raw = await (0,
|
|
2164
|
-
(0,
|
|
2577
|
+
const raw = await (0, import_promises4.readFile)(
|
|
2578
|
+
(0, import_node_path4.join)(this.root(bundlePath2), LOG_FILE),
|
|
2165
2579
|
"utf8"
|
|
2166
2580
|
).catch(() => "");
|
|
2167
2581
|
const result = parseLog(raw);
|
|
@@ -2212,14 +2626,14 @@ ${answer}
|
|
|
2212
2626
|
}
|
|
2213
2627
|
async mutate(bundlePath2, conceptId2, change, entry, changeBody = (body) => body) {
|
|
2214
2628
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
2215
|
-
const before = await (0,
|
|
2629
|
+
const before = await (0, import_promises4.readFile)(target, "utf8").catch(() => null);
|
|
2216
2630
|
if (before === null) throw new KbRecordNotFoundError(conceptId2);
|
|
2217
2631
|
const parsed = this.parse(conceptId2, before);
|
|
2218
2632
|
if (!parsed) throw new KbRecordNotFoundError(conceptId2);
|
|
2219
2633
|
const frontmatter = change(parsed.frontmatter);
|
|
2220
2634
|
const body = changeBody(parsed.body);
|
|
2221
2635
|
const contents = stringifyMarkdownWithFrontmatter(body, frontmatter);
|
|
2222
|
-
const witness = await (0,
|
|
2636
|
+
const witness = await (0, import_promises4.readFile)(target, "utf8").catch(() => null);
|
|
2223
2637
|
if (witness === null || digest(witness) !== digest(before)) {
|
|
2224
2638
|
throw new KbWriteConflictError(conceptId2);
|
|
2225
2639
|
}
|
|
@@ -2245,20 +2659,20 @@ ${answer}
|
|
|
2245
2659
|
*/
|
|
2246
2660
|
async publish(target, contents, overwrite, conceptId2) {
|
|
2247
2661
|
const staging = `${target}.${process.pid}.tmp`;
|
|
2248
|
-
await (0,
|
|
2662
|
+
await (0, import_promises4.writeFile)(staging, contents, "utf8");
|
|
2249
2663
|
try {
|
|
2250
2664
|
if (overwrite) {
|
|
2251
|
-
await (0,
|
|
2665
|
+
await (0, import_promises4.rename)(staging, target);
|
|
2252
2666
|
return;
|
|
2253
2667
|
}
|
|
2254
|
-
await (0,
|
|
2668
|
+
await (0, import_promises4.link)(staging, target);
|
|
2255
2669
|
} catch (error) {
|
|
2256
2670
|
if (error.code === "EEXIST") {
|
|
2257
2671
|
throw new KbRecordAlreadyExistsError(conceptId2);
|
|
2258
2672
|
}
|
|
2259
2673
|
throw error;
|
|
2260
2674
|
} finally {
|
|
2261
|
-
await (0,
|
|
2675
|
+
await (0, import_promises4.unlink)(staging).catch(() => void 0);
|
|
2262
2676
|
}
|
|
2263
2677
|
}
|
|
2264
2678
|
/**
|
|
@@ -2302,18 +2716,18 @@ ${answer}
|
|
|
2302
2716
|
* file must not fail the mutation it guards.
|
|
2303
2717
|
*/
|
|
2304
2718
|
async ensureGitattributes(root) {
|
|
2305
|
-
const target = (0,
|
|
2719
|
+
const target = (0, import_node_path4.join)(root, GITATTRIBUTES_FILE);
|
|
2306
2720
|
try {
|
|
2307
2721
|
let existing;
|
|
2308
2722
|
try {
|
|
2309
|
-
existing = await (0,
|
|
2723
|
+
existing = await (0, import_promises4.readFile)(target, "utf8");
|
|
2310
2724
|
} catch (error) {
|
|
2311
2725
|
if (error.code !== "ENOENT") throw error;
|
|
2312
2726
|
existing = null;
|
|
2313
2727
|
}
|
|
2314
2728
|
if (existing === null) {
|
|
2315
2729
|
try {
|
|
2316
|
-
await (0,
|
|
2730
|
+
await (0, import_promises4.writeFile)(target, appendUnionMergeLine(""), {
|
|
2317
2731
|
encoding: "utf8",
|
|
2318
2732
|
flag: "wx"
|
|
2319
2733
|
});
|
|
@@ -2334,7 +2748,7 @@ ${answer}
|
|
|
2334
2748
|
return;
|
|
2335
2749
|
}
|
|
2336
2750
|
if (!hasMergeDeclaration(existing)) {
|
|
2337
|
-
await (0,
|
|
2751
|
+
await (0, import_promises4.appendFile)(target, appendUnionMergeLine(existing), "utf8");
|
|
2338
2752
|
this.logger.info?.({
|
|
2339
2753
|
operation: "kb.gitattributes.ensure",
|
|
2340
2754
|
bundlePath: root,
|
|
@@ -2353,7 +2767,7 @@ ${answer}
|
|
|
2353
2767
|
async record(root, entry) {
|
|
2354
2768
|
await this.ensureGitattributes(root);
|
|
2355
2769
|
const line = renderLogEntry({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
|
|
2356
|
-
await (0,
|
|
2770
|
+
await (0, import_promises4.appendFile)((0, import_node_path4.join)(root, LOG_FILE), line, "utf8").catch((error) => {
|
|
2357
2771
|
this.logger.warn?.({
|
|
2358
2772
|
operation: "kb.log.append",
|
|
2359
2773
|
outcome: "failed",
|
|
@@ -2379,18 +2793,18 @@ ${answer}
|
|
|
2379
2793
|
};
|
|
2380
2794
|
}
|
|
2381
2795
|
root(bundlePath2) {
|
|
2382
|
-
return (0,
|
|
2796
|
+
return (0, import_node_path4.resolve)(bundlePath2);
|
|
2383
2797
|
}
|
|
2384
2798
|
// Concept ids are `<type>.<slug>` and map to a single file directly under the
|
|
2385
2799
|
// bundle root; anything carrying a separator would escape it.
|
|
2386
2800
|
recordPath(bundlePath2, conceptId2) {
|
|
2387
|
-
if (conceptId2.includes(
|
|
2801
|
+
if (conceptId2.includes(import_node_path4.sep) || conceptId2.includes("/")) {
|
|
2388
2802
|
throw new KbInvalidConceptIdError(
|
|
2389
2803
|
"concept id must not contain a path separator",
|
|
2390
2804
|
{ conceptId: conceptId2 }
|
|
2391
2805
|
);
|
|
2392
2806
|
}
|
|
2393
|
-
return (0,
|
|
2807
|
+
return (0, import_node_path4.join)(this.root(bundlePath2), `${conceptId2}.md`);
|
|
2394
2808
|
}
|
|
2395
2809
|
};
|
|
2396
2810
|
function estimateTokens(record) {
|
|
@@ -2619,18 +3033,18 @@ var KbBaseFrozenError = class extends Error {
|
|
|
2619
3033
|
};
|
|
2620
3034
|
|
|
2621
3035
|
// src/kb-pins/frozen.ts
|
|
2622
|
-
var
|
|
3036
|
+
var import_node_path7 = require("path");
|
|
2623
3037
|
|
|
2624
3038
|
// src/kb-pins/layers.ts
|
|
2625
|
-
var
|
|
2626
|
-
var
|
|
2627
|
-
var
|
|
3039
|
+
var import_promises5 = require("fs/promises");
|
|
3040
|
+
var import_node_os2 = require("os");
|
|
3041
|
+
var import_node_path6 = require("path");
|
|
2628
3042
|
|
|
2629
3043
|
// src/kb-pins/model.ts
|
|
2630
|
-
var
|
|
3044
|
+
var import_node_path5 = require("path");
|
|
2631
3045
|
var import_zod4 = require("zod");
|
|
2632
|
-
var PINS_FILE = (0,
|
|
2633
|
-
var PINS_LOCAL_FILE = (0,
|
|
3046
|
+
var PINS_FILE = (0, import_node_path5.join)(".strauss", "kb-pins.json");
|
|
3047
|
+
var PINS_LOCAL_FILE = (0, import_node_path5.join)(".strauss", "kb-pins.local.json");
|
|
2634
3048
|
var PIN_LAYERS = ["project", "local", "user"];
|
|
2635
3049
|
var pinSchema = import_zod4.z.object({
|
|
2636
3050
|
/** Relative to the manifest's root, so the file is committable. */
|
|
@@ -2677,13 +3091,13 @@ var pinsManifestSchema = import_zod4.z.object({
|
|
|
2677
3091
|
|
|
2678
3092
|
// src/kb-pins/layers.ts
|
|
2679
3093
|
function userRoot() {
|
|
2680
|
-
return process.env.STRAUSS_KB_USER_ROOT || (0,
|
|
3094
|
+
return process.env.STRAUSS_KB_USER_ROOT || (0, import_node_os2.homedir)();
|
|
2681
3095
|
}
|
|
2682
3096
|
function layerRoot(workspaceDir, layer) {
|
|
2683
|
-
return layer === "user" ? userRoot() : (0,
|
|
3097
|
+
return layer === "user" ? userRoot() : (0, import_node_path6.resolve)(workspaceDir);
|
|
2684
3098
|
}
|
|
2685
3099
|
function layerFile(workspaceDir, layer) {
|
|
2686
|
-
return (0,
|
|
3100
|
+
return (0, import_node_path6.join)(
|
|
2687
3101
|
layerRoot(workspaceDir, layer),
|
|
2688
3102
|
layer === "local" ? PINS_LOCAL_FILE : PINS_FILE
|
|
2689
3103
|
);
|
|
@@ -2692,7 +3106,7 @@ async function readPinsLayer(workspaceDir, layer) {
|
|
|
2692
3106
|
const file = layerFile(workspaceDir, layer);
|
|
2693
3107
|
let raw;
|
|
2694
3108
|
try {
|
|
2695
|
-
raw = await (0,
|
|
3109
|
+
raw = await (0, import_promises5.readFile)(file, "utf8");
|
|
2696
3110
|
} catch {
|
|
2697
3111
|
return { pins: [] };
|
|
2698
3112
|
}
|
|
@@ -2716,16 +3130,16 @@ async function readPinsLayer(workspaceDir, layer) {
|
|
|
2716
3130
|
}
|
|
2717
3131
|
async function writePinsLayer(workspaceDir, layer, manifest) {
|
|
2718
3132
|
const file = layerFile(workspaceDir, layer);
|
|
2719
|
-
await (0,
|
|
2720
|
-
await (0,
|
|
3133
|
+
await (0, import_promises5.mkdir)((0, import_node_path6.dirname)(file), { recursive: true });
|
|
3134
|
+
await (0, import_promises5.writeFile)(file, `${JSON.stringify(manifest, null, 2)}
|
|
2721
3135
|
`, "utf8");
|
|
2722
3136
|
}
|
|
2723
3137
|
function resolvePinPath(rootDir, path) {
|
|
2724
|
-
return (0,
|
|
3138
|
+
return (0, import_node_path6.isAbsolute)(path) ? (0, import_node_path6.resolve)(path) : (0, import_node_path6.resolve)(rootDir, path.split("/").join(import_node_path6.sep));
|
|
2725
3139
|
}
|
|
2726
3140
|
function storablePath(rootDir, bundlePath2) {
|
|
2727
|
-
const rel = (0,
|
|
2728
|
-
return (rel === "" ? "." : rel).split(
|
|
3141
|
+
const rel = (0, import_node_path6.relative)((0, import_node_path6.resolve)(rootDir), (0, import_node_path6.resolve)(bundlePath2));
|
|
3142
|
+
return (rel === "" ? "." : rel).split(import_node_path6.sep).join("/");
|
|
2729
3143
|
}
|
|
2730
3144
|
async function readMergedPins(workspaceDir) {
|
|
2731
3145
|
const manifests = {};
|
|
@@ -2753,7 +3167,7 @@ async function readMergedPins(workspaceDir) {
|
|
|
2753
3167
|
// src/kb-pins/frozen.ts
|
|
2754
3168
|
async function assertBaseNotFrozen(workspaceDir, bundlePath2) {
|
|
2755
3169
|
const merged = await readMergedPins(workspaceDir);
|
|
2756
|
-
const absolute = (0,
|
|
3170
|
+
const absolute = (0, import_node_path7.resolve)(bundlePath2);
|
|
2757
3171
|
const pin = merged.pins.find((entry) => entry.absolutePath === absolute);
|
|
2758
3172
|
if (pin?.frozen === true) {
|
|
2759
3173
|
throw new KbBaseFrozenError(pin.path, pin.layer);
|
|
@@ -2838,7 +3252,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
|
|
|
2838
3252
|
}
|
|
2839
3253
|
|
|
2840
3254
|
// src/kb-pins/unpin.ts
|
|
2841
|
-
var
|
|
3255
|
+
var import_node_path8 = require("path");
|
|
2842
3256
|
async function unpinBase(workspaceDir, bundlePath2) {
|
|
2843
3257
|
const layers = [];
|
|
2844
3258
|
for (const layer of PIN_LAYERS) {
|
|
@@ -2859,14 +3273,14 @@ async function unpinBase(workspaceDir, bundlePath2) {
|
|
|
2859
3273
|
}
|
|
2860
3274
|
}
|
|
2861
3275
|
return {
|
|
2862
|
-
path: storablePath((0,
|
|
3276
|
+
path: storablePath((0, import_node_path8.resolve)(workspaceDir), bundlePath2),
|
|
2863
3277
|
removed: layers.length > 0,
|
|
2864
3278
|
layers
|
|
2865
3279
|
};
|
|
2866
3280
|
}
|
|
2867
3281
|
|
|
2868
3282
|
// src/kb-context.ts
|
|
2869
|
-
var
|
|
3283
|
+
var import_promises6 = require("fs/promises");
|
|
2870
3284
|
var HEADING2 = "## Knowledge bases (pinned)";
|
|
2871
3285
|
var DEFAULT_CONTEXT_BUDGET = 4e3;
|
|
2872
3286
|
var CONTEXT_PROFILES = {
|
|
@@ -3025,7 +3439,7 @@ async function buildContext(store, workspaceDir, options = {}) {
|
|
|
3025
3439
|
operation: "kb.context.refused",
|
|
3026
3440
|
approxTokens: total,
|
|
3027
3441
|
budgetTokens,
|
|
3028
|
-
bases: bases.map((
|
|
3442
|
+
bases: bases.map((base2) => base2.path)
|
|
3029
3443
|
});
|
|
3030
3444
|
const refusal = [
|
|
3031
3445
|
HEADING2,
|
|
@@ -3035,7 +3449,7 @@ async function buildContext(store, workspaceDir, options = {}) {
|
|
|
3035
3449
|
"from a complete one. The pinned bases:",
|
|
3036
3450
|
"",
|
|
3037
3451
|
...bases.map(
|
|
3038
|
-
(
|
|
3452
|
+
(base2) => `- ${base2.path} \u2014 ~${base2.approxTokens} tokens (bundlePath: \`${base2.absolutePath}\`)`
|
|
3039
3453
|
),
|
|
3040
3454
|
"",
|
|
3041
3455
|
"For the question at hand, read what you need now \u2014 `kb_load` a base",
|
|
@@ -3070,13 +3484,13 @@ function toHookJson(block, event) {
|
|
|
3070
3484
|
var CONTEXT_BEGIN = "<!-- strauss-kb:begin -->";
|
|
3071
3485
|
var CONTEXT_END = "<!-- strauss-kb:end -->";
|
|
3072
3486
|
async function syncInstructions(file, block) {
|
|
3073
|
-
const existing = await (0,
|
|
3487
|
+
const existing = await (0, import_promises6.readFile)(file, "utf8").catch(() => null);
|
|
3074
3488
|
const region = block ? `${CONTEXT_BEGIN}
|
|
3075
3489
|
${block.trim()}
|
|
3076
3490
|
${CONTEXT_END}` : null;
|
|
3077
3491
|
if (existing === null) {
|
|
3078
3492
|
if (!region) return { file, action: "unchanged" };
|
|
3079
|
-
await (0,
|
|
3493
|
+
await (0, import_promises6.writeFile)(file, `${region}
|
|
3080
3494
|
`, "utf8");
|
|
3081
3495
|
return { file, action: "created" };
|
|
3082
3496
|
}
|
|
@@ -3087,11 +3501,11 @@ ${CONTEXT_END}` : null;
|
|
|
3087
3501
|
const after = existing.slice(end + CONTEXT_END.length);
|
|
3088
3502
|
const next = region ? `${before}${region}${after}` : `${before.replace(/\n+$/, "\n")}${after.replace(/^\n+/, "\n")}`;
|
|
3089
3503
|
if (next === existing) return { file, action: "unchanged" };
|
|
3090
|
-
await (0,
|
|
3504
|
+
await (0, import_promises6.writeFile)(file, next, "utf8");
|
|
3091
3505
|
return { file, action: region ? "replaced" : "removed" };
|
|
3092
3506
|
}
|
|
3093
3507
|
if (!region) return { file, action: "unchanged" };
|
|
3094
|
-
await (0,
|
|
3508
|
+
await (0, import_promises6.writeFile)(
|
|
3095
3509
|
file,
|
|
3096
3510
|
`${existing.replace(/\n*$/, "\n\n")}${region}
|
|
3097
3511
|
`,
|
|
@@ -3250,6 +3664,16 @@ function validateBundle(records) {
|
|
|
3250
3664
|
);
|
|
3251
3665
|
}
|
|
3252
3666
|
}
|
|
3667
|
+
for (const anchor of fm.strauss_anchors ?? []) {
|
|
3668
|
+
if (anchor.repo && !isCanonicalRepoUrl(anchor.repo)) {
|
|
3669
|
+
report(
|
|
3670
|
+
"anchor_repo",
|
|
3671
|
+
conceptId2,
|
|
3672
|
+
`anchor repo "${anchor.repo}" is not a full remote URL, so it cannot be resolved against a remote`,
|
|
3673
|
+
"warning"
|
|
3674
|
+
);
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3253
3677
|
if (fm.strauss_assumption && fm.sources?.length) {
|
|
3254
3678
|
report("assumption", conceptId2, "marked an assumption but cites sources");
|
|
3255
3679
|
}
|
|
@@ -3269,7 +3693,8 @@ var KB_DOCTOR_CHECKS = [
|
|
|
3269
3693
|
"orphaned",
|
|
3270
3694
|
"broken-supersession",
|
|
3271
3695
|
"superseded-but-cited",
|
|
3272
|
-
"drifted"
|
|
3696
|
+
"drifted",
|
|
3697
|
+
"unchecked"
|
|
3273
3698
|
];
|
|
3274
3699
|
var CHECK_HEADLINES = {
|
|
3275
3700
|
expired: "past its stale_after date",
|
|
@@ -3279,7 +3704,8 @@ var CHECK_HEADLINES = {
|
|
|
3279
3704
|
orphaned: "no other record links to it",
|
|
3280
3705
|
"broken-supersession": "the supersession pointers do not resolve",
|
|
3281
3706
|
"superseded-but-cited": "a live record's body links to one that no longer holds",
|
|
3282
|
-
drifted: "the code an anchor points at moved out from under its hash"
|
|
3707
|
+
drifted: "the code an anchor points at moved out from under its hash",
|
|
3708
|
+
unchecked: "an anchor in another repository nothing could reach"
|
|
3283
3709
|
};
|
|
3284
3710
|
var DAY_MS = 864e5;
|
|
3285
3711
|
function doctor(bundle, options = {}) {
|
|
@@ -3304,7 +3730,8 @@ function doctor(bundle, options = {}) {
|
|
|
3304
3730
|
group("orphaned", orphaned(bundle)),
|
|
3305
3731
|
group("broken-supersession", brokenSupersession(bundle, adjudicated)),
|
|
3306
3732
|
group("superseded-but-cited", supersededButCited(bundle, standings)),
|
|
3307
|
-
group("drifted", drifted(inForce))
|
|
3733
|
+
group("drifted", drifted(inForce)),
|
|
3734
|
+
group("unchecked", unchecked(inForce))
|
|
3308
3735
|
];
|
|
3309
3736
|
const counts = Object.fromEntries(
|
|
3310
3737
|
groups.map((entry) => [entry.check, entry.count])
|
|
@@ -3491,21 +3918,38 @@ function supersededButCited(bundle, standings) {
|
|
|
3491
3918
|
return findings;
|
|
3492
3919
|
}
|
|
3493
3920
|
function drifted(hits) {
|
|
3921
|
+
return anchorFindings(
|
|
3922
|
+
hits,
|
|
3923
|
+
"drifted",
|
|
3924
|
+
(count2) => count2 === 1 ? "anchor no longer matches" : "anchors no longer match"
|
|
3925
|
+
);
|
|
3926
|
+
}
|
|
3927
|
+
function unchecked(hits) {
|
|
3928
|
+
return anchorFindings(
|
|
3929
|
+
hits,
|
|
3930
|
+
"unchecked",
|
|
3931
|
+
(count2) => count2 === 1 ? "anchor was not checked" : "anchors were not checked"
|
|
3932
|
+
);
|
|
3933
|
+
}
|
|
3934
|
+
function anchorFindings(hits, kind, headline) {
|
|
3494
3935
|
const findings = [];
|
|
3495
3936
|
for (const hit of hits) {
|
|
3496
|
-
const warning = hit.warnings.find(
|
|
3937
|
+
const warning = hit.warnings.find(
|
|
3938
|
+
(entry) => entry.kind === kind
|
|
3939
|
+
);
|
|
3497
3940
|
if (!warning) continue;
|
|
3941
|
+
const byRepo = /* @__PURE__ */ new Map();
|
|
3942
|
+
for (const anchor of warning.anchors) {
|
|
3943
|
+
const repo = anchor.repo ?? "";
|
|
3944
|
+
byRepo.set(repo, [...byRepo.get(repo) ?? [], describeAnchor(anchor)]);
|
|
3945
|
+
}
|
|
3946
|
+
const detail = [...byRepo.entries()].map(
|
|
3947
|
+
([repo, entries]) => repo ? `${repo}: ${entries.join(", ")}` : entries.join(", ")
|
|
3948
|
+
);
|
|
3498
3949
|
findings.push(
|
|
3499
3950
|
finding(
|
|
3500
3951
|
hit.record,
|
|
3501
|
-
`${warning.anchors.length} ${warning.anchors.length
|
|
3502
|
-
const at = anchor.symbol ? `${anchor.file}:${anchor.symbol}` : anchor.file;
|
|
3503
|
-
if (anchor.reason) return `${at} (${anchor.reason})`;
|
|
3504
|
-
if (anchor.diffSize === null) {
|
|
3505
|
-
return `${at} (changed, size unrecorded)`;
|
|
3506
|
-
}
|
|
3507
|
-
return anchor.diffSize === 0 ? `${at} (content changed, same line count)` : `${at} (${anchor.diffSize} line${anchor.diffSize === 1 ? "" : "s"} apart)`;
|
|
3508
|
-
}).join(", ")}`
|
|
3952
|
+
`${warning.anchors.length} ${headline(warning.anchors.length)}: ${detail.join("; ")}`
|
|
3509
3953
|
)
|
|
3510
3954
|
);
|
|
3511
3955
|
}
|
|
@@ -3513,6 +3957,15 @@ function drifted(hits) {
|
|
|
3513
3957
|
(left, right) => left.conceptId.localeCompare(right.conceptId)
|
|
3514
3958
|
);
|
|
3515
3959
|
}
|
|
3960
|
+
function describeAnchor(anchor) {
|
|
3961
|
+
const at = anchor.symbol ? `${anchor.file}:${anchor.symbol}` : anchor.file;
|
|
3962
|
+
if (anchor.reason) return `${at} (${anchor.reason})`;
|
|
3963
|
+
if (anchor.remoteState === "drifted-on-default") {
|
|
3964
|
+
return `${at} (matches ref, moved on the default branch)`;
|
|
3965
|
+
}
|
|
3966
|
+
if (anchor.diffSize === null) return `${at} (changed, size unrecorded)`;
|
|
3967
|
+
return anchor.diffSize === 0 ? `${at} (content changed, same line count)` : `${at} (${anchor.diffSize} line${anchor.diffSize === 1 ? "" : "s"} apart)`;
|
|
3968
|
+
}
|
|
3516
3969
|
function replaces(later, earlier) {
|
|
3517
3970
|
return (later.frontmatter.strauss_supersedes ?? []).includes(earlier.conceptId) || earlier.frontmatter.strauss_superseded_by === later.conceptId;
|
|
3518
3971
|
}
|
|
@@ -3615,12 +4068,15 @@ function argvFlag(argv, name) {
|
|
|
3615
4068
|
var anchorResolveCommand = define({
|
|
3616
4069
|
name: "anchor-resolve",
|
|
3617
4070
|
tool: "kb_anchor_resolve",
|
|
3618
|
-
usage: "anchor-resolve <concept-id> [--repo-root <path>] [--rebaseline] [--restamp]",
|
|
3619
|
-
description: "Resolve a record's anchors
|
|
4071
|
+
usage: "anchor-resolve <concept-id> [--repo-root <path>] [--offline] [--rebaseline] [--restamp]",
|
|
4072
|
+
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.",
|
|
3620
4073
|
input: import_zod8.z.object({
|
|
3621
4074
|
bundlePath,
|
|
3622
4075
|
conceptId,
|
|
3623
4076
|
repoRoot: import_zod8.z.string().min(1).optional(),
|
|
4077
|
+
offline: import_zod8.z.boolean().optional().describe(
|
|
4078
|
+
"Resolve foreign anchors from the local repo cache only, never fetching."
|
|
4079
|
+
),
|
|
3624
4080
|
rebaseline: import_zod8.z.boolean().optional().describe(
|
|
3625
4081
|
"Accept the current code as the new baseline for anchors that drifted."
|
|
3626
4082
|
),
|
|
@@ -3632,10 +4088,11 @@ var anchorResolveCommand = define({
|
|
|
3632
4088
|
bundlePath: path,
|
|
3633
4089
|
conceptId: argv[1],
|
|
3634
4090
|
repoRoot: argvFlag(argv, "--repo-root"),
|
|
4091
|
+
offline: argv.includes("--offline"),
|
|
3635
4092
|
rebaseline: argv.includes("--rebaseline"),
|
|
3636
4093
|
restamp: argv.includes("--restamp")
|
|
3637
4094
|
}),
|
|
3638
|
-
run: async ({ store, actor, now }, { bundlePath: path, conceptId: id, repoRoot, rebaseline, restamp }) => {
|
|
4095
|
+
run: async ({ store, actor, now }, { bundlePath: path, conceptId: id, repoRoot, offline, rebaseline, restamp }) => {
|
|
3639
4096
|
const root = repoRoot ?? process.cwd();
|
|
3640
4097
|
const record = await store.read(path, id);
|
|
3641
4098
|
if (!record) throw new KbRecordNotFoundError(id);
|
|
@@ -3650,18 +4107,10 @@ var anchorResolveCommand = define({
|
|
|
3650
4107
|
}
|
|
3651
4108
|
const results = [];
|
|
3652
4109
|
const updated = [];
|
|
3653
|
-
const origin = new LazyOrigin(root);
|
|
3654
4110
|
let dirty = false;
|
|
3655
|
-
|
|
3656
|
-
const foreign = new Map(
|
|
3657
|
-
anchors.map((anchor) => [anchor, origin.isForeign(anchor)])
|
|
3658
|
-
);
|
|
3659
|
-
const reads = await readAnchorFiles(
|
|
3660
|
-
anchors.filter((anchor) => !foreign.get(anchor)).map((anchor) => anchor.file),
|
|
3661
|
-
anchorFileReader(root)
|
|
3662
|
-
);
|
|
4111
|
+
const sources = await readSources(anchors, root, offline === true);
|
|
3663
4112
|
for (const anchor of anchors) {
|
|
3664
|
-
const
|
|
4113
|
+
const base2 = {
|
|
3665
4114
|
file: anchor.file,
|
|
3666
4115
|
...anchor.symbol ? { symbol: anchor.symbol } : {},
|
|
3667
4116
|
// Carried onto unresolved findings too: an anchor that once hashed
|
|
@@ -3669,21 +4118,17 @@ var anchorResolveCommand = define({
|
|
|
3669
4118
|
// has to be able to tell it from one nobody ever stamped.
|
|
3670
4119
|
...anchor.hash ? { storedHash: anchor.hash } : {}
|
|
3671
4120
|
};
|
|
3672
|
-
|
|
3673
|
-
|
|
4121
|
+
const source = sources.get(anchor);
|
|
4122
|
+
if (source.repo) base2.repo = source.repo;
|
|
4123
|
+
if (!source.ok) {
|
|
4124
|
+
results.push({ ...base2, state: "unresolved", reason: source.reason });
|
|
3674
4125
|
updated.push(anchor);
|
|
3675
4126
|
continue;
|
|
3676
4127
|
}
|
|
3677
|
-
const
|
|
3678
|
-
if (!fileRead.ok) {
|
|
3679
|
-
results.push({ ...base, state: "unresolved", reason: fileRead.reason });
|
|
3680
|
-
updated.push(anchor);
|
|
3681
|
-
continue;
|
|
3682
|
-
}
|
|
3683
|
-
const resolved = resolveAnchor(fileRead.source, anchor);
|
|
4128
|
+
const resolved = resolveAnchor(source.source, anchor);
|
|
3684
4129
|
if (!resolved) {
|
|
3685
4130
|
results.push({
|
|
3686
|
-
...
|
|
4131
|
+
...base2,
|
|
3687
4132
|
state: "unresolved",
|
|
3688
4133
|
reason: "symbol-not-found"
|
|
3689
4134
|
});
|
|
@@ -3698,30 +4143,47 @@ var anchorResolveCommand = define({
|
|
|
3698
4143
|
lines: currentLines,
|
|
3699
4144
|
resolved_at: now()
|
|
3700
4145
|
};
|
|
4146
|
+
const pinned = anchor.ref !== void 0 && source.repo !== void 0;
|
|
3701
4147
|
if (!anchor.hash) {
|
|
3702
|
-
results.push({ ...
|
|
4148
|
+
results.push({ ...base2, state: "stamped", currentHash });
|
|
3703
4149
|
updated.push(stamped);
|
|
3704
4150
|
dirty = true;
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
state: "match",
|
|
3709
|
-
currentHash
|
|
3710
|
-
});
|
|
3711
|
-
const refresh = restamp || anchor.resolved_at === void 0;
|
|
3712
|
-
updated.push(refresh ? { ...anchor, resolved_at: now() } : anchor);
|
|
3713
|
-
if (refresh) dirty = true;
|
|
3714
|
-
} else {
|
|
4151
|
+
continue;
|
|
4152
|
+
}
|
|
4153
|
+
if (anchor.hash !== currentHash) {
|
|
3715
4154
|
results.push({
|
|
3716
|
-
...
|
|
4155
|
+
...base2,
|
|
3717
4156
|
state: "drifted",
|
|
3718
4157
|
currentHash,
|
|
3719
|
-
diffSize: anchor
|
|
4158
|
+
diffSize: lineDelta(anchor, currentLines),
|
|
4159
|
+
...pinned ? { remoteState: "drifted-from-ref" } : {},
|
|
3720
4160
|
...rebaseline ? { rebaselined: true } : {}
|
|
3721
4161
|
});
|
|
3722
4162
|
updated.push(rebaseline ? stamped : anchor);
|
|
3723
4163
|
if (rebaseline) dirty = true;
|
|
4164
|
+
continue;
|
|
4165
|
+
}
|
|
4166
|
+
const onDefault = pinned ? headHash(source, anchor) : void 0;
|
|
4167
|
+
if (onDefault && onDefault.hash !== anchor.hash) {
|
|
4168
|
+
results.push({
|
|
4169
|
+
...base2,
|
|
4170
|
+
state: "drifted",
|
|
4171
|
+
currentHash: onDefault.hash,
|
|
4172
|
+
diffSize: lineDelta(anchor, onDefault.lines),
|
|
4173
|
+
remoteState: "drifted-on-default"
|
|
4174
|
+
});
|
|
4175
|
+
updated.push(anchor);
|
|
4176
|
+
continue;
|
|
3724
4177
|
}
|
|
4178
|
+
results.push({
|
|
4179
|
+
...base2,
|
|
4180
|
+
state: "match",
|
|
4181
|
+
currentHash,
|
|
4182
|
+
...pinned ? { remoteState: "matches-ref" } : {}
|
|
4183
|
+
});
|
|
4184
|
+
const refresh = restamp || anchor.resolved_at === void 0;
|
|
4185
|
+
updated.push(refresh ? { ...anchor, resolved_at: now() } : anchor);
|
|
4186
|
+
if (refresh) dirty = true;
|
|
3725
4187
|
}
|
|
3726
4188
|
let frozen = false;
|
|
3727
4189
|
if (dirty) {
|
|
@@ -3734,16 +4196,19 @@ var anchorResolveCommand = define({
|
|
|
3734
4196
|
if (!frozen) await store.updateAnchors(path, id, updated, actor);
|
|
3735
4197
|
}
|
|
3736
4198
|
const frozenNote = frozen ? { frozen: true, note: "base is frozen: nothing was stamped" } : {};
|
|
3737
|
-
const
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
const
|
|
4199
|
+
const unreachable = results.filter(
|
|
4200
|
+
(entry) => isUncheckedReason(entry.reason)
|
|
4201
|
+
).length;
|
|
4202
|
+
const checked = results.length - unreachable;
|
|
4203
|
+
const matches2 = results.filter((entry) => entry.state === "match").length;
|
|
4204
|
+
const note = `${matches2}/${checked} anchors match${unreachable ? `, ${unreachable} unreachable` : ""}`;
|
|
4205
|
+
const clean = checked > 0 && matches2 === checked && unreachable === 0;
|
|
3741
4206
|
if (clean) {
|
|
3742
4207
|
try {
|
|
3743
4208
|
await store.verify(
|
|
3744
4209
|
path,
|
|
3745
4210
|
id,
|
|
3746
|
-
`anchor-resolve: ${
|
|
4211
|
+
`anchor-resolve: ${note} (regex resolver)`,
|
|
3747
4212
|
actor,
|
|
3748
4213
|
now()
|
|
3749
4214
|
);
|
|
@@ -3759,18 +4224,81 @@ var anchorResolveCommand = define({
|
|
|
3759
4224
|
}
|
|
3760
4225
|
return { conceptId: id, results, verified: true, ...frozenNote };
|
|
3761
4226
|
}
|
|
3762
|
-
return {
|
|
4227
|
+
return {
|
|
4228
|
+
conceptId: id,
|
|
4229
|
+
results,
|
|
4230
|
+
verified: false,
|
|
4231
|
+
...unreachable ? { note } : {},
|
|
4232
|
+
...frozenNote
|
|
4233
|
+
};
|
|
3763
4234
|
},
|
|
3764
4235
|
// A stored hash that no longer resolves is a broken anchor, not an absence:
|
|
3765
4236
|
// the file was deleted or the symbol renamed, and exiting zero on it would
|
|
3766
4237
|
// let the one edit that destroys an anchor pass the gate that exists to
|
|
3767
4238
|
// catch it. An anchor nobody ever stamped is still just unstamped, and one
|
|
3768
|
-
//
|
|
3769
|
-
//
|
|
4239
|
+
// whose remote nothing could reach was never checked — failing CI on either
|
|
4240
|
+
// would gate on work this command did not do.
|
|
3770
4241
|
failsWhen: (result) => result.results.some(
|
|
3771
|
-
(entry) => entry.state === "drifted" || entry.state === "unresolved" && entry.storedHash !== void 0 && entry.reason
|
|
4242
|
+
(entry) => entry.state === "drifted" || entry.state === "unresolved" && entry.storedHash !== void 0 && !isUncheckedReason(entry.reason)
|
|
3772
4243
|
)
|
|
3773
4244
|
});
|
|
4245
|
+
function lineDelta(anchor, current) {
|
|
4246
|
+
return anchor.lines === void 0 ? null : Math.abs(current - anchor.lines);
|
|
4247
|
+
}
|
|
4248
|
+
function headHash(source, anchor) {
|
|
4249
|
+
if (source.head === void 0) return void 0;
|
|
4250
|
+
const resolved = resolveAnchor(source.head, anchor);
|
|
4251
|
+
if (!resolved) return void 0;
|
|
4252
|
+
return {
|
|
4253
|
+
hash: hashAnchorText(resolved.text),
|
|
4254
|
+
lines: resolved.endLine - resolved.startLine + 1
|
|
4255
|
+
};
|
|
4256
|
+
}
|
|
4257
|
+
async function readSources(anchors, root, offline) {
|
|
4258
|
+
const origin = new LazyOrigin(root);
|
|
4259
|
+
if (anchors.some((anchor) => anchor.repo)) await origin.prime();
|
|
4260
|
+
const foreign = new Map(
|
|
4261
|
+
anchors.map((anchor) => [anchor, origin.isForeign(anchor)])
|
|
4262
|
+
);
|
|
4263
|
+
const local = anchors.filter((anchor) => !foreign.get(anchor));
|
|
4264
|
+
const remote = anchors.filter((anchor) => foreign.get(anchor));
|
|
4265
|
+
const reads = await readAnchorFiles(
|
|
4266
|
+
local.map((anchor) => anchor.file),
|
|
4267
|
+
anchorFileReader(root)
|
|
4268
|
+
);
|
|
4269
|
+
const blobs = await readRemoteAnchors(remote.flatMap(remoteWants), {
|
|
4270
|
+
offline
|
|
4271
|
+
});
|
|
4272
|
+
const sources = /* @__PURE__ */ new Map();
|
|
4273
|
+
for (const anchor of local) {
|
|
4274
|
+
const read = reads.get(anchor.file);
|
|
4275
|
+
sources.set(
|
|
4276
|
+
anchor,
|
|
4277
|
+
read.ok ? { ok: true, source: read.source } : { ok: false, reason: read.reason }
|
|
4278
|
+
);
|
|
4279
|
+
}
|
|
4280
|
+
for (const anchor of remote) {
|
|
4281
|
+
const repo = anchor.repo;
|
|
4282
|
+
const key2 = normalizeRepoUrl(repo);
|
|
4283
|
+
const atDefault = blobs.get(wantKey(key2, void 0, anchor.file));
|
|
4284
|
+
const primary = anchor.ref ? blobs.get(wantKey(key2, anchor.ref, anchor.file)) : atDefault;
|
|
4285
|
+
if (!primary?.ok) {
|
|
4286
|
+
sources.set(anchor, {
|
|
4287
|
+
ok: false,
|
|
4288
|
+
reason: primary?.ok === false ? primary.reason : "remote-unreachable",
|
|
4289
|
+
repo
|
|
4290
|
+
});
|
|
4291
|
+
continue;
|
|
4292
|
+
}
|
|
4293
|
+
sources.set(anchor, {
|
|
4294
|
+
ok: true,
|
|
4295
|
+
source: primary.source,
|
|
4296
|
+
repo,
|
|
4297
|
+
...anchor.ref && atDefault?.ok ? { head: atDefault.source } : {}
|
|
4298
|
+
});
|
|
4299
|
+
}
|
|
4300
|
+
return sources;
|
|
4301
|
+
}
|
|
3774
4302
|
|
|
3775
4303
|
// src/commands/answer.ts
|
|
3776
4304
|
var import_zod9 = require("zod");
|
|
@@ -3928,8 +4456,8 @@ var days = (what, fallback) => import_zod13.z.number().int().positive().optional
|
|
|
3928
4456
|
var doctorCommand = define({
|
|
3929
4457
|
name: "doctor",
|
|
3930
4458
|
tool: "kb_doctor",
|
|
3931
|
-
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--strict]",
|
|
3932
|
-
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.",
|
|
4459
|
+
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--offline] [--strict]",
|
|
4460
|
+
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.",
|
|
3933
4461
|
input: import_zod13.z.object({
|
|
3934
4462
|
bundlePath,
|
|
3935
4463
|
repoRoot: REPO_ROOT,
|
|
@@ -3945,6 +4473,9 @@ var doctorCommand = define({
|
|
|
3945
4473
|
"How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
|
|
3946
4474
|
DEFAULT_AGING_DAYS
|
|
3947
4475
|
),
|
|
4476
|
+
offline: import_zod13.z.boolean().optional().describe(
|
|
4477
|
+
"Read foreign anchors from the local repo cache only, never fetching."
|
|
4478
|
+
),
|
|
3948
4479
|
strict: import_zod13.z.boolean().optional().describe(
|
|
3949
4480
|
"Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
|
|
3950
4481
|
)
|
|
@@ -3964,13 +4495,23 @@ var doctorCommand = define({
|
|
|
3964
4495
|
...expiring2 !== void 0 ? { expiringDays: Number(expiring2) } : {},
|
|
3965
4496
|
...unverified2 !== void 0 ? { unverifiedDays: Number(unverified2) } : {},
|
|
3966
4497
|
...agingDays !== void 0 ? { agingDays: Number(agingDays) } : {},
|
|
4498
|
+
...argv.includes("--offline") ? { offline: true } : {},
|
|
3967
4499
|
...argv.includes("--strict") ? { strict: true } : {}
|
|
3968
4500
|
};
|
|
3969
4501
|
},
|
|
3970
|
-
run: async ({ store, now }, {
|
|
4502
|
+
run: async ({ store, now }, {
|
|
4503
|
+
bundlePath: path,
|
|
4504
|
+
expiringDays,
|
|
4505
|
+
unverifiedDays,
|
|
4506
|
+
agingDays,
|
|
4507
|
+
repoRoot,
|
|
4508
|
+
offline
|
|
4509
|
+
}) => {
|
|
3971
4510
|
const checkedAt = now();
|
|
3972
4511
|
const records = await store.list(path);
|
|
3973
|
-
const anchorDrift = await store.detectDrift(records, repoRoot
|
|
4512
|
+
const anchorDrift = await store.detectDrift(records, repoRoot, {
|
|
4513
|
+
offline: offline === true
|
|
4514
|
+
});
|
|
3974
4515
|
const report = doctor(records, {
|
|
3975
4516
|
...expiringDays !== void 0 ? { expiringDays } : {},
|
|
3976
4517
|
...unverifiedDays !== void 0 ? { unverifiedDays } : {},
|
|
@@ -4658,7 +5199,7 @@ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
|
4658
5199
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
4659
5200
|
|
|
4660
5201
|
// src/version.ts
|
|
4661
|
-
var VERSION = true ? "0.1.
|
|
5202
|
+
var VERSION = true ? "0.1.15" : "0.0.0-dev";
|
|
4662
5203
|
|
|
4663
5204
|
// src/mcp.ts
|
|
4664
5205
|
function createKbMcpServer() {
|
|
@@ -4697,7 +5238,7 @@ async function runKbMcpServer() {
|
|
|
4697
5238
|
}
|
|
4698
5239
|
|
|
4699
5240
|
// src/cli.ts
|
|
4700
|
-
var
|
|
5241
|
+
var import_node_path9 = require("path");
|
|
4701
5242
|
async function runKbCli(argv) {
|
|
4702
5243
|
const { flags, literal } = takeLiteral(argv);
|
|
4703
5244
|
const { bundle, rest: withFlags } = takeBundle(flags);
|
|
@@ -4754,7 +5295,7 @@ function takeLiteral(argv) {
|
|
|
4754
5295
|
function takeBundle(argv) {
|
|
4755
5296
|
const at = argv.indexOf("--bundle");
|
|
4756
5297
|
if (at === -1) {
|
|
4757
|
-
return { bundle: (0,
|
|
5298
|
+
return { bundle: (0, import_node_path9.join)(process.cwd(), KB_DIR), rest: argv };
|
|
4758
5299
|
}
|
|
4759
5300
|
const bundle = argv[at + 1];
|
|
4760
5301
|
if (!bundle) die("--bundle requires a path");
|
|
@@ -4869,6 +5410,7 @@ function usage() {
|
|
|
4869
5410
|
impact,
|
|
4870
5411
|
inboundIndex,
|
|
4871
5412
|
indexIsStale,
|
|
5413
|
+
isCanonicalRepoUrl,
|
|
4872
5414
|
isKbLinkRel,
|
|
4873
5415
|
isKbRecordType,
|
|
4874
5416
|
isNoDecisionRecord,
|
|
@@ -4892,11 +5434,13 @@ function usage() {
|
|
|
4892
5434
|
pinBase,
|
|
4893
5435
|
readMergedPins,
|
|
4894
5436
|
readPinsLayer,
|
|
5437
|
+
readRemoteAnchors,
|
|
4895
5438
|
regexResolver,
|
|
4896
5439
|
renderCatalogLine,
|
|
4897
5440
|
renderIndex,
|
|
4898
5441
|
renderIndexLine,
|
|
4899
5442
|
renderLogEntry,
|
|
5443
|
+
repoCacheDir,
|
|
4900
5444
|
resolveAnchor,
|
|
4901
5445
|
resolveHeads,
|
|
4902
5446
|
resolveHits,
|