@saasontools/strauss-kb 0.1.16 → 0.1.18
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 +48 -5
- package/README.md +44 -6
- package/dist/{chunk-H5W53NVU.js → chunk-GSOTMWZZ.js} +1765 -342
- package/dist/chunk-GSOTMWZZ.js.map +1 -0
- package/dist/{chunk-RINBOAQZ.js → chunk-IOIUS26S.js} +2 -2
- package/dist/{chunk-MEZCF646.js → chunk-ROGVYSMV.js} +2 -2
- package/dist/cli-main.cjs +1772 -363
- package/dist/cli-main.cjs.map +1 -1
- package/dist/cli-main.js +2 -2
- package/dist/index.cjs +1825 -381
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +504 -9
- package/dist/index.d.ts +504 -9
- package/dist/index.js +29 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp-main.cjs +1761 -352
- package/dist/mcp-main.cjs.map +1 -1
- package/dist/mcp-main.js +2 -2
- package/grammars/README.md +78 -0
- package/grammars/manifest.json +595 -0
- package/grammars/packs.json +127 -0
- package/package.json +7 -2
- package/dist/chunk-H5W53NVU.js.map +0 -1
- /package/dist/{chunk-RINBOAQZ.js.map → chunk-IOIUS26S.js.map} +0 -0
- /package/dist/{chunk-MEZCF646.js.map → chunk-ROGVYSMV.js.map} +0 -0
|
@@ -40,10 +40,24 @@ var kbAnchorSchema = z.object({
|
|
|
40
40
|
hash: z.string().regex(/^sha256:[0-9a-f]{64}$/, {
|
|
41
41
|
message: "hash must be sha256:<64 hex chars>"
|
|
42
42
|
}).optional(),
|
|
43
|
+
/**
|
|
44
|
+
* What `hash` was taken over: the span's raw text, or the normalised token
|
|
45
|
+
* stream a parser sees (`ast`). Absent means `raw`, which is what every
|
|
46
|
+
* anchor stamped before this field carries, so old hashes keep comparing
|
|
47
|
+
* the way they were written. An `ast` hash is blind to whitespace and
|
|
48
|
+
* comments, so reformatting the anchored code is not drift.
|
|
49
|
+
*/
|
|
50
|
+
hash_kind: z.enum(["raw", "ast"]).optional(),
|
|
43
51
|
/** ISO 8601 timestamp of the last successful resolution. */
|
|
44
52
|
resolved_at: z.string().min(1).optional(),
|
|
45
53
|
/** Line count of the text the hash was taken over. */
|
|
46
|
-
lines: z.number().int().positive().optional()
|
|
54
|
+
lines: z.number().int().positive().optional(),
|
|
55
|
+
/**
|
|
56
|
+
* Which resolver produced the hashed span. Absent means an anchor stamped
|
|
57
|
+
* before resolvers were named, which is read as `regex` — the only one
|
|
58
|
+
* there was. A hash from a different resolver is drift, not a match.
|
|
59
|
+
*/
|
|
60
|
+
resolver: z.enum(["tree-sitter", "regex"]).optional()
|
|
47
61
|
}).strict();
|
|
48
62
|
var kbLinkSchema = z.object({
|
|
49
63
|
target: z.string().min(1),
|
|
@@ -518,8 +532,8 @@ function safeSegment(value) {
|
|
|
518
532
|
function revRef(rev) {
|
|
519
533
|
const safe = rev.replace(/[^A-Za-z0-9_-]/g, "-").slice(0, 64);
|
|
520
534
|
let hash = 5381;
|
|
521
|
-
for (let
|
|
522
|
-
hash = (hash * 33 ^ rev.charCodeAt(
|
|
535
|
+
for (let at2 = 0; at2 < rev.length; at2++) {
|
|
536
|
+
hash = (hash * 33 ^ rev.charCodeAt(at2)) >>> 0;
|
|
523
537
|
}
|
|
524
538
|
return `refs/strauss/${safe}-${hash.toString(16)}`;
|
|
525
539
|
}
|
|
@@ -542,9 +556,9 @@ async function mapLimit(items, limit, fn) {
|
|
|
542
556
|
{ length: Math.min(limit, items.length) },
|
|
543
557
|
async () => {
|
|
544
558
|
while (!failed && next < items.length) {
|
|
545
|
-
const
|
|
559
|
+
const at2 = next++;
|
|
546
560
|
try {
|
|
547
|
-
out[
|
|
561
|
+
out[at2] = await fn(items[at2], at2);
|
|
548
562
|
} catch (error) {
|
|
549
563
|
failed = true;
|
|
550
564
|
throw error;
|
|
@@ -657,8 +671,8 @@ function repoUrlIsSafe(repo) {
|
|
|
657
671
|
if (!scheme?.[1]) return false;
|
|
658
672
|
if (!allowed.includes(scheme[1].toLowerCase())) return false;
|
|
659
673
|
const authority = url.slice(scheme[0].length).split("/")[0] ?? "";
|
|
660
|
-
const
|
|
661
|
-
return
|
|
674
|
+
const at2 = authority.lastIndexOf("@");
|
|
675
|
+
return at2 < 0 || !authority.slice(0, at2).includes(":");
|
|
662
676
|
}
|
|
663
677
|
function protocolArgs() {
|
|
664
678
|
const allowed = allowedProtocols();
|
|
@@ -709,18 +723,18 @@ async function readOneRepo(repo, url, declared, context) {
|
|
|
709
723
|
if (!repoUrlIsSafe(url)) return all({ ok: false, reason: "repo-invalid" });
|
|
710
724
|
const cache = cachePathFor(repo, context.cacheDir);
|
|
711
725
|
if (!cache) return all({ ok: false, reason: "remote-unreachable" });
|
|
712
|
-
const
|
|
726
|
+
const rejected2 = /* @__PURE__ */ new Map();
|
|
713
727
|
const usable = [];
|
|
714
728
|
for (const want of wants) {
|
|
715
729
|
const reason = wantReason(want);
|
|
716
730
|
if (reason)
|
|
717
|
-
|
|
731
|
+
rejected2.set(wantKey(repo, want.ref, want.file), { ok: false, reason });
|
|
718
732
|
else usable.push(want);
|
|
719
733
|
}
|
|
720
|
-
if (!usable.length) return
|
|
734
|
+
if (!usable.length) return rejected2;
|
|
721
735
|
wants = usable;
|
|
722
736
|
const opened = await openCache(cache, url, context);
|
|
723
|
-
if (opened) return new Map([...
|
|
737
|
+
if (opened) return new Map([...rejected2, ...all(opened)]);
|
|
724
738
|
const wantsDefault = wants.some((want) => want.ref === void 0);
|
|
725
739
|
const branch = wantsDefault ? await defaultBranch(cache, context) : {};
|
|
726
740
|
const revs = /* @__PURE__ */ new Map();
|
|
@@ -747,9 +761,9 @@ async function readOneRepo(repo, url, declared, context) {
|
|
|
747
761
|
}
|
|
748
762
|
);
|
|
749
763
|
return new Map([
|
|
750
|
-
...
|
|
764
|
+
...rejected2,
|
|
751
765
|
...wants.map(
|
|
752
|
-
(want,
|
|
766
|
+
(want, at2) => [wantKey(repo, want.ref, want.file), reads[at2]]
|
|
753
767
|
)
|
|
754
768
|
]);
|
|
755
769
|
}
|
|
@@ -799,13 +813,13 @@ async function defaultBranch(cache, context) {
|
|
|
799
813
|
if (!listed.ok) {
|
|
800
814
|
const reason = transportReason(listed.stderr);
|
|
801
815
|
if (reason !== "ref-not-found") {
|
|
802
|
-
const
|
|
803
|
-
return
|
|
816
|
+
const cached3 = await cachedBranch(cache);
|
|
817
|
+
return cached3 ? { name: cached3 } : { reason };
|
|
804
818
|
}
|
|
805
819
|
}
|
|
806
820
|
}
|
|
807
|
-
const
|
|
808
|
-
if (
|
|
821
|
+
const cached2 = await cachedBranch(cache);
|
|
822
|
+
if (cached2) return { name: cached2 };
|
|
809
823
|
return {
|
|
810
824
|
reason: context.offline ? "remote-unreachable" : "default-branch-unknown"
|
|
811
825
|
};
|
|
@@ -832,8 +846,8 @@ async function ensureRev(cache, rev, context) {
|
|
|
832
846
|
cwd: cache
|
|
833
847
|
}
|
|
834
848
|
);
|
|
835
|
-
const
|
|
836
|
-
if (
|
|
849
|
+
const cached2 = have.ok && have.stdout.trim().length > 0;
|
|
850
|
+
if (cached2 && (context.offline || IMMUTABLE_REV.test(rev))) return void 0;
|
|
837
851
|
if (context.offline) return { ok: false, reason: "remote-unreachable" };
|
|
838
852
|
const fetched = await git(
|
|
839
853
|
[
|
|
@@ -849,7 +863,7 @@ async function ensureRev(cache, rev, context) {
|
|
|
849
863
|
);
|
|
850
864
|
if (!fetched.ok) {
|
|
851
865
|
const reason = transportReason(fetched.stderr);
|
|
852
|
-
if (
|
|
866
|
+
if (cached2 && reason !== "ref-not-found") return void 0;
|
|
853
867
|
return { ok: false, reason };
|
|
854
868
|
}
|
|
855
869
|
const head = await git(["rev-parse", "FETCH_HEAD"], { cwd: cache });
|
|
@@ -960,67 +974,654 @@ async function readAnchorFiles(files, read, concurrency = DEFAULT_IO_CONCURRENCY
|
|
|
960
974
|
return { ok: false, reason: "file-unreadable" };
|
|
961
975
|
}
|
|
962
976
|
});
|
|
963
|
-
return new Map(wanted.map((file,
|
|
977
|
+
return new Map(wanted.map((file, at2) => [file, results[at2]]));
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// src/grammars/store.ts
|
|
981
|
+
import { createHash, randomBytes } from "crypto";
|
|
982
|
+
import { mkdir as mkdir2, readFile as readFile2, rename, rm, writeFile } from "fs/promises";
|
|
983
|
+
import { homedir as homedir2 } from "os";
|
|
984
|
+
import { dirname, join as join2 } from "path";
|
|
985
|
+
function grammarsCacheRoot(override) {
|
|
986
|
+
return override ?? process.env["STRAUSS_KB_GRAMMARS_DIR"] ?? join2(homedir2(), ".strauss", "grammars");
|
|
987
|
+
}
|
|
988
|
+
function grammarCachePath(root, language, sha2564, extension = "wasm") {
|
|
989
|
+
return join2(root, language, `${sha2564.slice(0, 12)}.${extension}`);
|
|
990
|
+
}
|
|
991
|
+
function sha256(bytes) {
|
|
992
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
993
|
+
}
|
|
994
|
+
function matches(bytes, entry) {
|
|
995
|
+
if (entry.bytes !== void 0 && bytes.byteLength !== entry.bytes)
|
|
996
|
+
return false;
|
|
997
|
+
return sha256(bytes) === entry.sha256;
|
|
998
|
+
}
|
|
999
|
+
async function verifyCached(path, entry) {
|
|
1000
|
+
let bytes;
|
|
1001
|
+
try {
|
|
1002
|
+
bytes = await readFile2(path);
|
|
1003
|
+
} catch {
|
|
1004
|
+
return false;
|
|
1005
|
+
}
|
|
1006
|
+
if (matches(bytes, entry)) return true;
|
|
1007
|
+
await rm(path, { force: true });
|
|
1008
|
+
return false;
|
|
1009
|
+
}
|
|
1010
|
+
async function writeCached(path, bytes) {
|
|
1011
|
+
await mkdir2(dirname(path), { recursive: true });
|
|
1012
|
+
const temporary = `${path}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
|
|
1013
|
+
try {
|
|
1014
|
+
await writeFile(temporary, bytes);
|
|
1015
|
+
await rename(temporary, path);
|
|
1016
|
+
} catch (error) {
|
|
1017
|
+
await rm(temporary, { force: true });
|
|
1018
|
+
throw error;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// src/grammars/manifest.ts
|
|
1023
|
+
import { existsSync, readFileSync } from "fs";
|
|
1024
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
1025
|
+
import { fileURLToPath } from "url";
|
|
1026
|
+
|
|
1027
|
+
// src/grammars/model.ts
|
|
1028
|
+
import { z as z4 } from "zod";
|
|
1029
|
+
var sha2562 = z4.string().regex(/^[0-9a-f]{64}$/);
|
|
1030
|
+
var grammarWasmSchema = z4.object({
|
|
1031
|
+
url: z4.string().min(1),
|
|
1032
|
+
sha256: sha2562,
|
|
1033
|
+
bytes: z4.number().int().positive()
|
|
1034
|
+
});
|
|
1035
|
+
var grammarTagsSchema = z4.object({ url: z4.string().min(1), sha256: sha2562 });
|
|
1036
|
+
var grammarPackSchema = z4.object({
|
|
1037
|
+
package: z4.string().min(1),
|
|
1038
|
+
wasm: grammarWasmSchema,
|
|
1039
|
+
tags: z4.array(grammarTagsSchema),
|
|
1040
|
+
license: z4.string().min(1),
|
|
1041
|
+
extensions: z4.array(z4.string().min(1))
|
|
1042
|
+
});
|
|
1043
|
+
var grammarManifestSchema = z4.object({
|
|
1044
|
+
/** The runtime the packs were proved against. */
|
|
1045
|
+
webTreeSitter: z4.string().min(1),
|
|
1046
|
+
linguist: z4.object({ tag: z4.string().min(1), commit: z4.string().min(1) }),
|
|
1047
|
+
packs: z4.record(z4.string().min(1), grammarPackSchema)
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
// src/grammars/manifest.ts
|
|
1051
|
+
var cached;
|
|
1052
|
+
function grammarManifest() {
|
|
1053
|
+
cached ??= grammarManifestSchema.parse(
|
|
1054
|
+
JSON.parse(readFileSync(grammarsDataPath("manifest.json"), "utf8"))
|
|
1055
|
+
);
|
|
1056
|
+
return cached;
|
|
1057
|
+
}
|
|
1058
|
+
function grammarsDataPath(...segments) {
|
|
1059
|
+
let dir = dirname2(fileURLToPath(import.meta.url));
|
|
1060
|
+
for (let up = 0; up < 5; up++) {
|
|
1061
|
+
const candidate = join3(dir, "grammars");
|
|
1062
|
+
if (existsSync(join3(candidate, "manifest.json")))
|
|
1063
|
+
return join3(candidate, ...segments);
|
|
1064
|
+
dir = dirname2(dir);
|
|
1065
|
+
}
|
|
1066
|
+
throw new Error("grammars/manifest.json is missing from the package");
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// src/grammars/index.ts
|
|
1070
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
1071
|
+
|
|
1072
|
+
// src/grammars/fetch.ts
|
|
1073
|
+
var ATTEMPTS = 3;
|
|
1074
|
+
var BACKOFF_MS = 250;
|
|
1075
|
+
function grammarUrl(url, override) {
|
|
1076
|
+
const base2 = grammarsBaseUrl(override);
|
|
1077
|
+
if (!base2) return url;
|
|
1078
|
+
let root = base2;
|
|
1079
|
+
while (root.endsWith("/")) root = root.slice(0, -1);
|
|
1080
|
+
const pinned = new URL(url);
|
|
1081
|
+
return `${root}${pinned.pathname}${pinned.search}`;
|
|
1082
|
+
}
|
|
1083
|
+
function grammarsBaseUrl(override) {
|
|
1084
|
+
return override ?? process.env["STRAUSS_KB_GRAMMARS_URL"];
|
|
1085
|
+
}
|
|
1086
|
+
async function downloadPart(url, name, entry, options = {}) {
|
|
1087
|
+
const log = options.log ?? ((line) => void process.stderr.write(line));
|
|
1088
|
+
const weight = entry.bytes === void 0 ? "" : ` (${size(entry.bytes)} from manifest)`;
|
|
1089
|
+
log(`strauss-kb: downloading ${name}${weight} from ${url}
|
|
1090
|
+
`);
|
|
1091
|
+
let cause = "";
|
|
1092
|
+
for (let attempt = 1; attempt <= ATTEMPTS; attempt++) {
|
|
1093
|
+
const outcome = await attemptDownload(url, entry, options.fetchTimeoutMs);
|
|
1094
|
+
if ("bytes" in outcome) return outcome;
|
|
1095
|
+
cause = outcome.cause;
|
|
1096
|
+
log(
|
|
1097
|
+
`strauss-kb: ${name} attempt ${attempt}/${ATTEMPTS} failed: ${cause}
|
|
1098
|
+
`
|
|
1099
|
+
);
|
|
1100
|
+
if (!outcome.retry) break;
|
|
1101
|
+
if (attempt < ATTEMPTS) await pause(BACKOFF_MS * attempt);
|
|
1102
|
+
}
|
|
1103
|
+
log(`strauss-kb: ${name} not downloaded: ${cause}
|
|
1104
|
+
`);
|
|
1105
|
+
return { cause };
|
|
1106
|
+
}
|
|
1107
|
+
async function attemptDownload(url, entry, timeoutMs) {
|
|
1108
|
+
try {
|
|
1109
|
+
const response = await fetch(url, {
|
|
1110
|
+
signal: AbortSignal.timeout(fetchTimeoutMs(timeoutMs))
|
|
1111
|
+
});
|
|
1112
|
+
if (!response.ok) {
|
|
1113
|
+
return {
|
|
1114
|
+
cause: `HTTP ${response.status}`,
|
|
1115
|
+
retry: response.status >= 500 || response.status === 429
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
1119
|
+
if (!matches(bytes, entry))
|
|
1120
|
+
return { cause: "sha256 mismatch", retry: false };
|
|
1121
|
+
return { bytes };
|
|
1122
|
+
} catch (error) {
|
|
1123
|
+
const timedOut = error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
|
|
1124
|
+
return { cause: timedOut ? "timeout" : "network error", retry: true };
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
function size(bytes) {
|
|
1128
|
+
return bytes >= 1024 * 1024 ? `${(bytes / (1024 * 1024)).toFixed(1)} MB` : `${Math.round(bytes / 1024)} KB`;
|
|
1129
|
+
}
|
|
1130
|
+
function pause(ms) {
|
|
1131
|
+
return new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// src/grammars/index.ts
|
|
1135
|
+
var inFlight = /* @__PURE__ */ new Map();
|
|
1136
|
+
var missing = /* @__PURE__ */ new Map();
|
|
1137
|
+
var uncompilable = /* @__PURE__ */ new Map();
|
|
1138
|
+
var rejected = /* @__PURE__ */ new Map();
|
|
1139
|
+
function grammarsDownloadDisabled() {
|
|
1140
|
+
return process.env["STRAUSS_KB_GRAMMARS"] === "off";
|
|
1141
|
+
}
|
|
1142
|
+
async function ensureGrammar(language, options = {}) {
|
|
1143
|
+
const pack2 = grammarManifest().packs[language];
|
|
1144
|
+
if (!pack2) return null;
|
|
1145
|
+
const root = grammarsCacheRoot(options.cacheRoot);
|
|
1146
|
+
const wasm = grammarCachePath(root, language, pack2.wasm.sha256);
|
|
1147
|
+
const key = `${wasm} ${grammarsBaseUrl(options.baseUrl) ?? ""}`;
|
|
1148
|
+
const existing = inFlight.get(key);
|
|
1149
|
+
if (existing) return existing;
|
|
1150
|
+
const pending = (async () => {
|
|
1151
|
+
const grammar = await ensurePart(
|
|
1152
|
+
wasm,
|
|
1153
|
+
`tree-sitter-${language}`,
|
|
1154
|
+
pack2.wasm,
|
|
1155
|
+
options
|
|
1156
|
+
);
|
|
1157
|
+
if (grammar !== true)
|
|
1158
|
+
return miss(language, `grammar tree-sitter-${language}`, grammar);
|
|
1159
|
+
const parts = [];
|
|
1160
|
+
const total = pack2.tags.length;
|
|
1161
|
+
for (const [at2, part] of pack2.tags.entries()) {
|
|
1162
|
+
const name = `${language} tags${total > 1 ? ` part ${at2 + 1}/${total}` : ""}`;
|
|
1163
|
+
const path = grammarCachePath(root, language, part.sha256, "scm");
|
|
1164
|
+
const held = await ensurePart(path, name, part, options);
|
|
1165
|
+
if (held !== true) return miss(language, name, held);
|
|
1166
|
+
parts.push(`; ${part.url}
|
|
1167
|
+
${lf(await readFile3(path, "utf8"))}`);
|
|
1168
|
+
}
|
|
1169
|
+
missing.delete(language);
|
|
1170
|
+
return { wasm, query: total ? parts.join("\n") : void 0 };
|
|
1171
|
+
})();
|
|
1172
|
+
inFlight.set(key, pending);
|
|
1173
|
+
const result = await pending;
|
|
1174
|
+
if (result === null) inFlight.delete(key);
|
|
1175
|
+
return result;
|
|
1176
|
+
}
|
|
1177
|
+
async function ensurePart(path, name, entry, options) {
|
|
1178
|
+
if (await verifyCached(path, entry)) return true;
|
|
1179
|
+
if (options.offline === true || grammarsDownloadDisabled()) return {};
|
|
1180
|
+
const download = await downloadPart(
|
|
1181
|
+
grammarUrl(entry.url, options.baseUrl),
|
|
1182
|
+
name,
|
|
1183
|
+
entry,
|
|
1184
|
+
options
|
|
1185
|
+
);
|
|
1186
|
+
if ("cause" in download) return { cause: download.cause };
|
|
1187
|
+
await writeCached(path, download.bytes).catch(() => null);
|
|
1188
|
+
return true;
|
|
1189
|
+
}
|
|
1190
|
+
function miss(language, subject, failure) {
|
|
1191
|
+
missing.set(language, { subject, ...failure });
|
|
1192
|
+
return null;
|
|
1193
|
+
}
|
|
1194
|
+
function lf(body) {
|
|
1195
|
+
return body.replace(/\r\n/g, "\n");
|
|
1196
|
+
}
|
|
1197
|
+
function noteUncompilableQuery(language, cause) {
|
|
1198
|
+
uncompilable.set(language, cause);
|
|
1199
|
+
}
|
|
1200
|
+
function noteRejectedGrammar(language, cause) {
|
|
1201
|
+
rejected.set(language, cause);
|
|
1202
|
+
}
|
|
1203
|
+
function grammarHints() {
|
|
1204
|
+
const manifest = grammarManifest();
|
|
1205
|
+
const packs = manifest.packs;
|
|
1206
|
+
const lines = /* @__PURE__ */ new Map();
|
|
1207
|
+
for (const [language, { subject, cause }] of missing)
|
|
1208
|
+
lines.set(
|
|
1209
|
+
language,
|
|
1210
|
+
`${subject} not cached${cause ? ` (${cause})` : ""}; run online once, or set STRAUSS_KB_GRAMMARS_DIR`
|
|
1211
|
+
);
|
|
1212
|
+
for (const [language, cause] of rejected)
|
|
1213
|
+
lines.set(
|
|
1214
|
+
language,
|
|
1215
|
+
`${packs[language]?.package ?? `tree-sitter-${language}`} rejected by web-tree-sitter ${manifest.webTreeSitter}${cause ? `: ${cause}` : ""}; re-pin with pnpm grammars pin ${language}`
|
|
1216
|
+
);
|
|
1217
|
+
for (const [language, cause] of uncompilable)
|
|
1218
|
+
lines.set(
|
|
1219
|
+
language,
|
|
1220
|
+
`tags query for ${language} does not compile against ${packs[language]?.package ?? `tree-sitter-${language}`}: ${cause}; re-pin with pnpm grammars pin ${language}`
|
|
1221
|
+
);
|
|
1222
|
+
return [...lines].sort(([a], [b]) => a.localeCompare(b)).map(([, line]) => line);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// src/tree-sitter-resolver/languages.ts
|
|
1226
|
+
import { extname } from "path";
|
|
1227
|
+
var table;
|
|
1228
|
+
function extensionTable() {
|
|
1229
|
+
const manifest = grammarManifest();
|
|
1230
|
+
if (table?.of !== manifest)
|
|
1231
|
+
table = {
|
|
1232
|
+
of: manifest,
|
|
1233
|
+
extensions: Object.fromEntries(
|
|
1234
|
+
Object.entries(manifest.packs).flatMap(
|
|
1235
|
+
([language, pack2]) => pack2.extensions.map((extension) => [extension, language])
|
|
1236
|
+
)
|
|
1237
|
+
)
|
|
1238
|
+
};
|
|
1239
|
+
return table.extensions;
|
|
1240
|
+
}
|
|
1241
|
+
function hasQuery(language) {
|
|
1242
|
+
return (grammarManifest().packs[language]?.tags.length ?? 0) > 0;
|
|
1243
|
+
}
|
|
1244
|
+
function languageForFile(file) {
|
|
1245
|
+
const language = extensionTable()[extname(file).toLowerCase()];
|
|
1246
|
+
return language && hasQuery(language) ? language : void 0;
|
|
1247
|
+
}
|
|
1248
|
+
function treeSitterLanguages() {
|
|
1249
|
+
return [...new Set(Object.values(extensionTable()))].filter(hasQuery).sort();
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// src/tree-sitter-resolver/resolver.ts
|
|
1253
|
+
import { createHash as createHash2 } from "crypto";
|
|
1254
|
+
import { Language, Parser, Query } from "web-tree-sitter";
|
|
1255
|
+
|
|
1256
|
+
// src/tree-sitter-resolver/definitions.ts
|
|
1257
|
+
var SCOPE_ONLY = "reference.implementation";
|
|
1258
|
+
function index(tree, query) {
|
|
1259
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1260
|
+
for (const match of query.matches(tree.rootNode)) {
|
|
1261
|
+
const nameNode = match.captures.find((capture) => capture.name === "name");
|
|
1262
|
+
const defNode = match.captures.find(
|
|
1263
|
+
(capture) => capture.name.startsWith("definition.") || capture.name === SCOPE_ONLY
|
|
1264
|
+
);
|
|
1265
|
+
if (!nameNode || !defNode) continue;
|
|
1266
|
+
const candidate = {
|
|
1267
|
+
node: defNode.node,
|
|
1268
|
+
name: nameNode.node.text,
|
|
1269
|
+
target: defNode.name !== SCOPE_ONLY
|
|
1270
|
+
};
|
|
1271
|
+
const existing = byName.get(nameNode.node.id);
|
|
1272
|
+
if (existing && width(existing.node) <= width(candidate.node)) continue;
|
|
1273
|
+
byName.set(nameNode.node.id, candidate);
|
|
1274
|
+
}
|
|
1275
|
+
const definitions = [...byName.values()];
|
|
1276
|
+
return {
|
|
1277
|
+
tree,
|
|
1278
|
+
byNodeId: new Map(
|
|
1279
|
+
definitions.map((definition) => [definition.node.id, definition])
|
|
1280
|
+
),
|
|
1281
|
+
definitions
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
function select(parsed, wanted) {
|
|
1285
|
+
const matches3 = parsed.definitions.filter(
|
|
1286
|
+
(definition) => definition.target && endsWith(chainOf(definition, parsed.byNodeId), wanted)
|
|
1287
|
+
);
|
|
1288
|
+
if (matches3.length < 2) return matches3;
|
|
1289
|
+
const bodied = matches3.filter(
|
|
1290
|
+
(definition) => definition.node.childForFieldName("body") !== null
|
|
1291
|
+
);
|
|
1292
|
+
return bodied.length === 1 ? bodied : matches3;
|
|
1293
|
+
}
|
|
1294
|
+
function chainOf(definition, byNodeId) {
|
|
1295
|
+
const chain = [definition.name];
|
|
1296
|
+
const receiver = definition.node.childForFieldName("receiver");
|
|
1297
|
+
const type = receiver && typeNameIn(receiver);
|
|
1298
|
+
if (type) chain.unshift(type);
|
|
1299
|
+
for (let node = definition.node.parent; node; node = node.parent) {
|
|
1300
|
+
const enclosing = byNodeId.get(node.id);
|
|
1301
|
+
if (enclosing && enclosing.node !== definition.node)
|
|
1302
|
+
chain.unshift(enclosing.name);
|
|
1303
|
+
}
|
|
1304
|
+
return chain;
|
|
1305
|
+
}
|
|
1306
|
+
function typeNameIn(receiver) {
|
|
1307
|
+
const stack = [receiver];
|
|
1308
|
+
while (stack.length) {
|
|
1309
|
+
const node = stack.pop();
|
|
1310
|
+
if (node.type === "type_identifier") return node.text;
|
|
1311
|
+
for (let at2 = 0; at2 < node.childCount; at2++) {
|
|
1312
|
+
const child = node.child(at2);
|
|
1313
|
+
if (child) stack.push(child);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
return void 0;
|
|
1317
|
+
}
|
|
1318
|
+
function endsWith(chain, wanted) {
|
|
1319
|
+
if (wanted.length > chain.length) return false;
|
|
1320
|
+
const offset = chain.length - wanted.length;
|
|
1321
|
+
return wanted.every((segment, at2) => chain[offset + at2] === segment);
|
|
1322
|
+
}
|
|
1323
|
+
function width(node) {
|
|
1324
|
+
return node.endIndex - node.startIndex;
|
|
1325
|
+
}
|
|
1326
|
+
function spanOf(definition, source) {
|
|
1327
|
+
let start = definition.node;
|
|
1328
|
+
let end = definition.node;
|
|
1329
|
+
for (let sibling = start.previousSibling; sibling?.type === "decorator"; sibling = sibling.previousSibling) {
|
|
1330
|
+
start = sibling;
|
|
1331
|
+
}
|
|
1332
|
+
const parent = end.parent;
|
|
1333
|
+
if (parent?.type === "export_statement" && parent.childForFieldName("declaration")?.id === end.id) {
|
|
1334
|
+
start = parent;
|
|
1335
|
+
end = parent;
|
|
1336
|
+
}
|
|
1337
|
+
const lines = source.split("\n");
|
|
1338
|
+
const startLine = start.startPosition.row;
|
|
1339
|
+
const endLine = end.endPosition.column === 0 && end.endPosition.row > startLine ? end.endPosition.row - 1 : end.endPosition.row;
|
|
1340
|
+
return {
|
|
1341
|
+
text: lines.slice(startLine, endLine + 1).join("\n"),
|
|
1342
|
+
startLine: startLine + 1,
|
|
1343
|
+
endLine: endLine + 1
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// src/tree-sitter-resolver/tokens.ts
|
|
1348
|
+
function tokens(root) {
|
|
1349
|
+
const out = [];
|
|
1350
|
+
const stack = [root];
|
|
1351
|
+
while (stack.length) {
|
|
1352
|
+
const node = stack.pop();
|
|
1353
|
+
if (node.type.includes("comment")) continue;
|
|
1354
|
+
if (node.childCount === 0) {
|
|
1355
|
+
const text = node.text.trim();
|
|
1356
|
+
if (text) out.push(text);
|
|
1357
|
+
continue;
|
|
1358
|
+
}
|
|
1359
|
+
for (let at2 = node.childCount - 1; at2 >= 0; at2--) {
|
|
1360
|
+
const child = node.child(at2);
|
|
1361
|
+
if (child) stack.push(child);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
return out;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// src/tree-sitter-resolver/resolver.ts
|
|
1368
|
+
var TREE_CACHE_LIMIT = 32;
|
|
1369
|
+
var TreeSitterResolver = class {
|
|
1370
|
+
name = "tree-sitter";
|
|
1371
|
+
grammars;
|
|
1372
|
+
loaded = /* @__PURE__ */ new Map();
|
|
1373
|
+
trees = /* @__PURE__ */ new Map();
|
|
1374
|
+
parser;
|
|
1375
|
+
initialized = false;
|
|
1376
|
+
/** Cache effectiveness, for tests and for the latency numbers. */
|
|
1377
|
+
stats = { parses: 0, cacheHits: 0 };
|
|
1378
|
+
constructor(options = {}) {
|
|
1379
|
+
this.grammars = options;
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* Loads the grammars these files need, once per language per process,
|
|
1383
|
+
* downloading each one on first use.
|
|
1384
|
+
*
|
|
1385
|
+
* A grammar that will not load is remembered as unavailable rather than
|
|
1386
|
+
* retried per anchor, and never throws: an unobtainable WASM is a finding.
|
|
1387
|
+
*/
|
|
1388
|
+
async prepare(files) {
|
|
1389
|
+
const wanted = /* @__PURE__ */ new Set();
|
|
1390
|
+
for (const file of files) {
|
|
1391
|
+
const language = languageForFile(file);
|
|
1392
|
+
if (language && !this.loaded.has(language)) wanted.add(language);
|
|
1393
|
+
}
|
|
1394
|
+
if (!wanted.size) return;
|
|
1395
|
+
if (!this.initialized) {
|
|
1396
|
+
try {
|
|
1397
|
+
await Parser.init();
|
|
1398
|
+
this.parser = new Parser();
|
|
1399
|
+
this.initialized = true;
|
|
1400
|
+
} catch {
|
|
1401
|
+
for (const language of wanted) this.loaded.set(language, null);
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
const languages = [...wanted];
|
|
1406
|
+
const loaded = await mapLimit(
|
|
1407
|
+
languages,
|
|
1408
|
+
Math.min(DEFAULT_IO_CONCURRENCY, languages.length),
|
|
1409
|
+
(language) => this.load(language)
|
|
1410
|
+
);
|
|
1411
|
+
languages.forEach(
|
|
1412
|
+
(language, at2) => this.loaded.set(language, loaded[at2] ?? null)
|
|
1413
|
+
);
|
|
1414
|
+
}
|
|
1415
|
+
/**
|
|
1416
|
+
* An unobtainable grammar, one this runtime refuses, and a query that will
|
|
1417
|
+
* not compile are three faults with three repairs; all are reported through
|
|
1418
|
+
* the grammars module so every hint has one home.
|
|
1419
|
+
*/
|
|
1420
|
+
async load(language) {
|
|
1421
|
+
let pack2;
|
|
1422
|
+
try {
|
|
1423
|
+
pack2 = await ensureGrammar(language, this.grammars);
|
|
1424
|
+
} catch {
|
|
1425
|
+
return null;
|
|
1426
|
+
}
|
|
1427
|
+
if (!pack2?.query) return null;
|
|
1428
|
+
let grammar;
|
|
1429
|
+
try {
|
|
1430
|
+
grammar = await Language.load(pack2.wasm);
|
|
1431
|
+
} catch (error) {
|
|
1432
|
+
noteRejectedGrammar(language, why(error));
|
|
1433
|
+
return null;
|
|
1434
|
+
}
|
|
1435
|
+
try {
|
|
1436
|
+
return { language: grammar, query: new Query(grammar, pack2.query) };
|
|
1437
|
+
} catch (error) {
|
|
1438
|
+
noteUncompilableQuery(language, why(error));
|
|
1439
|
+
return null;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
/**
|
|
1443
|
+
* Abstains on an extension with no grammar so the regex resolver gets a
|
|
1444
|
+
* turn; reports `resolver-unavailable` when the grammar exists in principle
|
|
1445
|
+
* but could not be loaded, because falling back there would silently trade a
|
|
1446
|
+
* precise span for a guessed one.
|
|
1447
|
+
*/
|
|
1448
|
+
attempt(source, symbol, file) {
|
|
1449
|
+
const language = file ? languageForFile(file) : void 0;
|
|
1450
|
+
if (!language) return { kind: "abstain" };
|
|
1451
|
+
if (!this.loaded.has(language)) return { kind: "abstain" };
|
|
1452
|
+
const loaded = this.loaded.get(language);
|
|
1453
|
+
if (!loaded) return { kind: "unresolved", reason: "resolver-unavailable" };
|
|
1454
|
+
const parsed = this.parse(language, loaded, source);
|
|
1455
|
+
if (!parsed) return { kind: "unresolved", reason: "resolver-unavailable" };
|
|
1456
|
+
const wanted = symbol.split(".").filter(Boolean);
|
|
1457
|
+
if (!wanted.length)
|
|
1458
|
+
return { kind: "unresolved", reason: "symbol-not-found" };
|
|
1459
|
+
const matches3 = select(parsed, wanted);
|
|
1460
|
+
if (!matches3.length)
|
|
1461
|
+
return { kind: "unresolved", reason: "symbol-not-found" };
|
|
1462
|
+
if (matches3.length > 1)
|
|
1463
|
+
return { kind: "unresolved", reason: "symbol-ambiguous" };
|
|
1464
|
+
return { kind: "resolved", span: spanOf(matches3[0], source) };
|
|
1465
|
+
}
|
|
1466
|
+
resolve(source, symbol, file) {
|
|
1467
|
+
const attempt = this.attempt(source, symbol, file);
|
|
1468
|
+
return attempt.kind === "resolved" ? attempt.span : null;
|
|
1469
|
+
}
|
|
1470
|
+
/** Parsed trees are keyed by content hash, so an unchanged file parses once. */
|
|
1471
|
+
parse(language, loaded, source) {
|
|
1472
|
+
const key = `${language}:${createHash2("sha256").update(source).digest("hex")}`;
|
|
1473
|
+
const cached2 = this.trees.get(key);
|
|
1474
|
+
if (cached2) {
|
|
1475
|
+
this.stats.cacheHits += 1;
|
|
1476
|
+
return cached2;
|
|
1477
|
+
}
|
|
1478
|
+
const parser = this.parser;
|
|
1479
|
+
if (!parser) return null;
|
|
1480
|
+
let parsed;
|
|
1481
|
+
try {
|
|
1482
|
+
parser.setLanguage(loaded.language);
|
|
1483
|
+
const tree = parser.parse(source);
|
|
1484
|
+
if (!tree) return null;
|
|
1485
|
+
parsed = index(tree, loaded.query);
|
|
1486
|
+
} catch {
|
|
1487
|
+
return null;
|
|
1488
|
+
}
|
|
1489
|
+
this.stats.parses += 1;
|
|
1490
|
+
if (this.trees.size >= TREE_CACHE_LIMIT) {
|
|
1491
|
+
const oldest = this.trees.keys().next();
|
|
1492
|
+
if (!oldest.done) {
|
|
1493
|
+
this.trees.get(oldest.value)?.tree.delete();
|
|
1494
|
+
this.trees.delete(oldest.value);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
this.trees.set(key, parsed);
|
|
1498
|
+
return parsed;
|
|
1499
|
+
}
|
|
1500
|
+
/**
|
|
1501
|
+
* Every definition this file declares, as dotted symbol and span.
|
|
1502
|
+
*
|
|
1503
|
+
* The inverse of `attempt`: that asks "where is this name", this asks "what
|
|
1504
|
+
* names are here". `moved` needs the second — the stored hash has to be
|
|
1505
|
+
* looked for at every definition in the repository, and there is no name to
|
|
1506
|
+
* ask about, since the whole question is which name now carries that code.
|
|
1507
|
+
*/
|
|
1508
|
+
spans(source, file) {
|
|
1509
|
+
const language = languageForFile(file);
|
|
1510
|
+
if (!language) return [];
|
|
1511
|
+
const loaded = this.loaded.get(language);
|
|
1512
|
+
if (!loaded) return [];
|
|
1513
|
+
const parsed = this.parse(language, loaded, source);
|
|
1514
|
+
if (!parsed) return [];
|
|
1515
|
+
return parsed.definitions.filter((definition) => definition.target).map((definition) => ({
|
|
1516
|
+
symbol: chainOf(definition, parsed.byNodeId).join("."),
|
|
1517
|
+
span: spanOf(definition, source)
|
|
1518
|
+
}));
|
|
1519
|
+
}
|
|
1520
|
+
/**
|
|
1521
|
+
* The token stream of a span: every leaf the parser sees, comments dropped,
|
|
1522
|
+
* joined by single spaces.
|
|
1523
|
+
*
|
|
1524
|
+
* This is what makes a reformat not be drift. Hashing it rather than the raw
|
|
1525
|
+
* text means indentation, line breaks, trailing commas the formatter moved,
|
|
1526
|
+
* and every comment above or inside the definition are outside the hash —
|
|
1527
|
+
* and a renamed identifier or a changed literal is still inside it, because
|
|
1528
|
+
* those are leaves.
|
|
1529
|
+
*
|
|
1530
|
+
* `null` when the file has no grammar, the grammar would not load, or the
|
|
1531
|
+
* text will not parse: no normalisation is better than a guessed one.
|
|
1532
|
+
*/
|
|
1533
|
+
normalize(text, file) {
|
|
1534
|
+
const language = file ? languageForFile(file) : void 0;
|
|
1535
|
+
if (!language) return null;
|
|
1536
|
+
const loaded = this.loaded.get(language);
|
|
1537
|
+
if (!loaded) return null;
|
|
1538
|
+
const parser = this.parser;
|
|
1539
|
+
if (!parser) return null;
|
|
1540
|
+
let tree;
|
|
1541
|
+
try {
|
|
1542
|
+
parser.setLanguage(loaded.language);
|
|
1543
|
+
tree = parser.parse(text);
|
|
1544
|
+
} catch {
|
|
1545
|
+
return null;
|
|
1546
|
+
}
|
|
1547
|
+
if (!tree) return null;
|
|
1548
|
+
try {
|
|
1549
|
+
return tokens(tree.rootNode).join(" ");
|
|
1550
|
+
} finally {
|
|
1551
|
+
tree.delete();
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
/** Drops cached trees. Grammars stay loaded — they are immutable. */
|
|
1555
|
+
reset() {
|
|
1556
|
+
for (const parsed of this.trees.values()) parsed.tree.delete();
|
|
1557
|
+
this.trees.clear();
|
|
1558
|
+
this.stats.parses = 0;
|
|
1559
|
+
this.stats.cacheHits = 0;
|
|
1560
|
+
}
|
|
1561
|
+
};
|
|
1562
|
+
function why(error) {
|
|
1563
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
1564
|
+
return text || "no reason given";
|
|
964
1565
|
}
|
|
965
1566
|
|
|
966
1567
|
// src/anchor-resolver/resolver.ts
|
|
967
|
-
import { createHash } from "crypto";
|
|
1568
|
+
import { createHash as createHash3 } from "crypto";
|
|
968
1569
|
var PARENT_SCOPE_LINES = 50;
|
|
969
1570
|
var CLEAN_STATE = { blockComment: false, template: false };
|
|
970
1571
|
function stripLine(line, state) {
|
|
971
1572
|
let out = "";
|
|
972
|
-
let
|
|
1573
|
+
let index2 = 0;
|
|
973
1574
|
let { blockComment, template } = state;
|
|
974
|
-
while (
|
|
975
|
-
const char = line[
|
|
976
|
-
const next = line[
|
|
1575
|
+
while (index2 < line.length) {
|
|
1576
|
+
const char = line[index2];
|
|
1577
|
+
const next = line[index2 + 1];
|
|
977
1578
|
if (blockComment) {
|
|
978
1579
|
if (char === "*" && next === "/") {
|
|
979
1580
|
blockComment = false;
|
|
980
|
-
|
|
1581
|
+
index2 += 2;
|
|
981
1582
|
continue;
|
|
982
1583
|
}
|
|
983
|
-
|
|
1584
|
+
index2 += 1;
|
|
984
1585
|
continue;
|
|
985
1586
|
}
|
|
986
1587
|
if (template) {
|
|
987
1588
|
if (char === "\\") {
|
|
988
|
-
|
|
1589
|
+
index2 += 2;
|
|
989
1590
|
continue;
|
|
990
1591
|
}
|
|
991
1592
|
if (char === "`") template = false;
|
|
992
|
-
|
|
1593
|
+
index2 += 1;
|
|
993
1594
|
continue;
|
|
994
1595
|
}
|
|
995
1596
|
if (char === "/" && next === "*") {
|
|
996
1597
|
blockComment = true;
|
|
997
|
-
|
|
1598
|
+
index2 += 2;
|
|
998
1599
|
continue;
|
|
999
1600
|
}
|
|
1000
1601
|
if (char === "/" && next === "/") break;
|
|
1001
1602
|
if (char === "`") {
|
|
1002
1603
|
template = true;
|
|
1003
|
-
|
|
1604
|
+
index2 += 1;
|
|
1004
1605
|
continue;
|
|
1005
1606
|
}
|
|
1006
1607
|
if (char === "'" || char === '"') {
|
|
1007
1608
|
const quote = char;
|
|
1008
|
-
|
|
1009
|
-
while (
|
|
1010
|
-
if (line[
|
|
1011
|
-
|
|
1609
|
+
index2 += 1;
|
|
1610
|
+
while (index2 < line.length) {
|
|
1611
|
+
if (line[index2] === "\\") {
|
|
1612
|
+
index2 += 2;
|
|
1012
1613
|
continue;
|
|
1013
1614
|
}
|
|
1014
|
-
if (line[
|
|
1015
|
-
|
|
1615
|
+
if (line[index2] === quote) {
|
|
1616
|
+
index2 += 1;
|
|
1016
1617
|
break;
|
|
1017
1618
|
}
|
|
1018
|
-
|
|
1619
|
+
index2 += 1;
|
|
1019
1620
|
}
|
|
1020
1621
|
continue;
|
|
1021
1622
|
}
|
|
1022
1623
|
out += char;
|
|
1023
|
-
|
|
1624
|
+
index2 += 1;
|
|
1024
1625
|
}
|
|
1025
1626
|
return { code: out, state: { blockComment, template } };
|
|
1026
1627
|
}
|
|
@@ -1035,8 +1636,8 @@ function captureBraceBlock(lines, matchLine) {
|
|
|
1035
1636
|
let depth = 0;
|
|
1036
1637
|
let opened = false;
|
|
1037
1638
|
let state = CLEAN_STATE;
|
|
1038
|
-
for (let
|
|
1039
|
-
const stripped = stripLine(lines[
|
|
1639
|
+
for (let index2 = matchLine; index2 < lines.length; index2++) {
|
|
1640
|
+
const stripped = stripLine(lines[index2] ?? "", state);
|
|
1040
1641
|
state = stripped.state;
|
|
1041
1642
|
for (const char of stripped.code) {
|
|
1042
1643
|
if (char === "{") {
|
|
@@ -1045,10 +1646,10 @@ function captureBraceBlock(lines, matchLine) {
|
|
|
1045
1646
|
} else if (char === "}") {
|
|
1046
1647
|
depth = Math.max(0, depth - 1);
|
|
1047
1648
|
} else if (char === ";" && !opened) {
|
|
1048
|
-
return span(lines, matchLine,
|
|
1649
|
+
return span(lines, matchLine, index2);
|
|
1049
1650
|
}
|
|
1050
1651
|
}
|
|
1051
|
-
if (opened && depth === 0) return span(lines, matchLine,
|
|
1652
|
+
if (opened && depth === 0) return span(lines, matchLine, index2);
|
|
1052
1653
|
}
|
|
1053
1654
|
return null;
|
|
1054
1655
|
}
|
|
@@ -1057,22 +1658,22 @@ function captureIndentedBlock(lines, matchLine) {
|
|
|
1057
1658
|
const header = lines[matchLine] ?? "";
|
|
1058
1659
|
const indent = header.length - header.trimStart().length;
|
|
1059
1660
|
let headerEnd = -1;
|
|
1060
|
-
for (let
|
|
1061
|
-
const code = stripLine(lines[
|
|
1661
|
+
for (let index2 = matchLine; index2 < lines.length && index2 <= matchLine + 20; index2++) {
|
|
1662
|
+
const code = stripLine(lines[index2] ?? "", CLEAN_STATE).code.trimEnd();
|
|
1062
1663
|
if (code.endsWith(":")) {
|
|
1063
|
-
headerEnd =
|
|
1664
|
+
headerEnd = index2;
|
|
1064
1665
|
break;
|
|
1065
1666
|
}
|
|
1066
|
-
if (code.includes(":")) return span(lines, matchLine,
|
|
1667
|
+
if (code.includes(":")) return span(lines, matchLine, index2);
|
|
1067
1668
|
}
|
|
1068
1669
|
if (headerEnd === -1) return null;
|
|
1069
1670
|
let end = headerEnd;
|
|
1070
|
-
for (let
|
|
1071
|
-
const line = lines[
|
|
1671
|
+
for (let index2 = headerEnd + 1; index2 < lines.length; index2++) {
|
|
1672
|
+
const line = lines[index2] ?? "";
|
|
1072
1673
|
if (line.trim() === "") continue;
|
|
1073
1674
|
const lineIndent = line.length - line.trimStart().length;
|
|
1074
1675
|
if (lineIndent <= indent) break;
|
|
1075
|
-
end =
|
|
1676
|
+
end = index2;
|
|
1076
1677
|
}
|
|
1077
1678
|
return end === headerEnd ? null : span(lines, matchLine, end);
|
|
1078
1679
|
}
|
|
@@ -1096,15 +1697,15 @@ var regexResolver = {
|
|
|
1096
1697
|
const lines = source.split("\n");
|
|
1097
1698
|
for (const tier of TIERS) {
|
|
1098
1699
|
const pattern = tier(escaped);
|
|
1099
|
-
let candidates = lines.map((line,
|
|
1700
|
+
let candidates = lines.map((line, index2) => ({ line, index: index2 })).filter((entry) => pattern.test(entry.line)).map((entry) => entry.index);
|
|
1100
1701
|
if (!candidates.length) continue;
|
|
1101
1702
|
if (parentPattern && candidates.length > 1) {
|
|
1102
1703
|
const distances = candidates.map(
|
|
1103
|
-
(
|
|
1704
|
+
(index2) => distanceToParent(lines, index2, parentPattern)
|
|
1104
1705
|
);
|
|
1105
1706
|
const nearest = Math.min(...distances);
|
|
1106
1707
|
if (Number.isFinite(nearest)) {
|
|
1107
|
-
candidates = candidates.filter((_,
|
|
1708
|
+
candidates = candidates.filter((_, at2) => distances[at2] === nearest);
|
|
1108
1709
|
}
|
|
1109
1710
|
}
|
|
1110
1711
|
if (candidates.length !== 1) return null;
|
|
@@ -1117,34 +1718,86 @@ var regexResolver = {
|
|
|
1117
1718
|
function escapeRegExp(value) {
|
|
1118
1719
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1119
1720
|
}
|
|
1120
|
-
function distanceToParent(lines,
|
|
1121
|
-
const floor = Math.max(0,
|
|
1122
|
-
for (let
|
|
1123
|
-
if (parent.test(lines[
|
|
1721
|
+
function distanceToParent(lines, index2, parent) {
|
|
1722
|
+
const floor = Math.max(0, index2 - PARENT_SCOPE_LINES);
|
|
1723
|
+
for (let at2 = index2; at2 >= floor; at2--) {
|
|
1724
|
+
if (parent.test(lines[at2] ?? "")) return index2 - at2;
|
|
1124
1725
|
}
|
|
1125
1726
|
return Number.POSITIVE_INFINITY;
|
|
1126
1727
|
}
|
|
1127
1728
|
function hashAnchorText(text) {
|
|
1128
|
-
return `sha256:${
|
|
1729
|
+
return `sha256:${createHash3("sha256").update(text.replace(/\r\n/g, "\n")).digest("hex")}`;
|
|
1129
1730
|
}
|
|
1130
1731
|
function resolveAnchor(source, anchor, resolver = regexResolver) {
|
|
1732
|
+
const outcome = resolveAnchorSpan(source, anchor, [resolver]);
|
|
1733
|
+
return outcome.ok ? outcome.span : null;
|
|
1734
|
+
}
|
|
1735
|
+
function resolveAnchorSpan(source, anchor, resolvers = [regexResolver]) {
|
|
1131
1736
|
const normalized = source.replace(/\r\n/g, "\n");
|
|
1132
1737
|
if (!anchor.symbol) {
|
|
1133
1738
|
const lines = normalized.split("\n");
|
|
1134
1739
|
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
1135
1740
|
return {
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1741
|
+
ok: true,
|
|
1742
|
+
span: {
|
|
1743
|
+
text: normalized,
|
|
1744
|
+
startLine: 1,
|
|
1745
|
+
endLine: Math.max(1, lines.length)
|
|
1746
|
+
}
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
for (const resolver of resolvers) {
|
|
1750
|
+
const attempt = resolver.attempt ? resolver.attempt(normalized, anchor.symbol, anchor.file) : fromResolve(resolver, normalized, anchor.symbol, anchor.file);
|
|
1751
|
+
if (attempt.kind === "abstain") continue;
|
|
1752
|
+
if (attempt.kind === "unresolved") {
|
|
1753
|
+
if (attempt.reason === "symbol-not-found") continue;
|
|
1754
|
+
return { ok: false, reason: attempt.reason };
|
|
1755
|
+
}
|
|
1756
|
+
const tokens2 = resolver.normalize?.(attempt.span.text, anchor.file);
|
|
1757
|
+
return {
|
|
1758
|
+
ok: true,
|
|
1759
|
+
span: attempt.span,
|
|
1760
|
+
...isResolverName(resolver.name) ? { resolver: resolver.name } : {},
|
|
1761
|
+
...tokens2 ? { normalized: tokens2 } : {}
|
|
1139
1762
|
};
|
|
1140
1763
|
}
|
|
1141
|
-
return
|
|
1764
|
+
return { ok: false, reason: "symbol-not-found" };
|
|
1765
|
+
}
|
|
1766
|
+
function fromResolve(resolver, source, symbol, file) {
|
|
1767
|
+
const span2 = resolver.resolve(source, symbol, file);
|
|
1768
|
+
return span2 ? { kind: "resolved", span: span2 } : { kind: "unresolved", reason: "symbol-not-found" };
|
|
1769
|
+
}
|
|
1770
|
+
function isResolverName(name) {
|
|
1771
|
+
return name === "tree-sitter" || name === "regex";
|
|
1772
|
+
}
|
|
1773
|
+
async function prepareResolvers(resolvers, files) {
|
|
1774
|
+
for (const resolver of resolvers) await resolver.prepare?.(files);
|
|
1775
|
+
}
|
|
1776
|
+
function defaultAnchorResolvers(grammars = {}) {
|
|
1777
|
+
return [new TreeSitterResolver(grammars), regexResolver];
|
|
1778
|
+
}
|
|
1779
|
+
function resolverChanged(source, anchor, produced) {
|
|
1780
|
+
const previous = anchor.resolver ?? "regex";
|
|
1781
|
+
if (!produced || !anchor.symbol || previous === produced) return false;
|
|
1782
|
+
if (previous !== "regex") return false;
|
|
1783
|
+
const before = regexResolver.resolve(
|
|
1784
|
+
source.replace(/\r\n/g, "\n"),
|
|
1785
|
+
anchor.symbol
|
|
1786
|
+
);
|
|
1787
|
+
return before !== null && hashAnchorText(before.text) === anchor.hash;
|
|
1788
|
+
}
|
|
1789
|
+
function anchorHashOf(anchor, outcome) {
|
|
1790
|
+
const stored = anchor.hash ? anchor.hash_kind ?? "raw" : void 0;
|
|
1791
|
+
const wanted = stored ?? (outcome.normalized ? "ast" : "raw");
|
|
1792
|
+
return wanted === "ast" && outcome.normalized ? { hash: hashAnchorText(outcome.normalized), kind: "ast" } : { hash: hashAnchorText(outcome.span.text), kind: "raw" };
|
|
1142
1793
|
}
|
|
1143
1794
|
|
|
1144
1795
|
// src/anchor-resolver/drift.ts
|
|
1145
1796
|
async function detectAnchorDrift(records, options = {}) {
|
|
1146
1797
|
const repoRoot = options.repoRoot ?? process.cwd();
|
|
1147
|
-
const
|
|
1798
|
+
const resolvers = options.resolvers ?? (options.resolver ? [options.resolver] : defaultAnchorResolvers({
|
|
1799
|
+
offline: options.remote?.offline === true
|
|
1800
|
+
}));
|
|
1148
1801
|
const origin = new LazyOrigin(repoRoot);
|
|
1149
1802
|
const planned = /* @__PURE__ */ new Map();
|
|
1150
1803
|
let declaresRepo = false;
|
|
@@ -1182,12 +1835,16 @@ async function detectAnchorDrift(records, options = {}) {
|
|
|
1182
1835
|
),
|
|
1183
1836
|
(options.readRemote ?? readRemoteAnchors)(wants, options.remote ?? {})
|
|
1184
1837
|
]);
|
|
1838
|
+
await prepareResolvers(resolvers, [
|
|
1839
|
+
...files,
|
|
1840
|
+
...wants.map((want) => want.file)
|
|
1841
|
+
]);
|
|
1185
1842
|
const drift = /* @__PURE__ */ new Map();
|
|
1186
1843
|
for (const record of records) {
|
|
1187
1844
|
const entries = [];
|
|
1188
1845
|
for (const { anchor, foreign } of planned.get(record.conceptId) ?? []) {
|
|
1189
1846
|
entries.push(
|
|
1190
|
-
foreign ? remoteEntry(anchor, remote,
|
|
1847
|
+
foreign ? remoteEntry(anchor, remote, resolvers) : localEntry(anchor, reads.get(anchor.file), resolvers)
|
|
1191
1848
|
);
|
|
1192
1849
|
}
|
|
1193
1850
|
if (entries.length) drift.set(record.conceptId, entries);
|
|
@@ -1213,50 +1870,94 @@ function unresolved(anchor, reason, repo) {
|
|
|
1213
1870
|
state: "unresolved",
|
|
1214
1871
|
diffSize: null,
|
|
1215
1872
|
...reason ? { reason } : {},
|
|
1216
|
-
...repo ? { repo } : {}
|
|
1873
|
+
...repo ? { repo } : {},
|
|
1874
|
+
...classOf(reason)
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
function provisionalDriftClass(entry) {
|
|
1878
|
+
if (entry.state === "unresolved") {
|
|
1879
|
+
return entry.reason === "file-missing" || entry.reason === "symbol-not-found" ? "gone" : void 0;
|
|
1880
|
+
}
|
|
1881
|
+
return entry.state === "drifted" ? "changed" : void 0;
|
|
1882
|
+
}
|
|
1883
|
+
function classOf(reason) {
|
|
1884
|
+
const settled = provisionalDriftClass({ state: "unresolved", reason });
|
|
1885
|
+
return settled ? { class: settled } : {};
|
|
1886
|
+
}
|
|
1887
|
+
function hashIn(source, anchor, resolvers) {
|
|
1888
|
+
const outcome = resolveAnchorSpan(source, anchor, resolvers);
|
|
1889
|
+
if (!outcome.ok) return { ok: false, reason: outcome.reason };
|
|
1890
|
+
const { hash, kind } = anchorHashOf(anchor, outcome);
|
|
1891
|
+
return {
|
|
1892
|
+
ok: true,
|
|
1893
|
+
current: {
|
|
1894
|
+
hash,
|
|
1895
|
+
kind,
|
|
1896
|
+
lines: outcome.span.endLine - outcome.span.startLine + 1,
|
|
1897
|
+
...outcome.resolver ? { resolver: outcome.resolver } : {}
|
|
1898
|
+
}
|
|
1217
1899
|
};
|
|
1218
1900
|
}
|
|
1219
|
-
function
|
|
1220
|
-
const resolved = resolveAnchor(source, anchor, resolver);
|
|
1221
|
-
if (!resolved) return null;
|
|
1901
|
+
function resolverExtras(source, anchor, current) {
|
|
1222
1902
|
return {
|
|
1223
|
-
|
|
1224
|
-
|
|
1903
|
+
...current.resolver ? { resolver: current.resolver } : {},
|
|
1904
|
+
...current.hash !== anchor.hash && resolverChanged(source, anchor, current.resolver) ? { reason: "resolver-changed" } : {}
|
|
1225
1905
|
};
|
|
1226
1906
|
}
|
|
1227
1907
|
function compared(anchor, current, extra = {}) {
|
|
1908
|
+
const matched = current.hash === anchor.hash;
|
|
1228
1909
|
return {
|
|
1229
1910
|
...base(anchor),
|
|
1230
|
-
state:
|
|
1911
|
+
state: matched ? "match" : "drifted",
|
|
1231
1912
|
currentHash: current.hash,
|
|
1913
|
+
hashKind: current.kind,
|
|
1232
1914
|
diffSize: anchor.lines === void 0 ? null : Math.abs(current.lines - anchor.lines),
|
|
1915
|
+
...matched ? {} : { class: "changed" },
|
|
1233
1916
|
...extra
|
|
1234
1917
|
};
|
|
1235
1918
|
}
|
|
1236
|
-
function localEntry(anchor, read,
|
|
1919
|
+
function localEntry(anchor, read, resolvers) {
|
|
1237
1920
|
if (!read.ok) return unresolved(anchor, read.reason);
|
|
1238
|
-
const
|
|
1239
|
-
|
|
1921
|
+
const found = hashIn(read.source, anchor, resolvers);
|
|
1922
|
+
if (!found.ok) return unresolved(anchor, found.reason);
|
|
1923
|
+
return compared(
|
|
1924
|
+
anchor,
|
|
1925
|
+
found.current,
|
|
1926
|
+
resolverExtras(read.source, anchor, found.current)
|
|
1927
|
+
);
|
|
1240
1928
|
}
|
|
1241
|
-
function remoteEntry(anchor, remote,
|
|
1929
|
+
function remoteEntry(anchor, remote, resolvers) {
|
|
1242
1930
|
const repo = anchor.repo;
|
|
1243
1931
|
const key = normalizeRepoUrl(repo);
|
|
1244
1932
|
const atDefault = remote.get(wantKey(key, void 0, anchor.file));
|
|
1245
1933
|
const primary = anchor.ref ? remote.get(wantKey(key, anchor.ref, anchor.file)) : atDefault;
|
|
1246
1934
|
if (!primary) return unresolved(anchor, "remote-unreachable", repo);
|
|
1247
1935
|
if (!primary.ok) return unresolved(anchor, primary.reason, repo);
|
|
1248
|
-
const
|
|
1249
|
-
if (!
|
|
1250
|
-
|
|
1936
|
+
const found = hashIn(primary.source, anchor, resolvers);
|
|
1937
|
+
if (!found.ok) return unresolved(anchor, found.reason, repo);
|
|
1938
|
+
const current = found.current;
|
|
1939
|
+
const extras = resolverExtras(primary.source, anchor, current);
|
|
1940
|
+
if (!anchor.ref) return compared(anchor, current, { repo, ...extras });
|
|
1251
1941
|
if (current.hash !== anchor.hash) {
|
|
1252
|
-
return compared(anchor, current, {
|
|
1942
|
+
return compared(anchor, current, {
|
|
1943
|
+
repo,
|
|
1944
|
+
...extras,
|
|
1945
|
+
remoteState: "drifted-from-ref"
|
|
1946
|
+
});
|
|
1253
1947
|
}
|
|
1254
|
-
const head = atDefault?.ok ? hashIn(atDefault.source, anchor,
|
|
1255
|
-
return head && head.hash !== anchor.hash ? {
|
|
1256
|
-
...compared(anchor, head, {
|
|
1948
|
+
const head = atDefault?.ok ? hashIn(atDefault.source, anchor, resolvers) : null;
|
|
1949
|
+
return head?.ok && head.current.hash !== anchor.hash ? {
|
|
1950
|
+
...compared(anchor, head.current, {
|
|
1951
|
+
repo,
|
|
1952
|
+
...head.current.resolver ? { resolver: head.current.resolver } : {}
|
|
1953
|
+
}),
|
|
1257
1954
|
state: "drifted",
|
|
1258
1955
|
remoteState: "drifted-on-default"
|
|
1259
|
-
} : compared(anchor, current, {
|
|
1956
|
+
} : compared(anchor, current, {
|
|
1957
|
+
repo,
|
|
1958
|
+
...extras,
|
|
1959
|
+
remoteState: "matches-ref"
|
|
1960
|
+
});
|
|
1260
1961
|
}
|
|
1261
1962
|
|
|
1262
1963
|
// src/errors.ts
|
|
@@ -1463,9 +2164,9 @@ var KbStampDigestBaselineError = class extends BaseError {
|
|
|
1463
2164
|
// src/kb-pins/budgets.ts
|
|
1464
2165
|
function asBudgets(value) {
|
|
1465
2166
|
if (value === null || typeof value !== "object") return {};
|
|
1466
|
-
const
|
|
2167
|
+
const table2 = value;
|
|
1467
2168
|
const pick = (key, min) => {
|
|
1468
|
-
const raw =
|
|
2169
|
+
const raw = table2[key];
|
|
1469
2170
|
return typeof raw === "number" && Number.isInteger(raw) && raw >= min ? raw : void 0;
|
|
1470
2171
|
};
|
|
1471
2172
|
const budgetTokens = pick("budgetTokens", 1);
|
|
@@ -1476,9 +2177,9 @@ function asBudgets(value) {
|
|
|
1476
2177
|
};
|
|
1477
2178
|
}
|
|
1478
2179
|
function contextProfileBudgets(manifest, profile) {
|
|
1479
|
-
const
|
|
1480
|
-
if (
|
|
1481
|
-
const entries =
|
|
2180
|
+
const table2 = manifest.context;
|
|
2181
|
+
if (table2 === null || typeof table2 !== "object") return {};
|
|
2182
|
+
const entries = table2;
|
|
1482
2183
|
return {
|
|
1483
2184
|
...asBudgets(entries["default"]),
|
|
1484
2185
|
...profile ? asBudgets(entries[profile]) : {}
|
|
@@ -1509,15 +2210,15 @@ var KbBaseFrozenError = class extends Error {
|
|
|
1509
2210
|
};
|
|
1510
2211
|
|
|
1511
2212
|
// src/kb-pins/model.ts
|
|
1512
|
-
import { join as
|
|
1513
|
-
import { z as
|
|
1514
|
-
var PINS_FILE =
|
|
1515
|
-
var PINS_LOCAL_FILE =
|
|
2213
|
+
import { join as join4 } from "path";
|
|
2214
|
+
import { z as z5 } from "zod";
|
|
2215
|
+
var PINS_FILE = join4(".strauss", "kb-pins.json");
|
|
2216
|
+
var PINS_LOCAL_FILE = join4(".strauss", "kb-pins.local.json");
|
|
1516
2217
|
var PIN_LAYERS = ["project", "local", "user"];
|
|
1517
|
-
var pinSchema =
|
|
2218
|
+
var pinSchema = z5.object({
|
|
1518
2219
|
/** Relative to the manifest's root, so the file is committable. */
|
|
1519
|
-
path:
|
|
1520
|
-
pinnedAt:
|
|
2220
|
+
path: z5.string().min(1),
|
|
2221
|
+
pinnedAt: z5.string().min(1).optional(),
|
|
1521
2222
|
/**
|
|
1522
2223
|
* How `context` renders this base. `full` preloads the whole base into
|
|
1523
2224
|
* the block regardless of the full-under threshold — for a base whose
|
|
@@ -1527,7 +2228,7 @@ var pinSchema = z4.object({
|
|
|
1527
2228
|
* Absent: the profile's full-under threshold decides. Invalid values
|
|
1528
2229
|
* degrade to absent rather than failing the manifest.
|
|
1529
2230
|
*/
|
|
1530
|
-
mode:
|
|
2231
|
+
mode: z5.enum(["full", "index"]).optional().catch(void 0),
|
|
1531
2232
|
/**
|
|
1532
2233
|
* Context profiles this pin surfaces in (e.g. only at session-start,
|
|
1533
2234
|
* not per turn). Absent: every profile. A run without a profile sees
|
|
@@ -1535,17 +2236,17 @@ var pinSchema = z4.object({
|
|
|
1535
2236
|
* that skill at point of use than pinned at all — pins are what every
|
|
1536
2237
|
* session should see.
|
|
1537
2238
|
*/
|
|
1538
|
-
profiles:
|
|
2239
|
+
profiles: z5.array(z5.string()).optional().catch(void 0),
|
|
1539
2240
|
/**
|
|
1540
2241
|
* The base is concluded — a finished piece of research, a frozen ADR
|
|
1541
2242
|
* set. Write commands against it refuse while this workspace holds the
|
|
1542
2243
|
* pin, and `context` labels it read-only. Workspace policy, not base
|
|
1543
2244
|
* state: the base itself stays copyable and writable elsewhere.
|
|
1544
2245
|
*/
|
|
1545
|
-
frozen:
|
|
2246
|
+
frozen: z5.boolean().optional().catch(void 0)
|
|
1546
2247
|
}).passthrough();
|
|
1547
|
-
var pinsManifestSchema =
|
|
1548
|
-
pins:
|
|
2248
|
+
var pinsManifestSchema = z5.object({
|
|
2249
|
+
pins: z5.array(pinSchema).default([]),
|
|
1549
2250
|
/**
|
|
1550
2251
|
* Per-repo budgets for the `context` command, keyed by profile —
|
|
1551
2252
|
* `"session-start"`, `"compact"`, `"turn"`, or `"default"` for all of
|
|
@@ -1554,21 +2255,21 @@ var pinsManifestSchema = z4.object({
|
|
|
1554
2255
|
* the index at every session start. `contextProfileBudgets` does the
|
|
1555
2256
|
* tolerant read.
|
|
1556
2257
|
*/
|
|
1557
|
-
context:
|
|
2258
|
+
context: z5.unknown().optional()
|
|
1558
2259
|
}).passthrough();
|
|
1559
2260
|
|
|
1560
2261
|
// src/kb-pins/layers.ts
|
|
1561
|
-
import { mkdir as
|
|
1562
|
-
import { homedir as
|
|
1563
|
-
import { dirname, isAbsolute as isAbsolute2, join as
|
|
2262
|
+
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
|
|
2263
|
+
import { homedir as homedir3 } from "os";
|
|
2264
|
+
import { dirname as dirname3, isAbsolute as isAbsolute2, join as join5, relative as relative2, resolve as resolve2, sep as sep2 } from "path";
|
|
1564
2265
|
function userRoot() {
|
|
1565
|
-
return process.env.STRAUSS_KB_USER_ROOT ||
|
|
2266
|
+
return process.env.STRAUSS_KB_USER_ROOT || homedir3();
|
|
1566
2267
|
}
|
|
1567
2268
|
function layerRoot(workspaceDir, layer) {
|
|
1568
2269
|
return layer === "user" ? userRoot() : resolve2(workspaceDir);
|
|
1569
2270
|
}
|
|
1570
2271
|
function layerFile(workspaceDir, layer) {
|
|
1571
|
-
return
|
|
2272
|
+
return join5(
|
|
1572
2273
|
layerRoot(workspaceDir, layer),
|
|
1573
2274
|
layer === "local" ? PINS_LOCAL_FILE : PINS_FILE
|
|
1574
2275
|
);
|
|
@@ -1577,7 +2278,7 @@ async function readPinsLayer(workspaceDir, layer) {
|
|
|
1577
2278
|
const file = layerFile(workspaceDir, layer);
|
|
1578
2279
|
let raw;
|
|
1579
2280
|
try {
|
|
1580
|
-
raw = await
|
|
2281
|
+
raw = await readFile4(file, "utf8");
|
|
1581
2282
|
} catch {
|
|
1582
2283
|
return { pins: [] };
|
|
1583
2284
|
}
|
|
@@ -1601,8 +2302,8 @@ async function readPinsLayer(workspaceDir, layer) {
|
|
|
1601
2302
|
}
|
|
1602
2303
|
async function writePinsLayer(workspaceDir, layer, manifest) {
|
|
1603
2304
|
const file = layerFile(workspaceDir, layer);
|
|
1604
|
-
await
|
|
1605
|
-
await
|
|
2305
|
+
await mkdir3(dirname3(file), { recursive: true });
|
|
2306
|
+
await writeFile2(file, `${JSON.stringify(manifest, null, 2)}
|
|
1606
2307
|
`, "utf8");
|
|
1607
2308
|
}
|
|
1608
2309
|
function resolvePinPath(rootDir, path) {
|
|
@@ -1668,7 +2369,7 @@ async function listPins(store, workspaceDir) {
|
|
|
1668
2369
|
}
|
|
1669
2370
|
|
|
1670
2371
|
// src/kb-pins/pin.ts
|
|
1671
|
-
async function pinBase(store, workspaceDir, bundlePath2,
|
|
2372
|
+
async function pinBase(store, workspaceDir, bundlePath2, at2, options = {}) {
|
|
1672
2373
|
const layer = options.layer ?? "project";
|
|
1673
2374
|
const root = layerRoot(workspaceDir, layer);
|
|
1674
2375
|
const manifest = await readPinsLayer(workspaceDir, layer);
|
|
@@ -1696,7 +2397,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
|
|
|
1696
2397
|
return {
|
|
1697
2398
|
path: existing.path,
|
|
1698
2399
|
layer,
|
|
1699
|
-
pinnedAt: existing.pinnedAt ??
|
|
2400
|
+
pinnedAt: existing.pinnedAt ?? at2,
|
|
1700
2401
|
alreadyPinned: true,
|
|
1701
2402
|
...updated.mode ? { mode: updated.mode } : {},
|
|
1702
2403
|
...updated.profiles ? { profiles: updated.profiles } : {},
|
|
@@ -1706,7 +2407,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
|
|
|
1706
2407
|
}
|
|
1707
2408
|
const entry = {
|
|
1708
2409
|
path: storablePath(root, bundlePath2),
|
|
1709
|
-
pinnedAt:
|
|
2410
|
+
pinnedAt: at2,
|
|
1710
2411
|
...fields
|
|
1711
2412
|
};
|
|
1712
2413
|
await writePinsLayer(workspaceDir, layer, {
|
|
@@ -1716,7 +2417,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
|
|
|
1716
2417
|
return {
|
|
1717
2418
|
path: entry.path,
|
|
1718
2419
|
layer,
|
|
1719
|
-
pinnedAt:
|
|
2420
|
+
pinnedAt: at2,
|
|
1720
2421
|
alreadyPinned: false,
|
|
1721
2422
|
...fields,
|
|
1722
2423
|
...warning ? { warning } : {}
|
|
@@ -1816,7 +2517,8 @@ function warningAnchor(entry) {
|
|
|
1816
2517
|
diffSize,
|
|
1817
2518
|
...reason !== void 0 ? { reason } : {},
|
|
1818
2519
|
...repo !== void 0 ? { repo } : {},
|
|
1819
|
-
...remoteState !== void 0 ? { remoteState } : {}
|
|
2520
|
+
...remoteState !== void 0 ? { remoteState } : {},
|
|
2521
|
+
...entry.class !== void 0 ? { class: entry.class } : {}
|
|
1820
2522
|
};
|
|
1821
2523
|
}
|
|
1822
2524
|
function resolveHeads(from, byId) {
|
|
@@ -1827,8 +2529,8 @@ function resolveHeads(from, byId) {
|
|
|
1827
2529
|
while (queue.length) {
|
|
1828
2530
|
const current = queue.shift();
|
|
1829
2531
|
const next = successors(current, byId);
|
|
1830
|
-
for (const
|
|
1831
|
-
warnings.push({ kind: "broken-chain", missing });
|
|
2532
|
+
for (const missing2 of next.missing) {
|
|
2533
|
+
warnings.push({ kind: "broken-chain", missing: missing2 });
|
|
1832
2534
|
}
|
|
1833
2535
|
if (!next.records.length) {
|
|
1834
2536
|
if (current.conceptId !== from.conceptId)
|
|
@@ -1859,13 +2561,13 @@ function successors(record, byId) {
|
|
|
1859
2561
|
}
|
|
1860
2562
|
}
|
|
1861
2563
|
const records = [];
|
|
1862
|
-
const
|
|
2564
|
+
const missing2 = [];
|
|
1863
2565
|
for (const id of ids) {
|
|
1864
2566
|
const found = byId.get(id);
|
|
1865
2567
|
if (found) records.push(found);
|
|
1866
|
-
else
|
|
2568
|
+
else missing2.push(id);
|
|
1867
2569
|
}
|
|
1868
|
-
return { records, missing };
|
|
2570
|
+
return { records, missing: missing2 };
|
|
1869
2571
|
}
|
|
1870
2572
|
|
|
1871
2573
|
// src/catalog.ts
|
|
@@ -1936,7 +2638,7 @@ function indexIsStale(stored, expected) {
|
|
|
1936
2638
|
}
|
|
1937
2639
|
|
|
1938
2640
|
// src/kb-context.ts
|
|
1939
|
-
import { readFile as
|
|
2641
|
+
import { readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
|
|
1940
2642
|
var HEADING2 = "## Knowledge bases (pinned)";
|
|
1941
2643
|
var DEFAULT_CONTEXT_BUDGET = 4e3;
|
|
1942
2644
|
var CONTEXT_PROFILES = {
|
|
@@ -2140,13 +2842,13 @@ function toHookJson(block, event) {
|
|
|
2140
2842
|
var CONTEXT_BEGIN = "<!-- strauss-kb:begin -->";
|
|
2141
2843
|
var CONTEXT_END = "<!-- strauss-kb:end -->";
|
|
2142
2844
|
async function syncInstructions(file, block) {
|
|
2143
|
-
const existing = await
|
|
2845
|
+
const existing = await readFile5(file, "utf8").catch(() => null);
|
|
2144
2846
|
const region = block ? `${CONTEXT_BEGIN}
|
|
2145
2847
|
${block.trim()}
|
|
2146
2848
|
${CONTEXT_END}` : null;
|
|
2147
2849
|
if (existing === null) {
|
|
2148
2850
|
if (!region) return { file, action: "unchanged" };
|
|
2149
|
-
await
|
|
2851
|
+
await writeFile3(file, `${region}
|
|
2150
2852
|
`, "utf8");
|
|
2151
2853
|
return { file, action: "created" };
|
|
2152
2854
|
}
|
|
@@ -2157,11 +2859,11 @@ ${CONTEXT_END}` : null;
|
|
|
2157
2859
|
const after = existing.slice(end + CONTEXT_END.length);
|
|
2158
2860
|
const next = region ? `${before}${region}${after}` : `${before.replace(/\n+$/, "\n")}${after.replace(/^\n+/, "\n")}`;
|
|
2159
2861
|
if (next === existing) return { file, action: "unchanged" };
|
|
2160
|
-
await
|
|
2862
|
+
await writeFile3(file, next, "utf8");
|
|
2161
2863
|
return { file, action: region ? "replaced" : "removed" };
|
|
2162
2864
|
}
|
|
2163
2865
|
if (!region) return { file, action: "unchanged" };
|
|
2164
|
-
await
|
|
2866
|
+
await writeFile3(
|
|
2165
2867
|
file,
|
|
2166
2868
|
`${existing.replace(/\n*$/, "\n\n")}${region}
|
|
2167
2869
|
`,
|
|
@@ -2170,6 +2872,405 @@ ${CONTEXT_END}` : null;
|
|
|
2170
2872
|
return { file, action: "appended" };
|
|
2171
2873
|
}
|
|
2172
2874
|
|
|
2875
|
+
// src/drift/git.ts
|
|
2876
|
+
import { execFile as execFile3 } from "child_process";
|
|
2877
|
+
import { promisify as promisify3 } from "util";
|
|
2878
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
2879
|
+
var MAX_GIT_OUTPUT_BYTES = 1048576;
|
|
2880
|
+
var GIT_TIMEOUT_MS = 5e3;
|
|
2881
|
+
async function git2(cwd, args) {
|
|
2882
|
+
const env = { ...process.env };
|
|
2883
|
+
delete env["GIT_DIR"];
|
|
2884
|
+
delete env["GIT_WORK_TREE"];
|
|
2885
|
+
delete env["GIT_INDEX_FILE"];
|
|
2886
|
+
try {
|
|
2887
|
+
const { stdout } = await execFileAsync3("git", ["-C", cwd, ...args], {
|
|
2888
|
+
timeout: GIT_TIMEOUT_MS,
|
|
2889
|
+
maxBuffer: MAX_GIT_OUTPUT_BYTES,
|
|
2890
|
+
env
|
|
2891
|
+
});
|
|
2892
|
+
return { ok: true, stdout };
|
|
2893
|
+
} catch {
|
|
2894
|
+
return { ok: false };
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
async function listRepoFiles(repoRoot) {
|
|
2898
|
+
const result = await git2(repoRoot, ["ls-files", "-z", "--cached"]);
|
|
2899
|
+
if (!result.ok) return [];
|
|
2900
|
+
return result.stdout.split("\0").filter(Boolean);
|
|
2901
|
+
}
|
|
2902
|
+
async function readOldSource(repoRoot, anchor) {
|
|
2903
|
+
if (!filePathIsSafe(anchor.file))
|
|
2904
|
+
return { ok: false, reason: "unrecoverable" };
|
|
2905
|
+
if (anchor.ref && refShapeIsSafe(anchor.ref)) {
|
|
2906
|
+
const shown2 = await showFile(repoRoot, anchor.ref, anchor.file);
|
|
2907
|
+
if (shown2 !== null) {
|
|
2908
|
+
return {
|
|
2909
|
+
ok: true,
|
|
2910
|
+
source: shown2,
|
|
2911
|
+
origin: { kind: "ref", ref: anchor.ref }
|
|
2912
|
+
};
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
const at2 = anchor.resolved_at;
|
|
2916
|
+
if (!at2 || Number.isNaN(Date.parse(at2))) {
|
|
2917
|
+
return { ok: false, reason: "unrecoverable" };
|
|
2918
|
+
}
|
|
2919
|
+
const found = await git2(repoRoot, [
|
|
2920
|
+
"log",
|
|
2921
|
+
"-1",
|
|
2922
|
+
"--format=%H",
|
|
2923
|
+
`--before=${at2}`,
|
|
2924
|
+
"--end-of-options",
|
|
2925
|
+
"HEAD",
|
|
2926
|
+
"--",
|
|
2927
|
+
anchor.file
|
|
2928
|
+
]);
|
|
2929
|
+
const sha = found.ok ? found.stdout.trim() : "";
|
|
2930
|
+
if (!sha || !refShapeIsSafe(sha))
|
|
2931
|
+
return { ok: false, reason: "unrecoverable" };
|
|
2932
|
+
const shown = await showFile(repoRoot, sha, anchor.file);
|
|
2933
|
+
if (shown === null) return { ok: false, reason: "unrecoverable" };
|
|
2934
|
+
return { ok: true, source: shown, origin: { kind: "history", ref: sha } };
|
|
2935
|
+
}
|
|
2936
|
+
async function showFile(repoRoot, ref, file) {
|
|
2937
|
+
const path = file.replace(/^\.\//, "");
|
|
2938
|
+
const result = await git2(repoRoot, [
|
|
2939
|
+
"show",
|
|
2940
|
+
"--end-of-options",
|
|
2941
|
+
`${ref}:${path}`
|
|
2942
|
+
]);
|
|
2943
|
+
return result.ok ? result.stdout : null;
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
// src/drift/moved.ts
|
|
2947
|
+
import { stat as stat2 } from "fs/promises";
|
|
2948
|
+
var MAX_MOVED_SEARCH_FILES = 2e3;
|
|
2949
|
+
var SEARCH_BATCH = 64;
|
|
2950
|
+
function movedSearch(repoRoot, options = {}) {
|
|
2951
|
+
const read = options.reader ?? anchorFileReader(repoRoot);
|
|
2952
|
+
const sizeOf = options.sizeOf ?? diskSize(repoRoot);
|
|
2953
|
+
const resolver = new TreeSitterResolver();
|
|
2954
|
+
let repoFiles;
|
|
2955
|
+
const prepared = /* @__PURE__ */ new Set();
|
|
2956
|
+
const filesForLanguage = async (language) => {
|
|
2957
|
+
repoFiles ??= listRepoFiles(repoRoot);
|
|
2958
|
+
return (await repoFiles).filter((file) => languageForFile(file) === language).slice(0, MAX_MOVED_SEARCH_FILES);
|
|
2959
|
+
};
|
|
2960
|
+
return {
|
|
2961
|
+
async find(anchor) {
|
|
2962
|
+
const stored = anchor.hash;
|
|
2963
|
+
if (!stored) return void 0;
|
|
2964
|
+
const language = languageForFile(anchor.file);
|
|
2965
|
+
if (!language) return sameFileWindow(anchor, read, stored);
|
|
2966
|
+
const candidates = await filesForLanguage(language);
|
|
2967
|
+
if (!prepared.has(language)) {
|
|
2968
|
+
await resolver.prepare(candidates.length ? candidates : [anchor.file]);
|
|
2969
|
+
prepared.add(language);
|
|
2970
|
+
}
|
|
2971
|
+
const floor = anchor.lines ?? 0;
|
|
2972
|
+
for (let at2 = 0; at2 < candidates.length; at2 += SEARCH_BATCH) {
|
|
2973
|
+
const batch = candidates.slice(at2, at2 + SEARCH_BATCH);
|
|
2974
|
+
const hits = await mapLimit(
|
|
2975
|
+
batch,
|
|
2976
|
+
DEFAULT_IO_CONCURRENCY,
|
|
2977
|
+
async (file) => {
|
|
2978
|
+
const size2 = await sizeOf(file);
|
|
2979
|
+
if (size2 !== null && size2 < floor) return void 0;
|
|
2980
|
+
return matchIn(resolver, read, anchor, stored, file);
|
|
2981
|
+
}
|
|
2982
|
+
);
|
|
2983
|
+
const found = hits.find((hit) => hit !== void 0);
|
|
2984
|
+
if (found) return found;
|
|
2985
|
+
}
|
|
2986
|
+
return void 0;
|
|
2987
|
+
}
|
|
2988
|
+
};
|
|
2989
|
+
}
|
|
2990
|
+
async function matchIn(resolver, read, anchor, stored, file) {
|
|
2991
|
+
const source = await read(file);
|
|
2992
|
+
if (!source.ok) return void 0;
|
|
2993
|
+
const normalized = source.source.replace(/\r\n/g, "\n");
|
|
2994
|
+
for (const found of resolver.spans(normalized, file)) {
|
|
2995
|
+
const text = anchor.hash_kind === "ast" ? resolver.normalize(found.span.text, file) : found.span.text;
|
|
2996
|
+
if (text === null || hashAnchorText(text) !== stored) continue;
|
|
2997
|
+
if (file === anchor.file && found.symbol === anchor.symbol) continue;
|
|
2998
|
+
return {
|
|
2999
|
+
file,
|
|
3000
|
+
symbol: found.symbol,
|
|
3001
|
+
startLine: found.span.startLine,
|
|
3002
|
+
endLine: found.span.endLine
|
|
3003
|
+
};
|
|
3004
|
+
}
|
|
3005
|
+
return void 0;
|
|
3006
|
+
}
|
|
3007
|
+
function diskSize(repoRoot) {
|
|
3008
|
+
return async (file) => {
|
|
3009
|
+
const path = anchorFilePath(repoRoot, file);
|
|
3010
|
+
if (path === null) return null;
|
|
3011
|
+
try {
|
|
3012
|
+
return (await stat2(path)).size;
|
|
3013
|
+
} catch {
|
|
3014
|
+
return null;
|
|
3015
|
+
}
|
|
3016
|
+
};
|
|
3017
|
+
}
|
|
3018
|
+
async function sameFileWindow(anchor, read, stored) {
|
|
3019
|
+
const height = anchor.lines;
|
|
3020
|
+
if (!height || anchor.hash_kind === "ast") return void 0;
|
|
3021
|
+
const source = await read(anchor.file);
|
|
3022
|
+
if (!source.ok) return void 0;
|
|
3023
|
+
const lines = source.source.replace(/\r\n/g, "\n").split("\n");
|
|
3024
|
+
for (let at2 = 0; at2 + height <= lines.length; at2++) {
|
|
3025
|
+
if (hashAnchorText(lines.slice(at2, at2 + height).join("\n")) !== stored) {
|
|
3026
|
+
continue;
|
|
3027
|
+
}
|
|
3028
|
+
return {
|
|
3029
|
+
file: anchor.file,
|
|
3030
|
+
...anchor.symbol ? { symbol: anchor.symbol } : {},
|
|
3031
|
+
startLine: at2 + 1,
|
|
3032
|
+
endLine: at2 + height
|
|
3033
|
+
};
|
|
3034
|
+
}
|
|
3035
|
+
return void 0;
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
// src/drift/classify.ts
|
|
3039
|
+
async function classifyDrift(repoRoot, record, entries, options = {}) {
|
|
3040
|
+
const anchors = (record.frontmatter.strauss_anchors ?? []).filter(
|
|
3041
|
+
(anchor) => anchor.hash
|
|
3042
|
+
);
|
|
3043
|
+
const reader = options.reader ?? anchorFileReader(repoRoot);
|
|
3044
|
+
const treeSitter = new TreeSitterResolver();
|
|
3045
|
+
const resolvers = [treeSitter, regexResolver];
|
|
3046
|
+
const search = options.search ?? movedSearch(repoRoot, { ...options.reader ? { reader } : {} });
|
|
3047
|
+
const wanted = [];
|
|
3048
|
+
entries.forEach((entry, at2) => {
|
|
3049
|
+
const anchor = anchors[at2];
|
|
3050
|
+
if (!anchor) return;
|
|
3051
|
+
if (entry.state === "match" || isUncheckedReason(entry.reason)) return;
|
|
3052
|
+
wanted.push({ anchor, entry });
|
|
3053
|
+
});
|
|
3054
|
+
if (!wanted.length) return [];
|
|
3055
|
+
await prepareResolvers(
|
|
3056
|
+
resolvers,
|
|
3057
|
+
wanted.map(({ anchor }) => anchor.file)
|
|
3058
|
+
);
|
|
3059
|
+
const out = [];
|
|
3060
|
+
for (const { anchor, entry } of wanted) {
|
|
3061
|
+
const movedTo = await search.find(anchor);
|
|
3062
|
+
if (movedTo) {
|
|
3063
|
+
out.push({
|
|
3064
|
+
anchor,
|
|
3065
|
+
entry: { ...entry, class: "moved", movedTo },
|
|
3066
|
+
class: "moved"
|
|
3067
|
+
});
|
|
3068
|
+
continue;
|
|
3069
|
+
}
|
|
3070
|
+
const newText = await currentText(reader, anchor, resolvers);
|
|
3071
|
+
const old = options.withHistory === false ? { ok: false, reason: "unrecoverable" } : await readOldSource(repoRoot, anchor);
|
|
3072
|
+
const oldText = old.ok ? spanIn(old.source, anchor, resolvers) : void 0;
|
|
3073
|
+
const settled = newText !== void 0 && oldText !== void 0 && sameTokens(treeSitter, anchor.file, oldText, newText) ? "cosmetic" : entry.class ?? "changed";
|
|
3074
|
+
out.push({
|
|
3075
|
+
anchor,
|
|
3076
|
+
entry: { ...entry, class: settled },
|
|
3077
|
+
class: settled,
|
|
3078
|
+
...newText !== void 0 ? { newText } : {},
|
|
3079
|
+
...oldText !== void 0 ? { oldText } : {},
|
|
3080
|
+
...old.ok ? { oldOrigin: old.origin } : {}
|
|
3081
|
+
});
|
|
3082
|
+
}
|
|
3083
|
+
return out;
|
|
3084
|
+
}
|
|
3085
|
+
function sameTokens(resolver, file, before, after) {
|
|
3086
|
+
if (before === after) return false;
|
|
3087
|
+
const left = resolver.normalize(before, file);
|
|
3088
|
+
const right = resolver.normalize(after, file);
|
|
3089
|
+
return left !== null && left === right;
|
|
3090
|
+
}
|
|
3091
|
+
async function currentText(reader, anchor, resolvers) {
|
|
3092
|
+
const read = await reader(anchor.file);
|
|
3093
|
+
if (!read.ok) return void 0;
|
|
3094
|
+
return spanIn(read.source, anchor, resolvers);
|
|
3095
|
+
}
|
|
3096
|
+
function spanIn(source, anchor, resolvers) {
|
|
3097
|
+
const outcome = resolveAnchorSpan(source, anchor, resolvers);
|
|
3098
|
+
return outcome.ok ? outcome.span.text : void 0;
|
|
3099
|
+
}
|
|
3100
|
+
|
|
3101
|
+
// src/drift/diff.ts
|
|
3102
|
+
var MAX_ANCHOR_DIFF_LINES = 200;
|
|
3103
|
+
var PACKET_DIFF_LINE_BUDGET = 200;
|
|
3104
|
+
var MIN_ANCHOR_DIFF_LINES = 12;
|
|
3105
|
+
function diffBudget(anchors) {
|
|
3106
|
+
if (anchors <= 0) return MAX_ANCHOR_DIFF_LINES;
|
|
3107
|
+
return Math.min(
|
|
3108
|
+
MAX_ANCHOR_DIFF_LINES,
|
|
3109
|
+
Math.max(
|
|
3110
|
+
MIN_ANCHOR_DIFF_LINES,
|
|
3111
|
+
Math.floor(PACKET_DIFF_LINE_BUDGET / anchors)
|
|
3112
|
+
)
|
|
3113
|
+
);
|
|
3114
|
+
}
|
|
3115
|
+
function unifiedDiff(before, after, options = {}) {
|
|
3116
|
+
const max = options.maxLines ?? MAX_ANCHOR_DIFF_LINES;
|
|
3117
|
+
const left = before.replace(/\r\n/g, "\n").split("\n");
|
|
3118
|
+
const right = after.replace(/\r\n/g, "\n").split("\n");
|
|
3119
|
+
const body = [];
|
|
3120
|
+
let added = 0;
|
|
3121
|
+
let removed = 0;
|
|
3122
|
+
for (const edit of edits(left, right)) {
|
|
3123
|
+
if (edit.kind === "same") body.push(` ${edit.line}`);
|
|
3124
|
+
else if (edit.kind === "remove") {
|
|
3125
|
+
body.push(`-${edit.line}`);
|
|
3126
|
+
removed += 1;
|
|
3127
|
+
} else {
|
|
3128
|
+
body.push(`+${edit.line}`);
|
|
3129
|
+
added += 1;
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
const truncated = body.length > max;
|
|
3133
|
+
const shown = truncated ? body.slice(0, max) : body;
|
|
3134
|
+
const header = `@@ -1,${left.length} +1,${right.length} @@${options.oldLabel ? ` ${options.oldLabel} \u2192 ${options.newLabel ?? ""}`.trimEnd() : ""}`;
|
|
3135
|
+
const lines = [header, ...shown];
|
|
3136
|
+
if (truncated) lines.push(`\u2026 ${body.length - max} more diff lines`);
|
|
3137
|
+
return { text: lines.join("\n"), added, removed, truncated };
|
|
3138
|
+
}
|
|
3139
|
+
function edits(left, right) {
|
|
3140
|
+
const rows = left.length;
|
|
3141
|
+
const cols = right.length;
|
|
3142
|
+
const table2 = Array.from(
|
|
3143
|
+
{ length: rows + 1 },
|
|
3144
|
+
() => new Array(cols + 1).fill(0)
|
|
3145
|
+
);
|
|
3146
|
+
for (let row2 = rows - 1; row2 >= 0; row2--) {
|
|
3147
|
+
for (let col2 = cols - 1; col2 >= 0; col2--) {
|
|
3148
|
+
table2[row2][col2] = left[row2] === right[col2] ? table2[row2 + 1][col2 + 1] + 1 : Math.max(
|
|
3149
|
+
table2[row2 + 1][col2],
|
|
3150
|
+
table2[row2][col2 + 1]
|
|
3151
|
+
);
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
const out = [];
|
|
3155
|
+
let row = 0;
|
|
3156
|
+
let col = 0;
|
|
3157
|
+
while (row < rows && col < cols) {
|
|
3158
|
+
if (left[row] === right[col]) {
|
|
3159
|
+
out.push({ kind: "same", line: left[row] });
|
|
3160
|
+
row += 1;
|
|
3161
|
+
col += 1;
|
|
3162
|
+
} else if (table2[row + 1][col] >= table2[row][col + 1]) {
|
|
3163
|
+
out.push({ kind: "remove", line: left[row] });
|
|
3164
|
+
row += 1;
|
|
3165
|
+
} else {
|
|
3166
|
+
out.push({ kind: "add", line: right[col] });
|
|
3167
|
+
col += 1;
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
for (; row < rows; row++)
|
|
3171
|
+
out.push({ kind: "remove", line: left[row] });
|
|
3172
|
+
for (; col < cols; col++)
|
|
3173
|
+
out.push({ kind: "add", line: right[col] });
|
|
3174
|
+
return out;
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
// src/drift/packet.ts
|
|
3178
|
+
var PRESUMED_INVALID = [
|
|
3179
|
+
"fact",
|
|
3180
|
+
"constraint",
|
|
3181
|
+
"contract"
|
|
3182
|
+
];
|
|
3183
|
+
var RATIONALE_SURVIVES = ["decision", "risk"];
|
|
3184
|
+
var DEFAULT_NOTES = {
|
|
3185
|
+
"presumed-invalidated": "the code this claim was taken from changed; presume it no longer holds until re-read",
|
|
3186
|
+
"rationale-may-survive": "the reasoning may outlive the code that implemented it; check whether it does",
|
|
3187
|
+
review: "re-read the record against the new code"
|
|
3188
|
+
};
|
|
3189
|
+
async function reassessPacket(repoRoot, record, entries, options = {}) {
|
|
3190
|
+
const classified = await classifyDrift(repoRoot, record, entries, {
|
|
3191
|
+
...options.reader ? { reader: options.reader } : {},
|
|
3192
|
+
...options.search ? { search: options.search } : {},
|
|
3193
|
+
withHistory: options.withDiff !== false
|
|
3194
|
+
});
|
|
3195
|
+
const open = classified.filter(
|
|
3196
|
+
(found) => found.class === "changed" || found.class === "gone"
|
|
3197
|
+
);
|
|
3198
|
+
if (!open.length) return { packet: null, classified };
|
|
3199
|
+
const budget = diffBudget(open.length);
|
|
3200
|
+
const anchors = open.map(
|
|
3201
|
+
(found) => anchorPacket(found, options.withDiff === true, budget)
|
|
3202
|
+
);
|
|
3203
|
+
const type = record.frontmatter.type;
|
|
3204
|
+
const fallback = isKbRecordType(type) ? PRESUMED_INVALID.includes(type) ? "presumed-invalidated" : RATIONALE_SURVIVES.includes(type) ? "rationale-may-survive" : "review" : "review";
|
|
3205
|
+
return {
|
|
3206
|
+
classified,
|
|
3207
|
+
packet: {
|
|
3208
|
+
conceptId: record.conceptId,
|
|
3209
|
+
title: record.frontmatter.title ?? null,
|
|
3210
|
+
type,
|
|
3211
|
+
standing: options.standing ?? "unsettled",
|
|
3212
|
+
why: record.frontmatter.description ?? null,
|
|
3213
|
+
claim: claimOf(record),
|
|
3214
|
+
anchors,
|
|
3215
|
+
impact: (options.impact?.impacted ?? []).map((entry) => ({
|
|
3216
|
+
conceptId: entry.conceptId,
|
|
3217
|
+
title: entry.title,
|
|
3218
|
+
standing: entry.standing,
|
|
3219
|
+
depth: entry.depth
|
|
3220
|
+
})),
|
|
3221
|
+
impactTruncated: options.impact?.truncated ?? false,
|
|
3222
|
+
default: fallback,
|
|
3223
|
+
defaultNote: DEFAULT_NOTES[fallback]
|
|
3224
|
+
}
|
|
3225
|
+
};
|
|
3226
|
+
}
|
|
3227
|
+
function anchorPacket(found, withDiff, maxLines) {
|
|
3228
|
+
const { entry } = found;
|
|
3229
|
+
const base2 = {
|
|
3230
|
+
file: entry.file,
|
|
3231
|
+
...entry.symbol ? { symbol: entry.symbol } : {},
|
|
3232
|
+
class: found.class,
|
|
3233
|
+
...entry.reason ? { reason: entry.reason } : {},
|
|
3234
|
+
storedHash: entry.storedHash,
|
|
3235
|
+
...entry.currentHash ? { currentHash: entry.currentHash } : {},
|
|
3236
|
+
diffSize: entry.diffSize,
|
|
3237
|
+
...entry.movedTo ? { movedTo: entry.movedTo } : {}
|
|
3238
|
+
};
|
|
3239
|
+
if (!withDiff) return base2;
|
|
3240
|
+
if (found.oldText === void 0 || !found.oldOrigin) {
|
|
3241
|
+
return { ...base2, diff: { status: "unrecoverable" } };
|
|
3242
|
+
}
|
|
3243
|
+
const rendered = unifiedDiff(found.oldText, found.newText ?? "", {
|
|
3244
|
+
maxLines
|
|
3245
|
+
});
|
|
3246
|
+
return {
|
|
3247
|
+
...base2,
|
|
3248
|
+
diff: {
|
|
3249
|
+
status: "ok",
|
|
3250
|
+
source: found.oldOrigin.kind,
|
|
3251
|
+
ref: found.oldOrigin.ref,
|
|
3252
|
+
unified: rendered.text,
|
|
3253
|
+
added: rendered.added,
|
|
3254
|
+
removed: rendered.removed,
|
|
3255
|
+
truncated: rendered.truncated
|
|
3256
|
+
}
|
|
3257
|
+
};
|
|
3258
|
+
}
|
|
3259
|
+
function claimOf(record) {
|
|
3260
|
+
const type = record.frontmatter.type;
|
|
3261
|
+
const section = isKbRecordType(type) ? RECORD_TYPES[type].sections[0] : void 0;
|
|
3262
|
+
if (!section) return null;
|
|
3263
|
+
const lines = record.body.replace(/\r\n/g, "\n").split("\n");
|
|
3264
|
+
const start = lines.findIndex(
|
|
3265
|
+
(line) => line.trim().toLowerCase() === `## ${section}`.toLowerCase()
|
|
3266
|
+
);
|
|
3267
|
+
if (start < 0) return null;
|
|
3268
|
+
const rest = lines.slice(start + 1);
|
|
3269
|
+
const end = rest.findIndex((line) => line.startsWith("## "));
|
|
3270
|
+
const text = (end < 0 ? rest : rest.slice(0, end)).join("\n").trim();
|
|
3271
|
+
return text ? { section, text } : null;
|
|
3272
|
+
}
|
|
3273
|
+
|
|
2173
3274
|
// src/kb-edges.ts
|
|
2174
3275
|
var KB_EDGE_KINDS = [
|
|
2175
3276
|
"body-link",
|
|
@@ -2365,6 +3466,18 @@ var CHECK_HEADLINES = {
|
|
|
2365
3466
|
unchecked: "an anchor in another repository nothing could reach"
|
|
2366
3467
|
};
|
|
2367
3468
|
var DAY_MS = 864e5;
|
|
3469
|
+
function anchorResolverCounts(bundle) {
|
|
3470
|
+
let treeSitter = 0;
|
|
3471
|
+
let regex = 0;
|
|
3472
|
+
for (const record of bundle) {
|
|
3473
|
+
for (const anchor of record.frontmatter.strauss_anchors ?? []) {
|
|
3474
|
+
if (!anchor.hash || !anchor.symbol) continue;
|
|
3475
|
+
if (anchor.resolver === "tree-sitter") treeSitter += 1;
|
|
3476
|
+
else regex += 1;
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
return { total: treeSitter + regex, treeSitter, regex };
|
|
3480
|
+
}
|
|
2368
3481
|
function doctor(bundle, options = {}) {
|
|
2369
3482
|
const thresholds = {
|
|
2370
3483
|
expiringDays: options.expiringDays ?? DEFAULT_EXPIRING_DAYS,
|
|
@@ -2400,6 +3513,7 @@ function doctor(bundle, options = {}) {
|
|
|
2400
3513
|
counts,
|
|
2401
3514
|
groups,
|
|
2402
3515
|
findingCount,
|
|
3516
|
+
anchorResolvers: anchorResolverCounts(bundle),
|
|
2403
3517
|
healthy: findingCount === 0
|
|
2404
3518
|
};
|
|
2405
3519
|
}
|
|
@@ -2416,18 +3530,18 @@ function expired(hits, now) {
|
|
|
2416
3530
|
for (const hit of hits) {
|
|
2417
3531
|
const raw = hit.record.frontmatter.stale_after;
|
|
2418
3532
|
if (!raw) continue;
|
|
2419
|
-
const
|
|
2420
|
-
if (Number.isNaN(
|
|
3533
|
+
const at2 = Date.parse(raw);
|
|
3534
|
+
if (Number.isNaN(at2)) {
|
|
2421
3535
|
findings.push(
|
|
2422
3536
|
finding(hit.record, `stale_after "${raw}" is not a readable date`)
|
|
2423
3537
|
);
|
|
2424
3538
|
continue;
|
|
2425
3539
|
}
|
|
2426
|
-
if (
|
|
3540
|
+
if (at2 < now.getTime()) {
|
|
2427
3541
|
findings.push(
|
|
2428
3542
|
finding(
|
|
2429
3543
|
hit.record,
|
|
2430
|
-
`stale since ${raw} (${daysBetween(
|
|
3544
|
+
`stale since ${raw} (${daysBetween(at2, now.getTime())} days ago)`
|
|
2431
3545
|
)
|
|
2432
3546
|
);
|
|
2433
3547
|
}
|
|
@@ -2440,12 +3554,12 @@ function expiring(hits, now, withinDays) {
|
|
|
2440
3554
|
for (const hit of hits) {
|
|
2441
3555
|
const raw = hit.record.frontmatter.stale_after;
|
|
2442
3556
|
if (!raw) continue;
|
|
2443
|
-
const
|
|
2444
|
-
if (Number.isNaN(
|
|
3557
|
+
const at2 = Date.parse(raw);
|
|
3558
|
+
if (Number.isNaN(at2) || at2 < now.getTime() || at2 > horizon) continue;
|
|
2445
3559
|
findings.push(
|
|
2446
3560
|
finding(
|
|
2447
3561
|
hit.record,
|
|
2448
|
-
`goes stale ${raw} (in ${daysBetween(now.getTime(),
|
|
3562
|
+
`goes stale ${raw} (in ${daysBetween(now.getTime(), at2)} days)`
|
|
2449
3563
|
)
|
|
2450
3564
|
);
|
|
2451
3565
|
}
|
|
@@ -2615,13 +3729,16 @@ function anchorFindings(hits, kind, headline) {
|
|
|
2615
3729
|
);
|
|
2616
3730
|
}
|
|
2617
3731
|
function describeAnchor(anchor) {
|
|
2618
|
-
const
|
|
2619
|
-
if (anchor.
|
|
3732
|
+
const at2 = anchor.symbol ? `${anchor.file}:${anchor.symbol}` : anchor.file;
|
|
3733
|
+
if (anchor.class === "gone") {
|
|
3734
|
+
return `${at2} gone${anchor.reason ? ` (${anchor.reason})` : ""}`;
|
|
3735
|
+
}
|
|
3736
|
+
if (anchor.reason) return `${at2} (${anchor.reason})`;
|
|
2620
3737
|
if (anchor.remoteState === "drifted-on-default") {
|
|
2621
|
-
return `${
|
|
3738
|
+
return `${at2} (matches ref, moved on the default branch)`;
|
|
2622
3739
|
}
|
|
2623
|
-
if (anchor.diffSize === null) return `${
|
|
2624
|
-
return anchor.diffSize === 0 ? `${
|
|
3740
|
+
if (anchor.diffSize === null) return `${at2} (changed, size unrecorded)`;
|
|
3741
|
+
return anchor.diffSize === 0 ? `${at2} (content changed, same line count)` : `${at2} (${anchor.diffSize} line${anchor.diffSize === 1 ? "" : "s"} apart)`;
|
|
2625
3742
|
}
|
|
2626
3743
|
function replaces(later, earlier) {
|
|
2627
3744
|
return (later.frontmatter.strauss_supersedes ?? []).includes(earlier.conceptId) || earlier.frontmatter.strauss_superseded_by === later.conceptId;
|
|
@@ -2638,17 +3755,17 @@ function daysBetween(from, to) {
|
|
|
2638
3755
|
return Math.max(0, Math.floor((to - from) / DAY_MS));
|
|
2639
3756
|
}
|
|
2640
3757
|
function ageInDays(record, now) {
|
|
2641
|
-
const
|
|
2642
|
-
if (!
|
|
2643
|
-
const written = Date.parse(
|
|
3758
|
+
const at2 = record.frontmatter.generated?.at;
|
|
3759
|
+
if (!at2) return null;
|
|
3760
|
+
const written = Date.parse(at2);
|
|
2644
3761
|
if (Number.isNaN(written)) return null;
|
|
2645
3762
|
return daysBetween(written, now.getTime());
|
|
2646
3763
|
}
|
|
2647
3764
|
|
|
2648
3765
|
// src/kb-log.ts
|
|
2649
|
-
import { z as
|
|
3766
|
+
import { z as z6 } from "zod";
|
|
2650
3767
|
var LOG_FILE = "log.jsonl";
|
|
2651
|
-
var kbLogEntrySchema =
|
|
3768
|
+
var kbLogEntrySchema = z6.object({
|
|
2652
3769
|
// Validated, not just `min(1)`: `at` is a sort key (see `parseLog`
|
|
2653
3770
|
// below), and a value that isn't actually chronological — a Unix
|
|
2654
3771
|
// timestamp, a human-typed date, garbage — would sort wrong without
|
|
@@ -2657,12 +3774,12 @@ var kbLogEntrySchema = z5.object({
|
|
|
2657
3774
|
// and rejects everything else, including a non-`Z` offset — so a
|
|
2658
3775
|
// malformed `at` is reported the same way a malformed line already is,
|
|
2659
3776
|
// rather than silently sorting into the wrong place.
|
|
2660
|
-
at:
|
|
2661
|
-
by:
|
|
2662
|
-
operation:
|
|
2663
|
-
conceptId:
|
|
3777
|
+
at: z6.iso.datetime(),
|
|
3778
|
+
by: z6.string().min(1),
|
|
3779
|
+
operation: z6.string().min(1),
|
|
3780
|
+
conceptId: z6.string().min(1),
|
|
2664
3781
|
/** Second concept id, where the operation relates two — supersession. */
|
|
2665
|
-
target:
|
|
3782
|
+
target: z6.string().min(1).optional()
|
|
2666
3783
|
}).strict();
|
|
2667
3784
|
function renderLogEntry(entry) {
|
|
2668
3785
|
return `${JSON.stringify(kbLogEntrySchema.parse(entry))}
|
|
@@ -2672,18 +3789,18 @@ function parseLog(raw) {
|
|
|
2672
3789
|
const entries = [];
|
|
2673
3790
|
const malformed = [];
|
|
2674
3791
|
const seen = /* @__PURE__ */ new Set();
|
|
2675
|
-
raw.split("\n").forEach((text,
|
|
3792
|
+
raw.split("\n").forEach((text, index2) => {
|
|
2676
3793
|
if (!text.trim()) return;
|
|
2677
3794
|
let value;
|
|
2678
3795
|
try {
|
|
2679
3796
|
value = JSON.parse(text);
|
|
2680
3797
|
} catch {
|
|
2681
|
-
malformed.push({ line:
|
|
3798
|
+
malformed.push({ line: index2 + 1, text });
|
|
2682
3799
|
return;
|
|
2683
3800
|
}
|
|
2684
3801
|
const parsed = kbLogEntrySchema.safeParse(value);
|
|
2685
3802
|
if (!parsed.success) {
|
|
2686
|
-
malformed.push({ line:
|
|
3803
|
+
malformed.push({ line: index2 + 1, text });
|
|
2687
3804
|
return;
|
|
2688
3805
|
}
|
|
2689
3806
|
const key = JSON.stringify(parsed.data);
|
|
@@ -2698,14 +3815,14 @@ function parseLog(raw) {
|
|
|
2698
3815
|
}
|
|
2699
3816
|
|
|
2700
3817
|
// src/json-schema.ts
|
|
2701
|
-
import { z as
|
|
3818
|
+
import { z as z7 } from "zod";
|
|
2702
3819
|
function kbJsonSchemas() {
|
|
2703
3820
|
return {
|
|
2704
|
-
recordFrontmatter:
|
|
3821
|
+
recordFrontmatter: z7.toJSONSchema(kbRecordFrontmatterSchema, {
|
|
2705
3822
|
io: "input"
|
|
2706
3823
|
}),
|
|
2707
|
-
composeInput:
|
|
2708
|
-
logEntry:
|
|
3824
|
+
composeInput: z7.toJSONSchema(composeInputSchema, { io: "input" }),
|
|
3825
|
+
logEntry: z7.toJSONSchema(kbLogEntrySchema, { io: "input" })
|
|
2709
3826
|
};
|
|
2710
3827
|
}
|
|
2711
3828
|
|
|
@@ -2753,18 +3870,18 @@ function trace(seedId, bundle, options = {}) {
|
|
|
2753
3870
|
return [...reached.values()].sort(byGeneratedAt);
|
|
2754
3871
|
}
|
|
2755
3872
|
function byGeneratedAt(left, right) {
|
|
2756
|
-
const
|
|
2757
|
-
return
|
|
3873
|
+
const at2 = (step) => step.record.frontmatter.generated?.at ?? "";
|
|
3874
|
+
return at2(left).localeCompare(at2(right)) || left.depth - right.depth;
|
|
2758
3875
|
}
|
|
2759
3876
|
|
|
2760
3877
|
// src/commands/anchor-resolve.ts
|
|
2761
|
-
import { z as
|
|
3878
|
+
import { z as z9 } from "zod";
|
|
2762
3879
|
|
|
2763
3880
|
// src/commands/model.ts
|
|
2764
|
-
import { z as
|
|
2765
|
-
var bundlePath =
|
|
2766
|
-
var conceptId =
|
|
2767
|
-
var REPO_ROOT =
|
|
3881
|
+
import { z as z8 } from "zod";
|
|
3882
|
+
var bundlePath = z8.string().min(1).describe("Absolute path to the knowledge base directory.");
|
|
3883
|
+
var conceptId = z8.string().min(1).describe("e.g. decision.cursor-v2");
|
|
3884
|
+
var REPO_ROOT = z8.string().min(1).optional().describe(
|
|
2768
3885
|
"Where the anchored source lives, for the drift check. Defaults to the working directory."
|
|
2769
3886
|
);
|
|
2770
3887
|
function define(command) {
|
|
@@ -2777,9 +3894,9 @@ function argvFlag(argv, name) {
|
|
|
2777
3894
|
if (!value2) throw new KbMissingFlagValueError(name);
|
|
2778
3895
|
return value2;
|
|
2779
3896
|
}
|
|
2780
|
-
const
|
|
2781
|
-
if (
|
|
2782
|
-
const value = argv[
|
|
3897
|
+
const at2 = argv.indexOf(name);
|
|
3898
|
+
if (at2 === -1) return void 0;
|
|
3899
|
+
const value = argv[at2 + 1];
|
|
2783
3900
|
if (value === void 0 || value.startsWith("--")) {
|
|
2784
3901
|
throw new KbMissingFlagValueError(name);
|
|
2785
3902
|
}
|
|
@@ -2787,22 +3904,30 @@ function argvFlag(argv, name) {
|
|
|
2787
3904
|
}
|
|
2788
3905
|
|
|
2789
3906
|
// src/commands/anchor-resolve.ts
|
|
3907
|
+
function resolverSummary(results) {
|
|
3908
|
+
const names = [
|
|
3909
|
+
...new Set(
|
|
3910
|
+
results.flatMap((entry) => entry.resolver ? [entry.resolver] : [])
|
|
3911
|
+
)
|
|
3912
|
+
].sort();
|
|
3913
|
+
return names.length ? `${names.join(" + ")} resolver` : "whole-file";
|
|
3914
|
+
}
|
|
2790
3915
|
var anchorResolveCommand = define({
|
|
2791
3916
|
name: "anchor-resolve",
|
|
2792
3917
|
tool: "kb_anchor_resolve",
|
|
2793
3918
|
usage: "anchor-resolve <concept-id> [--repo-root <path>] [--offline] [--rebaseline] [--restamp]",
|
|
2794
3919
|
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.",
|
|
2795
|
-
input:
|
|
3920
|
+
input: z9.object({
|
|
2796
3921
|
bundlePath,
|
|
2797
3922
|
conceptId,
|
|
2798
|
-
repoRoot:
|
|
2799
|
-
offline:
|
|
3923
|
+
repoRoot: z9.string().min(1).optional(),
|
|
3924
|
+
offline: z9.boolean().optional().describe(
|
|
2800
3925
|
"Resolve foreign anchors from the local repo cache only, never fetching."
|
|
2801
3926
|
),
|
|
2802
|
-
rebaseline:
|
|
3927
|
+
rebaseline: z9.boolean().optional().describe(
|
|
2803
3928
|
"Accept the current code as the new baseline for anchors that drifted."
|
|
2804
3929
|
),
|
|
2805
|
-
restamp:
|
|
3930
|
+
restamp: z9.boolean().optional().describe(
|
|
2806
3931
|
"Refresh `resolved_at` on anchors that already match. Off by default, so a green run writes nothing."
|
|
2807
3932
|
)
|
|
2808
3933
|
}),
|
|
@@ -2831,6 +3956,11 @@ var anchorResolveCommand = define({
|
|
|
2831
3956
|
const updated = [];
|
|
2832
3957
|
let dirty = false;
|
|
2833
3958
|
const sources = await readSources(anchors, root, offline === true);
|
|
3959
|
+
const resolvers = defaultAnchorResolvers({ offline: offline === true });
|
|
3960
|
+
await prepareResolvers(
|
|
3961
|
+
resolvers,
|
|
3962
|
+
anchors.map((anchor) => anchor.file)
|
|
3963
|
+
);
|
|
2834
3964
|
for (const anchor of anchors) {
|
|
2835
3965
|
const base2 = {
|
|
2836
3966
|
file: anchor.file,
|
|
@@ -2847,27 +3977,39 @@ var anchorResolveCommand = define({
|
|
|
2847
3977
|
updated.push(anchor);
|
|
2848
3978
|
continue;
|
|
2849
3979
|
}
|
|
2850
|
-
const
|
|
2851
|
-
if (!
|
|
3980
|
+
const outcome = resolveAnchorSpan(source.source, anchor, resolvers);
|
|
3981
|
+
if (!outcome.ok) {
|
|
2852
3982
|
results.push({
|
|
2853
3983
|
...base2,
|
|
2854
3984
|
state: "unresolved",
|
|
2855
|
-
reason:
|
|
3985
|
+
reason: outcome.reason
|
|
2856
3986
|
});
|
|
2857
3987
|
updated.push(anchor);
|
|
2858
3988
|
continue;
|
|
2859
3989
|
}
|
|
2860
|
-
const
|
|
3990
|
+
const resolved = outcome.span;
|
|
3991
|
+
const producedBy = outcome.resolver;
|
|
3992
|
+
const { hash: currentHash, kind } = anchorHashOf(anchor, outcome);
|
|
2861
3993
|
const currentLines = resolved.endLine - resolved.startLine + 1;
|
|
3994
|
+
const stampedKind = outcome.normalized ? "ast" : "raw";
|
|
3995
|
+
const stampedHash = outcome.normalized ? anchorHashOf({ ...anchor, hash: void 0 }, outcome).hash : currentHash;
|
|
2862
3996
|
const stamped = {
|
|
2863
3997
|
...anchor,
|
|
2864
|
-
hash:
|
|
3998
|
+
hash: stampedHash,
|
|
3999
|
+
hash_kind: stampedKind,
|
|
2865
4000
|
lines: currentLines,
|
|
2866
|
-
resolved_at: now()
|
|
4001
|
+
resolved_at: now(),
|
|
4002
|
+
...producedBy ? { resolver: producedBy } : {}
|
|
2867
4003
|
};
|
|
2868
4004
|
const pinned = anchor.ref !== void 0 && source.repo !== void 0;
|
|
2869
4005
|
if (!anchor.hash) {
|
|
2870
|
-
results.push({
|
|
4006
|
+
results.push({
|
|
4007
|
+
...base2,
|
|
4008
|
+
state: "stamped",
|
|
4009
|
+
currentHash: stampedHash,
|
|
4010
|
+
hashKind: stampedKind,
|
|
4011
|
+
...producedBy ? { resolver: producedBy } : {}
|
|
4012
|
+
});
|
|
2871
4013
|
updated.push(stamped);
|
|
2872
4014
|
dirty = true;
|
|
2873
4015
|
continue;
|
|
@@ -2877,7 +4019,12 @@ var anchorResolveCommand = define({
|
|
|
2877
4019
|
...base2,
|
|
2878
4020
|
state: "drifted",
|
|
2879
4021
|
currentHash,
|
|
4022
|
+
hashKind: kind,
|
|
2880
4023
|
diffSize: lineDelta(anchor, currentLines),
|
|
4024
|
+
...producedBy ? { resolver: producedBy } : {},
|
|
4025
|
+
// A regex-stamped anchor re-read by tree-sitter drifts because the
|
|
4026
|
+
// resolver changed, not because the code did.
|
|
4027
|
+
...resolverChanged(source.source, anchor, producedBy) ? { reason: "resolver-changed" } : {},
|
|
2881
4028
|
...pinned ? { remoteState: "drifted-from-ref" } : {},
|
|
2882
4029
|
...rebaseline ? { rebaselined: true } : {}
|
|
2883
4030
|
});
|
|
@@ -2885,7 +4032,7 @@ var anchorResolveCommand = define({
|
|
|
2885
4032
|
if (rebaseline) dirty = true;
|
|
2886
4033
|
continue;
|
|
2887
4034
|
}
|
|
2888
|
-
const onDefault = pinned ? headHash(source, anchor) : void 0;
|
|
4035
|
+
const onDefault = pinned ? headHash(source, anchor, resolvers) : void 0;
|
|
2889
4036
|
if (onDefault && onDefault.hash !== anchor.hash) {
|
|
2890
4037
|
results.push({
|
|
2891
4038
|
...base2,
|
|
@@ -2901,6 +4048,8 @@ var anchorResolveCommand = define({
|
|
|
2901
4048
|
...base2,
|
|
2902
4049
|
state: "match",
|
|
2903
4050
|
currentHash,
|
|
4051
|
+
hashKind: kind,
|
|
4052
|
+
...producedBy ? { resolver: producedBy } : {},
|
|
2904
4053
|
...pinned ? { remoteState: "matches-ref" } : {}
|
|
2905
4054
|
});
|
|
2906
4055
|
const refresh = restamp || anchor.resolved_at === void 0;
|
|
@@ -2918,19 +4067,21 @@ var anchorResolveCommand = define({
|
|
|
2918
4067
|
if (!frozen) await store.updateAnchors(path, id, updated, actor);
|
|
2919
4068
|
}
|
|
2920
4069
|
const frozenNote = frozen ? { frozen: true, note: "base is frozen: nothing was stamped" } : {};
|
|
4070
|
+
const hints = grammarHints();
|
|
4071
|
+
const hintNote = hints.length ? { hints } : {};
|
|
2921
4072
|
const unreachable = results.filter(
|
|
2922
4073
|
(entry) => isUncheckedReason(entry.reason)
|
|
2923
4074
|
).length;
|
|
2924
4075
|
const checked = results.length - unreachable;
|
|
2925
|
-
const
|
|
2926
|
-
const note = `${
|
|
2927
|
-
const clean = checked > 0 &&
|
|
4076
|
+
const matches3 = results.filter((entry) => entry.state === "match").length;
|
|
4077
|
+
const note = `${matches3}/${checked} anchors match${unreachable ? `, ${unreachable} unreachable` : ""}`;
|
|
4078
|
+
const clean = checked > 0 && matches3 === checked && unreachable === 0;
|
|
2928
4079
|
if (clean) {
|
|
2929
4080
|
try {
|
|
2930
4081
|
await store.verify(
|
|
2931
4082
|
path,
|
|
2932
4083
|
id,
|
|
2933
|
-
`anchor-resolve: ${note} (
|
|
4084
|
+
`anchor-resolve: ${note} (${resolverSummary(results)})`,
|
|
2934
4085
|
actor,
|
|
2935
4086
|
now()
|
|
2936
4087
|
);
|
|
@@ -2941,17 +4092,25 @@ var anchorResolveCommand = define({
|
|
|
2941
4092
|
results,
|
|
2942
4093
|
verified: false,
|
|
2943
4094
|
verifyRefused: "self-verification",
|
|
2944
|
-
...frozenNote
|
|
4095
|
+
...frozenNote,
|
|
4096
|
+
...hintNote
|
|
2945
4097
|
};
|
|
2946
4098
|
}
|
|
2947
|
-
return {
|
|
4099
|
+
return {
|
|
4100
|
+
conceptId: id,
|
|
4101
|
+
results,
|
|
4102
|
+
verified: true,
|
|
4103
|
+
...frozenNote,
|
|
4104
|
+
...hintNote
|
|
4105
|
+
};
|
|
2948
4106
|
}
|
|
2949
4107
|
return {
|
|
2950
4108
|
conceptId: id,
|
|
2951
4109
|
results,
|
|
2952
4110
|
verified: false,
|
|
2953
4111
|
...unreachable ? { note } : {},
|
|
2954
|
-
...frozenNote
|
|
4112
|
+
...frozenNote,
|
|
4113
|
+
...hintNote
|
|
2955
4114
|
};
|
|
2956
4115
|
},
|
|
2957
4116
|
// A stored hash that no longer resolves is a broken anchor, not an absence:
|
|
@@ -2967,13 +4126,13 @@ var anchorResolveCommand = define({
|
|
|
2967
4126
|
function lineDelta(anchor, current) {
|
|
2968
4127
|
return anchor.lines === void 0 ? null : Math.abs(current - anchor.lines);
|
|
2969
4128
|
}
|
|
2970
|
-
function headHash(source, anchor) {
|
|
4129
|
+
function headHash(source, anchor, resolvers) {
|
|
2971
4130
|
if (source.head === void 0) return void 0;
|
|
2972
|
-
const
|
|
2973
|
-
if (!
|
|
4131
|
+
const outcome = resolveAnchorSpan(source.head, anchor, resolvers);
|
|
4132
|
+
if (!outcome.ok) return void 0;
|
|
2974
4133
|
return {
|
|
2975
|
-
hash: hashAnchorText(
|
|
2976
|
-
lines:
|
|
4134
|
+
hash: hashAnchorText(outcome.span.text),
|
|
4135
|
+
lines: outcome.span.endLine - outcome.span.startLine + 1
|
|
2977
4136
|
};
|
|
2978
4137
|
}
|
|
2979
4138
|
async function readSources(anchors, root, offline) {
|
|
@@ -3023,13 +4182,13 @@ async function readSources(anchors, root, offline) {
|
|
|
3023
4182
|
}
|
|
3024
4183
|
|
|
3025
4184
|
// src/commands/answer.ts
|
|
3026
|
-
import { z as
|
|
4185
|
+
import { z as z10 } from "zod";
|
|
3027
4186
|
var answerCommand = define({
|
|
3028
4187
|
name: "answer",
|
|
3029
4188
|
tool: "kb_answer",
|
|
3030
4189
|
usage: "answer <concept-id> <answer...>",
|
|
3031
4190
|
description: "Resolve an open question: set status, stamp who and when, append an Answer section. If the answer overturns a decision or assumption, supersede that record explicitly.",
|
|
3032
|
-
input:
|
|
4191
|
+
input: z10.object({ bundlePath, conceptId, answer: z10.string().min(1) }),
|
|
3033
4192
|
fromArgv: (argv, path) => ({
|
|
3034
4193
|
bundlePath: path,
|
|
3035
4194
|
conceptId: argv[1],
|
|
@@ -3043,27 +4202,27 @@ var answerCommand = define({
|
|
|
3043
4202
|
});
|
|
3044
4203
|
|
|
3045
4204
|
// src/commands/backlinks.ts
|
|
3046
|
-
import { z as
|
|
4205
|
+
import { z as z11 } from "zod";
|
|
3047
4206
|
var backlinksCommand = define({
|
|
3048
4207
|
name: "backlinks",
|
|
3049
4208
|
tool: "kb_backlinks",
|
|
3050
4209
|
usage: "backlinks <concept-id>",
|
|
3051
4210
|
description: "Who points at this record: every inbound typed causal link (`strauss_links`), one hop, every rel including `related_to`, each with its rel and the standing of the record that made it. Use it when you need the exact edges \u2014 reviewing or renaming a record.",
|
|
3052
|
-
input:
|
|
4211
|
+
input: z11.object({ bundlePath, conceptId }),
|
|
3053
4212
|
fromArgv: (argv, path) => ({ bundlePath: path, conceptId: argv[1] }),
|
|
3054
4213
|
run: async ({ store }, { bundlePath: path, conceptId: id }) => store.backlinks(path, id)
|
|
3055
4214
|
});
|
|
3056
4215
|
|
|
3057
4216
|
// src/commands/catalog.ts
|
|
3058
|
-
import { z as
|
|
4217
|
+
import { z as z12 } from "zod";
|
|
3059
4218
|
var catalogCommand = define({
|
|
3060
4219
|
name: "catalog",
|
|
3061
4220
|
tool: "kb_catalog",
|
|
3062
4221
|
usage: "catalog [type]",
|
|
3063
4222
|
description: "Lists every record as one line \u2014 concept id, type, title, standing, and a stale flag \u2014 at roughly thirty tokens each. Pick this over kb_load once kb_load refuses: kb_catalog never refuses. Superseded records show only their replacement; fetch bodies with kb_load, kb_pack, kb_query, or kb_trace.",
|
|
3064
|
-
input:
|
|
4223
|
+
input: z12.object({
|
|
3065
4224
|
bundlePath,
|
|
3066
|
-
type:
|
|
4225
|
+
type: z12.enum(KB_RECORD_TYPES).optional()
|
|
3067
4226
|
}),
|
|
3068
4227
|
fromArgv: (argv, path) => ({
|
|
3069
4228
|
bundlePath: path,
|
|
@@ -3118,26 +4277,26 @@ function count(value, noun) {
|
|
|
3118
4277
|
}
|
|
3119
4278
|
|
|
3120
4279
|
// src/commands/context.ts
|
|
3121
|
-
import { z as
|
|
4280
|
+
import { z as z13 } from "zod";
|
|
3122
4281
|
var contextCommand = define({
|
|
3123
4282
|
name: "context",
|
|
3124
4283
|
tool: "kb_context",
|
|
3125
4284
|
usage: "context [--profile NAME] [--budget N] [--full-under N] [--format json] [--event NAME]",
|
|
3126
4285
|
description: "Index block of pinned bases (ids, titles, standing) for injection at context birth. Takes no bundlePath \u2014 reads the workspace pin manifests. Empty when nothing is pinned; refuses over budget rather than truncating. Budget precedence: flags, then the manifest `context[profile]` over `context.default`, then the built-in profile, then package defaults.",
|
|
3127
|
-
input:
|
|
3128
|
-
budgetTokens:
|
|
4286
|
+
input: z13.object({
|
|
4287
|
+
budgetTokens: z13.number().int().positive().optional().describe(
|
|
3129
4288
|
"Ceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000."
|
|
3130
4289
|
),
|
|
3131
|
-
fullUnderTokens:
|
|
4290
|
+
fullUnderTokens: z13.number().int().positive().optional().describe(
|
|
3132
4291
|
"Per-base rendering threshold, applied before the budget: a base whose complete load fits under this arrives as full records instead of index lines, and the whole block still answers to budgetTokens. Off by default \u2014 index-only is the safe default at a context birth, because injected bodies outlive the qualifiers on them; the session-start profile opts tiny bases in at 1500."
|
|
3133
4292
|
),
|
|
3134
|
-
profile:
|
|
4293
|
+
profile: z13.string().optional().describe(
|
|
3135
4294
|
"Named budget set: built-ins are session-start (full-under 1500), compact and turn (budget 2500); the manifests' `context` tables override per repo. Unknown names fall through to defaults rather than failing."
|
|
3136
4295
|
),
|
|
3137
|
-
format:
|
|
4296
|
+
format: z13.enum(["markdown", "json"]).optional().describe(
|
|
3138
4297
|
"CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
|
|
3139
4298
|
),
|
|
3140
|
-
event:
|
|
4299
|
+
event: z13.string().optional().describe(
|
|
3141
4300
|
"hookEventName stamped into the JSON envelope. Only meaningful with format=json."
|
|
3142
4301
|
)
|
|
3143
4302
|
}),
|
|
@@ -3173,14 +4332,168 @@ var contextCommand = define({
|
|
|
3173
4332
|
});
|
|
3174
4333
|
|
|
3175
4334
|
// src/commands/doctor.ts
|
|
3176
|
-
import { z as
|
|
3177
|
-
|
|
4335
|
+
import { z as z15 } from "zod";
|
|
4336
|
+
|
|
4337
|
+
// src/commands/reassess.ts
|
|
4338
|
+
import { z as z14 } from "zod";
|
|
4339
|
+
var reassessCommand = define({
|
|
4340
|
+
name: "reassess",
|
|
4341
|
+
tool: "kb_reassess",
|
|
4342
|
+
usage: "reassess <concept-id> [--repo-root <path>] [--with-diff]",
|
|
4343
|
+
description: "One drifted record, as something to judge: its claim, each anchor's drift class, the old-vs-new span diff, and the records that depend on it. Formatting-only drift is dropped. Empty when there is nothing to reassess. Writes: relocates moved anchors, keeping their hash; never verifies, supersedes, or changes standing.",
|
|
4344
|
+
input: z14.object({
|
|
4345
|
+
bundlePath,
|
|
4346
|
+
conceptId,
|
|
4347
|
+
repoRoot: REPO_ROOT,
|
|
4348
|
+
withDiff: z14.boolean().optional().describe(
|
|
4349
|
+
"Recover each anchor's committed span and render the diff. Reads git history."
|
|
4350
|
+
)
|
|
4351
|
+
}),
|
|
4352
|
+
fromArgv: (argv, path) => {
|
|
4353
|
+
const repoRoot = argvFlag(argv, "--repo-root");
|
|
4354
|
+
return {
|
|
4355
|
+
bundlePath: path,
|
|
4356
|
+
conceptId: argv[1],
|
|
4357
|
+
...repoRoot !== void 0 ? { repoRoot } : {},
|
|
4358
|
+
...argv.includes("--with-diff") ? { withDiff: true } : {}
|
|
4359
|
+
};
|
|
4360
|
+
},
|
|
4361
|
+
run: async ({ store, actor }, { bundlePath: path, conceptId: id, repoRoot, withDiff }) => {
|
|
4362
|
+
const root = repoRoot ?? process.cwd();
|
|
4363
|
+
const bundle = await store.list(path);
|
|
4364
|
+
const record = bundle.find((entry) => entry.conceptId === id);
|
|
4365
|
+
if (!record) throw new KbRecordNotFoundError(id);
|
|
4366
|
+
const drift = await store.detectDrift([record], repoRoot);
|
|
4367
|
+
const entries = drift?.get(id) ?? [];
|
|
4368
|
+
if (!entries.some((entry) => entry.state !== "match")) {
|
|
4369
|
+
return { conceptId: id, packet: null, rebaselined: [], cosmetic: 0 };
|
|
4370
|
+
}
|
|
4371
|
+
const standing = adjudicate(bundle, bundle).find(
|
|
4372
|
+
(hit) => hit.record.conceptId === id
|
|
4373
|
+
)?.standing;
|
|
4374
|
+
const impact2 = await store.impact(path, id);
|
|
4375
|
+
const { packet, classified } = await reassessPacket(root, record, entries, {
|
|
4376
|
+
...withDiff ? { withDiff: true } : {},
|
|
4377
|
+
impact: impact2,
|
|
4378
|
+
...standing ? { standing } : {}
|
|
4379
|
+
});
|
|
4380
|
+
const moves = classified.filter((found) => found.class === "moved");
|
|
4381
|
+
let frozen = false;
|
|
4382
|
+
const rebaselined = [];
|
|
4383
|
+
if (moves.length) {
|
|
4384
|
+
const relocated = /* @__PURE__ */ new Map();
|
|
4385
|
+
for (const found of moves) {
|
|
4386
|
+
const to = found.entry.movedTo;
|
|
4387
|
+
if (!to) continue;
|
|
4388
|
+
relocated.set(found.anchor, {
|
|
4389
|
+
...found.anchor,
|
|
4390
|
+
file: to.file,
|
|
4391
|
+
...to.symbol ? { symbol: to.symbol } : {}
|
|
4392
|
+
});
|
|
4393
|
+
rebaselined.push({
|
|
4394
|
+
file: found.anchor.file,
|
|
4395
|
+
...found.anchor.symbol ? { symbol: found.anchor.symbol } : {},
|
|
4396
|
+
toFile: to.file,
|
|
4397
|
+
...to.symbol ? { toSymbol: to.symbol } : {}
|
|
4398
|
+
});
|
|
4399
|
+
}
|
|
4400
|
+
try {
|
|
4401
|
+
await assertBaseNotFrozen(process.cwd(), path);
|
|
4402
|
+
} catch (error) {
|
|
4403
|
+
if (!(error instanceof KbBaseFrozenError)) throw error;
|
|
4404
|
+
frozen = true;
|
|
4405
|
+
}
|
|
4406
|
+
if (!frozen) {
|
|
4407
|
+
await store.updateAnchors(
|
|
4408
|
+
path,
|
|
4409
|
+
id,
|
|
4410
|
+
(record.frontmatter.strauss_anchors ?? []).map(
|
|
4411
|
+
(anchor) => relocated.get(anchor) ?? anchor
|
|
4412
|
+
),
|
|
4413
|
+
actor
|
|
4414
|
+
);
|
|
4415
|
+
}
|
|
4416
|
+
}
|
|
4417
|
+
return {
|
|
4418
|
+
conceptId: id,
|
|
4419
|
+
packet,
|
|
4420
|
+
rebaselined: frozen ? [] : rebaselined,
|
|
4421
|
+
cosmetic: classified.filter((found) => found.class === "cosmetic").length,
|
|
4422
|
+
...frozen ? {
|
|
4423
|
+
frozen: true,
|
|
4424
|
+
note: "base is frozen: nothing was rebaselined"
|
|
4425
|
+
} : {}
|
|
4426
|
+
};
|
|
4427
|
+
},
|
|
4428
|
+
render: (result) => renderReassess(result)
|
|
4429
|
+
});
|
|
4430
|
+
function renderReassess(result) {
|
|
4431
|
+
const lines = [];
|
|
4432
|
+
for (const move of result.rebaselined) {
|
|
4433
|
+
lines.push(
|
|
4434
|
+
`rebaselined: ${at(move.file, move.symbol)} \u2192 ${at(move.toFile, move.toSymbol)} (same code, new address)`
|
|
4435
|
+
);
|
|
4436
|
+
}
|
|
4437
|
+
if (result.cosmetic) {
|
|
4438
|
+
lines.push(
|
|
4439
|
+
`${result.cosmetic} anchor${result.cosmetic === 1 ? "" : "s"} changed formatting only.`
|
|
4440
|
+
);
|
|
4441
|
+
}
|
|
4442
|
+
if (result.note) lines.push(result.note);
|
|
4443
|
+
const packet = result.packet;
|
|
4444
|
+
if (!packet) {
|
|
4445
|
+
lines.push(`${result.conceptId}: nothing to reassess.`);
|
|
4446
|
+
return lines.join("\n");
|
|
4447
|
+
}
|
|
4448
|
+
lines.push(
|
|
4449
|
+
"",
|
|
4450
|
+
`# ${packet.conceptId}${packet.title ? ` \u2014 ${packet.title}` : ""}`,
|
|
4451
|
+
`type: ${packet.type} standing: ${packet.standing}`,
|
|
4452
|
+
...packet.why ? [`why: ${packet.why}`] : [],
|
|
4453
|
+
...packet.claim ? ["", `## ${packet.claim.section}`, packet.claim.text] : [],
|
|
4454
|
+
"",
|
|
4455
|
+
`## Anchors (${packet.anchors.length})`
|
|
4456
|
+
);
|
|
4457
|
+
for (const anchor of packet.anchors) {
|
|
4458
|
+
lines.push(
|
|
4459
|
+
`- ${at(anchor.file, anchor.symbol)} \u2014 ${anchor.class}${anchor.reason ? ` (${anchor.reason})` : ""}`
|
|
4460
|
+
);
|
|
4461
|
+
if (!anchor.diff) continue;
|
|
4462
|
+
if (anchor.diff.status === "unrecoverable") {
|
|
4463
|
+
lines.push(
|
|
4464
|
+
" diff: unrecoverable \u2014 no committed span to compare against"
|
|
4465
|
+
);
|
|
4466
|
+
continue;
|
|
4467
|
+
}
|
|
4468
|
+
lines.push(
|
|
4469
|
+
` diff vs ${anchor.diff.ref} (${anchor.diff.source}): +${anchor.diff.added} \u2212${anchor.diff.removed}`,
|
|
4470
|
+
...anchor.diff.unified.split("\n").map((line) => ` ${line}`)
|
|
4471
|
+
);
|
|
4472
|
+
}
|
|
4473
|
+
if (packet.impact.length) {
|
|
4474
|
+
lines.push("", `## Impact (${packet.impact.length})`);
|
|
4475
|
+
for (const entry of packet.impact) {
|
|
4476
|
+
lines.push(
|
|
4477
|
+
`- ${entry.conceptId} [${entry.standing}]${entry.title ? ` \u2014 ${entry.title}` : ""}`
|
|
4478
|
+
);
|
|
4479
|
+
}
|
|
4480
|
+
if (packet.impactTruncated) lines.push("- \u2026 walk truncated");
|
|
4481
|
+
}
|
|
4482
|
+
lines.push("", `Default: ${packet.default} \u2014 ${packet.defaultNote}.`);
|
|
4483
|
+
return lines.join("\n");
|
|
4484
|
+
}
|
|
4485
|
+
function at(file, symbol) {
|
|
4486
|
+
return symbol ? `${file}:${symbol}` : file;
|
|
4487
|
+
}
|
|
4488
|
+
|
|
4489
|
+
// src/commands/doctor.ts
|
|
4490
|
+
var days = (what, fallback) => z15.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
|
|
3178
4491
|
var doctorCommand = define({
|
|
3179
4492
|
name: "doctor",
|
|
3180
4493
|
tool: "kb_doctor",
|
|
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.
|
|
3183
|
-
input:
|
|
4494
|
+
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--offline] [--strict] [--drifted [--with-diff]]",
|
|
4495
|
+
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. `drifted` narrows it to a reassessment packet per drifted record, `with_diff` adding each anchor's old-vs-new span.",
|
|
4496
|
+
input: z15.object({
|
|
3184
4497
|
bundlePath,
|
|
3185
4498
|
repoRoot: REPO_ROOT,
|
|
3186
4499
|
expiringDays: days(
|
|
@@ -3195,11 +4508,17 @@ var doctorCommand = define({
|
|
|
3195
4508
|
"How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
|
|
3196
4509
|
DEFAULT_AGING_DAYS
|
|
3197
4510
|
),
|
|
3198
|
-
offline:
|
|
4511
|
+
offline: z15.boolean().optional().describe(
|
|
3199
4512
|
"Read foreign anchors from the local repo cache only, never fetching."
|
|
3200
4513
|
),
|
|
3201
|
-
strict:
|
|
4514
|
+
strict: z15.boolean().optional().describe(
|
|
3202
4515
|
"Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
|
|
4516
|
+
),
|
|
4517
|
+
drifted: z15.boolean().optional().describe(
|
|
4518
|
+
"Report only drift, as a reassessment packet per record: claim, per-anchor class, and what depends on it."
|
|
4519
|
+
),
|
|
4520
|
+
withDiff: z15.boolean().optional().describe(
|
|
4521
|
+
"With `drifted`: recover each anchor's committed span and render the old-vs-new diff. Reads git history."
|
|
3203
4522
|
)
|
|
3204
4523
|
}),
|
|
3205
4524
|
// Presence, not truthiness: `--expiring-days ""` is a caller who meant
|
|
@@ -3218,7 +4537,9 @@ var doctorCommand = define({
|
|
|
3218
4537
|
...unverified2 !== void 0 ? { unverifiedDays: Number(unverified2) } : {},
|
|
3219
4538
|
...agingDays !== void 0 ? { agingDays: Number(agingDays) } : {},
|
|
3220
4539
|
...argv.includes("--offline") ? { offline: true } : {},
|
|
3221
|
-
...argv.includes("--strict") ? { strict: true } : {}
|
|
4540
|
+
...argv.includes("--strict") ? { strict: true } : {},
|
|
4541
|
+
...argv.includes("--drifted") ? { drifted: true } : {},
|
|
4542
|
+
...argv.includes("--with-diff") ? { withDiff: true } : {}
|
|
3222
4543
|
};
|
|
3223
4544
|
},
|
|
3224
4545
|
run: async ({ store, now }, {
|
|
@@ -3227,7 +4548,9 @@ var doctorCommand = define({
|
|
|
3227
4548
|
unverifiedDays,
|
|
3228
4549
|
agingDays,
|
|
3229
4550
|
repoRoot,
|
|
3230
|
-
offline
|
|
4551
|
+
offline,
|
|
4552
|
+
drifted: drifted2,
|
|
4553
|
+
withDiff
|
|
3231
4554
|
}) => {
|
|
3232
4555
|
const checkedAt = now();
|
|
3233
4556
|
const records = await store.list(path);
|
|
@@ -3241,7 +4564,54 @@ var doctorCommand = define({
|
|
|
3241
4564
|
...anchorDrift !== void 0 ? { anchorDrift } : {},
|
|
3242
4565
|
now: new Date(checkedAt)
|
|
3243
4566
|
});
|
|
3244
|
-
|
|
4567
|
+
const hints = grammarHints();
|
|
4568
|
+
if (!drifted2) {
|
|
4569
|
+
return {
|
|
4570
|
+
bundlePath: path,
|
|
4571
|
+
checkedAt,
|
|
4572
|
+
...report,
|
|
4573
|
+
...hints.length ? { hints } : {}
|
|
4574
|
+
};
|
|
4575
|
+
}
|
|
4576
|
+
const standings = new Map(
|
|
4577
|
+
adjudicate(records, records, new Date(checkedAt)).map((hit) => [
|
|
4578
|
+
hit.record.conceptId,
|
|
4579
|
+
hit.standing
|
|
4580
|
+
])
|
|
4581
|
+
);
|
|
4582
|
+
const packets = [];
|
|
4583
|
+
const rebaselinable = [];
|
|
4584
|
+
const search = movedSearch(repoRoot ?? process.cwd());
|
|
4585
|
+
for (const found of report.groups.find((g) => g.check === "drifted")?.findings ?? []) {
|
|
4586
|
+
const record = records.find(
|
|
4587
|
+
(entry) => entry.conceptId === found.conceptId
|
|
4588
|
+
);
|
|
4589
|
+
if (!record) continue;
|
|
4590
|
+
const standing = standings.get(record.conceptId);
|
|
4591
|
+
const built = await reassessPacket(
|
|
4592
|
+
repoRoot ?? process.cwd(),
|
|
4593
|
+
record,
|
|
4594
|
+
anchorDrift?.get(record.conceptId) ?? [],
|
|
4595
|
+
{
|
|
4596
|
+
...withDiff ? { withDiff: true } : {},
|
|
4597
|
+
impact: await store.impact(path, record.conceptId),
|
|
4598
|
+
...standing ? { standing } : {},
|
|
4599
|
+
search
|
|
4600
|
+
}
|
|
4601
|
+
);
|
|
4602
|
+
if (built.packet) packets.push(built.packet);
|
|
4603
|
+
if (built.classified.some((entry) => entry.class === "moved")) {
|
|
4604
|
+
rebaselinable.push(record.conceptId);
|
|
4605
|
+
}
|
|
4606
|
+
}
|
|
4607
|
+
return {
|
|
4608
|
+
bundlePath: path,
|
|
4609
|
+
checkedAt,
|
|
4610
|
+
...report,
|
|
4611
|
+
packets,
|
|
4612
|
+
rebaselinable,
|
|
4613
|
+
...hints.length ? { hints } : {}
|
|
4614
|
+
};
|
|
3245
4615
|
},
|
|
3246
4616
|
render: (result) => render2(result),
|
|
3247
4617
|
// Only expiry, and only under --strict. The other seven checks report debt a
|
|
@@ -3253,18 +4623,22 @@ var doctorCommand = define({
|
|
|
3253
4623
|
failsWhen: (result, input) => input.strict === true && result.counts.expired > 0
|
|
3254
4624
|
});
|
|
3255
4625
|
function render2(result) {
|
|
4626
|
+
if (result.packets) return renderPackets(result);
|
|
3256
4627
|
const { thresholds } = result;
|
|
3257
4628
|
const lines = [
|
|
3258
4629
|
`# KB Doctor \u2014 ${result.bundlePath}`,
|
|
3259
4630
|
`records: ${result.recordCount}`,
|
|
3260
4631
|
`thresholds: expiring within ${thresholds.expiringDays}d, unverified over ${thresholds.unverifiedDays}d, aging over ${thresholds.agingDays}d`,
|
|
3261
4632
|
`checked: ${result.checkedAt}`,
|
|
4633
|
+
...result.anchorResolvers.total ? [
|
|
4634
|
+
`anchors: ${result.anchorResolvers.total} hashed \u2014 ${result.anchorResolvers.treeSitter} tree-sitter, ${result.anchorResolvers.regex} regex`
|
|
4635
|
+
] : [],
|
|
3262
4636
|
""
|
|
3263
4637
|
];
|
|
3264
|
-
const
|
|
4638
|
+
const width2 = Math.max(...result.groups.map((group2) => group2.check.length));
|
|
3265
4639
|
for (const group2 of result.groups) {
|
|
3266
4640
|
lines.push(
|
|
3267
|
-
` ${group2.check.padEnd(
|
|
4641
|
+
` ${group2.check.padEnd(width2)} ${String(group2.count).padStart(3)} ${group2.headline}`
|
|
3268
4642
|
);
|
|
3269
4643
|
}
|
|
3270
4644
|
for (const group2 of result.groups) {
|
|
@@ -3276,27 +4650,52 @@ function render2(result) {
|
|
|
3276
4650
|
);
|
|
3277
4651
|
}
|
|
3278
4652
|
}
|
|
4653
|
+
for (const hint of result.hints ?? []) lines.push("", hint);
|
|
3279
4654
|
lines.push(
|
|
3280
4655
|
"",
|
|
3281
4656
|
result.healthy ? "Nothing to repair." : `${result.findingCount} finding${result.findingCount === 1 ? "" : "s"} across ${result.groups.filter((group2) => group2.count).length} of ${result.groups.length} checks.`
|
|
3282
4657
|
);
|
|
3283
4658
|
return lines.join("\n");
|
|
3284
4659
|
}
|
|
4660
|
+
function renderPackets(result) {
|
|
4661
|
+
const packets = result.packets ?? [];
|
|
4662
|
+
const lines = [
|
|
4663
|
+
`# KB Drift \u2014 ${result.bundlePath}`,
|
|
4664
|
+
`checked: ${result.checkedAt}`,
|
|
4665
|
+
`${packets.length} record${packets.length === 1 ? "" : "s"} need a reading; ${result.counts.drifted} drifted in all.`
|
|
4666
|
+
];
|
|
4667
|
+
if (result.rebaselinable?.length) {
|
|
4668
|
+
lines.push(
|
|
4669
|
+
`moved, rebaseline with \`kb_reassess\`: ${result.rebaselinable.join(", ")}`
|
|
4670
|
+
);
|
|
4671
|
+
}
|
|
4672
|
+
for (const packet of packets) {
|
|
4673
|
+
lines.push(
|
|
4674
|
+
renderReassess({
|
|
4675
|
+
conceptId: packet.conceptId,
|
|
4676
|
+
packet,
|
|
4677
|
+
rebaselined: [],
|
|
4678
|
+
cosmetic: 0
|
|
4679
|
+
})
|
|
4680
|
+
);
|
|
4681
|
+
}
|
|
4682
|
+
return lines.join("\n");
|
|
4683
|
+
}
|
|
3285
4684
|
|
|
3286
4685
|
// src/commands/impact.ts
|
|
3287
|
-
import { z as
|
|
4686
|
+
import { z as z16 } from "zod";
|
|
3288
4687
|
var impactCommand = define({
|
|
3289
4688
|
name: "impact",
|
|
3290
4689
|
tool: "kb_impact",
|
|
3291
4690
|
usage: "impact <concept-id> [--depth N] [--rels a,b]",
|
|
3292
4691
|
description: "What breaks if this record changes: its transitive set of dependants, each with its standing. Each rel declares which of its ends depends on the other, and the walk follows each rel in its own direction. Naming `related_to` or an unknown rel in `rels` is an error. kb_backlinks gives one flat hop.",
|
|
3293
|
-
input:
|
|
4692
|
+
input: z16.object({
|
|
3294
4693
|
bundlePath,
|
|
3295
4694
|
conceptId,
|
|
3296
|
-
depth:
|
|
4695
|
+
depth: z16.number().int().positive().optional().describe(
|
|
3297
4696
|
"Hops out from the record. Unbounded when omitted; a walk this cuts reports truncated: true."
|
|
3298
4697
|
),
|
|
3299
|
-
rels:
|
|
4698
|
+
rels: z16.array(z16.enum(KB_CAUSAL_LINK_RELS)).optional().describe(
|
|
3300
4699
|
"Narrow which rels the walk follows. Defaults to every rel that carries a dependence \u2014 all but related_to."
|
|
3301
4700
|
)
|
|
3302
4701
|
}),
|
|
@@ -3317,13 +4716,13 @@ var impactCommand = define({
|
|
|
3317
4716
|
});
|
|
3318
4717
|
|
|
3319
4718
|
// src/commands/list.ts
|
|
3320
|
-
import { z as
|
|
4719
|
+
import { z as z17 } from "zod";
|
|
3321
4720
|
var listCommand = define({
|
|
3322
4721
|
name: "list",
|
|
3323
4722
|
tool: "kb_list",
|
|
3324
4723
|
usage: "list [type]",
|
|
3325
4724
|
description: "Every record, optionally one type. For enumerating; use kb_query for a question.",
|
|
3326
|
-
input:
|
|
4725
|
+
input: z17.object({ bundlePath, type: z17.enum(KB_RECORD_TYPES).optional() }),
|
|
3327
4726
|
fromArgv: (argv, path) => ({ bundlePath: path, type: argv[1] }),
|
|
3328
4727
|
run: async ({ store }, { bundlePath: path, type }) => (await store.list(path, type)).map((record) => ({
|
|
3329
4728
|
conceptId: record.conceptId,
|
|
@@ -3335,17 +4734,17 @@ var listCommand = define({
|
|
|
3335
4734
|
});
|
|
3336
4735
|
|
|
3337
4736
|
// src/commands/load.ts
|
|
3338
|
-
import { z as
|
|
4737
|
+
import { z as z18 } from "zod";
|
|
3339
4738
|
var loadCommand = define({
|
|
3340
4739
|
name: "load",
|
|
3341
4740
|
tool: "kb_load",
|
|
3342
4741
|
usage: "load [type] [--budget N | --all] [--repo-root PATH]",
|
|
3343
4742
|
description: "Load the whole base, each record with its standing \u2014 call it first, at the point of use, since compaction drops it. Superseded records arrive as stubs; kb_trace has the history. Over budget it refuses: kb_catalog, then kb_pack, or narrow with `type`; `all` bypasses. Never read record files directly \u2014 only kb_* tools resolve supersession. `digest` stamps the base's content, so hooks know when to reload.",
|
|
3344
|
-
input:
|
|
4743
|
+
input: z18.object({
|
|
3345
4744
|
bundlePath,
|
|
3346
|
-
type:
|
|
3347
|
-
budgetTokens:
|
|
3348
|
-
all:
|
|
4745
|
+
type: z18.enum(KB_RECORD_TYPES).optional(),
|
|
4746
|
+
budgetTokens: z18.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
|
|
4747
|
+
all: z18.boolean().optional().describe(
|
|
3349
4748
|
"Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
|
|
3350
4749
|
),
|
|
3351
4750
|
repoRoot: REPO_ROOT
|
|
@@ -3387,25 +4786,25 @@ var loadCommand = define({
|
|
|
3387
4786
|
});
|
|
3388
4787
|
|
|
3389
4788
|
// src/commands/log.ts
|
|
3390
|
-
import { z as
|
|
4789
|
+
import { z as z19 } from "zod";
|
|
3391
4790
|
var logCommand = define({
|
|
3392
4791
|
name: "log",
|
|
3393
4792
|
tool: "kb_log",
|
|
3394
4793
|
usage: "log",
|
|
3395
4794
|
description: "Who touched what, and when. Append-only; malformed lines are reported, never repaired.",
|
|
3396
|
-
input:
|
|
4795
|
+
input: z19.object({ bundlePath }),
|
|
3397
4796
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
3398
4797
|
run: ({ store }, { bundlePath: path }) => store.readLog(path)
|
|
3399
4798
|
});
|
|
3400
4799
|
|
|
3401
4800
|
// src/commands/no-decision.ts
|
|
3402
|
-
import { z as
|
|
4801
|
+
import { z as z20 } from "zod";
|
|
3403
4802
|
var noDecisionCommand = define({
|
|
3404
4803
|
name: "no-decision",
|
|
3405
4804
|
tool: "kb_no_decision",
|
|
3406
4805
|
usage: "no-decision <reason...>",
|
|
3407
4806
|
description: "Record in one sentence that a piece of work had nothing to decide. Idempotent.",
|
|
3408
|
-
input:
|
|
4807
|
+
input: z20.object({ bundlePath, reason: z20.string().min(1) }),
|
|
3409
4808
|
fromArgv: (argv, path) => ({
|
|
3410
4809
|
bundlePath: path,
|
|
3411
4810
|
reason: argv.slice(1).join(" ").trim()
|
|
@@ -3422,20 +4821,20 @@ var noDecisionCommand = define({
|
|
|
3422
4821
|
});
|
|
3423
4822
|
|
|
3424
4823
|
// src/commands/pack.ts
|
|
3425
|
-
import { z as
|
|
4824
|
+
import { z as z21 } from "zod";
|
|
3426
4825
|
var packCommand = define({
|
|
3427
4826
|
name: "pack",
|
|
3428
4827
|
tool: "kb_pack",
|
|
3429
4828
|
usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
|
|
3430
4829
|
description: "Bounded neighbourhood around one record: within `hops`, ranked, cut to `maxNodes`, with every cut record named under Excluded. Use when the base is over kb_load's budget and the work centres on a record you can name. Refuses over budget rather than truncating. Everything below the header is byte-stable across runs. Resolves supersession like kb_load.",
|
|
3431
|
-
input:
|
|
4830
|
+
input: z21.object({
|
|
3432
4831
|
bundlePath,
|
|
3433
4832
|
conceptId,
|
|
3434
|
-
hops:
|
|
3435
|
-
maxNodes:
|
|
4833
|
+
hops: z21.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
|
|
4834
|
+
maxNodes: z21.number().int().positive().optional().describe(
|
|
3436
4835
|
"How many records the pack may hold, root included. Defaults to 20."
|
|
3437
4836
|
),
|
|
3438
|
-
budgetTokens:
|
|
4837
|
+
budgetTokens: z21.number().int().positive().optional().describe(
|
|
3439
4838
|
"Approximate token ceiling over what is actually emitted. Defaults to 25000."
|
|
3440
4839
|
)
|
|
3441
4840
|
}),
|
|
@@ -3460,12 +4859,12 @@ var packCommand = define({
|
|
|
3460
4859
|
return render3(result, path, now());
|
|
3461
4860
|
}
|
|
3462
4861
|
});
|
|
3463
|
-
function render3(result, bundle,
|
|
4862
|
+
function render3(result, bundle, at2) {
|
|
3464
4863
|
const lines = [
|
|
3465
4864
|
`# KB Pack \u2014 ${result.root}`,
|
|
3466
4865
|
`bundle: ${bundle}`,
|
|
3467
4866
|
`budget: ~${result.tokensLoaded} of ${result.budgetTokens} tokens, ${result.recordCount} records`,
|
|
3468
|
-
`packed: ${
|
|
4867
|
+
`packed: ${at2}`,
|
|
3469
4868
|
"",
|
|
3470
4869
|
`## Records (${result.records.length})`
|
|
3471
4870
|
];
|
|
@@ -3522,22 +4921,22 @@ function warningLabel(warning) {
|
|
|
3522
4921
|
}
|
|
3523
4922
|
|
|
3524
4923
|
// src/commands/pin.ts
|
|
3525
|
-
import { z as
|
|
4924
|
+
import { z as z22 } from "zod";
|
|
3526
4925
|
var pinCommand = define({
|
|
3527
4926
|
name: "pin",
|
|
3528
4927
|
tool: "kb_pin",
|
|
3529
4928
|
usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
|
|
3530
4929
|
description: "Pin a base into a workspace manifest so kb_context surfaces it. Layers, nearest wins: project `.strauss/kb-pins.json` (default), `--local` (personal, gitignored), `--user` (`~/.strauss`). Idempotent; `--mode full|index`, `--profiles`, `--frozen`/`--unfreeze` update only those fields. A path with no records pins with a warning. Never touches the base itself.",
|
|
3531
|
-
input:
|
|
4930
|
+
input: z22.object({
|
|
3532
4931
|
bundlePath,
|
|
3533
|
-
mode:
|
|
4932
|
+
mode: z22.enum(["full", "index"]).optional().describe(
|
|
3534
4933
|
"full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
|
|
3535
4934
|
),
|
|
3536
|
-
profiles:
|
|
3537
|
-
layer:
|
|
4935
|
+
profiles: z22.array(z22.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
|
|
4936
|
+
layer: z22.enum(["project", "local", "user"]).optional().describe(
|
|
3538
4937
|
"Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
|
|
3539
4938
|
),
|
|
3540
|
-
frozen:
|
|
4939
|
+
frozen: z22.boolean().optional().describe(
|
|
3541
4940
|
"true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
|
|
3542
4941
|
)
|
|
3543
4942
|
}),
|
|
@@ -3566,29 +4965,29 @@ var pinCommand = define({
|
|
|
3566
4965
|
});
|
|
3567
4966
|
|
|
3568
4967
|
// src/commands/pins.ts
|
|
3569
|
-
import { z as
|
|
4968
|
+
import { z as z23 } from "zod";
|
|
3570
4969
|
var pinsCommand = define({
|
|
3571
4970
|
name: "pins",
|
|
3572
4971
|
tool: "kb_pins",
|
|
3573
4972
|
usage: "pins",
|
|
3574
4973
|
description: "Every pinned base across the manifest layers, with its layer and whether it resolves to records. Takes no bundlePath.",
|
|
3575
|
-
input:
|
|
4974
|
+
input: z23.object({}),
|
|
3576
4975
|
fromArgv: () => ({}),
|
|
3577
4976
|
run: ({ store }) => listPins(store, process.cwd())
|
|
3578
4977
|
});
|
|
3579
4978
|
|
|
3580
4979
|
// src/commands/query.ts
|
|
3581
|
-
import { z as
|
|
4980
|
+
import { z as z24 } from "zod";
|
|
3582
4981
|
var queryCommand = define({
|
|
3583
4982
|
name: "query",
|
|
3584
4983
|
tool: "kb_query",
|
|
3585
4984
|
usage: "query <text...> [--repo-root PATH]",
|
|
3586
4985
|
description: "Search; every hit carries its standing. Flagged, never filtered: a superseded hit returns with its replacement, a rejected one is marked. Prefer kb_load when the base fits its budget \u2014 a full read beats search. Results are volatile: place them at the tail, not the cached prefix. Never read record files directly.",
|
|
3587
|
-
input:
|
|
4986
|
+
input: z24.object({
|
|
3588
4987
|
bundlePath,
|
|
3589
|
-
text:
|
|
3590
|
-
type:
|
|
3591
|
-
includeNonCurrent:
|
|
4988
|
+
text: z24.string().optional(),
|
|
4989
|
+
type: z24.enum(KB_RECORD_TYPES).optional(),
|
|
4990
|
+
includeNonCurrent: z24.boolean().optional(),
|
|
3592
4991
|
repoRoot: REPO_ROOT
|
|
3593
4992
|
}),
|
|
3594
4993
|
// `--repo-root` is a flag, so its value must not fall into the search text.
|
|
@@ -3620,43 +5019,43 @@ var queryCommand = define({
|
|
|
3620
5019
|
});
|
|
3621
5020
|
|
|
3622
5021
|
// src/commands/read-index.ts
|
|
3623
|
-
import { z as
|
|
5022
|
+
import { z as z25 } from "zod";
|
|
3624
5023
|
var readIndexCommand = define({
|
|
3625
5024
|
name: "index",
|
|
3626
5025
|
tool: "kb_index",
|
|
3627
5026
|
usage: "index",
|
|
3628
5027
|
description: "The index \u2014 title, type, status, description per record \u2014 rebuilt if stale. Cheapest re-orientation after compaction: call it (or kb_context) first, then kb_load or fetch by id.",
|
|
3629
|
-
input:
|
|
5028
|
+
input: z25.object({ bundlePath }),
|
|
3630
5029
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
3631
5030
|
run: ({ store }, { bundlePath: path }) => store.readIndex(path)
|
|
3632
5031
|
});
|
|
3633
5032
|
|
|
3634
5033
|
// src/commands/schema.ts
|
|
3635
|
-
import { z as
|
|
5034
|
+
import { z as z26 } from "zod";
|
|
3636
5035
|
var schemaCommand = define({
|
|
3637
5036
|
name: "schema",
|
|
3638
5037
|
tool: "kb_schema",
|
|
3639
5038
|
usage: "schema",
|
|
3640
5039
|
description: "JSON Schema for frontmatter, write input, and log entries, generated from the enforcing code.",
|
|
3641
|
-
input:
|
|
5040
|
+
input: z26.object({}),
|
|
3642
5041
|
fromArgv: () => ({}),
|
|
3643
5042
|
run: () => Promise.resolve(kbJsonSchemas())
|
|
3644
5043
|
});
|
|
3645
5044
|
|
|
3646
5045
|
// src/commands/stamp.ts
|
|
3647
|
-
import { readFile as
|
|
3648
|
-
import { z as
|
|
5046
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
5047
|
+
import { z as z27 } from "zod";
|
|
3649
5048
|
var DIGEST = /^[0-9a-f]{64}$/;
|
|
3650
5049
|
var stampCommand = define({
|
|
3651
5050
|
name: "stamp",
|
|
3652
5051
|
tool: "kb_stamp",
|
|
3653
5052
|
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
|
|
3655
|
-
input:
|
|
3656
|
-
bundlePath:
|
|
5053
|
+
description: "Content stamp of a base \u2014 `load`'s digest, record counts, per-record digests, how many records have drifted anchors \u2014 without any bodies. Takes no bundlePath to stamp every pinned base. With `since`, reports only the bases that moved, naming the changed ids. Reads, never writes.",
|
|
5054
|
+
input: z27.object({
|
|
5055
|
+
bundlePath: z27.string().min(1).optional().describe(
|
|
3657
5056
|
"Absolute path to one knowledge base. Omit to stamp every pinned base."
|
|
3658
5057
|
),
|
|
3659
|
-
since:
|
|
5058
|
+
since: z27.string().min(1).optional().describe(
|
|
3660
5059
|
"Prior digest, or path to a prior `stamp --json`; only moved bases return, with changed ids when the baseline is a file."
|
|
3661
5060
|
)
|
|
3662
5061
|
}),
|
|
@@ -3695,7 +5094,7 @@ var stampCommand = define({
|
|
|
3695
5094
|
return reports;
|
|
3696
5095
|
},
|
|
3697
5096
|
render: (result) => result.map((report) => {
|
|
3698
|
-
const counts = `${report.recordCount} record(s), ${report.superseded} superseded`;
|
|
5097
|
+
const counts = `${report.recordCount} record(s), ${report.superseded} superseded${report.drifted ? `, ${report.drifted} drifted` : ""}`;
|
|
3699
5098
|
const head = `${report.path} ${report.digest} ${counts}${report.newestAt ? ` newest ${report.newestAt}` : ""}`;
|
|
3700
5099
|
return report.changed?.length ? `${head}
|
|
3701
5100
|
changed: ${report.changed.join(", ")}` : head;
|
|
@@ -3718,7 +5117,7 @@ async function readBaseline(since) {
|
|
|
3718
5117
|
if (DIGEST.test(since)) return { digest: since, byPath: /* @__PURE__ */ new Map() };
|
|
3719
5118
|
let parsed;
|
|
3720
5119
|
try {
|
|
3721
|
-
parsed = JSON.parse(await
|
|
5120
|
+
parsed = JSON.parse(await readFile6(since, "utf8"));
|
|
3722
5121
|
} catch {
|
|
3723
5122
|
throw new KbStampBaselineError(since);
|
|
3724
5123
|
}
|
|
@@ -3742,16 +5141,16 @@ async function readBaseline(since) {
|
|
|
3742
5141
|
}
|
|
3743
5142
|
|
|
3744
5143
|
// src/commands/status.ts
|
|
3745
|
-
import { z as
|
|
5144
|
+
import { z as z28 } from "zod";
|
|
3746
5145
|
var statusCommand = define({
|
|
3747
5146
|
name: "status",
|
|
3748
5147
|
tool: "kb_status",
|
|
3749
5148
|
usage: "status <concept-id> <status>",
|
|
3750
5149
|
description: "Move a record's status. Compare-and-swap: a concurrent change fails instead of being overwritten.",
|
|
3751
|
-
input:
|
|
5150
|
+
input: z28.object({
|
|
3752
5151
|
bundlePath,
|
|
3753
5152
|
conceptId,
|
|
3754
|
-
status:
|
|
5153
|
+
status: z28.enum(KB_RECORD_STATUSES)
|
|
3755
5154
|
}),
|
|
3756
5155
|
fromArgv: (argv, path) => ({
|
|
3757
5156
|
bundlePath: path,
|
|
@@ -3766,13 +5165,13 @@ var statusCommand = define({
|
|
|
3766
5165
|
});
|
|
3767
5166
|
|
|
3768
5167
|
// src/commands/supersede.ts
|
|
3769
|
-
import { z as
|
|
5168
|
+
import { z as z29 } from "zod";
|
|
3770
5169
|
var supersedeCommand = define({
|
|
3771
5170
|
name: "supersede",
|
|
3772
5171
|
tool: "kb_supersede",
|
|
3773
5172
|
usage: "supersede <concept-id> <replacement-id>",
|
|
3774
5173
|
description: "Mark a record superseded by another, linked in both directions. Use instead of editing a record whose meaning changed.",
|
|
3775
|
-
input:
|
|
5174
|
+
input: z29.object({ bundlePath, conceptId, replacementId: conceptId }),
|
|
3776
5175
|
fromArgv: (argv, path) => ({
|
|
3777
5176
|
bundlePath: path,
|
|
3778
5177
|
conceptId: argv[1],
|
|
@@ -3786,16 +5185,16 @@ var supersedeCommand = define({
|
|
|
3786
5185
|
});
|
|
3787
5186
|
|
|
3788
5187
|
// src/commands/sync-instructions.ts
|
|
3789
|
-
import { z as
|
|
5188
|
+
import { z as z30 } from "zod";
|
|
3790
5189
|
var syncInstructionsCommand = define({
|
|
3791
5190
|
name: "sync-instructions",
|
|
3792
5191
|
usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
|
|
3793
5192
|
description: "CLI-only: plant the kb_context block between sentinel comments in AGENTS.md or CLAUDE.md, idempotently.",
|
|
3794
|
-
input:
|
|
3795
|
-
file:
|
|
3796
|
-
budgetTokens:
|
|
3797
|
-
fullUnderTokens:
|
|
3798
|
-
profile:
|
|
5193
|
+
input: z30.object({
|
|
5194
|
+
file: z30.string().min(1).describe("The instruction file to edit in place."),
|
|
5195
|
+
budgetTokens: z30.number().int().positive().optional(),
|
|
5196
|
+
fullUnderTokens: z30.number().int().positive().optional(),
|
|
5197
|
+
profile: z30.string().optional()
|
|
3799
5198
|
}),
|
|
3800
5199
|
fromArgv: (argv) => {
|
|
3801
5200
|
const budget = argvFlag(argv, "--budget");
|
|
@@ -3821,17 +5220,17 @@ var syncInstructionsCommand = define({
|
|
|
3821
5220
|
});
|
|
3822
5221
|
|
|
3823
5222
|
// src/commands/trace.ts
|
|
3824
|
-
import { z as
|
|
5223
|
+
import { z as z31 } from "zod";
|
|
3825
5224
|
var traceCommand = define({
|
|
3826
5225
|
name: "trace",
|
|
3827
5226
|
tool: "kb_trace",
|
|
3828
5227
|
usage: "trace <concept-id> [edges...]",
|
|
3829
5228
|
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".',
|
|
3830
|
-
input:
|
|
5229
|
+
input: z31.object({
|
|
3831
5230
|
bundlePath,
|
|
3832
5231
|
conceptId,
|
|
3833
|
-
edges:
|
|
3834
|
-
depth:
|
|
5232
|
+
edges: z31.array(z31.enum(TRACE_EDGES)).optional(),
|
|
5233
|
+
depth: z31.number().int().positive().optional()
|
|
3835
5234
|
}),
|
|
3836
5235
|
fromArgv: (argv, path) => ({
|
|
3837
5236
|
bundlePath: path,
|
|
@@ -3853,37 +5252,37 @@ var traceCommand = define({
|
|
|
3853
5252
|
});
|
|
3854
5253
|
|
|
3855
5254
|
// src/commands/types.ts
|
|
3856
|
-
import { z as
|
|
5255
|
+
import { z as z32 } from "zod";
|
|
3857
5256
|
var typesCommand = define({
|
|
3858
5257
|
name: "types",
|
|
3859
5258
|
tool: "kb_types",
|
|
3860
5259
|
usage: "types",
|
|
3861
5260
|
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.",
|
|
3862
|
-
input:
|
|
5261
|
+
input: z32.object({}),
|
|
3863
5262
|
fromArgv: () => ({}),
|
|
3864
5263
|
run: () => Promise.resolve(RECORD_TYPES)
|
|
3865
5264
|
});
|
|
3866
5265
|
|
|
3867
5266
|
// src/commands/unpin.ts
|
|
3868
|
-
import { z as
|
|
5267
|
+
import { z as z33 } from "zod";
|
|
3869
5268
|
var unpinCommand = define({
|
|
3870
5269
|
name: "unpin",
|
|
3871
5270
|
tool: "kb_unpin",
|
|
3872
5271
|
usage: "unpin [bundle-path]",
|
|
3873
5272
|
description: "Remove a base from every manifest layer that holds it. Reports the layers touched.",
|
|
3874
|
-
input:
|
|
5273
|
+
input: z33.object({ bundlePath }),
|
|
3875
5274
|
fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
|
|
3876
5275
|
run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
|
|
3877
5276
|
});
|
|
3878
5277
|
|
|
3879
5278
|
// src/commands/validate.ts
|
|
3880
|
-
import { z as
|
|
5279
|
+
import { z as z34 } from "zod";
|
|
3881
5280
|
var validateCommand = define({
|
|
3882
5281
|
name: "validate",
|
|
3883
5282
|
tool: "kb_validate",
|
|
3884
5283
|
usage: "validate",
|
|
3885
5284
|
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.",
|
|
3886
|
-
input:
|
|
5285
|
+
input: z34.object({ bundlePath }),
|
|
3887
5286
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
3888
5287
|
run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
|
|
3889
5288
|
// Warnings never fail the exit code; every other severity does.
|
|
@@ -3893,16 +5292,16 @@ var validateCommand = define({
|
|
|
3893
5292
|
});
|
|
3894
5293
|
|
|
3895
5294
|
// src/commands/verify.ts
|
|
3896
|
-
import { z as
|
|
5295
|
+
import { z as z35 } from "zod";
|
|
3897
5296
|
var verifyCommand = define({
|
|
3898
5297
|
name: "verify",
|
|
3899
5298
|
tool: "kb_verify",
|
|
3900
5299
|
usage: "verify <concept-id> --note <text>",
|
|
3901
5300
|
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.",
|
|
3902
|
-
input:
|
|
5301
|
+
input: z35.object({
|
|
3903
5302
|
bundlePath,
|
|
3904
5303
|
conceptId,
|
|
3905
|
-
note:
|
|
5304
|
+
note: z35.string().refine((s) => s.trim().length > 0, {
|
|
3906
5305
|
message: "note must say what the check found"
|
|
3907
5306
|
})
|
|
3908
5307
|
}),
|
|
@@ -3922,15 +5321,15 @@ var verifyCommand = define({
|
|
|
3922
5321
|
});
|
|
3923
5322
|
|
|
3924
5323
|
// src/commands/write.ts
|
|
3925
|
-
import { z as
|
|
5324
|
+
import { z as z36 } from "zod";
|
|
3926
5325
|
var writeCommand = define({
|
|
3927
5326
|
name: "write",
|
|
3928
5327
|
tool: "kb_write",
|
|
3929
5328
|
usage: "write <type> < record.json",
|
|
3930
5329
|
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.",
|
|
3931
|
-
input:
|
|
5330
|
+
input: z36.object({
|
|
3932
5331
|
bundlePath,
|
|
3933
|
-
type:
|
|
5332
|
+
type: z36.enum(KB_RECORD_TYPES),
|
|
3934
5333
|
input: composeInputSchema
|
|
3935
5334
|
}),
|
|
3936
5335
|
fromArgv: async (argv, path, stdin) => ({
|
|
@@ -3954,13 +5353,13 @@ var writeCommand = define({
|
|
|
3954
5353
|
});
|
|
3955
5354
|
|
|
3956
5355
|
// src/commands/write-decision.ts
|
|
3957
|
-
import { z as
|
|
5356
|
+
import { z as z37 } from "zod";
|
|
3958
5357
|
var writeDecisionCommand = define({
|
|
3959
5358
|
name: "write-decision",
|
|
3960
5359
|
tool: "kb_write_decision",
|
|
3961
5360
|
usage: "write-decision < decision.json",
|
|
3962
5361
|
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.",
|
|
3963
|
-
input:
|
|
5362
|
+
input: z37.object({ bundlePath, input: decisionInputSchema }),
|
|
3964
5363
|
fromArgv: async (_argv, path, stdin) => ({
|
|
3965
5364
|
bundlePath: path,
|
|
3966
5365
|
input: JSON.parse(await stdin())
|
|
@@ -3990,6 +5389,7 @@ var KB_COMMANDS = [
|
|
|
3990
5389
|
answerCommand,
|
|
3991
5390
|
verifyCommand,
|
|
3992
5391
|
anchorResolveCommand,
|
|
5392
|
+
reassessCommand,
|
|
3993
5393
|
loadCommand,
|
|
3994
5394
|
catalogCommand,
|
|
3995
5395
|
packCommand,
|
|
@@ -4041,8 +5441,8 @@ function parseMarkdownWithFrontmatter(text, schema) {
|
|
|
4041
5441
|
}
|
|
4042
5442
|
|
|
4043
5443
|
// src/search-index.ts
|
|
4044
|
-
import { stat as
|
|
4045
|
-
import { join as
|
|
5444
|
+
import { stat as stat3 } from "fs/promises";
|
|
5445
|
+
import { join as join6 } from "path";
|
|
4046
5446
|
var SEARCH_INDEX_FILE = ".index.sqlite";
|
|
4047
5447
|
var COLLECTION = "kb";
|
|
4048
5448
|
async function searchBase(bundlePath2, query, options = {}) {
|
|
@@ -4051,7 +5451,7 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
4051
5451
|
let store = null;
|
|
4052
5452
|
try {
|
|
4053
5453
|
store = await qmd.createStore({
|
|
4054
|
-
dbPath:
|
|
5454
|
+
dbPath: join6(bundlePath2, SEARCH_INDEX_FILE),
|
|
4055
5455
|
config: {
|
|
4056
5456
|
collections: {
|
|
4057
5457
|
[COLLECTION]: {
|
|
@@ -4086,7 +5486,7 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
4086
5486
|
}
|
|
4087
5487
|
}
|
|
4088
5488
|
async function isStale(bundlePath2) {
|
|
4089
|
-
const indexAt = await
|
|
5489
|
+
const indexAt = await stat3(join6(bundlePath2, SEARCH_INDEX_FILE)).then((s) => s.mtimeMs).catch(() => 0);
|
|
4090
5490
|
if (!indexAt) return true;
|
|
4091
5491
|
const { readdir: readdir2 } = await import("fs/promises");
|
|
4092
5492
|
const names = (await readdir2(bundlePath2).catch(() => [])).filter(
|
|
@@ -4095,8 +5495,8 @@ async function isStale(bundlePath2) {
|
|
|
4095
5495
|
let stale = false;
|
|
4096
5496
|
await mapLimit(names, DEFAULT_IO_CONCURRENCY, async (name) => {
|
|
4097
5497
|
if (stale) return;
|
|
4098
|
-
const
|
|
4099
|
-
if (
|
|
5498
|
+
const at2 = await stat3(join6(bundlePath2, name)).then((s) => s.mtimeMs).catch(() => 0);
|
|
5499
|
+
if (at2 > indexAt) stale = true;
|
|
4100
5500
|
});
|
|
4101
5501
|
return stale;
|
|
4102
5502
|
}
|
|
@@ -4133,25 +5533,25 @@ async function loadQmd(logger) {
|
|
|
4133
5533
|
import {
|
|
4134
5534
|
appendFile,
|
|
4135
5535
|
link,
|
|
4136
|
-
mkdir as
|
|
5536
|
+
mkdir as mkdir4,
|
|
4137
5537
|
readdir,
|
|
4138
|
-
readFile as
|
|
4139
|
-
rename,
|
|
5538
|
+
readFile as readFile7,
|
|
5539
|
+
rename as rename2,
|
|
4140
5540
|
unlink,
|
|
4141
|
-
writeFile as
|
|
5541
|
+
writeFile as writeFile4
|
|
4142
5542
|
} from "fs/promises";
|
|
4143
|
-
import { join as
|
|
5543
|
+
import { join as join7, resolve as resolve5, sep as sep3 } from "path";
|
|
4144
5544
|
|
|
4145
5545
|
// src/kb-stamp.ts
|
|
4146
|
-
import { createHash as
|
|
4147
|
-
function
|
|
4148
|
-
return
|
|
5546
|
+
import { createHash as createHash4 } from "crypto";
|
|
5547
|
+
function sha2563(contents) {
|
|
5548
|
+
return createHash4("sha256").update(contents).digest("hex");
|
|
4149
5549
|
}
|
|
4150
5550
|
function bundleStamp(records, superseded) {
|
|
4151
5551
|
const entries = [
|
|
4152
5552
|
...records.map((hit) => ({
|
|
4153
5553
|
conceptId: hit.record.conceptId,
|
|
4154
|
-
digest: `current:${
|
|
5554
|
+
digest: `current:${sha2563(
|
|
4155
5555
|
stringifyMarkdownWithFrontmatter(
|
|
4156
5556
|
hit.record.body,
|
|
4157
5557
|
hit.record.frontmatter
|
|
@@ -4160,11 +5560,11 @@ function bundleStamp(records, superseded) {
|
|
|
4160
5560
|
})),
|
|
4161
5561
|
...superseded.map((entry) => ({
|
|
4162
5562
|
conceptId: entry.conceptId,
|
|
4163
|
-
digest: `superseded:${
|
|
5563
|
+
digest: `superseded:${sha2563(JSON.stringify(entry))}`
|
|
4164
5564
|
}))
|
|
4165
5565
|
].sort((a, b) => a.conceptId < b.conceptId ? -1 : 1);
|
|
4166
5566
|
return {
|
|
4167
|
-
digest:
|
|
5567
|
+
digest: sha2563(
|
|
4168
5568
|
entries.map((entry) => `${entry.conceptId}:${entry.digest}`).join("\n")
|
|
4169
5569
|
),
|
|
4170
5570
|
records: entries
|
|
@@ -4334,7 +5734,7 @@ function appendUnionMergeLine(contents) {
|
|
|
4334
5734
|
}
|
|
4335
5735
|
|
|
4336
5736
|
// src/kb-store.ts
|
|
4337
|
-
var KB_DIR =
|
|
5737
|
+
var KB_DIR = join7(".strauss", "kb");
|
|
4338
5738
|
var STORE_OWNED = /* @__PURE__ */ new Set([INDEX_FILE, LOG_FILE, SEARCH_INDEX_FILE]);
|
|
4339
5739
|
var DEFAULT_LOAD_BUDGET = 25e3;
|
|
4340
5740
|
var KbStore = class {
|
|
@@ -4365,7 +5765,7 @@ var KbStore = class {
|
|
|
4365
5765
|
const conceptId2 = `${input.type}.${input.slug}`;
|
|
4366
5766
|
const root = this.root(bundlePath2);
|
|
4367
5767
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
4368
|
-
await
|
|
5768
|
+
await mkdir4(root, { recursive: true });
|
|
4369
5769
|
await this.publish(
|
|
4370
5770
|
target,
|
|
4371
5771
|
stringifyMarkdownWithFrontmatter(input.body, frontmatter),
|
|
@@ -4404,7 +5804,7 @@ var KbStore = class {
|
|
|
4404
5804
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
4405
5805
|
let raw;
|
|
4406
5806
|
try {
|
|
4407
|
-
raw = await
|
|
5807
|
+
raw = await readFile7(target, "utf8");
|
|
4408
5808
|
} catch {
|
|
4409
5809
|
return null;
|
|
4410
5810
|
}
|
|
@@ -4429,7 +5829,7 @@ var KbStore = class {
|
|
|
4429
5829
|
const records = await mapLimit(
|
|
4430
5830
|
wanted,
|
|
4431
5831
|
DEFAULT_IO_CONCURRENCY,
|
|
4432
|
-
async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await
|
|
5832
|
+
async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await readFile7(join7(root, name), "utf8"))
|
|
4433
5833
|
);
|
|
4434
5834
|
return records.filter((record) => record !== null);
|
|
4435
5835
|
}
|
|
@@ -4477,8 +5877,8 @@ var KbStore = class {
|
|
|
4477
5877
|
* and the refusal is logged under its own operation name — `mutate` only
|
|
4478
5878
|
* logs what it publishes.
|
|
4479
5879
|
*/
|
|
4480
|
-
async verify(bundlePath2, conceptId2, note, actor = "unknown",
|
|
4481
|
-
const event = kbVerifiedEventSchema.parse({ by: actor, at, note });
|
|
5880
|
+
async verify(bundlePath2, conceptId2, note, actor = "unknown", at2 = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
5881
|
+
const event = kbVerifiedEventSchema.parse({ by: actor, at: at2, note });
|
|
4482
5882
|
const existing = await this.read(bundlePath2, conceptId2);
|
|
4483
5883
|
if (!existing) throw new KbRecordNotFoundError(conceptId2);
|
|
4484
5884
|
const generatedBy = existing.frontmatter.generated?.by;
|
|
@@ -4530,14 +5930,14 @@ var KbStore = class {
|
|
|
4530
5930
|
return superseded;
|
|
4531
5931
|
}
|
|
4532
5932
|
/** Resolves an open question, stamping who answered and when. */
|
|
4533
|
-
async answer(bundlePath2, conceptId2, answer, actor = "unknown",
|
|
5933
|
+
async answer(bundlePath2, conceptId2, answer, actor = "unknown", at2 = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
4534
5934
|
return this.mutate(
|
|
4535
5935
|
bundlePath2,
|
|
4536
5936
|
conceptId2,
|
|
4537
5937
|
(frontmatter) => ({
|
|
4538
5938
|
...frontmatter,
|
|
4539
5939
|
strauss_status: "resolved",
|
|
4540
|
-
strauss_answered: { by: actor, at }
|
|
5940
|
+
strauss_answered: { by: actor, at: at2 }
|
|
4541
5941
|
}),
|
|
4542
5942
|
{ operation: "answer", by: actor },
|
|
4543
5943
|
(body) => `${body.trimEnd()}
|
|
@@ -4586,7 +5986,7 @@ ${answer}
|
|
|
4586
5986
|
if (found.length) return found;
|
|
4587
5987
|
}
|
|
4588
5988
|
const lowered = needle.toLowerCase();
|
|
4589
|
-
return bundle.filter((record) =>
|
|
5989
|
+
return bundle.filter((record) => matches2(record, lowered));
|
|
4590
5990
|
}
|
|
4591
5991
|
/**
|
|
4592
5992
|
* Anchor drift over the records about to be handed back. Like the search
|
|
@@ -4706,24 +6106,34 @@ ${answer}
|
|
|
4706
6106
|
}
|
|
4707
6107
|
/**
|
|
4708
6108
|
* `load`'s digest without `load`'s bodies — the same records, adjudicated
|
|
4709
|
-
* the same way, handed back as a stamp.
|
|
4710
|
-
*
|
|
4711
|
-
*
|
|
6109
|
+
* the same way, handed back as a stamp.
|
|
6110
|
+
*
|
|
6111
|
+
* Drift is counted but kept out of the digest, which is what lets the reload
|
|
6112
|
+
* hook ask one question and get two answers: whether the base moved, and
|
|
6113
|
+
* whether the code under it did. A `load` and a `stamp` of the same base
|
|
6114
|
+
* still agree on the digest, because no warning has ever reached it.
|
|
4712
6115
|
*/
|
|
4713
|
-
async stamp(bundlePath2) {
|
|
6116
|
+
async stamp(bundlePath2, options = {}) {
|
|
4714
6117
|
const bundle = await this.list(bundlePath2);
|
|
4715
6118
|
const adjudicated = adjudicate(bundle, bundle, /* @__PURE__ */ new Date());
|
|
4716
6119
|
const current = adjudicated.filter((hit) => hit.standing !== "superseded");
|
|
4717
6120
|
const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(stub);
|
|
4718
6121
|
const stamped = bundleStamp(current, superseded);
|
|
4719
|
-
const dates = bundle.map((record) => record.frontmatter.generated?.at ?? null).filter((
|
|
6122
|
+
const dates = bundle.map((record) => record.frontmatter.generated?.at ?? null).filter((at2) => typeof at2 === "string").sort();
|
|
6123
|
+
const drift = await this.detectDrift(bundle, options.repoRoot);
|
|
6124
|
+
const drifted2 = drift === void 0 ? null : [...drift.values()].filter(
|
|
6125
|
+
(entries) => entries.some(
|
|
6126
|
+
(entry) => entry.state !== "match" && !isUncheckedReason(entry.reason)
|
|
6127
|
+
)
|
|
6128
|
+
).length;
|
|
4720
6129
|
return {
|
|
4721
6130
|
path: bundlePath2,
|
|
4722
6131
|
digest: stamped.digest,
|
|
4723
6132
|
recordCount: bundle.length,
|
|
4724
6133
|
superseded: superseded.length,
|
|
4725
6134
|
newestAt: dates.at(-1) ?? null,
|
|
4726
|
-
records: stamped.records
|
|
6135
|
+
records: stamped.records,
|
|
6136
|
+
drifted: drifted2
|
|
4727
6137
|
};
|
|
4728
6138
|
}
|
|
4729
6139
|
/** How a position was arrived at, as a timeline. See `trace.ts`. */
|
|
@@ -4756,11 +6166,11 @@ ${answer}
|
|
|
4756
6166
|
async readIndex(bundlePath2) {
|
|
4757
6167
|
const root = this.root(bundlePath2);
|
|
4758
6168
|
const expected = renderIndex(await this.list(bundlePath2));
|
|
4759
|
-
const stored = await
|
|
6169
|
+
const stored = await readFile7(join7(root, INDEX_FILE), "utf8").catch(
|
|
4760
6170
|
() => null
|
|
4761
6171
|
);
|
|
4762
6172
|
if (indexIsStale(stored, expected)) {
|
|
4763
|
-
await this.publish(
|
|
6173
|
+
await this.publish(join7(root, INDEX_FILE), expected, true, INDEX_FILE);
|
|
4764
6174
|
this.logger.info?.({
|
|
4765
6175
|
operation: "kb.index.repair",
|
|
4766
6176
|
bundlePath: root,
|
|
@@ -4777,8 +6187,8 @@ ${answer}
|
|
|
4777
6187
|
* knows which agent touched what. So a bad line is surfaced and left alone.
|
|
4778
6188
|
*/
|
|
4779
6189
|
async readLog(bundlePath2) {
|
|
4780
|
-
const raw = await
|
|
4781
|
-
|
|
6190
|
+
const raw = await readFile7(
|
|
6191
|
+
join7(this.root(bundlePath2), LOG_FILE),
|
|
4782
6192
|
"utf8"
|
|
4783
6193
|
).catch(() => "");
|
|
4784
6194
|
const result = parseLog(raw);
|
|
@@ -4829,15 +6239,15 @@ ${answer}
|
|
|
4829
6239
|
}
|
|
4830
6240
|
async mutate(bundlePath2, conceptId2, change, entry, changeBody = (body) => body) {
|
|
4831
6241
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
4832
|
-
const before = await
|
|
6242
|
+
const before = await readFile7(target, "utf8").catch(() => null);
|
|
4833
6243
|
if (before === null) throw new KbRecordNotFoundError(conceptId2);
|
|
4834
6244
|
const parsed = this.parse(conceptId2, before);
|
|
4835
6245
|
if (!parsed) throw new KbRecordNotFoundError(conceptId2);
|
|
4836
6246
|
const frontmatter = change(parsed.frontmatter);
|
|
4837
6247
|
const body = changeBody(parsed.body);
|
|
4838
6248
|
const contents = stringifyMarkdownWithFrontmatter(body, frontmatter);
|
|
4839
|
-
const witness = await
|
|
4840
|
-
if (witness === null ||
|
|
6249
|
+
const witness = await readFile7(target, "utf8").catch(() => null);
|
|
6250
|
+
if (witness === null || sha2563(witness) !== sha2563(before)) {
|
|
4841
6251
|
throw new KbWriteConflictError(conceptId2);
|
|
4842
6252
|
}
|
|
4843
6253
|
await this.publish(target, contents, true, conceptId2);
|
|
@@ -4862,10 +6272,10 @@ ${answer}
|
|
|
4862
6272
|
*/
|
|
4863
6273
|
async publish(target, contents, overwrite, conceptId2) {
|
|
4864
6274
|
const staging = `${target}.${process.pid}.tmp`;
|
|
4865
|
-
await
|
|
6275
|
+
await writeFile4(staging, contents, "utf8");
|
|
4866
6276
|
try {
|
|
4867
6277
|
if (overwrite) {
|
|
4868
|
-
await
|
|
6278
|
+
await rename2(staging, target);
|
|
4869
6279
|
return;
|
|
4870
6280
|
}
|
|
4871
6281
|
await link(staging, target);
|
|
@@ -4919,18 +6329,18 @@ ${answer}
|
|
|
4919
6329
|
* file must not fail the mutation it guards.
|
|
4920
6330
|
*/
|
|
4921
6331
|
async ensureGitattributes(root) {
|
|
4922
|
-
const target =
|
|
6332
|
+
const target = join7(root, GITATTRIBUTES_FILE);
|
|
4923
6333
|
try {
|
|
4924
6334
|
let existing;
|
|
4925
6335
|
try {
|
|
4926
|
-
existing = await
|
|
6336
|
+
existing = await readFile7(target, "utf8");
|
|
4927
6337
|
} catch (error) {
|
|
4928
6338
|
if (error.code !== "ENOENT") throw error;
|
|
4929
6339
|
existing = null;
|
|
4930
6340
|
}
|
|
4931
6341
|
if (existing === null) {
|
|
4932
6342
|
try {
|
|
4933
|
-
await
|
|
6343
|
+
await writeFile4(target, appendUnionMergeLine(""), {
|
|
4934
6344
|
encoding: "utf8",
|
|
4935
6345
|
flag: "wx"
|
|
4936
6346
|
});
|
|
@@ -4970,7 +6380,7 @@ ${answer}
|
|
|
4970
6380
|
async record(root, entry) {
|
|
4971
6381
|
await this.ensureGitattributes(root);
|
|
4972
6382
|
const line = renderLogEntry({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
|
|
4973
|
-
await appendFile(
|
|
6383
|
+
await appendFile(join7(root, LOG_FILE), line, "utf8").catch((error) => {
|
|
4974
6384
|
this.logger.warn?.({
|
|
4975
6385
|
operation: "kb.log.append",
|
|
4976
6386
|
outcome: "failed",
|
|
@@ -5007,7 +6417,7 @@ ${answer}
|
|
|
5007
6417
|
{ conceptId: conceptId2 }
|
|
5008
6418
|
);
|
|
5009
6419
|
}
|
|
5010
|
-
return
|
|
6420
|
+
return join7(this.root(bundlePath2), `${conceptId2}.md`);
|
|
5011
6421
|
}
|
|
5012
6422
|
};
|
|
5013
6423
|
function estimateTokens(record) {
|
|
@@ -5034,7 +6444,7 @@ function stub(hit) {
|
|
|
5034
6444
|
at: hit.record.frontmatter.generated?.at ?? null
|
|
5035
6445
|
};
|
|
5036
6446
|
}
|
|
5037
|
-
function
|
|
6447
|
+
function matches2(record, needle) {
|
|
5038
6448
|
const { title, description } = record.frontmatter;
|
|
5039
6449
|
return [record.conceptId, title, description, record.body].some(
|
|
5040
6450
|
(field) => field?.toLowerCase().includes(needle)
|
|
@@ -5126,12 +6536,12 @@ function byRank(left, right) {
|
|
|
5126
6536
|
) || left.record.conceptId.localeCompare(right.record.conceptId);
|
|
5127
6537
|
}
|
|
5128
6538
|
function typeRank(record) {
|
|
5129
|
-
const
|
|
5130
|
-
return
|
|
6539
|
+
const index2 = TYPE_PRIORITY.indexOf(record.frontmatter.type);
|
|
6540
|
+
return index2 === -1 ? TYPE_PRIORITY.length : index2;
|
|
5131
6541
|
}
|
|
5132
6542
|
|
|
5133
6543
|
// src/version.ts
|
|
5134
|
-
var VERSION = true ? "0.1.
|
|
6544
|
+
var VERSION = true ? "0.1.18" : "0.0.0-dev";
|
|
5135
6545
|
|
|
5136
6546
|
export {
|
|
5137
6547
|
kbSourceSchema,
|
|
@@ -5167,9 +6577,19 @@ export {
|
|
|
5167
6577
|
repoCacheDir,
|
|
5168
6578
|
readRemoteAnchors,
|
|
5169
6579
|
anchorFilePath,
|
|
6580
|
+
grammarsCacheRoot,
|
|
6581
|
+
grammarManifest,
|
|
6582
|
+
ensureGrammar,
|
|
6583
|
+
grammarHints,
|
|
6584
|
+
languageForFile,
|
|
6585
|
+
treeSitterLanguages,
|
|
6586
|
+
TreeSitterResolver,
|
|
5170
6587
|
regexResolver,
|
|
5171
6588
|
hashAnchorText,
|
|
5172
6589
|
resolveAnchor,
|
|
6590
|
+
resolveAnchorSpan,
|
|
6591
|
+
prepareResolvers,
|
|
6592
|
+
defaultAnchorResolvers,
|
|
5173
6593
|
detectAnchorDrift,
|
|
5174
6594
|
Fault,
|
|
5175
6595
|
ErrorTypes,
|
|
@@ -5210,6 +6630,9 @@ export {
|
|
|
5210
6630
|
CONTEXT_BEGIN,
|
|
5211
6631
|
CONTEXT_END,
|
|
5212
6632
|
syncInstructions,
|
|
6633
|
+
classifyDrift,
|
|
6634
|
+
unifiedDiff,
|
|
6635
|
+
reassessPacket,
|
|
5213
6636
|
KB_EDGE_KINDS,
|
|
5214
6637
|
DEFAULT_TYPED_LINK_RELS,
|
|
5215
6638
|
neighbours,
|
|
@@ -5247,4 +6670,4 @@ export {
|
|
|
5247
6670
|
KbStore,
|
|
5248
6671
|
VERSION
|
|
5249
6672
|
};
|
|
5250
|
-
//# sourceMappingURL=chunk-
|
|
6673
|
+
//# sourceMappingURL=chunk-GSOTMWZZ.js.map
|