@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
package/dist/cli-main.cjs
CHANGED
|
@@ -23,8 +23,12 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
23
|
mod
|
|
24
24
|
));
|
|
25
25
|
|
|
26
|
+
// ../../node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.26_supports-color@8.1.1_typescript@6.0.3_yaml@2.9.0/node_modules/tsup/assets/cjs_shims.js
|
|
27
|
+
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
28
|
+
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
29
|
+
|
|
26
30
|
// src/cli.ts
|
|
27
|
-
var
|
|
31
|
+
var import_node_path12 = require("path");
|
|
28
32
|
|
|
29
33
|
// src/decision-record.ts
|
|
30
34
|
var import_zod3 = require("zod");
|
|
@@ -74,10 +78,24 @@ var kbAnchorSchema = import_zod.z.object({
|
|
|
74
78
|
hash: import_zod.z.string().regex(/^sha256:[0-9a-f]{64}$/, {
|
|
75
79
|
message: "hash must be sha256:<64 hex chars>"
|
|
76
80
|
}).optional(),
|
|
81
|
+
/**
|
|
82
|
+
* What `hash` was taken over: the span's raw text, or the normalised token
|
|
83
|
+
* stream a parser sees (`ast`). Absent means `raw`, which is what every
|
|
84
|
+
* anchor stamped before this field carries, so old hashes keep comparing
|
|
85
|
+
* the way they were written. An `ast` hash is blind to whitespace and
|
|
86
|
+
* comments, so reformatting the anchored code is not drift.
|
|
87
|
+
*/
|
|
88
|
+
hash_kind: import_zod.z.enum(["raw", "ast"]).optional(),
|
|
77
89
|
/** ISO 8601 timestamp of the last successful resolution. */
|
|
78
90
|
resolved_at: import_zod.z.string().min(1).optional(),
|
|
79
91
|
/** Line count of the text the hash was taken over. */
|
|
80
|
-
lines: import_zod.z.number().int().positive().optional()
|
|
92
|
+
lines: import_zod.z.number().int().positive().optional(),
|
|
93
|
+
/**
|
|
94
|
+
* Which resolver produced the hashed span. Absent means an anchor stamped
|
|
95
|
+
* before resolvers were named, which is read as `regex` — the only one
|
|
96
|
+
* there was. A hash from a different resolver is drift, not a match.
|
|
97
|
+
*/
|
|
98
|
+
resolver: import_zod.z.enum(["tree-sitter", "regex"]).optional()
|
|
81
99
|
}).strict();
|
|
82
100
|
var kbLinkSchema = import_zod.z.object({
|
|
83
101
|
target: import_zod.z.string().min(1),
|
|
@@ -441,7 +459,7 @@ function composeNoDecisionRecord(reason, writtenBy, writtenAt) {
|
|
|
441
459
|
}
|
|
442
460
|
|
|
443
461
|
// src/commands/anchor-resolve.ts
|
|
444
|
-
var
|
|
462
|
+
var import_zod7 = require("zod");
|
|
445
463
|
|
|
446
464
|
// src/concurrency.ts
|
|
447
465
|
var DEFAULT_IO_CONCURRENCY = 16;
|
|
@@ -458,9 +476,9 @@ async function mapLimit(items, limit, fn) {
|
|
|
458
476
|
{ length: Math.min(limit, items.length) },
|
|
459
477
|
async () => {
|
|
460
478
|
while (!failed && next < items.length) {
|
|
461
|
-
const
|
|
479
|
+
const at2 = next++;
|
|
462
480
|
try {
|
|
463
|
-
out[
|
|
481
|
+
out[at2] = await fn(items[at2], at2);
|
|
464
482
|
} catch (error) {
|
|
465
483
|
failed = true;
|
|
466
484
|
throw error;
|
|
@@ -576,8 +594,8 @@ function safeSegment(value) {
|
|
|
576
594
|
function revRef(rev) {
|
|
577
595
|
const safe = rev.replace(/[^A-Za-z0-9_-]/g, "-").slice(0, 64);
|
|
578
596
|
let hash = 5381;
|
|
579
|
-
for (let
|
|
580
|
-
hash = (hash * 33 ^ rev.charCodeAt(
|
|
597
|
+
for (let at2 = 0; at2 < rev.length; at2++) {
|
|
598
|
+
hash = (hash * 33 ^ rev.charCodeAt(at2)) >>> 0;
|
|
581
599
|
}
|
|
582
600
|
return `refs/strauss/${safe}-${hash.toString(16)}`;
|
|
583
601
|
}
|
|
@@ -686,8 +704,8 @@ function repoUrlIsSafe(repo) {
|
|
|
686
704
|
if (!scheme?.[1]) return false;
|
|
687
705
|
if (!allowed.includes(scheme[1].toLowerCase())) return false;
|
|
688
706
|
const authority = url.slice(scheme[0].length).split("/")[0] ?? "";
|
|
689
|
-
const
|
|
690
|
-
return
|
|
707
|
+
const at2 = authority.lastIndexOf("@");
|
|
708
|
+
return at2 < 0 || !authority.slice(0, at2).includes(":");
|
|
691
709
|
}
|
|
692
710
|
function protocolArgs() {
|
|
693
711
|
const allowed = allowedProtocols();
|
|
@@ -738,18 +756,18 @@ async function readOneRepo(repo, url, declared, context) {
|
|
|
738
756
|
if (!repoUrlIsSafe(url)) return all({ ok: false, reason: "repo-invalid" });
|
|
739
757
|
const cache = cachePathFor(repo, context.cacheDir);
|
|
740
758
|
if (!cache) return all({ ok: false, reason: "remote-unreachable" });
|
|
741
|
-
const
|
|
759
|
+
const rejected2 = /* @__PURE__ */ new Map();
|
|
742
760
|
const usable = [];
|
|
743
761
|
for (const want of wants) {
|
|
744
762
|
const reason = wantReason(want);
|
|
745
763
|
if (reason)
|
|
746
|
-
|
|
764
|
+
rejected2.set(wantKey(repo, want.ref, want.file), { ok: false, reason });
|
|
747
765
|
else usable.push(want);
|
|
748
766
|
}
|
|
749
|
-
if (!usable.length) return
|
|
767
|
+
if (!usable.length) return rejected2;
|
|
750
768
|
wants = usable;
|
|
751
769
|
const opened = await openCache(cache, url, context);
|
|
752
|
-
if (opened) return new Map([...
|
|
770
|
+
if (opened) return new Map([...rejected2, ...all(opened)]);
|
|
753
771
|
const wantsDefault = wants.some((want) => want.ref === void 0);
|
|
754
772
|
const branch = wantsDefault ? await defaultBranch(cache, context) : {};
|
|
755
773
|
const revs = /* @__PURE__ */ new Map();
|
|
@@ -776,9 +794,9 @@ async function readOneRepo(repo, url, declared, context) {
|
|
|
776
794
|
}
|
|
777
795
|
);
|
|
778
796
|
return new Map([
|
|
779
|
-
...
|
|
797
|
+
...rejected2,
|
|
780
798
|
...wants.map(
|
|
781
|
-
(want,
|
|
799
|
+
(want, at2) => [wantKey(repo, want.ref, want.file), reads[at2]]
|
|
782
800
|
)
|
|
783
801
|
]);
|
|
784
802
|
}
|
|
@@ -828,13 +846,13 @@ async function defaultBranch(cache, context) {
|
|
|
828
846
|
if (!listed.ok) {
|
|
829
847
|
const reason = transportReason(listed.stderr);
|
|
830
848
|
if (reason !== "ref-not-found") {
|
|
831
|
-
const
|
|
832
|
-
return
|
|
849
|
+
const cached3 = await cachedBranch(cache);
|
|
850
|
+
return cached3 ? { name: cached3 } : { reason };
|
|
833
851
|
}
|
|
834
852
|
}
|
|
835
853
|
}
|
|
836
|
-
const
|
|
837
|
-
if (
|
|
854
|
+
const cached2 = await cachedBranch(cache);
|
|
855
|
+
if (cached2) return { name: cached2 };
|
|
838
856
|
return {
|
|
839
857
|
reason: context.offline ? "remote-unreachable" : "default-branch-unknown"
|
|
840
858
|
};
|
|
@@ -861,8 +879,8 @@ async function ensureRev(cache, rev, context) {
|
|
|
861
879
|
cwd: cache
|
|
862
880
|
}
|
|
863
881
|
);
|
|
864
|
-
const
|
|
865
|
-
if (
|
|
882
|
+
const cached2 = have.ok && have.stdout.trim().length > 0;
|
|
883
|
+
if (cached2 && (context.offline || IMMUTABLE_REV.test(rev))) return void 0;
|
|
866
884
|
if (context.offline) return { ok: false, reason: "remote-unreachable" };
|
|
867
885
|
const fetched = await git(
|
|
868
886
|
[
|
|
@@ -878,7 +896,7 @@ async function ensureRev(cache, rev, context) {
|
|
|
878
896
|
);
|
|
879
897
|
if (!fetched.ok) {
|
|
880
898
|
const reason = transportReason(fetched.stderr);
|
|
881
|
-
if (
|
|
899
|
+
if (cached2 && reason !== "ref-not-found") return void 0;
|
|
882
900
|
return { ok: false, reason };
|
|
883
901
|
}
|
|
884
902
|
const head = await git(["rev-parse", "FETCH_HEAD"], { cwd: cache });
|
|
@@ -989,67 +1007,655 @@ async function readAnchorFiles(files, read, concurrency = DEFAULT_IO_CONCURRENCY
|
|
|
989
1007
|
return { ok: false, reason: "file-unreadable" };
|
|
990
1008
|
}
|
|
991
1009
|
});
|
|
992
|
-
return new Map(wanted.map((file,
|
|
1010
|
+
return new Map(wanted.map((file, at2) => [file, results[at2]]));
|
|
993
1011
|
}
|
|
994
1012
|
|
|
995
1013
|
// src/anchor-resolver/resolver.ts
|
|
1014
|
+
var import_node_crypto3 = require("crypto");
|
|
1015
|
+
|
|
1016
|
+
// src/tree-sitter-resolver/languages.ts
|
|
1017
|
+
var import_node_path5 = require("path");
|
|
1018
|
+
|
|
1019
|
+
// src/grammars/index.ts
|
|
1020
|
+
var import_promises4 = require("fs/promises");
|
|
1021
|
+
|
|
1022
|
+
// src/grammars/store.ts
|
|
996
1023
|
var import_node_crypto = require("crypto");
|
|
1024
|
+
var import_promises3 = require("fs/promises");
|
|
1025
|
+
var import_node_os2 = require("os");
|
|
1026
|
+
var import_node_path3 = require("path");
|
|
1027
|
+
function grammarsCacheRoot(override) {
|
|
1028
|
+
return override ?? process.env["STRAUSS_KB_GRAMMARS_DIR"] ?? (0, import_node_path3.join)((0, import_node_os2.homedir)(), ".strauss", "grammars");
|
|
1029
|
+
}
|
|
1030
|
+
function grammarCachePath(root, language, sha2564, extension = "wasm") {
|
|
1031
|
+
return (0, import_node_path3.join)(root, language, `${sha2564.slice(0, 12)}.${extension}`);
|
|
1032
|
+
}
|
|
1033
|
+
function sha256(bytes) {
|
|
1034
|
+
return (0, import_node_crypto.createHash)("sha256").update(bytes).digest("hex");
|
|
1035
|
+
}
|
|
1036
|
+
function matches(bytes, entry) {
|
|
1037
|
+
if (entry.bytes !== void 0 && bytes.byteLength !== entry.bytes)
|
|
1038
|
+
return false;
|
|
1039
|
+
return sha256(bytes) === entry.sha256;
|
|
1040
|
+
}
|
|
1041
|
+
async function verifyCached(path, entry) {
|
|
1042
|
+
let bytes;
|
|
1043
|
+
try {
|
|
1044
|
+
bytes = await (0, import_promises3.readFile)(path);
|
|
1045
|
+
} catch {
|
|
1046
|
+
return false;
|
|
1047
|
+
}
|
|
1048
|
+
if (matches(bytes, entry)) return true;
|
|
1049
|
+
await (0, import_promises3.rm)(path, { force: true });
|
|
1050
|
+
return false;
|
|
1051
|
+
}
|
|
1052
|
+
async function writeCached(path, bytes) {
|
|
1053
|
+
await (0, import_promises3.mkdir)((0, import_node_path3.dirname)(path), { recursive: true });
|
|
1054
|
+
const temporary = `${path}.${process.pid}.${(0, import_node_crypto.randomBytes)(4).toString("hex")}.tmp`;
|
|
1055
|
+
try {
|
|
1056
|
+
await (0, import_promises3.writeFile)(temporary, bytes);
|
|
1057
|
+
await (0, import_promises3.rename)(temporary, path);
|
|
1058
|
+
} catch (error) {
|
|
1059
|
+
await (0, import_promises3.rm)(temporary, { force: true });
|
|
1060
|
+
throw error;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// src/grammars/fetch.ts
|
|
1065
|
+
var ATTEMPTS = 3;
|
|
1066
|
+
var BACKOFF_MS = 250;
|
|
1067
|
+
function grammarUrl(url, override) {
|
|
1068
|
+
const base2 = grammarsBaseUrl(override);
|
|
1069
|
+
if (!base2) return url;
|
|
1070
|
+
let root = base2;
|
|
1071
|
+
while (root.endsWith("/")) root = root.slice(0, -1);
|
|
1072
|
+
const pinned = new URL(url);
|
|
1073
|
+
return `${root}${pinned.pathname}${pinned.search}`;
|
|
1074
|
+
}
|
|
1075
|
+
function grammarsBaseUrl(override) {
|
|
1076
|
+
return override ?? process.env["STRAUSS_KB_GRAMMARS_URL"];
|
|
1077
|
+
}
|
|
1078
|
+
async function downloadPart(url, name, entry, options = {}) {
|
|
1079
|
+
const log = options.log ?? ((line) => void process.stderr.write(line));
|
|
1080
|
+
const weight = entry.bytes === void 0 ? "" : ` (${size(entry.bytes)} from manifest)`;
|
|
1081
|
+
log(`strauss-kb: downloading ${name}${weight} from ${url}
|
|
1082
|
+
`);
|
|
1083
|
+
let cause = "";
|
|
1084
|
+
for (let attempt = 1; attempt <= ATTEMPTS; attempt++) {
|
|
1085
|
+
const outcome = await attemptDownload(url, entry, options.fetchTimeoutMs);
|
|
1086
|
+
if ("bytes" in outcome) return outcome;
|
|
1087
|
+
cause = outcome.cause;
|
|
1088
|
+
log(
|
|
1089
|
+
`strauss-kb: ${name} attempt ${attempt}/${ATTEMPTS} failed: ${cause}
|
|
1090
|
+
`
|
|
1091
|
+
);
|
|
1092
|
+
if (!outcome.retry) break;
|
|
1093
|
+
if (attempt < ATTEMPTS) await pause(BACKOFF_MS * attempt);
|
|
1094
|
+
}
|
|
1095
|
+
log(`strauss-kb: ${name} not downloaded: ${cause}
|
|
1096
|
+
`);
|
|
1097
|
+
return { cause };
|
|
1098
|
+
}
|
|
1099
|
+
async function attemptDownload(url, entry, timeoutMs) {
|
|
1100
|
+
try {
|
|
1101
|
+
const response = await fetch(url, {
|
|
1102
|
+
signal: AbortSignal.timeout(fetchTimeoutMs(timeoutMs))
|
|
1103
|
+
});
|
|
1104
|
+
if (!response.ok) {
|
|
1105
|
+
return {
|
|
1106
|
+
cause: `HTTP ${response.status}`,
|
|
1107
|
+
retry: response.status >= 500 || response.status === 429
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
1111
|
+
if (!matches(bytes, entry))
|
|
1112
|
+
return { cause: "sha256 mismatch", retry: false };
|
|
1113
|
+
return { bytes };
|
|
1114
|
+
} catch (error) {
|
|
1115
|
+
const timedOut = error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
|
|
1116
|
+
return { cause: timedOut ? "timeout" : "network error", retry: true };
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
function size(bytes) {
|
|
1120
|
+
return bytes >= 1024 * 1024 ? `${(bytes / (1024 * 1024)).toFixed(1)} MB` : `${Math.round(bytes / 1024)} KB`;
|
|
1121
|
+
}
|
|
1122
|
+
function pause(ms) {
|
|
1123
|
+
return new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// src/grammars/manifest.ts
|
|
1127
|
+
var import_node_fs = require("fs");
|
|
1128
|
+
var import_node_path4 = require("path");
|
|
1129
|
+
var import_node_url = require("url");
|
|
1130
|
+
|
|
1131
|
+
// src/grammars/model.ts
|
|
1132
|
+
var import_zod4 = require("zod");
|
|
1133
|
+
var sha2562 = import_zod4.z.string().regex(/^[0-9a-f]{64}$/);
|
|
1134
|
+
var grammarWasmSchema = import_zod4.z.object({
|
|
1135
|
+
url: import_zod4.z.string().min(1),
|
|
1136
|
+
sha256: sha2562,
|
|
1137
|
+
bytes: import_zod4.z.number().int().positive()
|
|
1138
|
+
});
|
|
1139
|
+
var grammarTagsSchema = import_zod4.z.object({ url: import_zod4.z.string().min(1), sha256: sha2562 });
|
|
1140
|
+
var grammarPackSchema = import_zod4.z.object({
|
|
1141
|
+
package: import_zod4.z.string().min(1),
|
|
1142
|
+
wasm: grammarWasmSchema,
|
|
1143
|
+
tags: import_zod4.z.array(grammarTagsSchema),
|
|
1144
|
+
license: import_zod4.z.string().min(1),
|
|
1145
|
+
extensions: import_zod4.z.array(import_zod4.z.string().min(1))
|
|
1146
|
+
});
|
|
1147
|
+
var grammarManifestSchema = import_zod4.z.object({
|
|
1148
|
+
/** The runtime the packs were proved against. */
|
|
1149
|
+
webTreeSitter: import_zod4.z.string().min(1),
|
|
1150
|
+
linguist: import_zod4.z.object({ tag: import_zod4.z.string().min(1), commit: import_zod4.z.string().min(1) }),
|
|
1151
|
+
packs: import_zod4.z.record(import_zod4.z.string().min(1), grammarPackSchema)
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
// src/grammars/manifest.ts
|
|
1155
|
+
var cached;
|
|
1156
|
+
function grammarManifest() {
|
|
1157
|
+
cached ??= grammarManifestSchema.parse(
|
|
1158
|
+
JSON.parse((0, import_node_fs.readFileSync)(grammarsDataPath("manifest.json"), "utf8"))
|
|
1159
|
+
);
|
|
1160
|
+
return cached;
|
|
1161
|
+
}
|
|
1162
|
+
function grammarsDataPath(...segments) {
|
|
1163
|
+
let dir = (0, import_node_path4.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
1164
|
+
for (let up = 0; up < 5; up++) {
|
|
1165
|
+
const candidate = (0, import_node_path4.join)(dir, "grammars");
|
|
1166
|
+
if ((0, import_node_fs.existsSync)((0, import_node_path4.join)(candidate, "manifest.json")))
|
|
1167
|
+
return (0, import_node_path4.join)(candidate, ...segments);
|
|
1168
|
+
dir = (0, import_node_path4.dirname)(dir);
|
|
1169
|
+
}
|
|
1170
|
+
throw new Error("grammars/manifest.json is missing from the package");
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// src/grammars/index.ts
|
|
1174
|
+
var inFlight = /* @__PURE__ */ new Map();
|
|
1175
|
+
var missing = /* @__PURE__ */ new Map();
|
|
1176
|
+
var uncompilable = /* @__PURE__ */ new Map();
|
|
1177
|
+
var rejected = /* @__PURE__ */ new Map();
|
|
1178
|
+
function grammarsDownloadDisabled() {
|
|
1179
|
+
return process.env["STRAUSS_KB_GRAMMARS"] === "off";
|
|
1180
|
+
}
|
|
1181
|
+
async function ensureGrammar(language, options = {}) {
|
|
1182
|
+
const pack2 = grammarManifest().packs[language];
|
|
1183
|
+
if (!pack2) return null;
|
|
1184
|
+
const root = grammarsCacheRoot(options.cacheRoot);
|
|
1185
|
+
const wasm = grammarCachePath(root, language, pack2.wasm.sha256);
|
|
1186
|
+
const key = `${wasm} ${grammarsBaseUrl(options.baseUrl) ?? ""}`;
|
|
1187
|
+
const existing = inFlight.get(key);
|
|
1188
|
+
if (existing) return existing;
|
|
1189
|
+
const pending = (async () => {
|
|
1190
|
+
const grammar = await ensurePart(
|
|
1191
|
+
wasm,
|
|
1192
|
+
`tree-sitter-${language}`,
|
|
1193
|
+
pack2.wasm,
|
|
1194
|
+
options
|
|
1195
|
+
);
|
|
1196
|
+
if (grammar !== true)
|
|
1197
|
+
return miss(language, `grammar tree-sitter-${language}`, grammar);
|
|
1198
|
+
const parts = [];
|
|
1199
|
+
const total = pack2.tags.length;
|
|
1200
|
+
for (const [at2, part] of pack2.tags.entries()) {
|
|
1201
|
+
const name = `${language} tags${total > 1 ? ` part ${at2 + 1}/${total}` : ""}`;
|
|
1202
|
+
const path = grammarCachePath(root, language, part.sha256, "scm");
|
|
1203
|
+
const held = await ensurePart(path, name, part, options);
|
|
1204
|
+
if (held !== true) return miss(language, name, held);
|
|
1205
|
+
parts.push(`; ${part.url}
|
|
1206
|
+
${lf(await (0, import_promises4.readFile)(path, "utf8"))}`);
|
|
1207
|
+
}
|
|
1208
|
+
missing.delete(language);
|
|
1209
|
+
return { wasm, query: total ? parts.join("\n") : void 0 };
|
|
1210
|
+
})();
|
|
1211
|
+
inFlight.set(key, pending);
|
|
1212
|
+
const result = await pending;
|
|
1213
|
+
if (result === null) inFlight.delete(key);
|
|
1214
|
+
return result;
|
|
1215
|
+
}
|
|
1216
|
+
async function ensurePart(path, name, entry, options) {
|
|
1217
|
+
if (await verifyCached(path, entry)) return true;
|
|
1218
|
+
if (options.offline === true || grammarsDownloadDisabled()) return {};
|
|
1219
|
+
const download = await downloadPart(
|
|
1220
|
+
grammarUrl(entry.url, options.baseUrl),
|
|
1221
|
+
name,
|
|
1222
|
+
entry,
|
|
1223
|
+
options
|
|
1224
|
+
);
|
|
1225
|
+
if ("cause" in download) return { cause: download.cause };
|
|
1226
|
+
await writeCached(path, download.bytes).catch(() => null);
|
|
1227
|
+
return true;
|
|
1228
|
+
}
|
|
1229
|
+
function miss(language, subject, failure) {
|
|
1230
|
+
missing.set(language, { subject, ...failure });
|
|
1231
|
+
return null;
|
|
1232
|
+
}
|
|
1233
|
+
function lf(body) {
|
|
1234
|
+
return body.replace(/\r\n/g, "\n");
|
|
1235
|
+
}
|
|
1236
|
+
function noteUncompilableQuery(language, cause) {
|
|
1237
|
+
uncompilable.set(language, cause);
|
|
1238
|
+
}
|
|
1239
|
+
function noteRejectedGrammar(language, cause) {
|
|
1240
|
+
rejected.set(language, cause);
|
|
1241
|
+
}
|
|
1242
|
+
function grammarHints() {
|
|
1243
|
+
const manifest = grammarManifest();
|
|
1244
|
+
const packs = manifest.packs;
|
|
1245
|
+
const lines = /* @__PURE__ */ new Map();
|
|
1246
|
+
for (const [language, { subject, cause }] of missing)
|
|
1247
|
+
lines.set(
|
|
1248
|
+
language,
|
|
1249
|
+
`${subject} not cached${cause ? ` (${cause})` : ""}; run online once, or set STRAUSS_KB_GRAMMARS_DIR`
|
|
1250
|
+
);
|
|
1251
|
+
for (const [language, cause] of rejected)
|
|
1252
|
+
lines.set(
|
|
1253
|
+
language,
|
|
1254
|
+
`${packs[language]?.package ?? `tree-sitter-${language}`} rejected by web-tree-sitter ${manifest.webTreeSitter}${cause ? `: ${cause}` : ""}; re-pin with pnpm grammars pin ${language}`
|
|
1255
|
+
);
|
|
1256
|
+
for (const [language, cause] of uncompilable)
|
|
1257
|
+
lines.set(
|
|
1258
|
+
language,
|
|
1259
|
+
`tags query for ${language} does not compile against ${packs[language]?.package ?? `tree-sitter-${language}`}: ${cause}; re-pin with pnpm grammars pin ${language}`
|
|
1260
|
+
);
|
|
1261
|
+
return [...lines].sort(([a], [b]) => a.localeCompare(b)).map(([, line]) => line);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// src/tree-sitter-resolver/languages.ts
|
|
1265
|
+
var table;
|
|
1266
|
+
function extensionTable() {
|
|
1267
|
+
const manifest = grammarManifest();
|
|
1268
|
+
if (table?.of !== manifest)
|
|
1269
|
+
table = {
|
|
1270
|
+
of: manifest,
|
|
1271
|
+
extensions: Object.fromEntries(
|
|
1272
|
+
Object.entries(manifest.packs).flatMap(
|
|
1273
|
+
([language, pack2]) => pack2.extensions.map((extension) => [extension, language])
|
|
1274
|
+
)
|
|
1275
|
+
)
|
|
1276
|
+
};
|
|
1277
|
+
return table.extensions;
|
|
1278
|
+
}
|
|
1279
|
+
function hasQuery(language) {
|
|
1280
|
+
return (grammarManifest().packs[language]?.tags.length ?? 0) > 0;
|
|
1281
|
+
}
|
|
1282
|
+
function languageForFile(file) {
|
|
1283
|
+
const language = extensionTable()[(0, import_node_path5.extname)(file).toLowerCase()];
|
|
1284
|
+
return language && hasQuery(language) ? language : void 0;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// src/tree-sitter-resolver/resolver.ts
|
|
1288
|
+
var import_node_crypto2 = require("crypto");
|
|
1289
|
+
var import_web_tree_sitter = require("web-tree-sitter");
|
|
1290
|
+
|
|
1291
|
+
// src/tree-sitter-resolver/definitions.ts
|
|
1292
|
+
var SCOPE_ONLY = "reference.implementation";
|
|
1293
|
+
function index(tree, query) {
|
|
1294
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1295
|
+
for (const match of query.matches(tree.rootNode)) {
|
|
1296
|
+
const nameNode = match.captures.find((capture) => capture.name === "name");
|
|
1297
|
+
const defNode = match.captures.find(
|
|
1298
|
+
(capture) => capture.name.startsWith("definition.") || capture.name === SCOPE_ONLY
|
|
1299
|
+
);
|
|
1300
|
+
if (!nameNode || !defNode) continue;
|
|
1301
|
+
const candidate = {
|
|
1302
|
+
node: defNode.node,
|
|
1303
|
+
name: nameNode.node.text,
|
|
1304
|
+
target: defNode.name !== SCOPE_ONLY
|
|
1305
|
+
};
|
|
1306
|
+
const existing = byName.get(nameNode.node.id);
|
|
1307
|
+
if (existing && width(existing.node) <= width(candidate.node)) continue;
|
|
1308
|
+
byName.set(nameNode.node.id, candidate);
|
|
1309
|
+
}
|
|
1310
|
+
const definitions = [...byName.values()];
|
|
1311
|
+
return {
|
|
1312
|
+
tree,
|
|
1313
|
+
byNodeId: new Map(
|
|
1314
|
+
definitions.map((definition) => [definition.node.id, definition])
|
|
1315
|
+
),
|
|
1316
|
+
definitions
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
function select(parsed, wanted) {
|
|
1320
|
+
const matches3 = parsed.definitions.filter(
|
|
1321
|
+
(definition) => definition.target && endsWith(chainOf(definition, parsed.byNodeId), wanted)
|
|
1322
|
+
);
|
|
1323
|
+
if (matches3.length < 2) return matches3;
|
|
1324
|
+
const bodied = matches3.filter(
|
|
1325
|
+
(definition) => definition.node.childForFieldName("body") !== null
|
|
1326
|
+
);
|
|
1327
|
+
return bodied.length === 1 ? bodied : matches3;
|
|
1328
|
+
}
|
|
1329
|
+
function chainOf(definition, byNodeId) {
|
|
1330
|
+
const chain = [definition.name];
|
|
1331
|
+
const receiver = definition.node.childForFieldName("receiver");
|
|
1332
|
+
const type = receiver && typeNameIn(receiver);
|
|
1333
|
+
if (type) chain.unshift(type);
|
|
1334
|
+
for (let node = definition.node.parent; node; node = node.parent) {
|
|
1335
|
+
const enclosing = byNodeId.get(node.id);
|
|
1336
|
+
if (enclosing && enclosing.node !== definition.node)
|
|
1337
|
+
chain.unshift(enclosing.name);
|
|
1338
|
+
}
|
|
1339
|
+
return chain;
|
|
1340
|
+
}
|
|
1341
|
+
function typeNameIn(receiver) {
|
|
1342
|
+
const stack = [receiver];
|
|
1343
|
+
while (stack.length) {
|
|
1344
|
+
const node = stack.pop();
|
|
1345
|
+
if (node.type === "type_identifier") return node.text;
|
|
1346
|
+
for (let at2 = 0; at2 < node.childCount; at2++) {
|
|
1347
|
+
const child = node.child(at2);
|
|
1348
|
+
if (child) stack.push(child);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
return void 0;
|
|
1352
|
+
}
|
|
1353
|
+
function endsWith(chain, wanted) {
|
|
1354
|
+
if (wanted.length > chain.length) return false;
|
|
1355
|
+
const offset = chain.length - wanted.length;
|
|
1356
|
+
return wanted.every((segment, at2) => chain[offset + at2] === segment);
|
|
1357
|
+
}
|
|
1358
|
+
function width(node) {
|
|
1359
|
+
return node.endIndex - node.startIndex;
|
|
1360
|
+
}
|
|
1361
|
+
function spanOf(definition, source) {
|
|
1362
|
+
let start = definition.node;
|
|
1363
|
+
let end = definition.node;
|
|
1364
|
+
for (let sibling = start.previousSibling; sibling?.type === "decorator"; sibling = sibling.previousSibling) {
|
|
1365
|
+
start = sibling;
|
|
1366
|
+
}
|
|
1367
|
+
const parent = end.parent;
|
|
1368
|
+
if (parent?.type === "export_statement" && parent.childForFieldName("declaration")?.id === end.id) {
|
|
1369
|
+
start = parent;
|
|
1370
|
+
end = parent;
|
|
1371
|
+
}
|
|
1372
|
+
const lines = source.split("\n");
|
|
1373
|
+
const startLine = start.startPosition.row;
|
|
1374
|
+
const endLine = end.endPosition.column === 0 && end.endPosition.row > startLine ? end.endPosition.row - 1 : end.endPosition.row;
|
|
1375
|
+
return {
|
|
1376
|
+
text: lines.slice(startLine, endLine + 1).join("\n"),
|
|
1377
|
+
startLine: startLine + 1,
|
|
1378
|
+
endLine: endLine + 1
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// src/tree-sitter-resolver/tokens.ts
|
|
1383
|
+
function tokens(root) {
|
|
1384
|
+
const out = [];
|
|
1385
|
+
const stack = [root];
|
|
1386
|
+
while (stack.length) {
|
|
1387
|
+
const node = stack.pop();
|
|
1388
|
+
if (node.type.includes("comment")) continue;
|
|
1389
|
+
if (node.childCount === 0) {
|
|
1390
|
+
const text = node.text.trim();
|
|
1391
|
+
if (text) out.push(text);
|
|
1392
|
+
continue;
|
|
1393
|
+
}
|
|
1394
|
+
for (let at2 = node.childCount - 1; at2 >= 0; at2--) {
|
|
1395
|
+
const child = node.child(at2);
|
|
1396
|
+
if (child) stack.push(child);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
return out;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
// src/tree-sitter-resolver/resolver.ts
|
|
1403
|
+
var TREE_CACHE_LIMIT = 32;
|
|
1404
|
+
var TreeSitterResolver = class {
|
|
1405
|
+
name = "tree-sitter";
|
|
1406
|
+
grammars;
|
|
1407
|
+
loaded = /* @__PURE__ */ new Map();
|
|
1408
|
+
trees = /* @__PURE__ */ new Map();
|
|
1409
|
+
parser;
|
|
1410
|
+
initialized = false;
|
|
1411
|
+
/** Cache effectiveness, for tests and for the latency numbers. */
|
|
1412
|
+
stats = { parses: 0, cacheHits: 0 };
|
|
1413
|
+
constructor(options = {}) {
|
|
1414
|
+
this.grammars = options;
|
|
1415
|
+
}
|
|
1416
|
+
/**
|
|
1417
|
+
* Loads the grammars these files need, once per language per process,
|
|
1418
|
+
* downloading each one on first use.
|
|
1419
|
+
*
|
|
1420
|
+
* A grammar that will not load is remembered as unavailable rather than
|
|
1421
|
+
* retried per anchor, and never throws: an unobtainable WASM is a finding.
|
|
1422
|
+
*/
|
|
1423
|
+
async prepare(files) {
|
|
1424
|
+
const wanted = /* @__PURE__ */ new Set();
|
|
1425
|
+
for (const file of files) {
|
|
1426
|
+
const language = languageForFile(file);
|
|
1427
|
+
if (language && !this.loaded.has(language)) wanted.add(language);
|
|
1428
|
+
}
|
|
1429
|
+
if (!wanted.size) return;
|
|
1430
|
+
if (!this.initialized) {
|
|
1431
|
+
try {
|
|
1432
|
+
await import_web_tree_sitter.Parser.init();
|
|
1433
|
+
this.parser = new import_web_tree_sitter.Parser();
|
|
1434
|
+
this.initialized = true;
|
|
1435
|
+
} catch {
|
|
1436
|
+
for (const language of wanted) this.loaded.set(language, null);
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
const languages = [...wanted];
|
|
1441
|
+
const loaded = await mapLimit(
|
|
1442
|
+
languages,
|
|
1443
|
+
Math.min(DEFAULT_IO_CONCURRENCY, languages.length),
|
|
1444
|
+
(language) => this.load(language)
|
|
1445
|
+
);
|
|
1446
|
+
languages.forEach(
|
|
1447
|
+
(language, at2) => this.loaded.set(language, loaded[at2] ?? null)
|
|
1448
|
+
);
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* An unobtainable grammar, one this runtime refuses, and a query that will
|
|
1452
|
+
* not compile are three faults with three repairs; all are reported through
|
|
1453
|
+
* the grammars module so every hint has one home.
|
|
1454
|
+
*/
|
|
1455
|
+
async load(language) {
|
|
1456
|
+
let pack2;
|
|
1457
|
+
try {
|
|
1458
|
+
pack2 = await ensureGrammar(language, this.grammars);
|
|
1459
|
+
} catch {
|
|
1460
|
+
return null;
|
|
1461
|
+
}
|
|
1462
|
+
if (!pack2?.query) return null;
|
|
1463
|
+
let grammar;
|
|
1464
|
+
try {
|
|
1465
|
+
grammar = await import_web_tree_sitter.Language.load(pack2.wasm);
|
|
1466
|
+
} catch (error) {
|
|
1467
|
+
noteRejectedGrammar(language, why(error));
|
|
1468
|
+
return null;
|
|
1469
|
+
}
|
|
1470
|
+
try {
|
|
1471
|
+
return { language: grammar, query: new import_web_tree_sitter.Query(grammar, pack2.query) };
|
|
1472
|
+
} catch (error) {
|
|
1473
|
+
noteUncompilableQuery(language, why(error));
|
|
1474
|
+
return null;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
/**
|
|
1478
|
+
* Abstains on an extension with no grammar so the regex resolver gets a
|
|
1479
|
+
* turn; reports `resolver-unavailable` when the grammar exists in principle
|
|
1480
|
+
* but could not be loaded, because falling back there would silently trade a
|
|
1481
|
+
* precise span for a guessed one.
|
|
1482
|
+
*/
|
|
1483
|
+
attempt(source, symbol, file) {
|
|
1484
|
+
const language = file ? languageForFile(file) : void 0;
|
|
1485
|
+
if (!language) return { kind: "abstain" };
|
|
1486
|
+
if (!this.loaded.has(language)) return { kind: "abstain" };
|
|
1487
|
+
const loaded = this.loaded.get(language);
|
|
1488
|
+
if (!loaded) return { kind: "unresolved", reason: "resolver-unavailable" };
|
|
1489
|
+
const parsed = this.parse(language, loaded, source);
|
|
1490
|
+
if (!parsed) return { kind: "unresolved", reason: "resolver-unavailable" };
|
|
1491
|
+
const wanted = symbol.split(".").filter(Boolean);
|
|
1492
|
+
if (!wanted.length)
|
|
1493
|
+
return { kind: "unresolved", reason: "symbol-not-found" };
|
|
1494
|
+
const matches3 = select(parsed, wanted);
|
|
1495
|
+
if (!matches3.length)
|
|
1496
|
+
return { kind: "unresolved", reason: "symbol-not-found" };
|
|
1497
|
+
if (matches3.length > 1)
|
|
1498
|
+
return { kind: "unresolved", reason: "symbol-ambiguous" };
|
|
1499
|
+
return { kind: "resolved", span: spanOf(matches3[0], source) };
|
|
1500
|
+
}
|
|
1501
|
+
resolve(source, symbol, file) {
|
|
1502
|
+
const attempt = this.attempt(source, symbol, file);
|
|
1503
|
+
return attempt.kind === "resolved" ? attempt.span : null;
|
|
1504
|
+
}
|
|
1505
|
+
/** Parsed trees are keyed by content hash, so an unchanged file parses once. */
|
|
1506
|
+
parse(language, loaded, source) {
|
|
1507
|
+
const key = `${language}:${(0, import_node_crypto2.createHash)("sha256").update(source).digest("hex")}`;
|
|
1508
|
+
const cached2 = this.trees.get(key);
|
|
1509
|
+
if (cached2) {
|
|
1510
|
+
this.stats.cacheHits += 1;
|
|
1511
|
+
return cached2;
|
|
1512
|
+
}
|
|
1513
|
+
const parser = this.parser;
|
|
1514
|
+
if (!parser) return null;
|
|
1515
|
+
let parsed;
|
|
1516
|
+
try {
|
|
1517
|
+
parser.setLanguage(loaded.language);
|
|
1518
|
+
const tree = parser.parse(source);
|
|
1519
|
+
if (!tree) return null;
|
|
1520
|
+
parsed = index(tree, loaded.query);
|
|
1521
|
+
} catch {
|
|
1522
|
+
return null;
|
|
1523
|
+
}
|
|
1524
|
+
this.stats.parses += 1;
|
|
1525
|
+
if (this.trees.size >= TREE_CACHE_LIMIT) {
|
|
1526
|
+
const oldest = this.trees.keys().next();
|
|
1527
|
+
if (!oldest.done) {
|
|
1528
|
+
this.trees.get(oldest.value)?.tree.delete();
|
|
1529
|
+
this.trees.delete(oldest.value);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
this.trees.set(key, parsed);
|
|
1533
|
+
return parsed;
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* Every definition this file declares, as dotted symbol and span.
|
|
1537
|
+
*
|
|
1538
|
+
* The inverse of `attempt`: that asks "where is this name", this asks "what
|
|
1539
|
+
* names are here". `moved` needs the second — the stored hash has to be
|
|
1540
|
+
* looked for at every definition in the repository, and there is no name to
|
|
1541
|
+
* ask about, since the whole question is which name now carries that code.
|
|
1542
|
+
*/
|
|
1543
|
+
spans(source, file) {
|
|
1544
|
+
const language = languageForFile(file);
|
|
1545
|
+
if (!language) return [];
|
|
1546
|
+
const loaded = this.loaded.get(language);
|
|
1547
|
+
if (!loaded) return [];
|
|
1548
|
+
const parsed = this.parse(language, loaded, source);
|
|
1549
|
+
if (!parsed) return [];
|
|
1550
|
+
return parsed.definitions.filter((definition) => definition.target).map((definition) => ({
|
|
1551
|
+
symbol: chainOf(definition, parsed.byNodeId).join("."),
|
|
1552
|
+
span: spanOf(definition, source)
|
|
1553
|
+
}));
|
|
1554
|
+
}
|
|
1555
|
+
/**
|
|
1556
|
+
* The token stream of a span: every leaf the parser sees, comments dropped,
|
|
1557
|
+
* joined by single spaces.
|
|
1558
|
+
*
|
|
1559
|
+
* This is what makes a reformat not be drift. Hashing it rather than the raw
|
|
1560
|
+
* text means indentation, line breaks, trailing commas the formatter moved,
|
|
1561
|
+
* and every comment above or inside the definition are outside the hash —
|
|
1562
|
+
* and a renamed identifier or a changed literal is still inside it, because
|
|
1563
|
+
* those are leaves.
|
|
1564
|
+
*
|
|
1565
|
+
* `null` when the file has no grammar, the grammar would not load, or the
|
|
1566
|
+
* text will not parse: no normalisation is better than a guessed one.
|
|
1567
|
+
*/
|
|
1568
|
+
normalize(text, file) {
|
|
1569
|
+
const language = file ? languageForFile(file) : void 0;
|
|
1570
|
+
if (!language) return null;
|
|
1571
|
+
const loaded = this.loaded.get(language);
|
|
1572
|
+
if (!loaded) return null;
|
|
1573
|
+
const parser = this.parser;
|
|
1574
|
+
if (!parser) return null;
|
|
1575
|
+
let tree;
|
|
1576
|
+
try {
|
|
1577
|
+
parser.setLanguage(loaded.language);
|
|
1578
|
+
tree = parser.parse(text);
|
|
1579
|
+
} catch {
|
|
1580
|
+
return null;
|
|
1581
|
+
}
|
|
1582
|
+
if (!tree) return null;
|
|
1583
|
+
try {
|
|
1584
|
+
return tokens(tree.rootNode).join(" ");
|
|
1585
|
+
} finally {
|
|
1586
|
+
tree.delete();
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
/** Drops cached trees. Grammars stay loaded — they are immutable. */
|
|
1590
|
+
reset() {
|
|
1591
|
+
for (const parsed of this.trees.values()) parsed.tree.delete();
|
|
1592
|
+
this.trees.clear();
|
|
1593
|
+
this.stats.parses = 0;
|
|
1594
|
+
this.stats.cacheHits = 0;
|
|
1595
|
+
}
|
|
1596
|
+
};
|
|
1597
|
+
function why(error) {
|
|
1598
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
1599
|
+
return text || "no reason given";
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
// src/anchor-resolver/resolver.ts
|
|
997
1603
|
var PARENT_SCOPE_LINES = 50;
|
|
998
1604
|
var CLEAN_STATE = { blockComment: false, template: false };
|
|
999
1605
|
function stripLine(line, state) {
|
|
1000
1606
|
let out = "";
|
|
1001
|
-
let
|
|
1607
|
+
let index2 = 0;
|
|
1002
1608
|
let { blockComment, template } = state;
|
|
1003
|
-
while (
|
|
1004
|
-
const char = line[
|
|
1005
|
-
const next = line[
|
|
1609
|
+
while (index2 < line.length) {
|
|
1610
|
+
const char = line[index2];
|
|
1611
|
+
const next = line[index2 + 1];
|
|
1006
1612
|
if (blockComment) {
|
|
1007
1613
|
if (char === "*" && next === "/") {
|
|
1008
1614
|
blockComment = false;
|
|
1009
|
-
|
|
1615
|
+
index2 += 2;
|
|
1010
1616
|
continue;
|
|
1011
1617
|
}
|
|
1012
|
-
|
|
1618
|
+
index2 += 1;
|
|
1013
1619
|
continue;
|
|
1014
1620
|
}
|
|
1015
1621
|
if (template) {
|
|
1016
1622
|
if (char === "\\") {
|
|
1017
|
-
|
|
1623
|
+
index2 += 2;
|
|
1018
1624
|
continue;
|
|
1019
1625
|
}
|
|
1020
1626
|
if (char === "`") template = false;
|
|
1021
|
-
|
|
1627
|
+
index2 += 1;
|
|
1022
1628
|
continue;
|
|
1023
1629
|
}
|
|
1024
1630
|
if (char === "/" && next === "*") {
|
|
1025
1631
|
blockComment = true;
|
|
1026
|
-
|
|
1632
|
+
index2 += 2;
|
|
1027
1633
|
continue;
|
|
1028
1634
|
}
|
|
1029
1635
|
if (char === "/" && next === "/") break;
|
|
1030
1636
|
if (char === "`") {
|
|
1031
1637
|
template = true;
|
|
1032
|
-
|
|
1638
|
+
index2 += 1;
|
|
1033
1639
|
continue;
|
|
1034
1640
|
}
|
|
1035
1641
|
if (char === "'" || char === '"') {
|
|
1036
1642
|
const quote = char;
|
|
1037
|
-
|
|
1038
|
-
while (
|
|
1039
|
-
if (line[
|
|
1040
|
-
|
|
1643
|
+
index2 += 1;
|
|
1644
|
+
while (index2 < line.length) {
|
|
1645
|
+
if (line[index2] === "\\") {
|
|
1646
|
+
index2 += 2;
|
|
1041
1647
|
continue;
|
|
1042
1648
|
}
|
|
1043
|
-
if (line[
|
|
1044
|
-
|
|
1649
|
+
if (line[index2] === quote) {
|
|
1650
|
+
index2 += 1;
|
|
1045
1651
|
break;
|
|
1046
1652
|
}
|
|
1047
|
-
|
|
1653
|
+
index2 += 1;
|
|
1048
1654
|
}
|
|
1049
1655
|
continue;
|
|
1050
1656
|
}
|
|
1051
1657
|
out += char;
|
|
1052
|
-
|
|
1658
|
+
index2 += 1;
|
|
1053
1659
|
}
|
|
1054
1660
|
return { code: out, state: { blockComment, template } };
|
|
1055
1661
|
}
|
|
@@ -1064,8 +1670,8 @@ function captureBraceBlock(lines, matchLine) {
|
|
|
1064
1670
|
let depth = 0;
|
|
1065
1671
|
let opened = false;
|
|
1066
1672
|
let state = CLEAN_STATE;
|
|
1067
|
-
for (let
|
|
1068
|
-
const stripped = stripLine(lines[
|
|
1673
|
+
for (let index2 = matchLine; index2 < lines.length; index2++) {
|
|
1674
|
+
const stripped = stripLine(lines[index2] ?? "", state);
|
|
1069
1675
|
state = stripped.state;
|
|
1070
1676
|
for (const char of stripped.code) {
|
|
1071
1677
|
if (char === "{") {
|
|
@@ -1074,10 +1680,10 @@ function captureBraceBlock(lines, matchLine) {
|
|
|
1074
1680
|
} else if (char === "}") {
|
|
1075
1681
|
depth = Math.max(0, depth - 1);
|
|
1076
1682
|
} else if (char === ";" && !opened) {
|
|
1077
|
-
return span(lines, matchLine,
|
|
1683
|
+
return span(lines, matchLine, index2);
|
|
1078
1684
|
}
|
|
1079
1685
|
}
|
|
1080
|
-
if (opened && depth === 0) return span(lines, matchLine,
|
|
1686
|
+
if (opened && depth === 0) return span(lines, matchLine, index2);
|
|
1081
1687
|
}
|
|
1082
1688
|
return null;
|
|
1083
1689
|
}
|
|
@@ -1086,22 +1692,22 @@ function captureIndentedBlock(lines, matchLine) {
|
|
|
1086
1692
|
const header = lines[matchLine] ?? "";
|
|
1087
1693
|
const indent = header.length - header.trimStart().length;
|
|
1088
1694
|
let headerEnd = -1;
|
|
1089
|
-
for (let
|
|
1090
|
-
const code = stripLine(lines[
|
|
1695
|
+
for (let index2 = matchLine; index2 < lines.length && index2 <= matchLine + 20; index2++) {
|
|
1696
|
+
const code = stripLine(lines[index2] ?? "", CLEAN_STATE).code.trimEnd();
|
|
1091
1697
|
if (code.endsWith(":")) {
|
|
1092
|
-
headerEnd =
|
|
1698
|
+
headerEnd = index2;
|
|
1093
1699
|
break;
|
|
1094
1700
|
}
|
|
1095
|
-
if (code.includes(":")) return span(lines, matchLine,
|
|
1701
|
+
if (code.includes(":")) return span(lines, matchLine, index2);
|
|
1096
1702
|
}
|
|
1097
1703
|
if (headerEnd === -1) return null;
|
|
1098
1704
|
let end = headerEnd;
|
|
1099
|
-
for (let
|
|
1100
|
-
const line = lines[
|
|
1705
|
+
for (let index2 = headerEnd + 1; index2 < lines.length; index2++) {
|
|
1706
|
+
const line = lines[index2] ?? "";
|
|
1101
1707
|
if (line.trim() === "") continue;
|
|
1102
1708
|
const lineIndent = line.length - line.trimStart().length;
|
|
1103
1709
|
if (lineIndent <= indent) break;
|
|
1104
|
-
end =
|
|
1710
|
+
end = index2;
|
|
1105
1711
|
}
|
|
1106
1712
|
return end === headerEnd ? null : span(lines, matchLine, end);
|
|
1107
1713
|
}
|
|
@@ -1125,15 +1731,15 @@ var regexResolver = {
|
|
|
1125
1731
|
const lines = source.split("\n");
|
|
1126
1732
|
for (const tier of TIERS) {
|
|
1127
1733
|
const pattern = tier(escaped);
|
|
1128
|
-
let candidates = lines.map((line,
|
|
1734
|
+
let candidates = lines.map((line, index2) => ({ line, index: index2 })).filter((entry) => pattern.test(entry.line)).map((entry) => entry.index);
|
|
1129
1735
|
if (!candidates.length) continue;
|
|
1130
1736
|
if (parentPattern && candidates.length > 1) {
|
|
1131
1737
|
const distances = candidates.map(
|
|
1132
|
-
(
|
|
1738
|
+
(index2) => distanceToParent(lines, index2, parentPattern)
|
|
1133
1739
|
);
|
|
1134
1740
|
const nearest = Math.min(...distances);
|
|
1135
1741
|
if (Number.isFinite(nearest)) {
|
|
1136
|
-
candidates = candidates.filter((_,
|
|
1742
|
+
candidates = candidates.filter((_, at2) => distances[at2] === nearest);
|
|
1137
1743
|
}
|
|
1138
1744
|
}
|
|
1139
1745
|
if (candidates.length !== 1) return null;
|
|
@@ -1146,34 +1752,82 @@ var regexResolver = {
|
|
|
1146
1752
|
function escapeRegExp(value) {
|
|
1147
1753
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1148
1754
|
}
|
|
1149
|
-
function distanceToParent(lines,
|
|
1150
|
-
const floor = Math.max(0,
|
|
1151
|
-
for (let
|
|
1152
|
-
if (parent.test(lines[
|
|
1755
|
+
function distanceToParent(lines, index2, parent) {
|
|
1756
|
+
const floor = Math.max(0, index2 - PARENT_SCOPE_LINES);
|
|
1757
|
+
for (let at2 = index2; at2 >= floor; at2--) {
|
|
1758
|
+
if (parent.test(lines[at2] ?? "")) return index2 - at2;
|
|
1153
1759
|
}
|
|
1154
1760
|
return Number.POSITIVE_INFINITY;
|
|
1155
1761
|
}
|
|
1156
1762
|
function hashAnchorText(text) {
|
|
1157
|
-
return `sha256:${(0,
|
|
1763
|
+
return `sha256:${(0, import_node_crypto3.createHash)("sha256").update(text.replace(/\r\n/g, "\n")).digest("hex")}`;
|
|
1158
1764
|
}
|
|
1159
|
-
function
|
|
1765
|
+
function resolveAnchorSpan(source, anchor, resolvers = [regexResolver]) {
|
|
1160
1766
|
const normalized = source.replace(/\r\n/g, "\n");
|
|
1161
1767
|
if (!anchor.symbol) {
|
|
1162
1768
|
const lines = normalized.split("\n");
|
|
1163
1769
|
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
1164
1770
|
return {
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1771
|
+
ok: true,
|
|
1772
|
+
span: {
|
|
1773
|
+
text: normalized,
|
|
1774
|
+
startLine: 1,
|
|
1775
|
+
endLine: Math.max(1, lines.length)
|
|
1776
|
+
}
|
|
1168
1777
|
};
|
|
1169
1778
|
}
|
|
1170
|
-
|
|
1779
|
+
for (const resolver of resolvers) {
|
|
1780
|
+
const attempt = resolver.attempt ? resolver.attempt(normalized, anchor.symbol, anchor.file) : fromResolve(resolver, normalized, anchor.symbol, anchor.file);
|
|
1781
|
+
if (attempt.kind === "abstain") continue;
|
|
1782
|
+
if (attempt.kind === "unresolved") {
|
|
1783
|
+
if (attempt.reason === "symbol-not-found") continue;
|
|
1784
|
+
return { ok: false, reason: attempt.reason };
|
|
1785
|
+
}
|
|
1786
|
+
const tokens2 = resolver.normalize?.(attempt.span.text, anchor.file);
|
|
1787
|
+
return {
|
|
1788
|
+
ok: true,
|
|
1789
|
+
span: attempt.span,
|
|
1790
|
+
...isResolverName(resolver.name) ? { resolver: resolver.name } : {},
|
|
1791
|
+
...tokens2 ? { normalized: tokens2 } : {}
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
return { ok: false, reason: "symbol-not-found" };
|
|
1795
|
+
}
|
|
1796
|
+
function fromResolve(resolver, source, symbol, file) {
|
|
1797
|
+
const span2 = resolver.resolve(source, symbol, file);
|
|
1798
|
+
return span2 ? { kind: "resolved", span: span2 } : { kind: "unresolved", reason: "symbol-not-found" };
|
|
1799
|
+
}
|
|
1800
|
+
function isResolverName(name) {
|
|
1801
|
+
return name === "tree-sitter" || name === "regex";
|
|
1802
|
+
}
|
|
1803
|
+
async function prepareResolvers(resolvers, files) {
|
|
1804
|
+
for (const resolver of resolvers) await resolver.prepare?.(files);
|
|
1805
|
+
}
|
|
1806
|
+
function defaultAnchorResolvers(grammars = {}) {
|
|
1807
|
+
return [new TreeSitterResolver(grammars), regexResolver];
|
|
1808
|
+
}
|
|
1809
|
+
function resolverChanged(source, anchor, produced) {
|
|
1810
|
+
const previous = anchor.resolver ?? "regex";
|
|
1811
|
+
if (!produced || !anchor.symbol || previous === produced) return false;
|
|
1812
|
+
if (previous !== "regex") return false;
|
|
1813
|
+
const before = regexResolver.resolve(
|
|
1814
|
+
source.replace(/\r\n/g, "\n"),
|
|
1815
|
+
anchor.symbol
|
|
1816
|
+
);
|
|
1817
|
+
return before !== null && hashAnchorText(before.text) === anchor.hash;
|
|
1818
|
+
}
|
|
1819
|
+
function anchorHashOf(anchor, outcome) {
|
|
1820
|
+
const stored = anchor.hash ? anchor.hash_kind ?? "raw" : void 0;
|
|
1821
|
+
const wanted = stored ?? (outcome.normalized ? "ast" : "raw");
|
|
1822
|
+
return wanted === "ast" && outcome.normalized ? { hash: hashAnchorText(outcome.normalized), kind: "ast" } : { hash: hashAnchorText(outcome.span.text), kind: "raw" };
|
|
1171
1823
|
}
|
|
1172
1824
|
|
|
1173
1825
|
// src/anchor-resolver/drift.ts
|
|
1174
1826
|
async function detectAnchorDrift(records, options = {}) {
|
|
1175
1827
|
const repoRoot = options.repoRoot ?? process.cwd();
|
|
1176
|
-
const
|
|
1828
|
+
const resolvers = options.resolvers ?? (options.resolver ? [options.resolver] : defaultAnchorResolvers({
|
|
1829
|
+
offline: options.remote?.offline === true
|
|
1830
|
+
}));
|
|
1177
1831
|
const origin = new LazyOrigin(repoRoot);
|
|
1178
1832
|
const planned = /* @__PURE__ */ new Map();
|
|
1179
1833
|
let declaresRepo = false;
|
|
@@ -1211,12 +1865,16 @@ async function detectAnchorDrift(records, options = {}) {
|
|
|
1211
1865
|
),
|
|
1212
1866
|
(options.readRemote ?? readRemoteAnchors)(wants, options.remote ?? {})
|
|
1213
1867
|
]);
|
|
1868
|
+
await prepareResolvers(resolvers, [
|
|
1869
|
+
...files,
|
|
1870
|
+
...wants.map((want) => want.file)
|
|
1871
|
+
]);
|
|
1214
1872
|
const drift = /* @__PURE__ */ new Map();
|
|
1215
1873
|
for (const record of records) {
|
|
1216
1874
|
const entries = [];
|
|
1217
1875
|
for (const { anchor, foreign } of planned.get(record.conceptId) ?? []) {
|
|
1218
1876
|
entries.push(
|
|
1219
|
-
foreign ? remoteEntry(anchor, remote,
|
|
1877
|
+
foreign ? remoteEntry(anchor, remote, resolvers) : localEntry(anchor, reads.get(anchor.file), resolvers)
|
|
1220
1878
|
);
|
|
1221
1879
|
}
|
|
1222
1880
|
if (entries.length) drift.set(record.conceptId, entries);
|
|
@@ -1242,50 +1900,94 @@ function unresolved(anchor, reason, repo) {
|
|
|
1242
1900
|
state: "unresolved",
|
|
1243
1901
|
diffSize: null,
|
|
1244
1902
|
...reason ? { reason } : {},
|
|
1245
|
-
...repo ? { repo } : {}
|
|
1903
|
+
...repo ? { repo } : {},
|
|
1904
|
+
...classOf(reason)
|
|
1905
|
+
};
|
|
1906
|
+
}
|
|
1907
|
+
function provisionalDriftClass(entry) {
|
|
1908
|
+
if (entry.state === "unresolved") {
|
|
1909
|
+
return entry.reason === "file-missing" || entry.reason === "symbol-not-found" ? "gone" : void 0;
|
|
1910
|
+
}
|
|
1911
|
+
return entry.state === "drifted" ? "changed" : void 0;
|
|
1912
|
+
}
|
|
1913
|
+
function classOf(reason) {
|
|
1914
|
+
const settled = provisionalDriftClass({ state: "unresolved", reason });
|
|
1915
|
+
return settled ? { class: settled } : {};
|
|
1916
|
+
}
|
|
1917
|
+
function hashIn(source, anchor, resolvers) {
|
|
1918
|
+
const outcome = resolveAnchorSpan(source, anchor, resolvers);
|
|
1919
|
+
if (!outcome.ok) return { ok: false, reason: outcome.reason };
|
|
1920
|
+
const { hash, kind } = anchorHashOf(anchor, outcome);
|
|
1921
|
+
return {
|
|
1922
|
+
ok: true,
|
|
1923
|
+
current: {
|
|
1924
|
+
hash,
|
|
1925
|
+
kind,
|
|
1926
|
+
lines: outcome.span.endLine - outcome.span.startLine + 1,
|
|
1927
|
+
...outcome.resolver ? { resolver: outcome.resolver } : {}
|
|
1928
|
+
}
|
|
1246
1929
|
};
|
|
1247
1930
|
}
|
|
1248
|
-
function
|
|
1249
|
-
const resolved = resolveAnchor(source, anchor, resolver);
|
|
1250
|
-
if (!resolved) return null;
|
|
1931
|
+
function resolverExtras(source, anchor, current) {
|
|
1251
1932
|
return {
|
|
1252
|
-
|
|
1253
|
-
|
|
1933
|
+
...current.resolver ? { resolver: current.resolver } : {},
|
|
1934
|
+
...current.hash !== anchor.hash && resolverChanged(source, anchor, current.resolver) ? { reason: "resolver-changed" } : {}
|
|
1254
1935
|
};
|
|
1255
1936
|
}
|
|
1256
1937
|
function compared(anchor, current, extra = {}) {
|
|
1938
|
+
const matched = current.hash === anchor.hash;
|
|
1257
1939
|
return {
|
|
1258
1940
|
...base(anchor),
|
|
1259
|
-
state:
|
|
1941
|
+
state: matched ? "match" : "drifted",
|
|
1260
1942
|
currentHash: current.hash,
|
|
1943
|
+
hashKind: current.kind,
|
|
1261
1944
|
diffSize: anchor.lines === void 0 ? null : Math.abs(current.lines - anchor.lines),
|
|
1945
|
+
...matched ? {} : { class: "changed" },
|
|
1262
1946
|
...extra
|
|
1263
1947
|
};
|
|
1264
1948
|
}
|
|
1265
|
-
function localEntry(anchor, read,
|
|
1949
|
+
function localEntry(anchor, read, resolvers) {
|
|
1266
1950
|
if (!read.ok) return unresolved(anchor, read.reason);
|
|
1267
|
-
const
|
|
1268
|
-
|
|
1951
|
+
const found = hashIn(read.source, anchor, resolvers);
|
|
1952
|
+
if (!found.ok) return unresolved(anchor, found.reason);
|
|
1953
|
+
return compared(
|
|
1954
|
+
anchor,
|
|
1955
|
+
found.current,
|
|
1956
|
+
resolverExtras(read.source, anchor, found.current)
|
|
1957
|
+
);
|
|
1269
1958
|
}
|
|
1270
|
-
function remoteEntry(anchor, remote,
|
|
1959
|
+
function remoteEntry(anchor, remote, resolvers) {
|
|
1271
1960
|
const repo = anchor.repo;
|
|
1272
1961
|
const key = normalizeRepoUrl(repo);
|
|
1273
1962
|
const atDefault = remote.get(wantKey(key, void 0, anchor.file));
|
|
1274
1963
|
const primary = anchor.ref ? remote.get(wantKey(key, anchor.ref, anchor.file)) : atDefault;
|
|
1275
1964
|
if (!primary) return unresolved(anchor, "remote-unreachable", repo);
|
|
1276
1965
|
if (!primary.ok) return unresolved(anchor, primary.reason, repo);
|
|
1277
|
-
const
|
|
1278
|
-
if (!
|
|
1279
|
-
|
|
1966
|
+
const found = hashIn(primary.source, anchor, resolvers);
|
|
1967
|
+
if (!found.ok) return unresolved(anchor, found.reason, repo);
|
|
1968
|
+
const current = found.current;
|
|
1969
|
+
const extras = resolverExtras(primary.source, anchor, current);
|
|
1970
|
+
if (!anchor.ref) return compared(anchor, current, { repo, ...extras });
|
|
1280
1971
|
if (current.hash !== anchor.hash) {
|
|
1281
|
-
return compared(anchor, current, {
|
|
1972
|
+
return compared(anchor, current, {
|
|
1973
|
+
repo,
|
|
1974
|
+
...extras,
|
|
1975
|
+
remoteState: "drifted-from-ref"
|
|
1976
|
+
});
|
|
1282
1977
|
}
|
|
1283
|
-
const head = atDefault?.ok ? hashIn(atDefault.source, anchor,
|
|
1284
|
-
return head && head.hash !== anchor.hash ? {
|
|
1285
|
-
...compared(anchor, head, {
|
|
1978
|
+
const head = atDefault?.ok ? hashIn(atDefault.source, anchor, resolvers) : null;
|
|
1979
|
+
return head?.ok && head.current.hash !== anchor.hash ? {
|
|
1980
|
+
...compared(anchor, head.current, {
|
|
1981
|
+
repo,
|
|
1982
|
+
...head.current.resolver ? { resolver: head.current.resolver } : {}
|
|
1983
|
+
}),
|
|
1286
1984
|
state: "drifted",
|
|
1287
1985
|
remoteState: "drifted-on-default"
|
|
1288
|
-
} : compared(anchor, current, {
|
|
1986
|
+
} : compared(anchor, current, {
|
|
1987
|
+
repo,
|
|
1988
|
+
...extras,
|
|
1989
|
+
remoteState: "matches-ref"
|
|
1990
|
+
});
|
|
1289
1991
|
}
|
|
1290
1992
|
|
|
1291
1993
|
// src/errors.ts
|
|
@@ -1473,9 +2175,9 @@ var KbStampDigestBaselineError = class extends BaseError {
|
|
|
1473
2175
|
// src/kb-pins/budgets.ts
|
|
1474
2176
|
function asBudgets(value) {
|
|
1475
2177
|
if (value === null || typeof value !== "object") return {};
|
|
1476
|
-
const
|
|
2178
|
+
const table2 = value;
|
|
1477
2179
|
const pick = (key, min) => {
|
|
1478
|
-
const raw =
|
|
2180
|
+
const raw = table2[key];
|
|
1479
2181
|
return typeof raw === "number" && Number.isInteger(raw) && raw >= min ? raw : void 0;
|
|
1480
2182
|
};
|
|
1481
2183
|
const budgetTokens = pick("budgetTokens", 1);
|
|
@@ -1486,9 +2188,9 @@ function asBudgets(value) {
|
|
|
1486
2188
|
};
|
|
1487
2189
|
}
|
|
1488
2190
|
function contextProfileBudgets(manifest, profile) {
|
|
1489
|
-
const
|
|
1490
|
-
if (
|
|
1491
|
-
const entries =
|
|
2191
|
+
const table2 = manifest.context;
|
|
2192
|
+
if (table2 === null || typeof table2 !== "object") return {};
|
|
2193
|
+
const entries = table2;
|
|
1492
2194
|
return {
|
|
1493
2195
|
...asBudgets(entries["default"]),
|
|
1494
2196
|
...profile ? asBudgets(entries[profile]) : {}
|
|
@@ -1519,23 +2221,23 @@ var KbBaseFrozenError = class extends Error {
|
|
|
1519
2221
|
};
|
|
1520
2222
|
|
|
1521
2223
|
// src/kb-pins/frozen.ts
|
|
1522
|
-
var
|
|
2224
|
+
var import_node_path8 = require("path");
|
|
1523
2225
|
|
|
1524
2226
|
// src/kb-pins/layers.ts
|
|
1525
|
-
var
|
|
1526
|
-
var
|
|
1527
|
-
var
|
|
2227
|
+
var import_promises5 = require("fs/promises");
|
|
2228
|
+
var import_node_os3 = require("os");
|
|
2229
|
+
var import_node_path7 = require("path");
|
|
1528
2230
|
|
|
1529
2231
|
// src/kb-pins/model.ts
|
|
1530
|
-
var
|
|
1531
|
-
var
|
|
1532
|
-
var PINS_FILE = (0,
|
|
1533
|
-
var PINS_LOCAL_FILE = (0,
|
|
2232
|
+
var import_node_path6 = require("path");
|
|
2233
|
+
var import_zod5 = require("zod");
|
|
2234
|
+
var PINS_FILE = (0, import_node_path6.join)(".strauss", "kb-pins.json");
|
|
2235
|
+
var PINS_LOCAL_FILE = (0, import_node_path6.join)(".strauss", "kb-pins.local.json");
|
|
1534
2236
|
var PIN_LAYERS = ["project", "local", "user"];
|
|
1535
|
-
var pinSchema =
|
|
2237
|
+
var pinSchema = import_zod5.z.object({
|
|
1536
2238
|
/** Relative to the manifest's root, so the file is committable. */
|
|
1537
|
-
path:
|
|
1538
|
-
pinnedAt:
|
|
2239
|
+
path: import_zod5.z.string().min(1),
|
|
2240
|
+
pinnedAt: import_zod5.z.string().min(1).optional(),
|
|
1539
2241
|
/**
|
|
1540
2242
|
* How `context` renders this base. `full` preloads the whole base into
|
|
1541
2243
|
* the block regardless of the full-under threshold — for a base whose
|
|
@@ -1545,7 +2247,7 @@ var pinSchema = import_zod4.z.object({
|
|
|
1545
2247
|
* Absent: the profile's full-under threshold decides. Invalid values
|
|
1546
2248
|
* degrade to absent rather than failing the manifest.
|
|
1547
2249
|
*/
|
|
1548
|
-
mode:
|
|
2250
|
+
mode: import_zod5.z.enum(["full", "index"]).optional().catch(void 0),
|
|
1549
2251
|
/**
|
|
1550
2252
|
* Context profiles this pin surfaces in (e.g. only at session-start,
|
|
1551
2253
|
* not per turn). Absent: every profile. A run without a profile sees
|
|
@@ -1553,17 +2255,17 @@ var pinSchema = import_zod4.z.object({
|
|
|
1553
2255
|
* that skill at point of use than pinned at all — pins are what every
|
|
1554
2256
|
* session should see.
|
|
1555
2257
|
*/
|
|
1556
|
-
profiles:
|
|
2258
|
+
profiles: import_zod5.z.array(import_zod5.z.string()).optional().catch(void 0),
|
|
1557
2259
|
/**
|
|
1558
2260
|
* The base is concluded — a finished piece of research, a frozen ADR
|
|
1559
2261
|
* set. Write commands against it refuse while this workspace holds the
|
|
1560
2262
|
* pin, and `context` labels it read-only. Workspace policy, not base
|
|
1561
2263
|
* state: the base itself stays copyable and writable elsewhere.
|
|
1562
2264
|
*/
|
|
1563
|
-
frozen:
|
|
2265
|
+
frozen: import_zod5.z.boolean().optional().catch(void 0)
|
|
1564
2266
|
}).passthrough();
|
|
1565
|
-
var pinsManifestSchema =
|
|
1566
|
-
pins:
|
|
2267
|
+
var pinsManifestSchema = import_zod5.z.object({
|
|
2268
|
+
pins: import_zod5.z.array(pinSchema).default([]),
|
|
1567
2269
|
/**
|
|
1568
2270
|
* Per-repo budgets for the `context` command, keyed by profile —
|
|
1569
2271
|
* `"session-start"`, `"compact"`, `"turn"`, or `"default"` for all of
|
|
@@ -1572,18 +2274,18 @@ var pinsManifestSchema = import_zod4.z.object({
|
|
|
1572
2274
|
* the index at every session start. `contextProfileBudgets` does the
|
|
1573
2275
|
* tolerant read.
|
|
1574
2276
|
*/
|
|
1575
|
-
context:
|
|
2277
|
+
context: import_zod5.z.unknown().optional()
|
|
1576
2278
|
}).passthrough();
|
|
1577
2279
|
|
|
1578
2280
|
// src/kb-pins/layers.ts
|
|
1579
2281
|
function userRoot() {
|
|
1580
|
-
return process.env.STRAUSS_KB_USER_ROOT || (0,
|
|
2282
|
+
return process.env.STRAUSS_KB_USER_ROOT || (0, import_node_os3.homedir)();
|
|
1581
2283
|
}
|
|
1582
2284
|
function layerRoot(workspaceDir, layer) {
|
|
1583
|
-
return layer === "user" ? userRoot() : (0,
|
|
2285
|
+
return layer === "user" ? userRoot() : (0, import_node_path7.resolve)(workspaceDir);
|
|
1584
2286
|
}
|
|
1585
2287
|
function layerFile(workspaceDir, layer) {
|
|
1586
|
-
return (0,
|
|
2288
|
+
return (0, import_node_path7.join)(
|
|
1587
2289
|
layerRoot(workspaceDir, layer),
|
|
1588
2290
|
layer === "local" ? PINS_LOCAL_FILE : PINS_FILE
|
|
1589
2291
|
);
|
|
@@ -1592,7 +2294,7 @@ async function readPinsLayer(workspaceDir, layer) {
|
|
|
1592
2294
|
const file = layerFile(workspaceDir, layer);
|
|
1593
2295
|
let raw;
|
|
1594
2296
|
try {
|
|
1595
|
-
raw = await (0,
|
|
2297
|
+
raw = await (0, import_promises5.readFile)(file, "utf8");
|
|
1596
2298
|
} catch {
|
|
1597
2299
|
return { pins: [] };
|
|
1598
2300
|
}
|
|
@@ -1616,16 +2318,16 @@ async function readPinsLayer(workspaceDir, layer) {
|
|
|
1616
2318
|
}
|
|
1617
2319
|
async function writePinsLayer(workspaceDir, layer, manifest) {
|
|
1618
2320
|
const file = layerFile(workspaceDir, layer);
|
|
1619
|
-
await (0,
|
|
1620
|
-
await (0,
|
|
2321
|
+
await (0, import_promises5.mkdir)((0, import_node_path7.dirname)(file), { recursive: true });
|
|
2322
|
+
await (0, import_promises5.writeFile)(file, `${JSON.stringify(manifest, null, 2)}
|
|
1621
2323
|
`, "utf8");
|
|
1622
2324
|
}
|
|
1623
2325
|
function resolvePinPath(rootDir, path) {
|
|
1624
|
-
return (0,
|
|
2326
|
+
return (0, import_node_path7.isAbsolute)(path) ? (0, import_node_path7.resolve)(path) : (0, import_node_path7.resolve)(rootDir, path.split("/").join(import_node_path7.sep));
|
|
1625
2327
|
}
|
|
1626
2328
|
function storablePath(rootDir, bundlePath2) {
|
|
1627
|
-
const rel = (0,
|
|
1628
|
-
return (rel === "" ? "." : rel).split(
|
|
2329
|
+
const rel = (0, import_node_path7.relative)((0, import_node_path7.resolve)(rootDir), (0, import_node_path7.resolve)(bundlePath2));
|
|
2330
|
+
return (rel === "" ? "." : rel).split(import_node_path7.sep).join("/");
|
|
1629
2331
|
}
|
|
1630
2332
|
async function readMergedPins(workspaceDir) {
|
|
1631
2333
|
const manifests = {};
|
|
@@ -1653,7 +2355,7 @@ async function readMergedPins(workspaceDir) {
|
|
|
1653
2355
|
// src/kb-pins/frozen.ts
|
|
1654
2356
|
async function assertBaseNotFrozen(workspaceDir, bundlePath2) {
|
|
1655
2357
|
const merged = await readMergedPins(workspaceDir);
|
|
1656
|
-
const absolute = (0,
|
|
2358
|
+
const absolute = (0, import_node_path8.resolve)(bundlePath2);
|
|
1657
2359
|
const pin = merged.pins.find((entry) => entry.absolutePath === absolute);
|
|
1658
2360
|
if (pin?.frozen === true) {
|
|
1659
2361
|
throw new KbBaseFrozenError(pin.path, pin.layer);
|
|
@@ -1682,7 +2384,7 @@ async function listPins(store, workspaceDir) {
|
|
|
1682
2384
|
}
|
|
1683
2385
|
|
|
1684
2386
|
// src/kb-pins/pin.ts
|
|
1685
|
-
async function pinBase(store, workspaceDir, bundlePath2,
|
|
2387
|
+
async function pinBase(store, workspaceDir, bundlePath2, at2, options = {}) {
|
|
1686
2388
|
const layer = options.layer ?? "project";
|
|
1687
2389
|
const root = layerRoot(workspaceDir, layer);
|
|
1688
2390
|
const manifest = await readPinsLayer(workspaceDir, layer);
|
|
@@ -1710,7 +2412,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
|
|
|
1710
2412
|
return {
|
|
1711
2413
|
path: existing.path,
|
|
1712
2414
|
layer,
|
|
1713
|
-
pinnedAt: existing.pinnedAt ??
|
|
2415
|
+
pinnedAt: existing.pinnedAt ?? at2,
|
|
1714
2416
|
alreadyPinned: true,
|
|
1715
2417
|
...updated.mode ? { mode: updated.mode } : {},
|
|
1716
2418
|
...updated.profiles ? { profiles: updated.profiles } : {},
|
|
@@ -1720,7 +2422,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
|
|
|
1720
2422
|
}
|
|
1721
2423
|
const entry = {
|
|
1722
2424
|
path: storablePath(root, bundlePath2),
|
|
1723
|
-
pinnedAt:
|
|
2425
|
+
pinnedAt: at2,
|
|
1724
2426
|
...fields
|
|
1725
2427
|
};
|
|
1726
2428
|
await writePinsLayer(workspaceDir, layer, {
|
|
@@ -1730,7 +2432,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
|
|
|
1730
2432
|
return {
|
|
1731
2433
|
path: entry.path,
|
|
1732
2434
|
layer,
|
|
1733
|
-
pinnedAt:
|
|
2435
|
+
pinnedAt: at2,
|
|
1734
2436
|
alreadyPinned: false,
|
|
1735
2437
|
...fields,
|
|
1736
2438
|
...warning ? { warning } : {}
|
|
@@ -1738,7 +2440,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
|
|
|
1738
2440
|
}
|
|
1739
2441
|
|
|
1740
2442
|
// src/kb-pins/unpin.ts
|
|
1741
|
-
var
|
|
2443
|
+
var import_node_path9 = require("path");
|
|
1742
2444
|
async function unpinBase(workspaceDir, bundlePath2) {
|
|
1743
2445
|
const layers = [];
|
|
1744
2446
|
for (const layer of PIN_LAYERS) {
|
|
@@ -1759,17 +2461,17 @@ async function unpinBase(workspaceDir, bundlePath2) {
|
|
|
1759
2461
|
}
|
|
1760
2462
|
}
|
|
1761
2463
|
return {
|
|
1762
|
-
path: storablePath((0,
|
|
2464
|
+
path: storablePath((0, import_node_path9.resolve)(workspaceDir), bundlePath2),
|
|
1763
2465
|
removed: layers.length > 0,
|
|
1764
2466
|
layers
|
|
1765
2467
|
};
|
|
1766
2468
|
}
|
|
1767
2469
|
|
|
1768
2470
|
// src/commands/model.ts
|
|
1769
|
-
var
|
|
1770
|
-
var bundlePath =
|
|
1771
|
-
var conceptId =
|
|
1772
|
-
var REPO_ROOT =
|
|
2471
|
+
var import_zod6 = require("zod");
|
|
2472
|
+
var bundlePath = import_zod6.z.string().min(1).describe("Absolute path to the knowledge base directory.");
|
|
2473
|
+
var conceptId = import_zod6.z.string().min(1).describe("e.g. decision.cursor-v2");
|
|
2474
|
+
var REPO_ROOT = import_zod6.z.string().min(1).optional().describe(
|
|
1773
2475
|
"Where the anchored source lives, for the drift check. Defaults to the working directory."
|
|
1774
2476
|
);
|
|
1775
2477
|
function define(command) {
|
|
@@ -1782,9 +2484,9 @@ function argvFlag(argv, name) {
|
|
|
1782
2484
|
if (!value2) throw new KbMissingFlagValueError(name);
|
|
1783
2485
|
return value2;
|
|
1784
2486
|
}
|
|
1785
|
-
const
|
|
1786
|
-
if (
|
|
1787
|
-
const value = argv[
|
|
2487
|
+
const at2 = argv.indexOf(name);
|
|
2488
|
+
if (at2 === -1) return void 0;
|
|
2489
|
+
const value = argv[at2 + 1];
|
|
1788
2490
|
if (value === void 0 || value.startsWith("--")) {
|
|
1789
2491
|
throw new KbMissingFlagValueError(name);
|
|
1790
2492
|
}
|
|
@@ -1792,22 +2494,30 @@ function argvFlag(argv, name) {
|
|
|
1792
2494
|
}
|
|
1793
2495
|
|
|
1794
2496
|
// src/commands/anchor-resolve.ts
|
|
2497
|
+
function resolverSummary(results) {
|
|
2498
|
+
const names = [
|
|
2499
|
+
...new Set(
|
|
2500
|
+
results.flatMap((entry) => entry.resolver ? [entry.resolver] : [])
|
|
2501
|
+
)
|
|
2502
|
+
].sort();
|
|
2503
|
+
return names.length ? `${names.join(" + ")} resolver` : "whole-file";
|
|
2504
|
+
}
|
|
1795
2505
|
var anchorResolveCommand = define({
|
|
1796
2506
|
name: "anchor-resolve",
|
|
1797
2507
|
tool: "kb_anchor_resolve",
|
|
1798
2508
|
usage: "anchor-resolve <concept-id> [--repo-root <path>] [--offline] [--rebaseline] [--restamp]",
|
|
1799
2509
|
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.",
|
|
1800
|
-
input:
|
|
2510
|
+
input: import_zod7.z.object({
|
|
1801
2511
|
bundlePath,
|
|
1802
2512
|
conceptId,
|
|
1803
|
-
repoRoot:
|
|
1804
|
-
offline:
|
|
2513
|
+
repoRoot: import_zod7.z.string().min(1).optional(),
|
|
2514
|
+
offline: import_zod7.z.boolean().optional().describe(
|
|
1805
2515
|
"Resolve foreign anchors from the local repo cache only, never fetching."
|
|
1806
2516
|
),
|
|
1807
|
-
rebaseline:
|
|
2517
|
+
rebaseline: import_zod7.z.boolean().optional().describe(
|
|
1808
2518
|
"Accept the current code as the new baseline for anchors that drifted."
|
|
1809
2519
|
),
|
|
1810
|
-
restamp:
|
|
2520
|
+
restamp: import_zod7.z.boolean().optional().describe(
|
|
1811
2521
|
"Refresh `resolved_at` on anchors that already match. Off by default, so a green run writes nothing."
|
|
1812
2522
|
)
|
|
1813
2523
|
}),
|
|
@@ -1836,6 +2546,11 @@ var anchorResolveCommand = define({
|
|
|
1836
2546
|
const updated = [];
|
|
1837
2547
|
let dirty = false;
|
|
1838
2548
|
const sources = await readSources(anchors, root, offline === true);
|
|
2549
|
+
const resolvers = defaultAnchorResolvers({ offline: offline === true });
|
|
2550
|
+
await prepareResolvers(
|
|
2551
|
+
resolvers,
|
|
2552
|
+
anchors.map((anchor) => anchor.file)
|
|
2553
|
+
);
|
|
1839
2554
|
for (const anchor of anchors) {
|
|
1840
2555
|
const base2 = {
|
|
1841
2556
|
file: anchor.file,
|
|
@@ -1852,27 +2567,39 @@ var anchorResolveCommand = define({
|
|
|
1852
2567
|
updated.push(anchor);
|
|
1853
2568
|
continue;
|
|
1854
2569
|
}
|
|
1855
|
-
const
|
|
1856
|
-
if (!
|
|
2570
|
+
const outcome = resolveAnchorSpan(source.source, anchor, resolvers);
|
|
2571
|
+
if (!outcome.ok) {
|
|
1857
2572
|
results.push({
|
|
1858
2573
|
...base2,
|
|
1859
2574
|
state: "unresolved",
|
|
1860
|
-
reason:
|
|
2575
|
+
reason: outcome.reason
|
|
1861
2576
|
});
|
|
1862
2577
|
updated.push(anchor);
|
|
1863
2578
|
continue;
|
|
1864
2579
|
}
|
|
1865
|
-
const
|
|
2580
|
+
const resolved = outcome.span;
|
|
2581
|
+
const producedBy = outcome.resolver;
|
|
2582
|
+
const { hash: currentHash, kind } = anchorHashOf(anchor, outcome);
|
|
1866
2583
|
const currentLines = resolved.endLine - resolved.startLine + 1;
|
|
2584
|
+
const stampedKind = outcome.normalized ? "ast" : "raw";
|
|
2585
|
+
const stampedHash = outcome.normalized ? anchorHashOf({ ...anchor, hash: void 0 }, outcome).hash : currentHash;
|
|
1867
2586
|
const stamped = {
|
|
1868
2587
|
...anchor,
|
|
1869
|
-
hash:
|
|
2588
|
+
hash: stampedHash,
|
|
2589
|
+
hash_kind: stampedKind,
|
|
1870
2590
|
lines: currentLines,
|
|
1871
|
-
resolved_at: now()
|
|
2591
|
+
resolved_at: now(),
|
|
2592
|
+
...producedBy ? { resolver: producedBy } : {}
|
|
1872
2593
|
};
|
|
1873
2594
|
const pinned = anchor.ref !== void 0 && source.repo !== void 0;
|
|
1874
2595
|
if (!anchor.hash) {
|
|
1875
|
-
results.push({
|
|
2596
|
+
results.push({
|
|
2597
|
+
...base2,
|
|
2598
|
+
state: "stamped",
|
|
2599
|
+
currentHash: stampedHash,
|
|
2600
|
+
hashKind: stampedKind,
|
|
2601
|
+
...producedBy ? { resolver: producedBy } : {}
|
|
2602
|
+
});
|
|
1876
2603
|
updated.push(stamped);
|
|
1877
2604
|
dirty = true;
|
|
1878
2605
|
continue;
|
|
@@ -1882,7 +2609,12 @@ var anchorResolveCommand = define({
|
|
|
1882
2609
|
...base2,
|
|
1883
2610
|
state: "drifted",
|
|
1884
2611
|
currentHash,
|
|
2612
|
+
hashKind: kind,
|
|
1885
2613
|
diffSize: lineDelta(anchor, currentLines),
|
|
2614
|
+
...producedBy ? { resolver: producedBy } : {},
|
|
2615
|
+
// A regex-stamped anchor re-read by tree-sitter drifts because the
|
|
2616
|
+
// resolver changed, not because the code did.
|
|
2617
|
+
...resolverChanged(source.source, anchor, producedBy) ? { reason: "resolver-changed" } : {},
|
|
1886
2618
|
...pinned ? { remoteState: "drifted-from-ref" } : {},
|
|
1887
2619
|
...rebaseline ? { rebaselined: true } : {}
|
|
1888
2620
|
});
|
|
@@ -1890,7 +2622,7 @@ var anchorResolveCommand = define({
|
|
|
1890
2622
|
if (rebaseline) dirty = true;
|
|
1891
2623
|
continue;
|
|
1892
2624
|
}
|
|
1893
|
-
const onDefault = pinned ? headHash(source, anchor) : void 0;
|
|
2625
|
+
const onDefault = pinned ? headHash(source, anchor, resolvers) : void 0;
|
|
1894
2626
|
if (onDefault && onDefault.hash !== anchor.hash) {
|
|
1895
2627
|
results.push({
|
|
1896
2628
|
...base2,
|
|
@@ -1906,6 +2638,8 @@ var anchorResolveCommand = define({
|
|
|
1906
2638
|
...base2,
|
|
1907
2639
|
state: "match",
|
|
1908
2640
|
currentHash,
|
|
2641
|
+
hashKind: kind,
|
|
2642
|
+
...producedBy ? { resolver: producedBy } : {},
|
|
1909
2643
|
...pinned ? { remoteState: "matches-ref" } : {}
|
|
1910
2644
|
});
|
|
1911
2645
|
const refresh = restamp || anchor.resolved_at === void 0;
|
|
@@ -1923,19 +2657,21 @@ var anchorResolveCommand = define({
|
|
|
1923
2657
|
if (!frozen) await store.updateAnchors(path, id, updated, actor);
|
|
1924
2658
|
}
|
|
1925
2659
|
const frozenNote = frozen ? { frozen: true, note: "base is frozen: nothing was stamped" } : {};
|
|
2660
|
+
const hints = grammarHints();
|
|
2661
|
+
const hintNote = hints.length ? { hints } : {};
|
|
1926
2662
|
const unreachable = results.filter(
|
|
1927
2663
|
(entry) => isUncheckedReason(entry.reason)
|
|
1928
2664
|
).length;
|
|
1929
2665
|
const checked = results.length - unreachable;
|
|
1930
|
-
const
|
|
1931
|
-
const note = `${
|
|
1932
|
-
const clean = checked > 0 &&
|
|
2666
|
+
const matches3 = results.filter((entry) => entry.state === "match").length;
|
|
2667
|
+
const note = `${matches3}/${checked} anchors match${unreachable ? `, ${unreachable} unreachable` : ""}`;
|
|
2668
|
+
const clean = checked > 0 && matches3 === checked && unreachable === 0;
|
|
1933
2669
|
if (clean) {
|
|
1934
2670
|
try {
|
|
1935
2671
|
await store.verify(
|
|
1936
2672
|
path,
|
|
1937
2673
|
id,
|
|
1938
|
-
`anchor-resolve: ${note} (
|
|
2674
|
+
`anchor-resolve: ${note} (${resolverSummary(results)})`,
|
|
1939
2675
|
actor,
|
|
1940
2676
|
now()
|
|
1941
2677
|
);
|
|
@@ -1946,17 +2682,25 @@ var anchorResolveCommand = define({
|
|
|
1946
2682
|
results,
|
|
1947
2683
|
verified: false,
|
|
1948
2684
|
verifyRefused: "self-verification",
|
|
1949
|
-
...frozenNote
|
|
2685
|
+
...frozenNote,
|
|
2686
|
+
...hintNote
|
|
1950
2687
|
};
|
|
1951
2688
|
}
|
|
1952
|
-
return {
|
|
2689
|
+
return {
|
|
2690
|
+
conceptId: id,
|
|
2691
|
+
results,
|
|
2692
|
+
verified: true,
|
|
2693
|
+
...frozenNote,
|
|
2694
|
+
...hintNote
|
|
2695
|
+
};
|
|
1953
2696
|
}
|
|
1954
2697
|
return {
|
|
1955
2698
|
conceptId: id,
|
|
1956
2699
|
results,
|
|
1957
2700
|
verified: false,
|
|
1958
2701
|
...unreachable ? { note } : {},
|
|
1959
|
-
...frozenNote
|
|
2702
|
+
...frozenNote,
|
|
2703
|
+
...hintNote
|
|
1960
2704
|
};
|
|
1961
2705
|
},
|
|
1962
2706
|
// A stored hash that no longer resolves is a broken anchor, not an absence:
|
|
@@ -1972,13 +2716,13 @@ var anchorResolveCommand = define({
|
|
|
1972
2716
|
function lineDelta(anchor, current) {
|
|
1973
2717
|
return anchor.lines === void 0 ? null : Math.abs(current - anchor.lines);
|
|
1974
2718
|
}
|
|
1975
|
-
function headHash(source, anchor) {
|
|
2719
|
+
function headHash(source, anchor, resolvers) {
|
|
1976
2720
|
if (source.head === void 0) return void 0;
|
|
1977
|
-
const
|
|
1978
|
-
if (!
|
|
2721
|
+
const outcome = resolveAnchorSpan(source.head, anchor, resolvers);
|
|
2722
|
+
if (!outcome.ok) return void 0;
|
|
1979
2723
|
return {
|
|
1980
|
-
hash: hashAnchorText(
|
|
1981
|
-
lines:
|
|
2724
|
+
hash: hashAnchorText(outcome.span.text),
|
|
2725
|
+
lines: outcome.span.endLine - outcome.span.startLine + 1
|
|
1982
2726
|
};
|
|
1983
2727
|
}
|
|
1984
2728
|
async function readSources(anchors, root, offline) {
|
|
@@ -2028,13 +2772,13 @@ async function readSources(anchors, root, offline) {
|
|
|
2028
2772
|
}
|
|
2029
2773
|
|
|
2030
2774
|
// src/commands/answer.ts
|
|
2031
|
-
var
|
|
2775
|
+
var import_zod8 = require("zod");
|
|
2032
2776
|
var answerCommand = define({
|
|
2033
2777
|
name: "answer",
|
|
2034
2778
|
tool: "kb_answer",
|
|
2035
2779
|
usage: "answer <concept-id> <answer...>",
|
|
2036
2780
|
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.",
|
|
2037
|
-
input:
|
|
2781
|
+
input: import_zod8.z.object({ bundlePath, conceptId, answer: import_zod8.z.string().min(1) }),
|
|
2038
2782
|
fromArgv: (argv, path) => ({
|
|
2039
2783
|
bundlePath: path,
|
|
2040
2784
|
conceptId: argv[1],
|
|
@@ -2048,19 +2792,19 @@ var answerCommand = define({
|
|
|
2048
2792
|
});
|
|
2049
2793
|
|
|
2050
2794
|
// src/commands/backlinks.ts
|
|
2051
|
-
var
|
|
2795
|
+
var import_zod9 = require("zod");
|
|
2052
2796
|
var backlinksCommand = define({
|
|
2053
2797
|
name: "backlinks",
|
|
2054
2798
|
tool: "kb_backlinks",
|
|
2055
2799
|
usage: "backlinks <concept-id>",
|
|
2056
2800
|
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.",
|
|
2057
|
-
input:
|
|
2801
|
+
input: import_zod9.z.object({ bundlePath, conceptId }),
|
|
2058
2802
|
fromArgv: (argv, path) => ({ bundlePath: path, conceptId: argv[1] }),
|
|
2059
2803
|
run: async ({ store }, { bundlePath: path, conceptId: id }) => store.backlinks(path, id)
|
|
2060
2804
|
});
|
|
2061
2805
|
|
|
2062
2806
|
// src/commands/catalog.ts
|
|
2063
|
-
var
|
|
2807
|
+
var import_zod10 = require("zod");
|
|
2064
2808
|
|
|
2065
2809
|
// src/adjudicate.ts
|
|
2066
2810
|
var STANDING = {
|
|
@@ -2127,7 +2871,8 @@ function warningAnchor(entry) {
|
|
|
2127
2871
|
diffSize,
|
|
2128
2872
|
...reason !== void 0 ? { reason } : {},
|
|
2129
2873
|
...repo !== void 0 ? { repo } : {},
|
|
2130
|
-
...remoteState !== void 0 ? { remoteState } : {}
|
|
2874
|
+
...remoteState !== void 0 ? { remoteState } : {},
|
|
2875
|
+
...entry.class !== void 0 ? { class: entry.class } : {}
|
|
2131
2876
|
};
|
|
2132
2877
|
}
|
|
2133
2878
|
function resolveHeads(from, byId) {
|
|
@@ -2138,8 +2883,8 @@ function resolveHeads(from, byId) {
|
|
|
2138
2883
|
while (queue.length) {
|
|
2139
2884
|
const current = queue.shift();
|
|
2140
2885
|
const next = successors(current, byId);
|
|
2141
|
-
for (const
|
|
2142
|
-
warnings.push({ kind: "broken-chain", missing });
|
|
2886
|
+
for (const missing2 of next.missing) {
|
|
2887
|
+
warnings.push({ kind: "broken-chain", missing: missing2 });
|
|
2143
2888
|
}
|
|
2144
2889
|
if (!next.records.length) {
|
|
2145
2890
|
if (current.conceptId !== from.conceptId)
|
|
@@ -2170,13 +2915,13 @@ function successors(record, byId) {
|
|
|
2170
2915
|
}
|
|
2171
2916
|
}
|
|
2172
2917
|
const records = [];
|
|
2173
|
-
const
|
|
2918
|
+
const missing2 = [];
|
|
2174
2919
|
for (const id of ids) {
|
|
2175
2920
|
const found = byId.get(id);
|
|
2176
2921
|
if (found) records.push(found);
|
|
2177
|
-
else
|
|
2922
|
+
else missing2.push(id);
|
|
2178
2923
|
}
|
|
2179
|
-
return { records, missing };
|
|
2924
|
+
return { records, missing: missing2 };
|
|
2180
2925
|
}
|
|
2181
2926
|
|
|
2182
2927
|
// src/catalog.ts
|
|
@@ -2231,9 +2976,9 @@ var catalogCommand = define({
|
|
|
2231
2976
|
tool: "kb_catalog",
|
|
2232
2977
|
usage: "catalog [type]",
|
|
2233
2978
|
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.",
|
|
2234
|
-
input:
|
|
2979
|
+
input: import_zod10.z.object({
|
|
2235
2980
|
bundlePath,
|
|
2236
|
-
type:
|
|
2981
|
+
type: import_zod10.z.enum(KB_RECORD_TYPES).optional()
|
|
2237
2982
|
}),
|
|
2238
2983
|
fromArgv: (argv, path) => ({
|
|
2239
2984
|
bundlePath: path,
|
|
@@ -2288,10 +3033,10 @@ function count(value, noun) {
|
|
|
2288
3033
|
}
|
|
2289
3034
|
|
|
2290
3035
|
// src/commands/context.ts
|
|
2291
|
-
var
|
|
3036
|
+
var import_zod11 = require("zod");
|
|
2292
3037
|
|
|
2293
3038
|
// src/kb-context.ts
|
|
2294
|
-
var
|
|
3039
|
+
var import_promises6 = require("fs/promises");
|
|
2295
3040
|
|
|
2296
3041
|
// src/kb-index.ts
|
|
2297
3042
|
var INDEX_FILE = "INDEX.md";
|
|
@@ -2518,13 +3263,13 @@ function toHookJson(block, event) {
|
|
|
2518
3263
|
var CONTEXT_BEGIN = "<!-- strauss-kb:begin -->";
|
|
2519
3264
|
var CONTEXT_END = "<!-- strauss-kb:end -->";
|
|
2520
3265
|
async function syncInstructions(file, block) {
|
|
2521
|
-
const existing = await (0,
|
|
3266
|
+
const existing = await (0, import_promises6.readFile)(file, "utf8").catch(() => null);
|
|
2522
3267
|
const region = block ? `${CONTEXT_BEGIN}
|
|
2523
3268
|
${block.trim()}
|
|
2524
3269
|
${CONTEXT_END}` : null;
|
|
2525
3270
|
if (existing === null) {
|
|
2526
3271
|
if (!region) return { file, action: "unchanged" };
|
|
2527
|
-
await (0,
|
|
3272
|
+
await (0, import_promises6.writeFile)(file, `${region}
|
|
2528
3273
|
`, "utf8");
|
|
2529
3274
|
return { file, action: "created" };
|
|
2530
3275
|
}
|
|
@@ -2535,11 +3280,11 @@ ${CONTEXT_END}` : null;
|
|
|
2535
3280
|
const after = existing.slice(end + CONTEXT_END.length);
|
|
2536
3281
|
const next = region ? `${before}${region}${after}` : `${before.replace(/\n+$/, "\n")}${after.replace(/^\n+/, "\n")}`;
|
|
2537
3282
|
if (next === existing) return { file, action: "unchanged" };
|
|
2538
|
-
await (0,
|
|
3283
|
+
await (0, import_promises6.writeFile)(file, next, "utf8");
|
|
2539
3284
|
return { file, action: region ? "replaced" : "removed" };
|
|
2540
3285
|
}
|
|
2541
3286
|
if (!region) return { file, action: "unchanged" };
|
|
2542
|
-
await (0,
|
|
3287
|
+
await (0, import_promises6.writeFile)(
|
|
2543
3288
|
file,
|
|
2544
3289
|
`${existing.replace(/\n*$/, "\n\n")}${region}
|
|
2545
3290
|
`,
|
|
@@ -2554,20 +3299,20 @@ var contextCommand = define({
|
|
|
2554
3299
|
tool: "kb_context",
|
|
2555
3300
|
usage: "context [--profile NAME] [--budget N] [--full-under N] [--format json] [--event NAME]",
|
|
2556
3301
|
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.",
|
|
2557
|
-
input:
|
|
2558
|
-
budgetTokens:
|
|
3302
|
+
input: import_zod11.z.object({
|
|
3303
|
+
budgetTokens: import_zod11.z.number().int().positive().optional().describe(
|
|
2559
3304
|
"Ceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000."
|
|
2560
3305
|
),
|
|
2561
|
-
fullUnderTokens:
|
|
3306
|
+
fullUnderTokens: import_zod11.z.number().int().positive().optional().describe(
|
|
2562
3307
|
"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."
|
|
2563
3308
|
),
|
|
2564
|
-
profile:
|
|
3309
|
+
profile: import_zod11.z.string().optional().describe(
|
|
2565
3310
|
"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."
|
|
2566
3311
|
),
|
|
2567
|
-
format:
|
|
3312
|
+
format: import_zod11.z.enum(["markdown", "json"]).optional().describe(
|
|
2568
3313
|
"CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
|
|
2569
3314
|
),
|
|
2570
|
-
event:
|
|
3315
|
+
event: import_zod11.z.string().optional().describe(
|
|
2571
3316
|
"hookEventName stamped into the JSON envelope. Only meaningful with format=json."
|
|
2572
3317
|
)
|
|
2573
3318
|
}),
|
|
@@ -2603,7 +3348,406 @@ var contextCommand = define({
|
|
|
2603
3348
|
});
|
|
2604
3349
|
|
|
2605
3350
|
// src/commands/doctor.ts
|
|
2606
|
-
var
|
|
3351
|
+
var import_zod13 = require("zod");
|
|
3352
|
+
|
|
3353
|
+
// src/drift/git.ts
|
|
3354
|
+
var import_node_child_process3 = require("child_process");
|
|
3355
|
+
var import_node_util3 = require("util");
|
|
3356
|
+
var execFileAsync3 = (0, import_node_util3.promisify)(import_node_child_process3.execFile);
|
|
3357
|
+
var MAX_GIT_OUTPUT_BYTES = 1048576;
|
|
3358
|
+
var GIT_TIMEOUT_MS = 5e3;
|
|
3359
|
+
async function git2(cwd, args) {
|
|
3360
|
+
const env = { ...process.env };
|
|
3361
|
+
delete env["GIT_DIR"];
|
|
3362
|
+
delete env["GIT_WORK_TREE"];
|
|
3363
|
+
delete env["GIT_INDEX_FILE"];
|
|
3364
|
+
try {
|
|
3365
|
+
const { stdout } = await execFileAsync3("git", ["-C", cwd, ...args], {
|
|
3366
|
+
timeout: GIT_TIMEOUT_MS,
|
|
3367
|
+
maxBuffer: MAX_GIT_OUTPUT_BYTES,
|
|
3368
|
+
env
|
|
3369
|
+
});
|
|
3370
|
+
return { ok: true, stdout };
|
|
3371
|
+
} catch {
|
|
3372
|
+
return { ok: false };
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
async function listRepoFiles(repoRoot) {
|
|
3376
|
+
const result = await git2(repoRoot, ["ls-files", "-z", "--cached"]);
|
|
3377
|
+
if (!result.ok) return [];
|
|
3378
|
+
return result.stdout.split("\0").filter(Boolean);
|
|
3379
|
+
}
|
|
3380
|
+
async function readOldSource(repoRoot, anchor) {
|
|
3381
|
+
if (!filePathIsSafe(anchor.file))
|
|
3382
|
+
return { ok: false, reason: "unrecoverable" };
|
|
3383
|
+
if (anchor.ref && refShapeIsSafe(anchor.ref)) {
|
|
3384
|
+
const shown2 = await showFile(repoRoot, anchor.ref, anchor.file);
|
|
3385
|
+
if (shown2 !== null) {
|
|
3386
|
+
return {
|
|
3387
|
+
ok: true,
|
|
3388
|
+
source: shown2,
|
|
3389
|
+
origin: { kind: "ref", ref: anchor.ref }
|
|
3390
|
+
};
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
const at2 = anchor.resolved_at;
|
|
3394
|
+
if (!at2 || Number.isNaN(Date.parse(at2))) {
|
|
3395
|
+
return { ok: false, reason: "unrecoverable" };
|
|
3396
|
+
}
|
|
3397
|
+
const found = await git2(repoRoot, [
|
|
3398
|
+
"log",
|
|
3399
|
+
"-1",
|
|
3400
|
+
"--format=%H",
|
|
3401
|
+
`--before=${at2}`,
|
|
3402
|
+
"--end-of-options",
|
|
3403
|
+
"HEAD",
|
|
3404
|
+
"--",
|
|
3405
|
+
anchor.file
|
|
3406
|
+
]);
|
|
3407
|
+
const sha = found.ok ? found.stdout.trim() : "";
|
|
3408
|
+
if (!sha || !refShapeIsSafe(sha))
|
|
3409
|
+
return { ok: false, reason: "unrecoverable" };
|
|
3410
|
+
const shown = await showFile(repoRoot, sha, anchor.file);
|
|
3411
|
+
if (shown === null) return { ok: false, reason: "unrecoverable" };
|
|
3412
|
+
return { ok: true, source: shown, origin: { kind: "history", ref: sha } };
|
|
3413
|
+
}
|
|
3414
|
+
async function showFile(repoRoot, ref, file) {
|
|
3415
|
+
const path = file.replace(/^\.\//, "");
|
|
3416
|
+
const result = await git2(repoRoot, [
|
|
3417
|
+
"show",
|
|
3418
|
+
"--end-of-options",
|
|
3419
|
+
`${ref}:${path}`
|
|
3420
|
+
]);
|
|
3421
|
+
return result.ok ? result.stdout : null;
|
|
3422
|
+
}
|
|
3423
|
+
|
|
3424
|
+
// src/drift/moved.ts
|
|
3425
|
+
var import_promises7 = require("fs/promises");
|
|
3426
|
+
var MAX_MOVED_SEARCH_FILES = 2e3;
|
|
3427
|
+
var SEARCH_BATCH = 64;
|
|
3428
|
+
function movedSearch(repoRoot, options = {}) {
|
|
3429
|
+
const read = options.reader ?? anchorFileReader(repoRoot);
|
|
3430
|
+
const sizeOf = options.sizeOf ?? diskSize(repoRoot);
|
|
3431
|
+
const resolver = new TreeSitterResolver();
|
|
3432
|
+
let repoFiles;
|
|
3433
|
+
const prepared = /* @__PURE__ */ new Set();
|
|
3434
|
+
const filesForLanguage = async (language) => {
|
|
3435
|
+
repoFiles ??= listRepoFiles(repoRoot);
|
|
3436
|
+
return (await repoFiles).filter((file) => languageForFile(file) === language).slice(0, MAX_MOVED_SEARCH_FILES);
|
|
3437
|
+
};
|
|
3438
|
+
return {
|
|
3439
|
+
async find(anchor) {
|
|
3440
|
+
const stored = anchor.hash;
|
|
3441
|
+
if (!stored) return void 0;
|
|
3442
|
+
const language = languageForFile(anchor.file);
|
|
3443
|
+
if (!language) return sameFileWindow(anchor, read, stored);
|
|
3444
|
+
const candidates = await filesForLanguage(language);
|
|
3445
|
+
if (!prepared.has(language)) {
|
|
3446
|
+
await resolver.prepare(candidates.length ? candidates : [anchor.file]);
|
|
3447
|
+
prepared.add(language);
|
|
3448
|
+
}
|
|
3449
|
+
const floor = anchor.lines ?? 0;
|
|
3450
|
+
for (let at2 = 0; at2 < candidates.length; at2 += SEARCH_BATCH) {
|
|
3451
|
+
const batch = candidates.slice(at2, at2 + SEARCH_BATCH);
|
|
3452
|
+
const hits = await mapLimit(
|
|
3453
|
+
batch,
|
|
3454
|
+
DEFAULT_IO_CONCURRENCY,
|
|
3455
|
+
async (file) => {
|
|
3456
|
+
const size2 = await sizeOf(file);
|
|
3457
|
+
if (size2 !== null && size2 < floor) return void 0;
|
|
3458
|
+
return matchIn(resolver, read, anchor, stored, file);
|
|
3459
|
+
}
|
|
3460
|
+
);
|
|
3461
|
+
const found = hits.find((hit) => hit !== void 0);
|
|
3462
|
+
if (found) return found;
|
|
3463
|
+
}
|
|
3464
|
+
return void 0;
|
|
3465
|
+
}
|
|
3466
|
+
};
|
|
3467
|
+
}
|
|
3468
|
+
async function matchIn(resolver, read, anchor, stored, file) {
|
|
3469
|
+
const source = await read(file);
|
|
3470
|
+
if (!source.ok) return void 0;
|
|
3471
|
+
const normalized = source.source.replace(/\r\n/g, "\n");
|
|
3472
|
+
for (const found of resolver.spans(normalized, file)) {
|
|
3473
|
+
const text = anchor.hash_kind === "ast" ? resolver.normalize(found.span.text, file) : found.span.text;
|
|
3474
|
+
if (text === null || hashAnchorText(text) !== stored) continue;
|
|
3475
|
+
if (file === anchor.file && found.symbol === anchor.symbol) continue;
|
|
3476
|
+
return {
|
|
3477
|
+
file,
|
|
3478
|
+
symbol: found.symbol,
|
|
3479
|
+
startLine: found.span.startLine,
|
|
3480
|
+
endLine: found.span.endLine
|
|
3481
|
+
};
|
|
3482
|
+
}
|
|
3483
|
+
return void 0;
|
|
3484
|
+
}
|
|
3485
|
+
function diskSize(repoRoot) {
|
|
3486
|
+
return async (file) => {
|
|
3487
|
+
const path = anchorFilePath(repoRoot, file);
|
|
3488
|
+
if (path === null) return null;
|
|
3489
|
+
try {
|
|
3490
|
+
return (await (0, import_promises7.stat)(path)).size;
|
|
3491
|
+
} catch {
|
|
3492
|
+
return null;
|
|
3493
|
+
}
|
|
3494
|
+
};
|
|
3495
|
+
}
|
|
3496
|
+
async function sameFileWindow(anchor, read, stored) {
|
|
3497
|
+
const height = anchor.lines;
|
|
3498
|
+
if (!height || anchor.hash_kind === "ast") return void 0;
|
|
3499
|
+
const source = await read(anchor.file);
|
|
3500
|
+
if (!source.ok) return void 0;
|
|
3501
|
+
const lines = source.source.replace(/\r\n/g, "\n").split("\n");
|
|
3502
|
+
for (let at2 = 0; at2 + height <= lines.length; at2++) {
|
|
3503
|
+
if (hashAnchorText(lines.slice(at2, at2 + height).join("\n")) !== stored) {
|
|
3504
|
+
continue;
|
|
3505
|
+
}
|
|
3506
|
+
return {
|
|
3507
|
+
file: anchor.file,
|
|
3508
|
+
...anchor.symbol ? { symbol: anchor.symbol } : {},
|
|
3509
|
+
startLine: at2 + 1,
|
|
3510
|
+
endLine: at2 + height
|
|
3511
|
+
};
|
|
3512
|
+
}
|
|
3513
|
+
return void 0;
|
|
3514
|
+
}
|
|
3515
|
+
|
|
3516
|
+
// src/drift/classify.ts
|
|
3517
|
+
async function classifyDrift(repoRoot, record, entries, options = {}) {
|
|
3518
|
+
const anchors = (record.frontmatter.strauss_anchors ?? []).filter(
|
|
3519
|
+
(anchor) => anchor.hash
|
|
3520
|
+
);
|
|
3521
|
+
const reader = options.reader ?? anchorFileReader(repoRoot);
|
|
3522
|
+
const treeSitter = new TreeSitterResolver();
|
|
3523
|
+
const resolvers = [treeSitter, regexResolver];
|
|
3524
|
+
const search = options.search ?? movedSearch(repoRoot, { ...options.reader ? { reader } : {} });
|
|
3525
|
+
const wanted = [];
|
|
3526
|
+
entries.forEach((entry, at2) => {
|
|
3527
|
+
const anchor = anchors[at2];
|
|
3528
|
+
if (!anchor) return;
|
|
3529
|
+
if (entry.state === "match" || isUncheckedReason(entry.reason)) return;
|
|
3530
|
+
wanted.push({ anchor, entry });
|
|
3531
|
+
});
|
|
3532
|
+
if (!wanted.length) return [];
|
|
3533
|
+
await prepareResolvers(
|
|
3534
|
+
resolvers,
|
|
3535
|
+
wanted.map(({ anchor }) => anchor.file)
|
|
3536
|
+
);
|
|
3537
|
+
const out = [];
|
|
3538
|
+
for (const { anchor, entry } of wanted) {
|
|
3539
|
+
const movedTo = await search.find(anchor);
|
|
3540
|
+
if (movedTo) {
|
|
3541
|
+
out.push({
|
|
3542
|
+
anchor,
|
|
3543
|
+
entry: { ...entry, class: "moved", movedTo },
|
|
3544
|
+
class: "moved"
|
|
3545
|
+
});
|
|
3546
|
+
continue;
|
|
3547
|
+
}
|
|
3548
|
+
const newText = await currentText(reader, anchor, resolvers);
|
|
3549
|
+
const old = options.withHistory === false ? { ok: false, reason: "unrecoverable" } : await readOldSource(repoRoot, anchor);
|
|
3550
|
+
const oldText = old.ok ? spanIn(old.source, anchor, resolvers) : void 0;
|
|
3551
|
+
const settled = newText !== void 0 && oldText !== void 0 && sameTokens(treeSitter, anchor.file, oldText, newText) ? "cosmetic" : entry.class ?? "changed";
|
|
3552
|
+
out.push({
|
|
3553
|
+
anchor,
|
|
3554
|
+
entry: { ...entry, class: settled },
|
|
3555
|
+
class: settled,
|
|
3556
|
+
...newText !== void 0 ? { newText } : {},
|
|
3557
|
+
...oldText !== void 0 ? { oldText } : {},
|
|
3558
|
+
...old.ok ? { oldOrigin: old.origin } : {}
|
|
3559
|
+
});
|
|
3560
|
+
}
|
|
3561
|
+
return out;
|
|
3562
|
+
}
|
|
3563
|
+
function sameTokens(resolver, file, before, after) {
|
|
3564
|
+
if (before === after) return false;
|
|
3565
|
+
const left = resolver.normalize(before, file);
|
|
3566
|
+
const right = resolver.normalize(after, file);
|
|
3567
|
+
return left !== null && left === right;
|
|
3568
|
+
}
|
|
3569
|
+
async function currentText(reader, anchor, resolvers) {
|
|
3570
|
+
const read = await reader(anchor.file);
|
|
3571
|
+
if (!read.ok) return void 0;
|
|
3572
|
+
return spanIn(read.source, anchor, resolvers);
|
|
3573
|
+
}
|
|
3574
|
+
function spanIn(source, anchor, resolvers) {
|
|
3575
|
+
const outcome = resolveAnchorSpan(source, anchor, resolvers);
|
|
3576
|
+
return outcome.ok ? outcome.span.text : void 0;
|
|
3577
|
+
}
|
|
3578
|
+
|
|
3579
|
+
// src/drift/diff.ts
|
|
3580
|
+
var MAX_ANCHOR_DIFF_LINES = 200;
|
|
3581
|
+
var PACKET_DIFF_LINE_BUDGET = 200;
|
|
3582
|
+
var MIN_ANCHOR_DIFF_LINES = 12;
|
|
3583
|
+
function diffBudget(anchors) {
|
|
3584
|
+
if (anchors <= 0) return MAX_ANCHOR_DIFF_LINES;
|
|
3585
|
+
return Math.min(
|
|
3586
|
+
MAX_ANCHOR_DIFF_LINES,
|
|
3587
|
+
Math.max(
|
|
3588
|
+
MIN_ANCHOR_DIFF_LINES,
|
|
3589
|
+
Math.floor(PACKET_DIFF_LINE_BUDGET / anchors)
|
|
3590
|
+
)
|
|
3591
|
+
);
|
|
3592
|
+
}
|
|
3593
|
+
function unifiedDiff(before, after, options = {}) {
|
|
3594
|
+
const max = options.maxLines ?? MAX_ANCHOR_DIFF_LINES;
|
|
3595
|
+
const left = before.replace(/\r\n/g, "\n").split("\n");
|
|
3596
|
+
const right = after.replace(/\r\n/g, "\n").split("\n");
|
|
3597
|
+
const body = [];
|
|
3598
|
+
let added = 0;
|
|
3599
|
+
let removed = 0;
|
|
3600
|
+
for (const edit of edits(left, right)) {
|
|
3601
|
+
if (edit.kind === "same") body.push(` ${edit.line}`);
|
|
3602
|
+
else if (edit.kind === "remove") {
|
|
3603
|
+
body.push(`-${edit.line}`);
|
|
3604
|
+
removed += 1;
|
|
3605
|
+
} else {
|
|
3606
|
+
body.push(`+${edit.line}`);
|
|
3607
|
+
added += 1;
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
const truncated = body.length > max;
|
|
3611
|
+
const shown = truncated ? body.slice(0, max) : body;
|
|
3612
|
+
const header = `@@ -1,${left.length} +1,${right.length} @@${options.oldLabel ? ` ${options.oldLabel} \u2192 ${options.newLabel ?? ""}`.trimEnd() : ""}`;
|
|
3613
|
+
const lines = [header, ...shown];
|
|
3614
|
+
if (truncated) lines.push(`\u2026 ${body.length - max} more diff lines`);
|
|
3615
|
+
return { text: lines.join("\n"), added, removed, truncated };
|
|
3616
|
+
}
|
|
3617
|
+
function edits(left, right) {
|
|
3618
|
+
const rows = left.length;
|
|
3619
|
+
const cols = right.length;
|
|
3620
|
+
const table2 = Array.from(
|
|
3621
|
+
{ length: rows + 1 },
|
|
3622
|
+
() => new Array(cols + 1).fill(0)
|
|
3623
|
+
);
|
|
3624
|
+
for (let row2 = rows - 1; row2 >= 0; row2--) {
|
|
3625
|
+
for (let col2 = cols - 1; col2 >= 0; col2--) {
|
|
3626
|
+
table2[row2][col2] = left[row2] === right[col2] ? table2[row2 + 1][col2 + 1] + 1 : Math.max(
|
|
3627
|
+
table2[row2 + 1][col2],
|
|
3628
|
+
table2[row2][col2 + 1]
|
|
3629
|
+
);
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
const out = [];
|
|
3633
|
+
let row = 0;
|
|
3634
|
+
let col = 0;
|
|
3635
|
+
while (row < rows && col < cols) {
|
|
3636
|
+
if (left[row] === right[col]) {
|
|
3637
|
+
out.push({ kind: "same", line: left[row] });
|
|
3638
|
+
row += 1;
|
|
3639
|
+
col += 1;
|
|
3640
|
+
} else if (table2[row + 1][col] >= table2[row][col + 1]) {
|
|
3641
|
+
out.push({ kind: "remove", line: left[row] });
|
|
3642
|
+
row += 1;
|
|
3643
|
+
} else {
|
|
3644
|
+
out.push({ kind: "add", line: right[col] });
|
|
3645
|
+
col += 1;
|
|
3646
|
+
}
|
|
3647
|
+
}
|
|
3648
|
+
for (; row < rows; row++)
|
|
3649
|
+
out.push({ kind: "remove", line: left[row] });
|
|
3650
|
+
for (; col < cols; col++)
|
|
3651
|
+
out.push({ kind: "add", line: right[col] });
|
|
3652
|
+
return out;
|
|
3653
|
+
}
|
|
3654
|
+
|
|
3655
|
+
// src/drift/packet.ts
|
|
3656
|
+
var PRESUMED_INVALID = [
|
|
3657
|
+
"fact",
|
|
3658
|
+
"constraint",
|
|
3659
|
+
"contract"
|
|
3660
|
+
];
|
|
3661
|
+
var RATIONALE_SURVIVES = ["decision", "risk"];
|
|
3662
|
+
var DEFAULT_NOTES = {
|
|
3663
|
+
"presumed-invalidated": "the code this claim was taken from changed; presume it no longer holds until re-read",
|
|
3664
|
+
"rationale-may-survive": "the reasoning may outlive the code that implemented it; check whether it does",
|
|
3665
|
+
review: "re-read the record against the new code"
|
|
3666
|
+
};
|
|
3667
|
+
async function reassessPacket(repoRoot, record, entries, options = {}) {
|
|
3668
|
+
const classified = await classifyDrift(repoRoot, record, entries, {
|
|
3669
|
+
...options.reader ? { reader: options.reader } : {},
|
|
3670
|
+
...options.search ? { search: options.search } : {},
|
|
3671
|
+
withHistory: options.withDiff !== false
|
|
3672
|
+
});
|
|
3673
|
+
const open = classified.filter(
|
|
3674
|
+
(found) => found.class === "changed" || found.class === "gone"
|
|
3675
|
+
);
|
|
3676
|
+
if (!open.length) return { packet: null, classified };
|
|
3677
|
+
const budget = diffBudget(open.length);
|
|
3678
|
+
const anchors = open.map(
|
|
3679
|
+
(found) => anchorPacket(found, options.withDiff === true, budget)
|
|
3680
|
+
);
|
|
3681
|
+
const type = record.frontmatter.type;
|
|
3682
|
+
const fallback = isKbRecordType(type) ? PRESUMED_INVALID.includes(type) ? "presumed-invalidated" : RATIONALE_SURVIVES.includes(type) ? "rationale-may-survive" : "review" : "review";
|
|
3683
|
+
return {
|
|
3684
|
+
classified,
|
|
3685
|
+
packet: {
|
|
3686
|
+
conceptId: record.conceptId,
|
|
3687
|
+
title: record.frontmatter.title ?? null,
|
|
3688
|
+
type,
|
|
3689
|
+
standing: options.standing ?? "unsettled",
|
|
3690
|
+
why: record.frontmatter.description ?? null,
|
|
3691
|
+
claim: claimOf(record),
|
|
3692
|
+
anchors,
|
|
3693
|
+
impact: (options.impact?.impacted ?? []).map((entry) => ({
|
|
3694
|
+
conceptId: entry.conceptId,
|
|
3695
|
+
title: entry.title,
|
|
3696
|
+
standing: entry.standing,
|
|
3697
|
+
depth: entry.depth
|
|
3698
|
+
})),
|
|
3699
|
+
impactTruncated: options.impact?.truncated ?? false,
|
|
3700
|
+
default: fallback,
|
|
3701
|
+
defaultNote: DEFAULT_NOTES[fallback]
|
|
3702
|
+
}
|
|
3703
|
+
};
|
|
3704
|
+
}
|
|
3705
|
+
function anchorPacket(found, withDiff, maxLines) {
|
|
3706
|
+
const { entry } = found;
|
|
3707
|
+
const base2 = {
|
|
3708
|
+
file: entry.file,
|
|
3709
|
+
...entry.symbol ? { symbol: entry.symbol } : {},
|
|
3710
|
+
class: found.class,
|
|
3711
|
+
...entry.reason ? { reason: entry.reason } : {},
|
|
3712
|
+
storedHash: entry.storedHash,
|
|
3713
|
+
...entry.currentHash ? { currentHash: entry.currentHash } : {},
|
|
3714
|
+
diffSize: entry.diffSize,
|
|
3715
|
+
...entry.movedTo ? { movedTo: entry.movedTo } : {}
|
|
3716
|
+
};
|
|
3717
|
+
if (!withDiff) return base2;
|
|
3718
|
+
if (found.oldText === void 0 || !found.oldOrigin) {
|
|
3719
|
+
return { ...base2, diff: { status: "unrecoverable" } };
|
|
3720
|
+
}
|
|
3721
|
+
const rendered = unifiedDiff(found.oldText, found.newText ?? "", {
|
|
3722
|
+
maxLines
|
|
3723
|
+
});
|
|
3724
|
+
return {
|
|
3725
|
+
...base2,
|
|
3726
|
+
diff: {
|
|
3727
|
+
status: "ok",
|
|
3728
|
+
source: found.oldOrigin.kind,
|
|
3729
|
+
ref: found.oldOrigin.ref,
|
|
3730
|
+
unified: rendered.text,
|
|
3731
|
+
added: rendered.added,
|
|
3732
|
+
removed: rendered.removed,
|
|
3733
|
+
truncated: rendered.truncated
|
|
3734
|
+
}
|
|
3735
|
+
};
|
|
3736
|
+
}
|
|
3737
|
+
function claimOf(record) {
|
|
3738
|
+
const type = record.frontmatter.type;
|
|
3739
|
+
const section = isKbRecordType(type) ? RECORD_TYPES[type].sections[0] : void 0;
|
|
3740
|
+
if (!section) return null;
|
|
3741
|
+
const lines = record.body.replace(/\r\n/g, "\n").split("\n");
|
|
3742
|
+
const start = lines.findIndex(
|
|
3743
|
+
(line) => line.trim().toLowerCase() === `## ${section}`.toLowerCase()
|
|
3744
|
+
);
|
|
3745
|
+
if (start < 0) return null;
|
|
3746
|
+
const rest = lines.slice(start + 1);
|
|
3747
|
+
const end = rest.findIndex((line) => line.startsWith("## "));
|
|
3748
|
+
const text = (end < 0 ? rest : rest.slice(0, end)).join("\n").trim();
|
|
3749
|
+
return text ? { section, text } : null;
|
|
3750
|
+
}
|
|
2607
3751
|
|
|
2608
3752
|
// src/kb-edges.ts
|
|
2609
3753
|
var KB_EDGE_KINDS = [
|
|
@@ -2789,6 +3933,18 @@ var CHECK_HEADLINES = {
|
|
|
2789
3933
|
unchecked: "an anchor in another repository nothing could reach"
|
|
2790
3934
|
};
|
|
2791
3935
|
var DAY_MS = 864e5;
|
|
3936
|
+
function anchorResolverCounts(bundle) {
|
|
3937
|
+
let treeSitter = 0;
|
|
3938
|
+
let regex = 0;
|
|
3939
|
+
for (const record of bundle) {
|
|
3940
|
+
for (const anchor of record.frontmatter.strauss_anchors ?? []) {
|
|
3941
|
+
if (!anchor.hash || !anchor.symbol) continue;
|
|
3942
|
+
if (anchor.resolver === "tree-sitter") treeSitter += 1;
|
|
3943
|
+
else regex += 1;
|
|
3944
|
+
}
|
|
3945
|
+
}
|
|
3946
|
+
return { total: treeSitter + regex, treeSitter, regex };
|
|
3947
|
+
}
|
|
2792
3948
|
function doctor(bundle, options = {}) {
|
|
2793
3949
|
const thresholds = {
|
|
2794
3950
|
expiringDays: options.expiringDays ?? DEFAULT_EXPIRING_DAYS,
|
|
@@ -2824,6 +3980,7 @@ function doctor(bundle, options = {}) {
|
|
|
2824
3980
|
counts,
|
|
2825
3981
|
groups,
|
|
2826
3982
|
findingCount,
|
|
3983
|
+
anchorResolvers: anchorResolverCounts(bundle),
|
|
2827
3984
|
healthy: findingCount === 0
|
|
2828
3985
|
};
|
|
2829
3986
|
}
|
|
@@ -2840,18 +3997,18 @@ function expired(hits, now) {
|
|
|
2840
3997
|
for (const hit of hits) {
|
|
2841
3998
|
const raw = hit.record.frontmatter.stale_after;
|
|
2842
3999
|
if (!raw) continue;
|
|
2843
|
-
const
|
|
2844
|
-
if (Number.isNaN(
|
|
4000
|
+
const at2 = Date.parse(raw);
|
|
4001
|
+
if (Number.isNaN(at2)) {
|
|
2845
4002
|
findings.push(
|
|
2846
4003
|
finding(hit.record, `stale_after "${raw}" is not a readable date`)
|
|
2847
4004
|
);
|
|
2848
4005
|
continue;
|
|
2849
4006
|
}
|
|
2850
|
-
if (
|
|
4007
|
+
if (at2 < now.getTime()) {
|
|
2851
4008
|
findings.push(
|
|
2852
4009
|
finding(
|
|
2853
4010
|
hit.record,
|
|
2854
|
-
`stale since ${raw} (${daysBetween(
|
|
4011
|
+
`stale since ${raw} (${daysBetween(at2, now.getTime())} days ago)`
|
|
2855
4012
|
)
|
|
2856
4013
|
);
|
|
2857
4014
|
}
|
|
@@ -2864,12 +4021,12 @@ function expiring(hits, now, withinDays) {
|
|
|
2864
4021
|
for (const hit of hits) {
|
|
2865
4022
|
const raw = hit.record.frontmatter.stale_after;
|
|
2866
4023
|
if (!raw) continue;
|
|
2867
|
-
const
|
|
2868
|
-
if (Number.isNaN(
|
|
4024
|
+
const at2 = Date.parse(raw);
|
|
4025
|
+
if (Number.isNaN(at2) || at2 < now.getTime() || at2 > horizon) continue;
|
|
2869
4026
|
findings.push(
|
|
2870
4027
|
finding(
|
|
2871
4028
|
hit.record,
|
|
2872
|
-
`goes stale ${raw} (in ${daysBetween(now.getTime(),
|
|
4029
|
+
`goes stale ${raw} (in ${daysBetween(now.getTime(), at2)} days)`
|
|
2873
4030
|
)
|
|
2874
4031
|
);
|
|
2875
4032
|
}
|
|
@@ -3039,13 +4196,16 @@ function anchorFindings(hits, kind, headline) {
|
|
|
3039
4196
|
);
|
|
3040
4197
|
}
|
|
3041
4198
|
function describeAnchor(anchor) {
|
|
3042
|
-
const
|
|
3043
|
-
if (anchor.
|
|
4199
|
+
const at2 = anchor.symbol ? `${anchor.file}:${anchor.symbol}` : anchor.file;
|
|
4200
|
+
if (anchor.class === "gone") {
|
|
4201
|
+
return `${at2} gone${anchor.reason ? ` (${anchor.reason})` : ""}`;
|
|
4202
|
+
}
|
|
4203
|
+
if (anchor.reason) return `${at2} (${anchor.reason})`;
|
|
3044
4204
|
if (anchor.remoteState === "drifted-on-default") {
|
|
3045
|
-
return `${
|
|
4205
|
+
return `${at2} (matches ref, moved on the default branch)`;
|
|
3046
4206
|
}
|
|
3047
|
-
if (anchor.diffSize === null) return `${
|
|
3048
|
-
return anchor.diffSize === 0 ? `${
|
|
4207
|
+
if (anchor.diffSize === null) return `${at2} (changed, size unrecorded)`;
|
|
4208
|
+
return anchor.diffSize === 0 ? `${at2} (content changed, same line count)` : `${at2} (${anchor.diffSize} line${anchor.diffSize === 1 ? "" : "s"} apart)`;
|
|
3049
4209
|
}
|
|
3050
4210
|
function replaces(later, earlier) {
|
|
3051
4211
|
return (later.frontmatter.strauss_supersedes ?? []).includes(earlier.conceptId) || earlier.frontmatter.strauss_superseded_by === later.conceptId;
|
|
@@ -3062,21 +4222,173 @@ function daysBetween(from, to) {
|
|
|
3062
4222
|
return Math.max(0, Math.floor((to - from) / DAY_MS));
|
|
3063
4223
|
}
|
|
3064
4224
|
function ageInDays(record, now) {
|
|
3065
|
-
const
|
|
3066
|
-
if (!
|
|
3067
|
-
const written = Date.parse(
|
|
4225
|
+
const at2 = record.frontmatter.generated?.at;
|
|
4226
|
+
if (!at2) return null;
|
|
4227
|
+
const written = Date.parse(at2);
|
|
3068
4228
|
if (Number.isNaN(written)) return null;
|
|
3069
4229
|
return daysBetween(written, now.getTime());
|
|
3070
4230
|
}
|
|
3071
4231
|
|
|
4232
|
+
// src/commands/reassess.ts
|
|
4233
|
+
var import_zod12 = require("zod");
|
|
4234
|
+
var reassessCommand = define({
|
|
4235
|
+
name: "reassess",
|
|
4236
|
+
tool: "kb_reassess",
|
|
4237
|
+
usage: "reassess <concept-id> [--repo-root <path>] [--with-diff]",
|
|
4238
|
+
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.",
|
|
4239
|
+
input: import_zod12.z.object({
|
|
4240
|
+
bundlePath,
|
|
4241
|
+
conceptId,
|
|
4242
|
+
repoRoot: REPO_ROOT,
|
|
4243
|
+
withDiff: import_zod12.z.boolean().optional().describe(
|
|
4244
|
+
"Recover each anchor's committed span and render the diff. Reads git history."
|
|
4245
|
+
)
|
|
4246
|
+
}),
|
|
4247
|
+
fromArgv: (argv, path) => {
|
|
4248
|
+
const repoRoot = argvFlag(argv, "--repo-root");
|
|
4249
|
+
return {
|
|
4250
|
+
bundlePath: path,
|
|
4251
|
+
conceptId: argv[1],
|
|
4252
|
+
...repoRoot !== void 0 ? { repoRoot } : {},
|
|
4253
|
+
...argv.includes("--with-diff") ? { withDiff: true } : {}
|
|
4254
|
+
};
|
|
4255
|
+
},
|
|
4256
|
+
run: async ({ store, actor }, { bundlePath: path, conceptId: id, repoRoot, withDiff }) => {
|
|
4257
|
+
const root = repoRoot ?? process.cwd();
|
|
4258
|
+
const bundle = await store.list(path);
|
|
4259
|
+
const record = bundle.find((entry) => entry.conceptId === id);
|
|
4260
|
+
if (!record) throw new KbRecordNotFoundError(id);
|
|
4261
|
+
const drift = await store.detectDrift([record], repoRoot);
|
|
4262
|
+
const entries = drift?.get(id) ?? [];
|
|
4263
|
+
if (!entries.some((entry) => entry.state !== "match")) {
|
|
4264
|
+
return { conceptId: id, packet: null, rebaselined: [], cosmetic: 0 };
|
|
4265
|
+
}
|
|
4266
|
+
const standing = adjudicate(bundle, bundle).find(
|
|
4267
|
+
(hit) => hit.record.conceptId === id
|
|
4268
|
+
)?.standing;
|
|
4269
|
+
const impact2 = await store.impact(path, id);
|
|
4270
|
+
const { packet, classified } = await reassessPacket(root, record, entries, {
|
|
4271
|
+
...withDiff ? { withDiff: true } : {},
|
|
4272
|
+
impact: impact2,
|
|
4273
|
+
...standing ? { standing } : {}
|
|
4274
|
+
});
|
|
4275
|
+
const moves = classified.filter((found) => found.class === "moved");
|
|
4276
|
+
let frozen = false;
|
|
4277
|
+
const rebaselined = [];
|
|
4278
|
+
if (moves.length) {
|
|
4279
|
+
const relocated = /* @__PURE__ */ new Map();
|
|
4280
|
+
for (const found of moves) {
|
|
4281
|
+
const to = found.entry.movedTo;
|
|
4282
|
+
if (!to) continue;
|
|
4283
|
+
relocated.set(found.anchor, {
|
|
4284
|
+
...found.anchor,
|
|
4285
|
+
file: to.file,
|
|
4286
|
+
...to.symbol ? { symbol: to.symbol } : {}
|
|
4287
|
+
});
|
|
4288
|
+
rebaselined.push({
|
|
4289
|
+
file: found.anchor.file,
|
|
4290
|
+
...found.anchor.symbol ? { symbol: found.anchor.symbol } : {},
|
|
4291
|
+
toFile: to.file,
|
|
4292
|
+
...to.symbol ? { toSymbol: to.symbol } : {}
|
|
4293
|
+
});
|
|
4294
|
+
}
|
|
4295
|
+
try {
|
|
4296
|
+
await assertBaseNotFrozen(process.cwd(), path);
|
|
4297
|
+
} catch (error) {
|
|
4298
|
+
if (!(error instanceof KbBaseFrozenError)) throw error;
|
|
4299
|
+
frozen = true;
|
|
4300
|
+
}
|
|
4301
|
+
if (!frozen) {
|
|
4302
|
+
await store.updateAnchors(
|
|
4303
|
+
path,
|
|
4304
|
+
id,
|
|
4305
|
+
(record.frontmatter.strauss_anchors ?? []).map(
|
|
4306
|
+
(anchor) => relocated.get(anchor) ?? anchor
|
|
4307
|
+
),
|
|
4308
|
+
actor
|
|
4309
|
+
);
|
|
4310
|
+
}
|
|
4311
|
+
}
|
|
4312
|
+
return {
|
|
4313
|
+
conceptId: id,
|
|
4314
|
+
packet,
|
|
4315
|
+
rebaselined: frozen ? [] : rebaselined,
|
|
4316
|
+
cosmetic: classified.filter((found) => found.class === "cosmetic").length,
|
|
4317
|
+
...frozen ? {
|
|
4318
|
+
frozen: true,
|
|
4319
|
+
note: "base is frozen: nothing was rebaselined"
|
|
4320
|
+
} : {}
|
|
4321
|
+
};
|
|
4322
|
+
},
|
|
4323
|
+
render: (result) => renderReassess(result)
|
|
4324
|
+
});
|
|
4325
|
+
function renderReassess(result) {
|
|
4326
|
+
const lines = [];
|
|
4327
|
+
for (const move of result.rebaselined) {
|
|
4328
|
+
lines.push(
|
|
4329
|
+
`rebaselined: ${at(move.file, move.symbol)} \u2192 ${at(move.toFile, move.toSymbol)} (same code, new address)`
|
|
4330
|
+
);
|
|
4331
|
+
}
|
|
4332
|
+
if (result.cosmetic) {
|
|
4333
|
+
lines.push(
|
|
4334
|
+
`${result.cosmetic} anchor${result.cosmetic === 1 ? "" : "s"} changed formatting only.`
|
|
4335
|
+
);
|
|
4336
|
+
}
|
|
4337
|
+
if (result.note) lines.push(result.note);
|
|
4338
|
+
const packet = result.packet;
|
|
4339
|
+
if (!packet) {
|
|
4340
|
+
lines.push(`${result.conceptId}: nothing to reassess.`);
|
|
4341
|
+
return lines.join("\n");
|
|
4342
|
+
}
|
|
4343
|
+
lines.push(
|
|
4344
|
+
"",
|
|
4345
|
+
`# ${packet.conceptId}${packet.title ? ` \u2014 ${packet.title}` : ""}`,
|
|
4346
|
+
`type: ${packet.type} standing: ${packet.standing}`,
|
|
4347
|
+
...packet.why ? [`why: ${packet.why}`] : [],
|
|
4348
|
+
...packet.claim ? ["", `## ${packet.claim.section}`, packet.claim.text] : [],
|
|
4349
|
+
"",
|
|
4350
|
+
`## Anchors (${packet.anchors.length})`
|
|
4351
|
+
);
|
|
4352
|
+
for (const anchor of packet.anchors) {
|
|
4353
|
+
lines.push(
|
|
4354
|
+
`- ${at(anchor.file, anchor.symbol)} \u2014 ${anchor.class}${anchor.reason ? ` (${anchor.reason})` : ""}`
|
|
4355
|
+
);
|
|
4356
|
+
if (!anchor.diff) continue;
|
|
4357
|
+
if (anchor.diff.status === "unrecoverable") {
|
|
4358
|
+
lines.push(
|
|
4359
|
+
" diff: unrecoverable \u2014 no committed span to compare against"
|
|
4360
|
+
);
|
|
4361
|
+
continue;
|
|
4362
|
+
}
|
|
4363
|
+
lines.push(
|
|
4364
|
+
` diff vs ${anchor.diff.ref} (${anchor.diff.source}): +${anchor.diff.added} \u2212${anchor.diff.removed}`,
|
|
4365
|
+
...anchor.diff.unified.split("\n").map((line) => ` ${line}`)
|
|
4366
|
+
);
|
|
4367
|
+
}
|
|
4368
|
+
if (packet.impact.length) {
|
|
4369
|
+
lines.push("", `## Impact (${packet.impact.length})`);
|
|
4370
|
+
for (const entry of packet.impact) {
|
|
4371
|
+
lines.push(
|
|
4372
|
+
`- ${entry.conceptId} [${entry.standing}]${entry.title ? ` \u2014 ${entry.title}` : ""}`
|
|
4373
|
+
);
|
|
4374
|
+
}
|
|
4375
|
+
if (packet.impactTruncated) lines.push("- \u2026 walk truncated");
|
|
4376
|
+
}
|
|
4377
|
+
lines.push("", `Default: ${packet.default} \u2014 ${packet.defaultNote}.`);
|
|
4378
|
+
return lines.join("\n");
|
|
4379
|
+
}
|
|
4380
|
+
function at(file, symbol) {
|
|
4381
|
+
return symbol ? `${file}:${symbol}` : file;
|
|
4382
|
+
}
|
|
4383
|
+
|
|
3072
4384
|
// src/commands/doctor.ts
|
|
3073
|
-
var days = (what, fallback) =>
|
|
4385
|
+
var days = (what, fallback) => import_zod13.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
|
|
3074
4386
|
var doctorCommand = define({
|
|
3075
4387
|
name: "doctor",
|
|
3076
4388
|
tool: "kb_doctor",
|
|
3077
|
-
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--offline] [--strict]",
|
|
3078
|
-
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.
|
|
3079
|
-
input:
|
|
4389
|
+
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--offline] [--strict] [--drifted [--with-diff]]",
|
|
4390
|
+
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.",
|
|
4391
|
+
input: import_zod13.z.object({
|
|
3080
4392
|
bundlePath,
|
|
3081
4393
|
repoRoot: REPO_ROOT,
|
|
3082
4394
|
expiringDays: days(
|
|
@@ -3091,11 +4403,17 @@ var doctorCommand = define({
|
|
|
3091
4403
|
"How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
|
|
3092
4404
|
DEFAULT_AGING_DAYS
|
|
3093
4405
|
),
|
|
3094
|
-
offline:
|
|
4406
|
+
offline: import_zod13.z.boolean().optional().describe(
|
|
3095
4407
|
"Read foreign anchors from the local repo cache only, never fetching."
|
|
3096
4408
|
),
|
|
3097
|
-
strict:
|
|
4409
|
+
strict: import_zod13.z.boolean().optional().describe(
|
|
3098
4410
|
"Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
|
|
4411
|
+
),
|
|
4412
|
+
drifted: import_zod13.z.boolean().optional().describe(
|
|
4413
|
+
"Report only drift, as a reassessment packet per record: claim, per-anchor class, and what depends on it."
|
|
4414
|
+
),
|
|
4415
|
+
withDiff: import_zod13.z.boolean().optional().describe(
|
|
4416
|
+
"With `drifted`: recover each anchor's committed span and render the old-vs-new diff. Reads git history."
|
|
3099
4417
|
)
|
|
3100
4418
|
}),
|
|
3101
4419
|
// Presence, not truthiness: `--expiring-days ""` is a caller who meant
|
|
@@ -3114,7 +4432,9 @@ var doctorCommand = define({
|
|
|
3114
4432
|
...unverified2 !== void 0 ? { unverifiedDays: Number(unverified2) } : {},
|
|
3115
4433
|
...agingDays !== void 0 ? { agingDays: Number(agingDays) } : {},
|
|
3116
4434
|
...argv.includes("--offline") ? { offline: true } : {},
|
|
3117
|
-
...argv.includes("--strict") ? { strict: true } : {}
|
|
4435
|
+
...argv.includes("--strict") ? { strict: true } : {},
|
|
4436
|
+
...argv.includes("--drifted") ? { drifted: true } : {},
|
|
4437
|
+
...argv.includes("--with-diff") ? { withDiff: true } : {}
|
|
3118
4438
|
};
|
|
3119
4439
|
},
|
|
3120
4440
|
run: async ({ store, now }, {
|
|
@@ -3123,7 +4443,9 @@ var doctorCommand = define({
|
|
|
3123
4443
|
unverifiedDays,
|
|
3124
4444
|
agingDays,
|
|
3125
4445
|
repoRoot,
|
|
3126
|
-
offline
|
|
4446
|
+
offline,
|
|
4447
|
+
drifted: drifted2,
|
|
4448
|
+
withDiff
|
|
3127
4449
|
}) => {
|
|
3128
4450
|
const checkedAt = now();
|
|
3129
4451
|
const records = await store.list(path);
|
|
@@ -3137,7 +4459,54 @@ var doctorCommand = define({
|
|
|
3137
4459
|
...anchorDrift !== void 0 ? { anchorDrift } : {},
|
|
3138
4460
|
now: new Date(checkedAt)
|
|
3139
4461
|
});
|
|
3140
|
-
|
|
4462
|
+
const hints = grammarHints();
|
|
4463
|
+
if (!drifted2) {
|
|
4464
|
+
return {
|
|
4465
|
+
bundlePath: path,
|
|
4466
|
+
checkedAt,
|
|
4467
|
+
...report,
|
|
4468
|
+
...hints.length ? { hints } : {}
|
|
4469
|
+
};
|
|
4470
|
+
}
|
|
4471
|
+
const standings = new Map(
|
|
4472
|
+
adjudicate(records, records, new Date(checkedAt)).map((hit) => [
|
|
4473
|
+
hit.record.conceptId,
|
|
4474
|
+
hit.standing
|
|
4475
|
+
])
|
|
4476
|
+
);
|
|
4477
|
+
const packets = [];
|
|
4478
|
+
const rebaselinable = [];
|
|
4479
|
+
const search = movedSearch(repoRoot ?? process.cwd());
|
|
4480
|
+
for (const found of report.groups.find((g) => g.check === "drifted")?.findings ?? []) {
|
|
4481
|
+
const record = records.find(
|
|
4482
|
+
(entry) => entry.conceptId === found.conceptId
|
|
4483
|
+
);
|
|
4484
|
+
if (!record) continue;
|
|
4485
|
+
const standing = standings.get(record.conceptId);
|
|
4486
|
+
const built = await reassessPacket(
|
|
4487
|
+
repoRoot ?? process.cwd(),
|
|
4488
|
+
record,
|
|
4489
|
+
anchorDrift?.get(record.conceptId) ?? [],
|
|
4490
|
+
{
|
|
4491
|
+
...withDiff ? { withDiff: true } : {},
|
|
4492
|
+
impact: await store.impact(path, record.conceptId),
|
|
4493
|
+
...standing ? { standing } : {},
|
|
4494
|
+
search
|
|
4495
|
+
}
|
|
4496
|
+
);
|
|
4497
|
+
if (built.packet) packets.push(built.packet);
|
|
4498
|
+
if (built.classified.some((entry) => entry.class === "moved")) {
|
|
4499
|
+
rebaselinable.push(record.conceptId);
|
|
4500
|
+
}
|
|
4501
|
+
}
|
|
4502
|
+
return {
|
|
4503
|
+
bundlePath: path,
|
|
4504
|
+
checkedAt,
|
|
4505
|
+
...report,
|
|
4506
|
+
packets,
|
|
4507
|
+
rebaselinable,
|
|
4508
|
+
...hints.length ? { hints } : {}
|
|
4509
|
+
};
|
|
3141
4510
|
},
|
|
3142
4511
|
render: (result) => render2(result),
|
|
3143
4512
|
// Only expiry, and only under --strict. The other seven checks report debt a
|
|
@@ -3149,18 +4518,22 @@ var doctorCommand = define({
|
|
|
3149
4518
|
failsWhen: (result, input) => input.strict === true && result.counts.expired > 0
|
|
3150
4519
|
});
|
|
3151
4520
|
function render2(result) {
|
|
4521
|
+
if (result.packets) return renderPackets(result);
|
|
3152
4522
|
const { thresholds } = result;
|
|
3153
4523
|
const lines = [
|
|
3154
4524
|
`# KB Doctor \u2014 ${result.bundlePath}`,
|
|
3155
4525
|
`records: ${result.recordCount}`,
|
|
3156
4526
|
`thresholds: expiring within ${thresholds.expiringDays}d, unverified over ${thresholds.unverifiedDays}d, aging over ${thresholds.agingDays}d`,
|
|
3157
4527
|
`checked: ${result.checkedAt}`,
|
|
4528
|
+
...result.anchorResolvers.total ? [
|
|
4529
|
+
`anchors: ${result.anchorResolvers.total} hashed \u2014 ${result.anchorResolvers.treeSitter} tree-sitter, ${result.anchorResolvers.regex} regex`
|
|
4530
|
+
] : [],
|
|
3158
4531
|
""
|
|
3159
4532
|
];
|
|
3160
|
-
const
|
|
4533
|
+
const width2 = Math.max(...result.groups.map((group2) => group2.check.length));
|
|
3161
4534
|
for (const group2 of result.groups) {
|
|
3162
4535
|
lines.push(
|
|
3163
|
-
` ${group2.check.padEnd(
|
|
4536
|
+
` ${group2.check.padEnd(width2)} ${String(group2.count).padStart(3)} ${group2.headline}`
|
|
3164
4537
|
);
|
|
3165
4538
|
}
|
|
3166
4539
|
for (const group2 of result.groups) {
|
|
@@ -3172,27 +4545,52 @@ function render2(result) {
|
|
|
3172
4545
|
);
|
|
3173
4546
|
}
|
|
3174
4547
|
}
|
|
4548
|
+
for (const hint of result.hints ?? []) lines.push("", hint);
|
|
3175
4549
|
lines.push(
|
|
3176
4550
|
"",
|
|
3177
4551
|
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.`
|
|
3178
4552
|
);
|
|
3179
4553
|
return lines.join("\n");
|
|
3180
4554
|
}
|
|
4555
|
+
function renderPackets(result) {
|
|
4556
|
+
const packets = result.packets ?? [];
|
|
4557
|
+
const lines = [
|
|
4558
|
+
`# KB Drift \u2014 ${result.bundlePath}`,
|
|
4559
|
+
`checked: ${result.checkedAt}`,
|
|
4560
|
+
`${packets.length} record${packets.length === 1 ? "" : "s"} need a reading; ${result.counts.drifted} drifted in all.`
|
|
4561
|
+
];
|
|
4562
|
+
if (result.rebaselinable?.length) {
|
|
4563
|
+
lines.push(
|
|
4564
|
+
`moved, rebaseline with \`kb_reassess\`: ${result.rebaselinable.join(", ")}`
|
|
4565
|
+
);
|
|
4566
|
+
}
|
|
4567
|
+
for (const packet of packets) {
|
|
4568
|
+
lines.push(
|
|
4569
|
+
renderReassess({
|
|
4570
|
+
conceptId: packet.conceptId,
|
|
4571
|
+
packet,
|
|
4572
|
+
rebaselined: [],
|
|
4573
|
+
cosmetic: 0
|
|
4574
|
+
})
|
|
4575
|
+
);
|
|
4576
|
+
}
|
|
4577
|
+
return lines.join("\n");
|
|
4578
|
+
}
|
|
3181
4579
|
|
|
3182
4580
|
// src/commands/impact.ts
|
|
3183
|
-
var
|
|
4581
|
+
var import_zod14 = require("zod");
|
|
3184
4582
|
var impactCommand = define({
|
|
3185
4583
|
name: "impact",
|
|
3186
4584
|
tool: "kb_impact",
|
|
3187
4585
|
usage: "impact <concept-id> [--depth N] [--rels a,b]",
|
|
3188
4586
|
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.",
|
|
3189
|
-
input:
|
|
4587
|
+
input: import_zod14.z.object({
|
|
3190
4588
|
bundlePath,
|
|
3191
4589
|
conceptId,
|
|
3192
|
-
depth:
|
|
4590
|
+
depth: import_zod14.z.number().int().positive().optional().describe(
|
|
3193
4591
|
"Hops out from the record. Unbounded when omitted; a walk this cuts reports truncated: true."
|
|
3194
4592
|
),
|
|
3195
|
-
rels:
|
|
4593
|
+
rels: import_zod14.z.array(import_zod14.z.enum(KB_CAUSAL_LINK_RELS)).optional().describe(
|
|
3196
4594
|
"Narrow which rels the walk follows. Defaults to every rel that carries a dependence \u2014 all but related_to."
|
|
3197
4595
|
)
|
|
3198
4596
|
}),
|
|
@@ -3213,13 +4611,13 @@ var impactCommand = define({
|
|
|
3213
4611
|
});
|
|
3214
4612
|
|
|
3215
4613
|
// src/commands/list.ts
|
|
3216
|
-
var
|
|
4614
|
+
var import_zod15 = require("zod");
|
|
3217
4615
|
var listCommand = define({
|
|
3218
4616
|
name: "list",
|
|
3219
4617
|
tool: "kb_list",
|
|
3220
4618
|
usage: "list [type]",
|
|
3221
4619
|
description: "Every record, optionally one type. For enumerating; use kb_query for a question.",
|
|
3222
|
-
input:
|
|
4620
|
+
input: import_zod15.z.object({ bundlePath, type: import_zod15.z.enum(KB_RECORD_TYPES).optional() }),
|
|
3223
4621
|
fromArgv: (argv, path) => ({ bundlePath: path, type: argv[1] }),
|
|
3224
4622
|
run: async ({ store }, { bundlePath: path, type }) => (await store.list(path, type)).map((record) => ({
|
|
3225
4623
|
conceptId: record.conceptId,
|
|
@@ -3231,17 +4629,17 @@ var listCommand = define({
|
|
|
3231
4629
|
});
|
|
3232
4630
|
|
|
3233
4631
|
// src/commands/load.ts
|
|
3234
|
-
var
|
|
4632
|
+
var import_zod16 = require("zod");
|
|
3235
4633
|
var loadCommand = define({
|
|
3236
4634
|
name: "load",
|
|
3237
4635
|
tool: "kb_load",
|
|
3238
4636
|
usage: "load [type] [--budget N | --all] [--repo-root PATH]",
|
|
3239
4637
|
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.",
|
|
3240
|
-
input:
|
|
4638
|
+
input: import_zod16.z.object({
|
|
3241
4639
|
bundlePath,
|
|
3242
|
-
type:
|
|
3243
|
-
budgetTokens:
|
|
3244
|
-
all:
|
|
4640
|
+
type: import_zod16.z.enum(KB_RECORD_TYPES).optional(),
|
|
4641
|
+
budgetTokens: import_zod16.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
|
|
4642
|
+
all: import_zod16.z.boolean().optional().describe(
|
|
3245
4643
|
"Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
|
|
3246
4644
|
),
|
|
3247
4645
|
repoRoot: REPO_ROOT
|
|
@@ -3283,25 +4681,25 @@ var loadCommand = define({
|
|
|
3283
4681
|
});
|
|
3284
4682
|
|
|
3285
4683
|
// src/commands/log.ts
|
|
3286
|
-
var
|
|
4684
|
+
var import_zod17 = require("zod");
|
|
3287
4685
|
var logCommand = define({
|
|
3288
4686
|
name: "log",
|
|
3289
4687
|
tool: "kb_log",
|
|
3290
4688
|
usage: "log",
|
|
3291
4689
|
description: "Who touched what, and when. Append-only; malformed lines are reported, never repaired.",
|
|
3292
|
-
input:
|
|
4690
|
+
input: import_zod17.z.object({ bundlePath }),
|
|
3293
4691
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
3294
4692
|
run: ({ store }, { bundlePath: path }) => store.readLog(path)
|
|
3295
4693
|
});
|
|
3296
4694
|
|
|
3297
4695
|
// src/commands/no-decision.ts
|
|
3298
|
-
var
|
|
4696
|
+
var import_zod18 = require("zod");
|
|
3299
4697
|
var noDecisionCommand = define({
|
|
3300
4698
|
name: "no-decision",
|
|
3301
4699
|
tool: "kb_no_decision",
|
|
3302
4700
|
usage: "no-decision <reason...>",
|
|
3303
4701
|
description: "Record in one sentence that a piece of work had nothing to decide. Idempotent.",
|
|
3304
|
-
input:
|
|
4702
|
+
input: import_zod18.z.object({ bundlePath, reason: import_zod18.z.string().min(1) }),
|
|
3305
4703
|
fromArgv: (argv, path) => ({
|
|
3306
4704
|
bundlePath: path,
|
|
3307
4705
|
reason: argv.slice(1).join(" ").trim()
|
|
@@ -3318,20 +4716,20 @@ var noDecisionCommand = define({
|
|
|
3318
4716
|
});
|
|
3319
4717
|
|
|
3320
4718
|
// src/commands/pack.ts
|
|
3321
|
-
var
|
|
4719
|
+
var import_zod19 = require("zod");
|
|
3322
4720
|
var packCommand = define({
|
|
3323
4721
|
name: "pack",
|
|
3324
4722
|
tool: "kb_pack",
|
|
3325
4723
|
usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
|
|
3326
4724
|
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.",
|
|
3327
|
-
input:
|
|
4725
|
+
input: import_zod19.z.object({
|
|
3328
4726
|
bundlePath,
|
|
3329
4727
|
conceptId,
|
|
3330
|
-
hops:
|
|
3331
|
-
maxNodes:
|
|
4728
|
+
hops: import_zod19.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
|
|
4729
|
+
maxNodes: import_zod19.z.number().int().positive().optional().describe(
|
|
3332
4730
|
"How many records the pack may hold, root included. Defaults to 20."
|
|
3333
4731
|
),
|
|
3334
|
-
budgetTokens:
|
|
4732
|
+
budgetTokens: import_zod19.z.number().int().positive().optional().describe(
|
|
3335
4733
|
"Approximate token ceiling over what is actually emitted. Defaults to 25000."
|
|
3336
4734
|
)
|
|
3337
4735
|
}),
|
|
@@ -3356,12 +4754,12 @@ var packCommand = define({
|
|
|
3356
4754
|
return render3(result, path, now());
|
|
3357
4755
|
}
|
|
3358
4756
|
});
|
|
3359
|
-
function render3(result, bundle,
|
|
4757
|
+
function render3(result, bundle, at2) {
|
|
3360
4758
|
const lines = [
|
|
3361
4759
|
`# KB Pack \u2014 ${result.root}`,
|
|
3362
4760
|
`bundle: ${bundle}`,
|
|
3363
4761
|
`budget: ~${result.tokensLoaded} of ${result.budgetTokens} tokens, ${result.recordCount} records`,
|
|
3364
|
-
`packed: ${
|
|
4762
|
+
`packed: ${at2}`,
|
|
3365
4763
|
"",
|
|
3366
4764
|
`## Records (${result.records.length})`
|
|
3367
4765
|
];
|
|
@@ -3418,22 +4816,22 @@ function warningLabel(warning) {
|
|
|
3418
4816
|
}
|
|
3419
4817
|
|
|
3420
4818
|
// src/commands/pin.ts
|
|
3421
|
-
var
|
|
4819
|
+
var import_zod20 = require("zod");
|
|
3422
4820
|
var pinCommand = define({
|
|
3423
4821
|
name: "pin",
|
|
3424
4822
|
tool: "kb_pin",
|
|
3425
4823
|
usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
|
|
3426
4824
|
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.",
|
|
3427
|
-
input:
|
|
4825
|
+
input: import_zod20.z.object({
|
|
3428
4826
|
bundlePath,
|
|
3429
|
-
mode:
|
|
4827
|
+
mode: import_zod20.z.enum(["full", "index"]).optional().describe(
|
|
3430
4828
|
"full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
|
|
3431
4829
|
),
|
|
3432
|
-
profiles:
|
|
3433
|
-
layer:
|
|
4830
|
+
profiles: import_zod20.z.array(import_zod20.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
|
|
4831
|
+
layer: import_zod20.z.enum(["project", "local", "user"]).optional().describe(
|
|
3434
4832
|
"Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
|
|
3435
4833
|
),
|
|
3436
|
-
frozen:
|
|
4834
|
+
frozen: import_zod20.z.boolean().optional().describe(
|
|
3437
4835
|
"true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
|
|
3438
4836
|
)
|
|
3439
4837
|
}),
|
|
@@ -3462,29 +4860,29 @@ var pinCommand = define({
|
|
|
3462
4860
|
});
|
|
3463
4861
|
|
|
3464
4862
|
// src/commands/pins.ts
|
|
3465
|
-
var
|
|
4863
|
+
var import_zod21 = require("zod");
|
|
3466
4864
|
var pinsCommand = define({
|
|
3467
4865
|
name: "pins",
|
|
3468
4866
|
tool: "kb_pins",
|
|
3469
4867
|
usage: "pins",
|
|
3470
4868
|
description: "Every pinned base across the manifest layers, with its layer and whether it resolves to records. Takes no bundlePath.",
|
|
3471
|
-
input:
|
|
4869
|
+
input: import_zod21.z.object({}),
|
|
3472
4870
|
fromArgv: () => ({}),
|
|
3473
4871
|
run: ({ store }) => listPins(store, process.cwd())
|
|
3474
4872
|
});
|
|
3475
4873
|
|
|
3476
4874
|
// src/commands/query.ts
|
|
3477
|
-
var
|
|
4875
|
+
var import_zod22 = require("zod");
|
|
3478
4876
|
var queryCommand = define({
|
|
3479
4877
|
name: "query",
|
|
3480
4878
|
tool: "kb_query",
|
|
3481
4879
|
usage: "query <text...> [--repo-root PATH]",
|
|
3482
4880
|
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.",
|
|
3483
|
-
input:
|
|
4881
|
+
input: import_zod22.z.object({
|
|
3484
4882
|
bundlePath,
|
|
3485
|
-
text:
|
|
3486
|
-
type:
|
|
3487
|
-
includeNonCurrent:
|
|
4883
|
+
text: import_zod22.z.string().optional(),
|
|
4884
|
+
type: import_zod22.z.enum(KB_RECORD_TYPES).optional(),
|
|
4885
|
+
includeNonCurrent: import_zod22.z.boolean().optional(),
|
|
3488
4886
|
repoRoot: REPO_ROOT
|
|
3489
4887
|
}),
|
|
3490
4888
|
// `--repo-root` is a flag, so its value must not fall into the search text.
|
|
@@ -3516,27 +4914,27 @@ var queryCommand = define({
|
|
|
3516
4914
|
});
|
|
3517
4915
|
|
|
3518
4916
|
// src/commands/read-index.ts
|
|
3519
|
-
var
|
|
4917
|
+
var import_zod23 = require("zod");
|
|
3520
4918
|
var readIndexCommand = define({
|
|
3521
4919
|
name: "index",
|
|
3522
4920
|
tool: "kb_index",
|
|
3523
4921
|
usage: "index",
|
|
3524
4922
|
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.",
|
|
3525
|
-
input:
|
|
4923
|
+
input: import_zod23.z.object({ bundlePath }),
|
|
3526
4924
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
3527
4925
|
run: ({ store }, { bundlePath: path }) => store.readIndex(path)
|
|
3528
4926
|
});
|
|
3529
4927
|
|
|
3530
4928
|
// src/commands/schema.ts
|
|
3531
|
-
var
|
|
4929
|
+
var import_zod26 = require("zod");
|
|
3532
4930
|
|
|
3533
4931
|
// src/json-schema.ts
|
|
3534
|
-
var
|
|
4932
|
+
var import_zod25 = require("zod");
|
|
3535
4933
|
|
|
3536
4934
|
// src/kb-log.ts
|
|
3537
|
-
var
|
|
4935
|
+
var import_zod24 = require("zod");
|
|
3538
4936
|
var LOG_FILE = "log.jsonl";
|
|
3539
|
-
var kbLogEntrySchema =
|
|
4937
|
+
var kbLogEntrySchema = import_zod24.z.object({
|
|
3540
4938
|
// Validated, not just `min(1)`: `at` is a sort key (see `parseLog`
|
|
3541
4939
|
// below), and a value that isn't actually chronological — a Unix
|
|
3542
4940
|
// timestamp, a human-typed date, garbage — would sort wrong without
|
|
@@ -3545,12 +4943,12 @@ var kbLogEntrySchema = import_zod22.z.object({
|
|
|
3545
4943
|
// and rejects everything else, including a non-`Z` offset — so a
|
|
3546
4944
|
// malformed `at` is reported the same way a malformed line already is,
|
|
3547
4945
|
// rather than silently sorting into the wrong place.
|
|
3548
|
-
at:
|
|
3549
|
-
by:
|
|
3550
|
-
operation:
|
|
3551
|
-
conceptId:
|
|
4946
|
+
at: import_zod24.z.iso.datetime(),
|
|
4947
|
+
by: import_zod24.z.string().min(1),
|
|
4948
|
+
operation: import_zod24.z.string().min(1),
|
|
4949
|
+
conceptId: import_zod24.z.string().min(1),
|
|
3552
4950
|
/** Second concept id, where the operation relates two — supersession. */
|
|
3553
|
-
target:
|
|
4951
|
+
target: import_zod24.z.string().min(1).optional()
|
|
3554
4952
|
}).strict();
|
|
3555
4953
|
function renderLogEntry(entry) {
|
|
3556
4954
|
return `${JSON.stringify(kbLogEntrySchema.parse(entry))}
|
|
@@ -3560,18 +4958,18 @@ function parseLog(raw) {
|
|
|
3560
4958
|
const entries = [];
|
|
3561
4959
|
const malformed = [];
|
|
3562
4960
|
const seen = /* @__PURE__ */ new Set();
|
|
3563
|
-
raw.split("\n").forEach((text,
|
|
4961
|
+
raw.split("\n").forEach((text, index2) => {
|
|
3564
4962
|
if (!text.trim()) return;
|
|
3565
4963
|
let value;
|
|
3566
4964
|
try {
|
|
3567
4965
|
value = JSON.parse(text);
|
|
3568
4966
|
} catch {
|
|
3569
|
-
malformed.push({ line:
|
|
4967
|
+
malformed.push({ line: index2 + 1, text });
|
|
3570
4968
|
return;
|
|
3571
4969
|
}
|
|
3572
4970
|
const parsed = kbLogEntrySchema.safeParse(value);
|
|
3573
4971
|
if (!parsed.success) {
|
|
3574
|
-
malformed.push({ line:
|
|
4972
|
+
malformed.push({ line: index2 + 1, text });
|
|
3575
4973
|
return;
|
|
3576
4974
|
}
|
|
3577
4975
|
const key = JSON.stringify(parsed.data);
|
|
@@ -3588,11 +4986,11 @@ function parseLog(raw) {
|
|
|
3588
4986
|
// src/json-schema.ts
|
|
3589
4987
|
function kbJsonSchemas() {
|
|
3590
4988
|
return {
|
|
3591
|
-
recordFrontmatter:
|
|
4989
|
+
recordFrontmatter: import_zod25.z.toJSONSchema(kbRecordFrontmatterSchema, {
|
|
3592
4990
|
io: "input"
|
|
3593
4991
|
}),
|
|
3594
|
-
composeInput:
|
|
3595
|
-
logEntry:
|
|
4992
|
+
composeInput: import_zod25.z.toJSONSchema(composeInputSchema, { io: "input" }),
|
|
4993
|
+
logEntry: import_zod25.z.toJSONSchema(kbLogEntrySchema, { io: "input" })
|
|
3596
4994
|
};
|
|
3597
4995
|
}
|
|
3598
4996
|
|
|
@@ -3602,25 +5000,25 @@ var schemaCommand = define({
|
|
|
3602
5000
|
tool: "kb_schema",
|
|
3603
5001
|
usage: "schema",
|
|
3604
5002
|
description: "JSON Schema for frontmatter, write input, and log entries, generated from the enforcing code.",
|
|
3605
|
-
input:
|
|
5003
|
+
input: import_zod26.z.object({}),
|
|
3606
5004
|
fromArgv: () => ({}),
|
|
3607
5005
|
run: () => Promise.resolve(kbJsonSchemas())
|
|
3608
5006
|
});
|
|
3609
5007
|
|
|
3610
5008
|
// src/commands/stamp.ts
|
|
3611
|
-
var
|
|
3612
|
-
var
|
|
5009
|
+
var import_promises8 = require("fs/promises");
|
|
5010
|
+
var import_zod27 = require("zod");
|
|
3613
5011
|
var DIGEST = /^[0-9a-f]{64}$/;
|
|
3614
5012
|
var stampCommand = define({
|
|
3615
5013
|
name: "stamp",
|
|
3616
5014
|
tool: "kb_stamp",
|
|
3617
5015
|
usage: "stamp [--bundle PATH] [--since DIGEST|FILE]",
|
|
3618
|
-
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
|
|
3619
|
-
input:
|
|
3620
|
-
bundlePath:
|
|
5016
|
+
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.",
|
|
5017
|
+
input: import_zod27.z.object({
|
|
5018
|
+
bundlePath: import_zod27.z.string().min(1).optional().describe(
|
|
3621
5019
|
"Absolute path to one knowledge base. Omit to stamp every pinned base."
|
|
3622
5020
|
),
|
|
3623
|
-
since:
|
|
5021
|
+
since: import_zod27.z.string().min(1).optional().describe(
|
|
3624
5022
|
"Prior digest, or path to a prior `stamp --json`; only moved bases return, with changed ids when the baseline is a file."
|
|
3625
5023
|
)
|
|
3626
5024
|
}),
|
|
@@ -3659,7 +5057,7 @@ var stampCommand = define({
|
|
|
3659
5057
|
return reports;
|
|
3660
5058
|
},
|
|
3661
5059
|
render: (result) => result.map((report) => {
|
|
3662
|
-
const counts = `${report.recordCount} record(s), ${report.superseded} superseded`;
|
|
5060
|
+
const counts = `${report.recordCount} record(s), ${report.superseded} superseded${report.drifted ? `, ${report.drifted} drifted` : ""}`;
|
|
3663
5061
|
const head = `${report.path} ${report.digest} ${counts}${report.newestAt ? ` newest ${report.newestAt}` : ""}`;
|
|
3664
5062
|
return report.changed?.length ? `${head}
|
|
3665
5063
|
changed: ${report.changed.join(", ")}` : head;
|
|
@@ -3682,7 +5080,7 @@ async function readBaseline(since) {
|
|
|
3682
5080
|
if (DIGEST.test(since)) return { digest: since, byPath: /* @__PURE__ */ new Map() };
|
|
3683
5081
|
let parsed;
|
|
3684
5082
|
try {
|
|
3685
|
-
parsed = JSON.parse(await (0,
|
|
5083
|
+
parsed = JSON.parse(await (0, import_promises8.readFile)(since, "utf8"));
|
|
3686
5084
|
} catch {
|
|
3687
5085
|
throw new KbStampBaselineError(since);
|
|
3688
5086
|
}
|
|
@@ -3706,16 +5104,16 @@ async function readBaseline(since) {
|
|
|
3706
5104
|
}
|
|
3707
5105
|
|
|
3708
5106
|
// src/commands/status.ts
|
|
3709
|
-
var
|
|
5107
|
+
var import_zod28 = require("zod");
|
|
3710
5108
|
var statusCommand = define({
|
|
3711
5109
|
name: "status",
|
|
3712
5110
|
tool: "kb_status",
|
|
3713
5111
|
usage: "status <concept-id> <status>",
|
|
3714
5112
|
description: "Move a record's status. Compare-and-swap: a concurrent change fails instead of being overwritten.",
|
|
3715
|
-
input:
|
|
5113
|
+
input: import_zod28.z.object({
|
|
3716
5114
|
bundlePath,
|
|
3717
5115
|
conceptId,
|
|
3718
|
-
status:
|
|
5116
|
+
status: import_zod28.z.enum(KB_RECORD_STATUSES)
|
|
3719
5117
|
}),
|
|
3720
5118
|
fromArgv: (argv, path) => ({
|
|
3721
5119
|
bundlePath: path,
|
|
@@ -3730,13 +5128,13 @@ var statusCommand = define({
|
|
|
3730
5128
|
});
|
|
3731
5129
|
|
|
3732
5130
|
// src/commands/supersede.ts
|
|
3733
|
-
var
|
|
5131
|
+
var import_zod29 = require("zod");
|
|
3734
5132
|
var supersedeCommand = define({
|
|
3735
5133
|
name: "supersede",
|
|
3736
5134
|
tool: "kb_supersede",
|
|
3737
5135
|
usage: "supersede <concept-id> <replacement-id>",
|
|
3738
5136
|
description: "Mark a record superseded by another, linked in both directions. Use instead of editing a record whose meaning changed.",
|
|
3739
|
-
input:
|
|
5137
|
+
input: import_zod29.z.object({ bundlePath, conceptId, replacementId: conceptId }),
|
|
3740
5138
|
fromArgv: (argv, path) => ({
|
|
3741
5139
|
bundlePath: path,
|
|
3742
5140
|
conceptId: argv[1],
|
|
@@ -3750,16 +5148,16 @@ var supersedeCommand = define({
|
|
|
3750
5148
|
});
|
|
3751
5149
|
|
|
3752
5150
|
// src/commands/sync-instructions.ts
|
|
3753
|
-
var
|
|
5151
|
+
var import_zod30 = require("zod");
|
|
3754
5152
|
var syncInstructionsCommand = define({
|
|
3755
5153
|
name: "sync-instructions",
|
|
3756
5154
|
usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
|
|
3757
5155
|
description: "CLI-only: plant the kb_context block between sentinel comments in AGENTS.md or CLAUDE.md, idempotently.",
|
|
3758
|
-
input:
|
|
3759
|
-
file:
|
|
3760
|
-
budgetTokens:
|
|
3761
|
-
fullUnderTokens:
|
|
3762
|
-
profile:
|
|
5156
|
+
input: import_zod30.z.object({
|
|
5157
|
+
file: import_zod30.z.string().min(1).describe("The instruction file to edit in place."),
|
|
5158
|
+
budgetTokens: import_zod30.z.number().int().positive().optional(),
|
|
5159
|
+
fullUnderTokens: import_zod30.z.number().int().positive().optional(),
|
|
5160
|
+
profile: import_zod30.z.string().optional()
|
|
3763
5161
|
}),
|
|
3764
5162
|
fromArgv: (argv) => {
|
|
3765
5163
|
const budget = argvFlag(argv, "--budget");
|
|
@@ -3785,7 +5183,7 @@ var syncInstructionsCommand = define({
|
|
|
3785
5183
|
});
|
|
3786
5184
|
|
|
3787
5185
|
// src/commands/trace.ts
|
|
3788
|
-
var
|
|
5186
|
+
var import_zod31 = require("zod");
|
|
3789
5187
|
|
|
3790
5188
|
// src/trace.ts
|
|
3791
5189
|
var TRACE_EDGES = [
|
|
@@ -3831,8 +5229,8 @@ function trace(seedId, bundle, options = {}) {
|
|
|
3831
5229
|
return [...reached.values()].sort(byGeneratedAt);
|
|
3832
5230
|
}
|
|
3833
5231
|
function byGeneratedAt(left, right) {
|
|
3834
|
-
const
|
|
3835
|
-
return
|
|
5232
|
+
const at2 = (step) => step.record.frontmatter.generated?.at ?? "";
|
|
5233
|
+
return at2(left).localeCompare(at2(right)) || left.depth - right.depth;
|
|
3836
5234
|
}
|
|
3837
5235
|
|
|
3838
5236
|
// src/commands/trace.ts
|
|
@@ -3841,11 +5239,11 @@ var traceCommand = define({
|
|
|
3841
5239
|
tool: "kb_trace",
|
|
3842
5240
|
usage: "trace <concept-id> [edges...]",
|
|
3843
5241
|
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".',
|
|
3844
|
-
input:
|
|
5242
|
+
input: import_zod31.z.object({
|
|
3845
5243
|
bundlePath,
|
|
3846
5244
|
conceptId,
|
|
3847
|
-
edges:
|
|
3848
|
-
depth:
|
|
5245
|
+
edges: import_zod31.z.array(import_zod31.z.enum(TRACE_EDGES)).optional(),
|
|
5246
|
+
depth: import_zod31.z.number().int().positive().optional()
|
|
3849
5247
|
}),
|
|
3850
5248
|
fromArgv: (argv, path) => ({
|
|
3851
5249
|
bundlePath: path,
|
|
@@ -3867,37 +5265,37 @@ var traceCommand = define({
|
|
|
3867
5265
|
});
|
|
3868
5266
|
|
|
3869
5267
|
// src/commands/types.ts
|
|
3870
|
-
var
|
|
5268
|
+
var import_zod32 = require("zod");
|
|
3871
5269
|
var typesCommand = define({
|
|
3872
5270
|
name: "types",
|
|
3873
5271
|
tool: "kb_types",
|
|
3874
5272
|
usage: "types",
|
|
3875
5273
|
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.",
|
|
3876
|
-
input:
|
|
5274
|
+
input: import_zod32.z.object({}),
|
|
3877
5275
|
fromArgv: () => ({}),
|
|
3878
5276
|
run: () => Promise.resolve(RECORD_TYPES)
|
|
3879
5277
|
});
|
|
3880
5278
|
|
|
3881
5279
|
// src/commands/unpin.ts
|
|
3882
|
-
var
|
|
5280
|
+
var import_zod33 = require("zod");
|
|
3883
5281
|
var unpinCommand = define({
|
|
3884
5282
|
name: "unpin",
|
|
3885
5283
|
tool: "kb_unpin",
|
|
3886
5284
|
usage: "unpin [bundle-path]",
|
|
3887
5285
|
description: "Remove a base from every manifest layer that holds it. Reports the layers touched.",
|
|
3888
|
-
input:
|
|
5286
|
+
input: import_zod33.z.object({ bundlePath }),
|
|
3889
5287
|
fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
|
|
3890
5288
|
run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
|
|
3891
5289
|
});
|
|
3892
5290
|
|
|
3893
5291
|
// src/commands/validate.ts
|
|
3894
|
-
var
|
|
5292
|
+
var import_zod34 = require("zod");
|
|
3895
5293
|
var validateCommand = define({
|
|
3896
5294
|
name: "validate",
|
|
3897
5295
|
tool: "kb_validate",
|
|
3898
5296
|
usage: "validate",
|
|
3899
5297
|
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.",
|
|
3900
|
-
input:
|
|
5298
|
+
input: import_zod34.z.object({ bundlePath }),
|
|
3901
5299
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
3902
5300
|
run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
|
|
3903
5301
|
// Warnings never fail the exit code; every other severity does.
|
|
@@ -3907,16 +5305,16 @@ var validateCommand = define({
|
|
|
3907
5305
|
});
|
|
3908
5306
|
|
|
3909
5307
|
// src/commands/verify.ts
|
|
3910
|
-
var
|
|
5308
|
+
var import_zod35 = require("zod");
|
|
3911
5309
|
var verifyCommand = define({
|
|
3912
5310
|
name: "verify",
|
|
3913
5311
|
tool: "kb_verify",
|
|
3914
5312
|
usage: "verify <concept-id> --note <text>",
|
|
3915
5313
|
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.",
|
|
3916
|
-
input:
|
|
5314
|
+
input: import_zod35.z.object({
|
|
3917
5315
|
bundlePath,
|
|
3918
5316
|
conceptId,
|
|
3919
|
-
note:
|
|
5317
|
+
note: import_zod35.z.string().refine((s) => s.trim().length > 0, {
|
|
3920
5318
|
message: "note must say what the check found"
|
|
3921
5319
|
})
|
|
3922
5320
|
}),
|
|
@@ -3936,15 +5334,15 @@ var verifyCommand = define({
|
|
|
3936
5334
|
});
|
|
3937
5335
|
|
|
3938
5336
|
// src/commands/write.ts
|
|
3939
|
-
var
|
|
5337
|
+
var import_zod36 = require("zod");
|
|
3940
5338
|
var writeCommand = define({
|
|
3941
5339
|
name: "write",
|
|
3942
5340
|
tool: "kb_write",
|
|
3943
5341
|
usage: "write <type> < record.json",
|
|
3944
5342
|
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.",
|
|
3945
|
-
input:
|
|
5343
|
+
input: import_zod36.z.object({
|
|
3946
5344
|
bundlePath,
|
|
3947
|
-
type:
|
|
5345
|
+
type: import_zod36.z.enum(KB_RECORD_TYPES),
|
|
3948
5346
|
input: composeInputSchema
|
|
3949
5347
|
}),
|
|
3950
5348
|
fromArgv: async (argv, path, stdin) => ({
|
|
@@ -3968,13 +5366,13 @@ var writeCommand = define({
|
|
|
3968
5366
|
});
|
|
3969
5367
|
|
|
3970
5368
|
// src/commands/write-decision.ts
|
|
3971
|
-
var
|
|
5369
|
+
var import_zod37 = require("zod");
|
|
3972
5370
|
var writeDecisionCommand = define({
|
|
3973
5371
|
name: "write-decision",
|
|
3974
5372
|
tool: "kb_write_decision",
|
|
3975
5373
|
usage: "write-decision < decision.json",
|
|
3976
5374
|
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.",
|
|
3977
|
-
input:
|
|
5375
|
+
input: import_zod37.z.object({ bundlePath, input: decisionInputSchema }),
|
|
3978
5376
|
fromArgv: async (_argv, path, stdin) => ({
|
|
3979
5377
|
bundlePath: path,
|
|
3980
5378
|
input: JSON.parse(await stdin())
|
|
@@ -4004,6 +5402,7 @@ var KB_COMMANDS = [
|
|
|
4004
5402
|
answerCommand,
|
|
4005
5403
|
verifyCommand,
|
|
4006
5404
|
anchorResolveCommand,
|
|
5405
|
+
reassessCommand,
|
|
4007
5406
|
loadCommand,
|
|
4008
5407
|
catalogCommand,
|
|
4009
5408
|
packCommand,
|
|
@@ -4030,8 +5429,8 @@ var KB_COMMANDS_BY_NAME = new Map(
|
|
|
4030
5429
|
);
|
|
4031
5430
|
|
|
4032
5431
|
// src/kb-store.ts
|
|
4033
|
-
var
|
|
4034
|
-
var
|
|
5432
|
+
var import_promises10 = require("fs/promises");
|
|
5433
|
+
var import_node_path11 = require("path");
|
|
4035
5434
|
|
|
4036
5435
|
// src/markdown.ts
|
|
4037
5436
|
var import_gray_matter = __toESM(require("gray-matter"), 1);
|
|
@@ -4059,15 +5458,15 @@ function parseMarkdownWithFrontmatter(text, schema) {
|
|
|
4059
5458
|
}
|
|
4060
5459
|
|
|
4061
5460
|
// src/kb-stamp.ts
|
|
4062
|
-
var
|
|
4063
|
-
function
|
|
4064
|
-
return (0,
|
|
5461
|
+
var import_node_crypto4 = require("crypto");
|
|
5462
|
+
function sha2563(contents) {
|
|
5463
|
+
return (0, import_node_crypto4.createHash)("sha256").update(contents).digest("hex");
|
|
4065
5464
|
}
|
|
4066
5465
|
function bundleStamp(records, superseded) {
|
|
4067
5466
|
const entries = [
|
|
4068
5467
|
...records.map((hit) => ({
|
|
4069
5468
|
conceptId: hit.record.conceptId,
|
|
4070
|
-
digest: `current:${
|
|
5469
|
+
digest: `current:${sha2563(
|
|
4071
5470
|
stringifyMarkdownWithFrontmatter(
|
|
4072
5471
|
hit.record.body,
|
|
4073
5472
|
hit.record.frontmatter
|
|
@@ -4076,11 +5475,11 @@ function bundleStamp(records, superseded) {
|
|
|
4076
5475
|
})),
|
|
4077
5476
|
...superseded.map((entry) => ({
|
|
4078
5477
|
conceptId: entry.conceptId,
|
|
4079
|
-
digest: `superseded:${
|
|
5478
|
+
digest: `superseded:${sha2563(JSON.stringify(entry))}`
|
|
4080
5479
|
}))
|
|
4081
5480
|
].sort((a, b) => a.conceptId < b.conceptId ? -1 : 1);
|
|
4082
5481
|
return {
|
|
4083
|
-
digest:
|
|
5482
|
+
digest: sha2563(
|
|
4084
5483
|
entries.map((entry) => `${entry.conceptId}:${entry.digest}`).join("\n")
|
|
4085
5484
|
),
|
|
4086
5485
|
records: entries
|
|
@@ -4091,8 +5490,8 @@ function bundleDigest(records, superseded) {
|
|
|
4091
5490
|
}
|
|
4092
5491
|
|
|
4093
5492
|
// src/search-index.ts
|
|
4094
|
-
var
|
|
4095
|
-
var
|
|
5493
|
+
var import_promises9 = require("fs/promises");
|
|
5494
|
+
var import_node_path10 = require("path");
|
|
4096
5495
|
var SEARCH_INDEX_FILE = ".index.sqlite";
|
|
4097
5496
|
var COLLECTION = "kb";
|
|
4098
5497
|
async function searchBase(bundlePath2, query, options = {}) {
|
|
@@ -4101,7 +5500,7 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
4101
5500
|
let store = null;
|
|
4102
5501
|
try {
|
|
4103
5502
|
store = await qmd.createStore({
|
|
4104
|
-
dbPath: (0,
|
|
5503
|
+
dbPath: (0, import_node_path10.join)(bundlePath2, SEARCH_INDEX_FILE),
|
|
4105
5504
|
config: {
|
|
4106
5505
|
collections: {
|
|
4107
5506
|
[COLLECTION]: {
|
|
@@ -4136,7 +5535,7 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
4136
5535
|
}
|
|
4137
5536
|
}
|
|
4138
5537
|
async function isStale(bundlePath2) {
|
|
4139
|
-
const indexAt = await (0,
|
|
5538
|
+
const indexAt = await (0, import_promises9.stat)((0, import_node_path10.join)(bundlePath2, SEARCH_INDEX_FILE)).then((s) => s.mtimeMs).catch(() => 0);
|
|
4140
5539
|
if (!indexAt) return true;
|
|
4141
5540
|
const { readdir: readdir2 } = await import("fs/promises");
|
|
4142
5541
|
const names = (await readdir2(bundlePath2).catch(() => [])).filter(
|
|
@@ -4145,8 +5544,8 @@ async function isStale(bundlePath2) {
|
|
|
4145
5544
|
let stale = false;
|
|
4146
5545
|
await mapLimit(names, DEFAULT_IO_CONCURRENCY, async (name) => {
|
|
4147
5546
|
if (stale) return;
|
|
4148
|
-
const
|
|
4149
|
-
if (
|
|
5547
|
+
const at2 = await (0, import_promises9.stat)((0, import_node_path10.join)(bundlePath2, name)).then((s) => s.mtimeMs).catch(() => 0);
|
|
5548
|
+
if (at2 > indexAt) stale = true;
|
|
4150
5549
|
});
|
|
4151
5550
|
return stale;
|
|
4152
5551
|
}
|
|
@@ -4259,8 +5658,8 @@ function byRank(left, right) {
|
|
|
4259
5658
|
) || left.record.conceptId.localeCompare(right.record.conceptId);
|
|
4260
5659
|
}
|
|
4261
5660
|
function typeRank(record) {
|
|
4262
|
-
const
|
|
4263
|
-
return
|
|
5661
|
+
const index2 = TYPE_PRIORITY.indexOf(record.frontmatter.type);
|
|
5662
|
+
return index2 === -1 ? TYPE_PRIORITY.length : index2;
|
|
4264
5663
|
}
|
|
4265
5664
|
|
|
4266
5665
|
// src/kb-links/inbound.ts
|
|
@@ -4423,7 +5822,7 @@ function appendUnionMergeLine(contents) {
|
|
|
4423
5822
|
}
|
|
4424
5823
|
|
|
4425
5824
|
// src/kb-store.ts
|
|
4426
|
-
var KB_DIR = (0,
|
|
5825
|
+
var KB_DIR = (0, import_node_path11.join)(".strauss", "kb");
|
|
4427
5826
|
var STORE_OWNED = /* @__PURE__ */ new Set([INDEX_FILE, LOG_FILE, SEARCH_INDEX_FILE]);
|
|
4428
5827
|
var DEFAULT_LOAD_BUDGET = 25e3;
|
|
4429
5828
|
var KbStore = class {
|
|
@@ -4454,7 +5853,7 @@ var KbStore = class {
|
|
|
4454
5853
|
const conceptId2 = `${input.type}.${input.slug}`;
|
|
4455
5854
|
const root = this.root(bundlePath2);
|
|
4456
5855
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
4457
|
-
await (0,
|
|
5856
|
+
await (0, import_promises10.mkdir)(root, { recursive: true });
|
|
4458
5857
|
await this.publish(
|
|
4459
5858
|
target,
|
|
4460
5859
|
stringifyMarkdownWithFrontmatter(input.body, frontmatter),
|
|
@@ -4493,7 +5892,7 @@ var KbStore = class {
|
|
|
4493
5892
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
4494
5893
|
let raw;
|
|
4495
5894
|
try {
|
|
4496
|
-
raw = await (0,
|
|
5895
|
+
raw = await (0, import_promises10.readFile)(target, "utf8");
|
|
4497
5896
|
} catch {
|
|
4498
5897
|
return null;
|
|
4499
5898
|
}
|
|
@@ -4510,7 +5909,7 @@ var KbStore = class {
|
|
|
4510
5909
|
const root = this.root(bundlePath2);
|
|
4511
5910
|
let names;
|
|
4512
5911
|
try {
|
|
4513
|
-
names = await (0,
|
|
5912
|
+
names = await (0, import_promises10.readdir)(root);
|
|
4514
5913
|
} catch {
|
|
4515
5914
|
return [];
|
|
4516
5915
|
}
|
|
@@ -4518,7 +5917,7 @@ var KbStore = class {
|
|
|
4518
5917
|
const records = await mapLimit(
|
|
4519
5918
|
wanted,
|
|
4520
5919
|
DEFAULT_IO_CONCURRENCY,
|
|
4521
|
-
async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await (0,
|
|
5920
|
+
async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await (0, import_promises10.readFile)((0, import_node_path11.join)(root, name), "utf8"))
|
|
4522
5921
|
);
|
|
4523
5922
|
return records.filter((record) => record !== null);
|
|
4524
5923
|
}
|
|
@@ -4566,8 +5965,8 @@ var KbStore = class {
|
|
|
4566
5965
|
* and the refusal is logged under its own operation name — `mutate` only
|
|
4567
5966
|
* logs what it publishes.
|
|
4568
5967
|
*/
|
|
4569
|
-
async verify(bundlePath2, conceptId2, note, actor = "unknown",
|
|
4570
|
-
const event = kbVerifiedEventSchema.parse({ by: actor, at, note });
|
|
5968
|
+
async verify(bundlePath2, conceptId2, note, actor = "unknown", at2 = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
5969
|
+
const event = kbVerifiedEventSchema.parse({ by: actor, at: at2, note });
|
|
4571
5970
|
const existing = await this.read(bundlePath2, conceptId2);
|
|
4572
5971
|
if (!existing) throw new KbRecordNotFoundError(conceptId2);
|
|
4573
5972
|
const generatedBy = existing.frontmatter.generated?.by;
|
|
@@ -4619,14 +6018,14 @@ var KbStore = class {
|
|
|
4619
6018
|
return superseded;
|
|
4620
6019
|
}
|
|
4621
6020
|
/** Resolves an open question, stamping who answered and when. */
|
|
4622
|
-
async answer(bundlePath2, conceptId2, answer, actor = "unknown",
|
|
6021
|
+
async answer(bundlePath2, conceptId2, answer, actor = "unknown", at2 = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
4623
6022
|
return this.mutate(
|
|
4624
6023
|
bundlePath2,
|
|
4625
6024
|
conceptId2,
|
|
4626
6025
|
(frontmatter) => ({
|
|
4627
6026
|
...frontmatter,
|
|
4628
6027
|
strauss_status: "resolved",
|
|
4629
|
-
strauss_answered: { by: actor, at }
|
|
6028
|
+
strauss_answered: { by: actor, at: at2 }
|
|
4630
6029
|
}),
|
|
4631
6030
|
{ operation: "answer", by: actor },
|
|
4632
6031
|
(body) => `${body.trimEnd()}
|
|
@@ -4675,7 +6074,7 @@ ${answer}
|
|
|
4675
6074
|
if (found.length) return found;
|
|
4676
6075
|
}
|
|
4677
6076
|
const lowered = needle.toLowerCase();
|
|
4678
|
-
return bundle.filter((record) =>
|
|
6077
|
+
return bundle.filter((record) => matches2(record, lowered));
|
|
4679
6078
|
}
|
|
4680
6079
|
/**
|
|
4681
6080
|
* Anchor drift over the records about to be handed back. Like the search
|
|
@@ -4795,24 +6194,34 @@ ${answer}
|
|
|
4795
6194
|
}
|
|
4796
6195
|
/**
|
|
4797
6196
|
* `load`'s digest without `load`'s bodies — the same records, adjudicated
|
|
4798
|
-
* the same way, handed back as a stamp.
|
|
4799
|
-
*
|
|
4800
|
-
*
|
|
6197
|
+
* the same way, handed back as a stamp.
|
|
6198
|
+
*
|
|
6199
|
+
* Drift is counted but kept out of the digest, which is what lets the reload
|
|
6200
|
+
* hook ask one question and get two answers: whether the base moved, and
|
|
6201
|
+
* whether the code under it did. A `load` and a `stamp` of the same base
|
|
6202
|
+
* still agree on the digest, because no warning has ever reached it.
|
|
4801
6203
|
*/
|
|
4802
|
-
async stamp(bundlePath2) {
|
|
6204
|
+
async stamp(bundlePath2, options = {}) {
|
|
4803
6205
|
const bundle = await this.list(bundlePath2);
|
|
4804
6206
|
const adjudicated = adjudicate(bundle, bundle, /* @__PURE__ */ new Date());
|
|
4805
6207
|
const current = adjudicated.filter((hit) => hit.standing !== "superseded");
|
|
4806
6208
|
const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(stub);
|
|
4807
6209
|
const stamped = bundleStamp(current, superseded);
|
|
4808
|
-
const dates = bundle.map((record) => record.frontmatter.generated?.at ?? null).filter((
|
|
6210
|
+
const dates = bundle.map((record) => record.frontmatter.generated?.at ?? null).filter((at2) => typeof at2 === "string").sort();
|
|
6211
|
+
const drift = await this.detectDrift(bundle, options.repoRoot);
|
|
6212
|
+
const drifted2 = drift === void 0 ? null : [...drift.values()].filter(
|
|
6213
|
+
(entries) => entries.some(
|
|
6214
|
+
(entry) => entry.state !== "match" && !isUncheckedReason(entry.reason)
|
|
6215
|
+
)
|
|
6216
|
+
).length;
|
|
4809
6217
|
return {
|
|
4810
6218
|
path: bundlePath2,
|
|
4811
6219
|
digest: stamped.digest,
|
|
4812
6220
|
recordCount: bundle.length,
|
|
4813
6221
|
superseded: superseded.length,
|
|
4814
6222
|
newestAt: dates.at(-1) ?? null,
|
|
4815
|
-
records: stamped.records
|
|
6223
|
+
records: stamped.records,
|
|
6224
|
+
drifted: drifted2
|
|
4816
6225
|
};
|
|
4817
6226
|
}
|
|
4818
6227
|
/** How a position was arrived at, as a timeline. See `trace.ts`. */
|
|
@@ -4845,11 +6254,11 @@ ${answer}
|
|
|
4845
6254
|
async readIndex(bundlePath2) {
|
|
4846
6255
|
const root = this.root(bundlePath2);
|
|
4847
6256
|
const expected = renderIndex(await this.list(bundlePath2));
|
|
4848
|
-
const stored = await (0,
|
|
6257
|
+
const stored = await (0, import_promises10.readFile)((0, import_node_path11.join)(root, INDEX_FILE), "utf8").catch(
|
|
4849
6258
|
() => null
|
|
4850
6259
|
);
|
|
4851
6260
|
if (indexIsStale(stored, expected)) {
|
|
4852
|
-
await this.publish((0,
|
|
6261
|
+
await this.publish((0, import_node_path11.join)(root, INDEX_FILE), expected, true, INDEX_FILE);
|
|
4853
6262
|
this.logger.info?.({
|
|
4854
6263
|
operation: "kb.index.repair",
|
|
4855
6264
|
bundlePath: root,
|
|
@@ -4866,8 +6275,8 @@ ${answer}
|
|
|
4866
6275
|
* knows which agent touched what. So a bad line is surfaced and left alone.
|
|
4867
6276
|
*/
|
|
4868
6277
|
async readLog(bundlePath2) {
|
|
4869
|
-
const raw = await (0,
|
|
4870
|
-
(0,
|
|
6278
|
+
const raw = await (0, import_promises10.readFile)(
|
|
6279
|
+
(0, import_node_path11.join)(this.root(bundlePath2), LOG_FILE),
|
|
4871
6280
|
"utf8"
|
|
4872
6281
|
).catch(() => "");
|
|
4873
6282
|
const result = parseLog(raw);
|
|
@@ -4918,15 +6327,15 @@ ${answer}
|
|
|
4918
6327
|
}
|
|
4919
6328
|
async mutate(bundlePath2, conceptId2, change, entry, changeBody = (body) => body) {
|
|
4920
6329
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
4921
|
-
const before = await (0,
|
|
6330
|
+
const before = await (0, import_promises10.readFile)(target, "utf8").catch(() => null);
|
|
4922
6331
|
if (before === null) throw new KbRecordNotFoundError(conceptId2);
|
|
4923
6332
|
const parsed = this.parse(conceptId2, before);
|
|
4924
6333
|
if (!parsed) throw new KbRecordNotFoundError(conceptId2);
|
|
4925
6334
|
const frontmatter = change(parsed.frontmatter);
|
|
4926
6335
|
const body = changeBody(parsed.body);
|
|
4927
6336
|
const contents = stringifyMarkdownWithFrontmatter(body, frontmatter);
|
|
4928
|
-
const witness = await (0,
|
|
4929
|
-
if (witness === null ||
|
|
6337
|
+
const witness = await (0, import_promises10.readFile)(target, "utf8").catch(() => null);
|
|
6338
|
+
if (witness === null || sha2563(witness) !== sha2563(before)) {
|
|
4930
6339
|
throw new KbWriteConflictError(conceptId2);
|
|
4931
6340
|
}
|
|
4932
6341
|
await this.publish(target, contents, true, conceptId2);
|
|
@@ -4951,20 +6360,20 @@ ${answer}
|
|
|
4951
6360
|
*/
|
|
4952
6361
|
async publish(target, contents, overwrite, conceptId2) {
|
|
4953
6362
|
const staging = `${target}.${process.pid}.tmp`;
|
|
4954
|
-
await (0,
|
|
6363
|
+
await (0, import_promises10.writeFile)(staging, contents, "utf8");
|
|
4955
6364
|
try {
|
|
4956
6365
|
if (overwrite) {
|
|
4957
|
-
await (0,
|
|
6366
|
+
await (0, import_promises10.rename)(staging, target);
|
|
4958
6367
|
return;
|
|
4959
6368
|
}
|
|
4960
|
-
await (0,
|
|
6369
|
+
await (0, import_promises10.link)(staging, target);
|
|
4961
6370
|
} catch (error) {
|
|
4962
6371
|
if (error.code === "EEXIST") {
|
|
4963
6372
|
throw new KbRecordAlreadyExistsError(conceptId2);
|
|
4964
6373
|
}
|
|
4965
6374
|
throw error;
|
|
4966
6375
|
} finally {
|
|
4967
|
-
await (0,
|
|
6376
|
+
await (0, import_promises10.unlink)(staging).catch(() => void 0);
|
|
4968
6377
|
}
|
|
4969
6378
|
}
|
|
4970
6379
|
/**
|
|
@@ -5008,18 +6417,18 @@ ${answer}
|
|
|
5008
6417
|
* file must not fail the mutation it guards.
|
|
5009
6418
|
*/
|
|
5010
6419
|
async ensureGitattributes(root) {
|
|
5011
|
-
const target = (0,
|
|
6420
|
+
const target = (0, import_node_path11.join)(root, GITATTRIBUTES_FILE);
|
|
5012
6421
|
try {
|
|
5013
6422
|
let existing;
|
|
5014
6423
|
try {
|
|
5015
|
-
existing = await (0,
|
|
6424
|
+
existing = await (0, import_promises10.readFile)(target, "utf8");
|
|
5016
6425
|
} catch (error) {
|
|
5017
6426
|
if (error.code !== "ENOENT") throw error;
|
|
5018
6427
|
existing = null;
|
|
5019
6428
|
}
|
|
5020
6429
|
if (existing === null) {
|
|
5021
6430
|
try {
|
|
5022
|
-
await (0,
|
|
6431
|
+
await (0, import_promises10.writeFile)(target, appendUnionMergeLine(""), {
|
|
5023
6432
|
encoding: "utf8",
|
|
5024
6433
|
flag: "wx"
|
|
5025
6434
|
});
|
|
@@ -5040,7 +6449,7 @@ ${answer}
|
|
|
5040
6449
|
return;
|
|
5041
6450
|
}
|
|
5042
6451
|
if (!hasMergeDeclaration(existing)) {
|
|
5043
|
-
await (0,
|
|
6452
|
+
await (0, import_promises10.appendFile)(target, appendUnionMergeLine(existing), "utf8");
|
|
5044
6453
|
this.logger.info?.({
|
|
5045
6454
|
operation: "kb.gitattributes.ensure",
|
|
5046
6455
|
bundlePath: root,
|
|
@@ -5059,7 +6468,7 @@ ${answer}
|
|
|
5059
6468
|
async record(root, entry) {
|
|
5060
6469
|
await this.ensureGitattributes(root);
|
|
5061
6470
|
const line = renderLogEntry({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
|
|
5062
|
-
await (0,
|
|
6471
|
+
await (0, import_promises10.appendFile)((0, import_node_path11.join)(root, LOG_FILE), line, "utf8").catch((error) => {
|
|
5063
6472
|
this.logger.warn?.({
|
|
5064
6473
|
operation: "kb.log.append",
|
|
5065
6474
|
outcome: "failed",
|
|
@@ -5085,18 +6494,18 @@ ${answer}
|
|
|
5085
6494
|
};
|
|
5086
6495
|
}
|
|
5087
6496
|
root(bundlePath2) {
|
|
5088
|
-
return (0,
|
|
6497
|
+
return (0, import_node_path11.resolve)(bundlePath2);
|
|
5089
6498
|
}
|
|
5090
6499
|
// Concept ids are `<type>.<slug>` and map to a single file directly under the
|
|
5091
6500
|
// bundle root; anything carrying a separator would escape it.
|
|
5092
6501
|
recordPath(bundlePath2, conceptId2) {
|
|
5093
|
-
if (conceptId2.includes(
|
|
6502
|
+
if (conceptId2.includes(import_node_path11.sep) || conceptId2.includes("/")) {
|
|
5094
6503
|
throw new KbInvalidConceptIdError(
|
|
5095
6504
|
"concept id must not contain a path separator",
|
|
5096
6505
|
{ conceptId: conceptId2 }
|
|
5097
6506
|
);
|
|
5098
6507
|
}
|
|
5099
|
-
return (0,
|
|
6508
|
+
return (0, import_node_path11.join)(this.root(bundlePath2), `${conceptId2}.md`);
|
|
5100
6509
|
}
|
|
5101
6510
|
};
|
|
5102
6511
|
function estimateTokens(record) {
|
|
@@ -5123,7 +6532,7 @@ function stub(hit) {
|
|
|
5123
6532
|
at: hit.record.frontmatter.generated?.at ?? null
|
|
5124
6533
|
};
|
|
5125
6534
|
}
|
|
5126
|
-
function
|
|
6535
|
+
function matches2(record, needle) {
|
|
5127
6536
|
const { title, description } = record.frontmatter;
|
|
5128
6537
|
return [record.conceptId, title, description, record.body].some(
|
|
5129
6538
|
(field) => field?.toLowerCase().includes(needle)
|
|
@@ -5136,7 +6545,7 @@ function normalizeActor(id) {
|
|
|
5136
6545
|
}
|
|
5137
6546
|
|
|
5138
6547
|
// src/version.ts
|
|
5139
|
-
var VERSION = true ? "0.1.
|
|
6548
|
+
var VERSION = true ? "0.1.18" : "0.0.0-dev";
|
|
5140
6549
|
|
|
5141
6550
|
// src/cli.ts
|
|
5142
6551
|
async function runKbCli(argv) {
|
|
@@ -5193,21 +6602,21 @@ async function runKbCli(argv) {
|
|
|
5193
6602
|
`);
|
|
5194
6603
|
}
|
|
5195
6604
|
function takeLiteral(argv) {
|
|
5196
|
-
const
|
|
5197
|
-
if (
|
|
5198
|
-
return { flags: argv.slice(0,
|
|
6605
|
+
const at2 = argv.indexOf("--");
|
|
6606
|
+
if (at2 === -1) return { flags: argv, literal: [] };
|
|
6607
|
+
return { flags: argv.slice(0, at2), literal: argv.slice(at2 + 1) };
|
|
5199
6608
|
}
|
|
5200
6609
|
function takeBundle(argv) {
|
|
5201
|
-
const
|
|
5202
|
-
if (
|
|
5203
|
-
return { bundle: (0,
|
|
6610
|
+
const at2 = argv.indexOf("--bundle");
|
|
6611
|
+
if (at2 === -1) {
|
|
6612
|
+
return { bundle: (0, import_node_path12.join)(process.cwd(), KB_DIR), explicit: false, rest: argv };
|
|
5204
6613
|
}
|
|
5205
|
-
const bundle = argv[
|
|
6614
|
+
const bundle = argv[at2 + 1];
|
|
5206
6615
|
if (!bundle) die("--bundle requires a path");
|
|
5207
6616
|
return {
|
|
5208
6617
|
bundle,
|
|
5209
6618
|
explicit: true,
|
|
5210
|
-
rest: [...argv.slice(0,
|
|
6619
|
+
rest: [...argv.slice(0, at2), ...argv.slice(at2 + 2)]
|
|
5211
6620
|
};
|
|
5212
6621
|
}
|
|
5213
6622
|
function readStdin() {
|
|
@@ -5230,14 +6639,14 @@ function summarise(description) {
|
|
|
5230
6639
|
return sentence.length > 78 ? `${sentence.slice(0, 75)}\u2026` : sentence;
|
|
5231
6640
|
}
|
|
5232
6641
|
function usage() {
|
|
5233
|
-
const
|
|
6642
|
+
const width2 = Math.max(...KB_COMMANDS.map((command) => command.usage.length));
|
|
5234
6643
|
return [
|
|
5235
6644
|
"strauss-kb \u2014 knowledge base commands",
|
|
5236
6645
|
"",
|
|
5237
6646
|
"Usage: strauss-kb [--bundle PATH] <command> [args]",
|
|
5238
6647
|
"",
|
|
5239
6648
|
...KB_COMMANDS.map(
|
|
5240
|
-
(command) => ` ${command.usage.padEnd(
|
|
6649
|
+
(command) => ` ${command.usage.padEnd(width2)} ${summarise(command.description)}`
|
|
5241
6650
|
),
|
|
5242
6651
|
"",
|
|
5243
6652
|
` --bundle PATH defaults to ./${KB_DIR}`,
|