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