@saasontools/strauss-kb 0.1.16 → 0.1.17
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 +22 -5
- package/README.md +14 -4
- package/dist/{chunk-RINBOAQZ.js → chunk-RMJUGTAQ.js} +2 -2
- package/dist/{chunk-MEZCF646.js → chunk-SA3A2SPY.js} +2 -2
- package/dist/{chunk-H5W53NVU.js → chunk-ZKIQOBHT.js} +949 -284
- package/dist/chunk-ZKIQOBHT.js.map +1 -0
- package/dist/cli-main.cjs +955 -299
- package/dist/cli-main.cjs.map +1 -1
- package/dist/cli-main.js +2 -2
- package/dist/index.cjs +1000 -317
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +214 -5
- package/dist/index.d.ts +214 -5
- package/dist/index.js +23 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp-main.cjs +951 -295
- 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-RMJUGTAQ.js.map} +0 -0
- /package/dist/{chunk-MEZCF646.js.map → chunk-SA3A2SPY.js.map} +0 -0
|
@@ -43,7 +43,13 @@ var kbAnchorSchema = z.object({
|
|
|
43
43
|
/** ISO 8601 timestamp of the last successful resolution. */
|
|
44
44
|
resolved_at: z.string().min(1).optional(),
|
|
45
45
|
/** Line count of the text the hash was taken over. */
|
|
46
|
-
lines: z.number().int().positive().optional()
|
|
46
|
+
lines: z.number().int().positive().optional(),
|
|
47
|
+
/**
|
|
48
|
+
* Which resolver produced the hashed span. Absent means an anchor stamped
|
|
49
|
+
* before resolvers were named, which is read as `regex` — the only one
|
|
50
|
+
* there was. A hash from a different resolver is drift, not a match.
|
|
51
|
+
*/
|
|
52
|
+
resolver: z.enum(["tree-sitter", "regex"]).optional()
|
|
47
53
|
}).strict();
|
|
48
54
|
var kbLinkSchema = z.object({
|
|
49
55
|
target: z.string().min(1),
|
|
@@ -709,18 +715,18 @@ async function readOneRepo(repo, url, declared, context) {
|
|
|
709
715
|
if (!repoUrlIsSafe(url)) return all({ ok: false, reason: "repo-invalid" });
|
|
710
716
|
const cache = cachePathFor(repo, context.cacheDir);
|
|
711
717
|
if (!cache) return all({ ok: false, reason: "remote-unreachable" });
|
|
712
|
-
const
|
|
718
|
+
const rejected2 = /* @__PURE__ */ new Map();
|
|
713
719
|
const usable = [];
|
|
714
720
|
for (const want of wants) {
|
|
715
721
|
const reason = wantReason(want);
|
|
716
722
|
if (reason)
|
|
717
|
-
|
|
723
|
+
rejected2.set(wantKey(repo, want.ref, want.file), { ok: false, reason });
|
|
718
724
|
else usable.push(want);
|
|
719
725
|
}
|
|
720
|
-
if (!usable.length) return
|
|
726
|
+
if (!usable.length) return rejected2;
|
|
721
727
|
wants = usable;
|
|
722
728
|
const opened = await openCache(cache, url, context);
|
|
723
|
-
if (opened) return new Map([...
|
|
729
|
+
if (opened) return new Map([...rejected2, ...all(opened)]);
|
|
724
730
|
const wantsDefault = wants.some((want) => want.ref === void 0);
|
|
725
731
|
const branch = wantsDefault ? await defaultBranch(cache, context) : {};
|
|
726
732
|
const revs = /* @__PURE__ */ new Map();
|
|
@@ -747,7 +753,7 @@ async function readOneRepo(repo, url, declared, context) {
|
|
|
747
753
|
}
|
|
748
754
|
);
|
|
749
755
|
return new Map([
|
|
750
|
-
...
|
|
756
|
+
...rejected2,
|
|
751
757
|
...wants.map(
|
|
752
758
|
(want, at) => [wantKey(repo, want.ref, want.file), reads[at]]
|
|
753
759
|
)
|
|
@@ -799,13 +805,13 @@ async function defaultBranch(cache, context) {
|
|
|
799
805
|
if (!listed.ok) {
|
|
800
806
|
const reason = transportReason(listed.stderr);
|
|
801
807
|
if (reason !== "ref-not-found") {
|
|
802
|
-
const
|
|
803
|
-
return
|
|
808
|
+
const cached3 = await cachedBranch(cache);
|
|
809
|
+
return cached3 ? { name: cached3 } : { reason };
|
|
804
810
|
}
|
|
805
811
|
}
|
|
806
812
|
}
|
|
807
|
-
const
|
|
808
|
-
if (
|
|
813
|
+
const cached2 = await cachedBranch(cache);
|
|
814
|
+
if (cached2) return { name: cached2 };
|
|
809
815
|
return {
|
|
810
816
|
reason: context.offline ? "remote-unreachable" : "default-branch-unknown"
|
|
811
817
|
};
|
|
@@ -832,8 +838,8 @@ async function ensureRev(cache, rev, context) {
|
|
|
832
838
|
cwd: cache
|
|
833
839
|
}
|
|
834
840
|
);
|
|
835
|
-
const
|
|
836
|
-
if (
|
|
841
|
+
const cached2 = have.ok && have.stdout.trim().length > 0;
|
|
842
|
+
if (cached2 && (context.offline || IMMUTABLE_REV.test(rev))) return void 0;
|
|
837
843
|
if (context.offline) return { ok: false, reason: "remote-unreachable" };
|
|
838
844
|
const fetched = await git(
|
|
839
845
|
[
|
|
@@ -849,7 +855,7 @@ async function ensureRev(cache, rev, context) {
|
|
|
849
855
|
);
|
|
850
856
|
if (!fetched.ok) {
|
|
851
857
|
const reason = transportReason(fetched.stderr);
|
|
852
|
-
if (
|
|
858
|
+
if (cached2 && reason !== "ref-not-found") return void 0;
|
|
853
859
|
return { ok: false, reason };
|
|
854
860
|
}
|
|
855
861
|
const head = await git(["rev-parse", "FETCH_HEAD"], { cwd: cache });
|
|
@@ -963,64 +969,577 @@ async function readAnchorFiles(files, read, concurrency = DEFAULT_IO_CONCURRENCY
|
|
|
963
969
|
return new Map(wanted.map((file, at) => [file, results[at]]));
|
|
964
970
|
}
|
|
965
971
|
|
|
972
|
+
// src/grammars/store.ts
|
|
973
|
+
import { createHash, randomBytes } from "crypto";
|
|
974
|
+
import { mkdir as mkdir2, readFile as readFile2, rename, rm, writeFile } from "fs/promises";
|
|
975
|
+
import { homedir as homedir2 } from "os";
|
|
976
|
+
import { dirname, join as join2 } from "path";
|
|
977
|
+
function grammarsCacheRoot(override) {
|
|
978
|
+
return override ?? process.env["STRAUSS_KB_GRAMMARS_DIR"] ?? join2(homedir2(), ".strauss", "grammars");
|
|
979
|
+
}
|
|
980
|
+
function grammarCachePath(root, language, sha2564, extension = "wasm") {
|
|
981
|
+
return join2(root, language, `${sha2564.slice(0, 12)}.${extension}`);
|
|
982
|
+
}
|
|
983
|
+
function sha256(bytes) {
|
|
984
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
985
|
+
}
|
|
986
|
+
function matches(bytes, entry) {
|
|
987
|
+
if (entry.bytes !== void 0 && bytes.byteLength !== entry.bytes)
|
|
988
|
+
return false;
|
|
989
|
+
return sha256(bytes) === entry.sha256;
|
|
990
|
+
}
|
|
991
|
+
async function verifyCached(path, entry) {
|
|
992
|
+
let bytes;
|
|
993
|
+
try {
|
|
994
|
+
bytes = await readFile2(path);
|
|
995
|
+
} catch {
|
|
996
|
+
return false;
|
|
997
|
+
}
|
|
998
|
+
if (matches(bytes, entry)) return true;
|
|
999
|
+
await rm(path, { force: true });
|
|
1000
|
+
return false;
|
|
1001
|
+
}
|
|
1002
|
+
async function writeCached(path, bytes) {
|
|
1003
|
+
await mkdir2(dirname(path), { recursive: true });
|
|
1004
|
+
const temporary = `${path}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
|
|
1005
|
+
try {
|
|
1006
|
+
await writeFile(temporary, bytes);
|
|
1007
|
+
await rename(temporary, path);
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
await rm(temporary, { force: true });
|
|
1010
|
+
throw error;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// src/grammars/manifest.ts
|
|
1015
|
+
import { existsSync, readFileSync } from "fs";
|
|
1016
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
1017
|
+
import { fileURLToPath } from "url";
|
|
1018
|
+
|
|
1019
|
+
// src/grammars/model.ts
|
|
1020
|
+
import { z as z4 } from "zod";
|
|
1021
|
+
var sha2562 = z4.string().regex(/^[0-9a-f]{64}$/);
|
|
1022
|
+
var grammarWasmSchema = z4.object({
|
|
1023
|
+
url: z4.string().min(1),
|
|
1024
|
+
sha256: sha2562,
|
|
1025
|
+
bytes: z4.number().int().positive()
|
|
1026
|
+
});
|
|
1027
|
+
var grammarTagsSchema = z4.object({ url: z4.string().min(1), sha256: sha2562 });
|
|
1028
|
+
var grammarPackSchema = z4.object({
|
|
1029
|
+
package: z4.string().min(1),
|
|
1030
|
+
wasm: grammarWasmSchema,
|
|
1031
|
+
tags: z4.array(grammarTagsSchema),
|
|
1032
|
+
license: z4.string().min(1),
|
|
1033
|
+
extensions: z4.array(z4.string().min(1))
|
|
1034
|
+
});
|
|
1035
|
+
var grammarManifestSchema = z4.object({
|
|
1036
|
+
/** The runtime the packs were proved against. */
|
|
1037
|
+
webTreeSitter: z4.string().min(1),
|
|
1038
|
+
linguist: z4.object({ tag: z4.string().min(1), commit: z4.string().min(1) }),
|
|
1039
|
+
packs: z4.record(z4.string().min(1), grammarPackSchema)
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
// src/grammars/manifest.ts
|
|
1043
|
+
var cached;
|
|
1044
|
+
function grammarManifest() {
|
|
1045
|
+
cached ??= grammarManifestSchema.parse(
|
|
1046
|
+
JSON.parse(readFileSync(grammarsDataPath("manifest.json"), "utf8"))
|
|
1047
|
+
);
|
|
1048
|
+
return cached;
|
|
1049
|
+
}
|
|
1050
|
+
function grammarsDataPath(...segments) {
|
|
1051
|
+
let dir = dirname2(fileURLToPath(import.meta.url));
|
|
1052
|
+
for (let up = 0; up < 5; up++) {
|
|
1053
|
+
const candidate = join3(dir, "grammars");
|
|
1054
|
+
if (existsSync(join3(candidate, "manifest.json")))
|
|
1055
|
+
return join3(candidate, ...segments);
|
|
1056
|
+
dir = dirname2(dir);
|
|
1057
|
+
}
|
|
1058
|
+
throw new Error("grammars/manifest.json is missing from the package");
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// src/grammars/index.ts
|
|
1062
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
1063
|
+
|
|
1064
|
+
// src/grammars/fetch.ts
|
|
1065
|
+
var ATTEMPTS = 3;
|
|
1066
|
+
var BACKOFF_MS = 250;
|
|
1067
|
+
function grammarUrl(url, override) {
|
|
1068
|
+
const base2 = grammarsBaseUrl(override);
|
|
1069
|
+
if (!base2) return url;
|
|
1070
|
+
let root = base2;
|
|
1071
|
+
while (root.endsWith("/")) root = root.slice(0, -1);
|
|
1072
|
+
const pinned = new URL(url);
|
|
1073
|
+
return `${root}${pinned.pathname}${pinned.search}`;
|
|
1074
|
+
}
|
|
1075
|
+
function grammarsBaseUrl(override) {
|
|
1076
|
+
return override ?? process.env["STRAUSS_KB_GRAMMARS_URL"];
|
|
1077
|
+
}
|
|
1078
|
+
async function downloadPart(url, name, entry, options = {}) {
|
|
1079
|
+
const log = options.log ?? ((line) => void process.stderr.write(line));
|
|
1080
|
+
const weight = entry.bytes === void 0 ? "" : ` (${size(entry.bytes)} from manifest)`;
|
|
1081
|
+
log(`strauss-kb: downloading ${name}${weight} from ${url}
|
|
1082
|
+
`);
|
|
1083
|
+
let cause = "";
|
|
1084
|
+
for (let attempt = 1; attempt <= ATTEMPTS; attempt++) {
|
|
1085
|
+
const outcome = await attemptDownload(url, entry, options.fetchTimeoutMs);
|
|
1086
|
+
if ("bytes" in outcome) return outcome;
|
|
1087
|
+
cause = outcome.cause;
|
|
1088
|
+
log(
|
|
1089
|
+
`strauss-kb: ${name} attempt ${attempt}/${ATTEMPTS} failed: ${cause}
|
|
1090
|
+
`
|
|
1091
|
+
);
|
|
1092
|
+
if (!outcome.retry) break;
|
|
1093
|
+
if (attempt < ATTEMPTS) await pause(BACKOFF_MS * attempt);
|
|
1094
|
+
}
|
|
1095
|
+
log(`strauss-kb: ${name} not downloaded: ${cause}
|
|
1096
|
+
`);
|
|
1097
|
+
return { cause };
|
|
1098
|
+
}
|
|
1099
|
+
async function attemptDownload(url, entry, timeoutMs) {
|
|
1100
|
+
try {
|
|
1101
|
+
const response = await fetch(url, {
|
|
1102
|
+
signal: AbortSignal.timeout(fetchTimeoutMs(timeoutMs))
|
|
1103
|
+
});
|
|
1104
|
+
if (!response.ok) {
|
|
1105
|
+
return {
|
|
1106
|
+
cause: `HTTP ${response.status}`,
|
|
1107
|
+
retry: response.status >= 500 || response.status === 429
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
1111
|
+
if (!matches(bytes, entry))
|
|
1112
|
+
return { cause: "sha256 mismatch", retry: false };
|
|
1113
|
+
return { bytes };
|
|
1114
|
+
} catch (error) {
|
|
1115
|
+
const timedOut = error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
|
|
1116
|
+
return { cause: timedOut ? "timeout" : "network error", retry: true };
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
function size(bytes) {
|
|
1120
|
+
return bytes >= 1024 * 1024 ? `${(bytes / (1024 * 1024)).toFixed(1)} MB` : `${Math.round(bytes / 1024)} KB`;
|
|
1121
|
+
}
|
|
1122
|
+
function pause(ms) {
|
|
1123
|
+
return new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// src/grammars/index.ts
|
|
1127
|
+
var inFlight = /* @__PURE__ */ new Map();
|
|
1128
|
+
var missing = /* @__PURE__ */ new Map();
|
|
1129
|
+
var uncompilable = /* @__PURE__ */ new Map();
|
|
1130
|
+
var rejected = /* @__PURE__ */ new Map();
|
|
1131
|
+
function grammarsDownloadDisabled() {
|
|
1132
|
+
return process.env["STRAUSS_KB_GRAMMARS"] === "off";
|
|
1133
|
+
}
|
|
1134
|
+
async function ensureGrammar(language, options = {}) {
|
|
1135
|
+
const pack2 = grammarManifest().packs[language];
|
|
1136
|
+
if (!pack2) return null;
|
|
1137
|
+
const root = grammarsCacheRoot(options.cacheRoot);
|
|
1138
|
+
const wasm = grammarCachePath(root, language, pack2.wasm.sha256);
|
|
1139
|
+
const key = `${wasm} ${grammarsBaseUrl(options.baseUrl) ?? ""}`;
|
|
1140
|
+
const existing = inFlight.get(key);
|
|
1141
|
+
if (existing) return existing;
|
|
1142
|
+
const pending = (async () => {
|
|
1143
|
+
const grammar = await ensurePart(
|
|
1144
|
+
wasm,
|
|
1145
|
+
`tree-sitter-${language}`,
|
|
1146
|
+
pack2.wasm,
|
|
1147
|
+
options
|
|
1148
|
+
);
|
|
1149
|
+
if (grammar !== true)
|
|
1150
|
+
return miss(language, `grammar tree-sitter-${language}`, grammar);
|
|
1151
|
+
const parts = [];
|
|
1152
|
+
const total = pack2.tags.length;
|
|
1153
|
+
for (const [at, part] of pack2.tags.entries()) {
|
|
1154
|
+
const name = `${language} tags${total > 1 ? ` part ${at + 1}/${total}` : ""}`;
|
|
1155
|
+
const path = grammarCachePath(root, language, part.sha256, "scm");
|
|
1156
|
+
const held = await ensurePart(path, name, part, options);
|
|
1157
|
+
if (held !== true) return miss(language, name, held);
|
|
1158
|
+
parts.push(`; ${part.url}
|
|
1159
|
+
${lf(await readFile3(path, "utf8"))}`);
|
|
1160
|
+
}
|
|
1161
|
+
missing.delete(language);
|
|
1162
|
+
return { wasm, query: total ? parts.join("\n") : void 0 };
|
|
1163
|
+
})();
|
|
1164
|
+
inFlight.set(key, pending);
|
|
1165
|
+
const result = await pending;
|
|
1166
|
+
if (result === null) inFlight.delete(key);
|
|
1167
|
+
return result;
|
|
1168
|
+
}
|
|
1169
|
+
async function ensurePart(path, name, entry, options) {
|
|
1170
|
+
if (await verifyCached(path, entry)) return true;
|
|
1171
|
+
if (options.offline === true || grammarsDownloadDisabled()) return {};
|
|
1172
|
+
const download = await downloadPart(
|
|
1173
|
+
grammarUrl(entry.url, options.baseUrl),
|
|
1174
|
+
name,
|
|
1175
|
+
entry,
|
|
1176
|
+
options
|
|
1177
|
+
);
|
|
1178
|
+
if ("cause" in download) return { cause: download.cause };
|
|
1179
|
+
await writeCached(path, download.bytes).catch(() => null);
|
|
1180
|
+
return true;
|
|
1181
|
+
}
|
|
1182
|
+
function miss(language, subject, failure) {
|
|
1183
|
+
missing.set(language, { subject, ...failure });
|
|
1184
|
+
return null;
|
|
1185
|
+
}
|
|
1186
|
+
function lf(body) {
|
|
1187
|
+
return body.replace(/\r\n/g, "\n");
|
|
1188
|
+
}
|
|
1189
|
+
function noteUncompilableQuery(language, cause) {
|
|
1190
|
+
uncompilable.set(language, cause);
|
|
1191
|
+
}
|
|
1192
|
+
function noteRejectedGrammar(language, cause) {
|
|
1193
|
+
rejected.set(language, cause);
|
|
1194
|
+
}
|
|
1195
|
+
function grammarHints() {
|
|
1196
|
+
const manifest = grammarManifest();
|
|
1197
|
+
const packs = manifest.packs;
|
|
1198
|
+
const lines = /* @__PURE__ */ new Map();
|
|
1199
|
+
for (const [language, { subject, cause }] of missing)
|
|
1200
|
+
lines.set(
|
|
1201
|
+
language,
|
|
1202
|
+
`${subject} not cached${cause ? ` (${cause})` : ""}; run online once, or set STRAUSS_KB_GRAMMARS_DIR`
|
|
1203
|
+
);
|
|
1204
|
+
for (const [language, cause] of rejected)
|
|
1205
|
+
lines.set(
|
|
1206
|
+
language,
|
|
1207
|
+
`${packs[language]?.package ?? `tree-sitter-${language}`} rejected by web-tree-sitter ${manifest.webTreeSitter}${cause ? `: ${cause}` : ""}; re-pin with pnpm grammars pin ${language}`
|
|
1208
|
+
);
|
|
1209
|
+
for (const [language, cause] of uncompilable)
|
|
1210
|
+
lines.set(
|
|
1211
|
+
language,
|
|
1212
|
+
`tags query for ${language} does not compile against ${packs[language]?.package ?? `tree-sitter-${language}`}: ${cause}; re-pin with pnpm grammars pin ${language}`
|
|
1213
|
+
);
|
|
1214
|
+
return [...lines].sort(([a], [b]) => a.localeCompare(b)).map(([, line]) => line);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// src/tree-sitter-resolver/languages.ts
|
|
1218
|
+
import { extname } from "path";
|
|
1219
|
+
var table;
|
|
1220
|
+
function extensionTable() {
|
|
1221
|
+
const manifest = grammarManifest();
|
|
1222
|
+
if (table?.of !== manifest)
|
|
1223
|
+
table = {
|
|
1224
|
+
of: manifest,
|
|
1225
|
+
extensions: Object.fromEntries(
|
|
1226
|
+
Object.entries(manifest.packs).flatMap(
|
|
1227
|
+
([language, pack2]) => pack2.extensions.map((extension) => [extension, language])
|
|
1228
|
+
)
|
|
1229
|
+
)
|
|
1230
|
+
};
|
|
1231
|
+
return table.extensions;
|
|
1232
|
+
}
|
|
1233
|
+
function hasQuery(language) {
|
|
1234
|
+
return (grammarManifest().packs[language]?.tags.length ?? 0) > 0;
|
|
1235
|
+
}
|
|
1236
|
+
function languageForFile(file) {
|
|
1237
|
+
const language = extensionTable()[extname(file).toLowerCase()];
|
|
1238
|
+
return language && hasQuery(language) ? language : void 0;
|
|
1239
|
+
}
|
|
1240
|
+
function treeSitterLanguages() {
|
|
1241
|
+
return [...new Set(Object.values(extensionTable()))].filter(hasQuery).sort();
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// src/tree-sitter-resolver/resolver.ts
|
|
1245
|
+
import { createHash as createHash2 } from "crypto";
|
|
1246
|
+
import { Language, Parser, Query } from "web-tree-sitter";
|
|
1247
|
+
|
|
1248
|
+
// src/tree-sitter-resolver/definitions.ts
|
|
1249
|
+
var SCOPE_ONLY = "reference.implementation";
|
|
1250
|
+
function index(tree, query) {
|
|
1251
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1252
|
+
for (const match of query.matches(tree.rootNode)) {
|
|
1253
|
+
const nameNode = match.captures.find((capture) => capture.name === "name");
|
|
1254
|
+
const defNode = match.captures.find(
|
|
1255
|
+
(capture) => capture.name.startsWith("definition.") || capture.name === SCOPE_ONLY
|
|
1256
|
+
);
|
|
1257
|
+
if (!nameNode || !defNode) continue;
|
|
1258
|
+
const candidate = {
|
|
1259
|
+
node: defNode.node,
|
|
1260
|
+
name: nameNode.node.text,
|
|
1261
|
+
target: defNode.name !== SCOPE_ONLY
|
|
1262
|
+
};
|
|
1263
|
+
const existing = byName.get(nameNode.node.id);
|
|
1264
|
+
if (existing && width(existing.node) <= width(candidate.node)) continue;
|
|
1265
|
+
byName.set(nameNode.node.id, candidate);
|
|
1266
|
+
}
|
|
1267
|
+
const definitions = [...byName.values()];
|
|
1268
|
+
return {
|
|
1269
|
+
tree,
|
|
1270
|
+
byNodeId: new Map(
|
|
1271
|
+
definitions.map((definition) => [definition.node.id, definition])
|
|
1272
|
+
),
|
|
1273
|
+
definitions
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
function select(parsed, wanted) {
|
|
1277
|
+
const matches3 = parsed.definitions.filter(
|
|
1278
|
+
(definition) => definition.target && endsWith(chainOf(definition, parsed.byNodeId), wanted)
|
|
1279
|
+
);
|
|
1280
|
+
if (matches3.length < 2) return matches3;
|
|
1281
|
+
const bodied = matches3.filter(
|
|
1282
|
+
(definition) => definition.node.childForFieldName("body") !== null
|
|
1283
|
+
);
|
|
1284
|
+
return bodied.length === 1 ? bodied : matches3;
|
|
1285
|
+
}
|
|
1286
|
+
function chainOf(definition, byNodeId) {
|
|
1287
|
+
const chain = [definition.name];
|
|
1288
|
+
const receiver = definition.node.childForFieldName("receiver");
|
|
1289
|
+
const type = receiver && typeNameIn(receiver);
|
|
1290
|
+
if (type) chain.unshift(type);
|
|
1291
|
+
for (let node = definition.node.parent; node; node = node.parent) {
|
|
1292
|
+
const enclosing = byNodeId.get(node.id);
|
|
1293
|
+
if (enclosing && enclosing.node !== definition.node)
|
|
1294
|
+
chain.unshift(enclosing.name);
|
|
1295
|
+
}
|
|
1296
|
+
return chain;
|
|
1297
|
+
}
|
|
1298
|
+
function typeNameIn(receiver) {
|
|
1299
|
+
const stack = [receiver];
|
|
1300
|
+
while (stack.length) {
|
|
1301
|
+
const node = stack.pop();
|
|
1302
|
+
if (node.type === "type_identifier") return node.text;
|
|
1303
|
+
for (let at = 0; at < node.childCount; at++) {
|
|
1304
|
+
const child = node.child(at);
|
|
1305
|
+
if (child) stack.push(child);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
return void 0;
|
|
1309
|
+
}
|
|
1310
|
+
function endsWith(chain, wanted) {
|
|
1311
|
+
if (wanted.length > chain.length) return false;
|
|
1312
|
+
const offset = chain.length - wanted.length;
|
|
1313
|
+
return wanted.every((segment, at) => chain[offset + at] === segment);
|
|
1314
|
+
}
|
|
1315
|
+
function width(node) {
|
|
1316
|
+
return node.endIndex - node.startIndex;
|
|
1317
|
+
}
|
|
1318
|
+
function spanOf(definition, source) {
|
|
1319
|
+
let start = definition.node;
|
|
1320
|
+
let end = definition.node;
|
|
1321
|
+
for (let sibling = start.previousSibling; sibling?.type === "decorator"; sibling = sibling.previousSibling) {
|
|
1322
|
+
start = sibling;
|
|
1323
|
+
}
|
|
1324
|
+
const parent = end.parent;
|
|
1325
|
+
if (parent?.type === "export_statement" && parent.childForFieldName("declaration")?.id === end.id) {
|
|
1326
|
+
start = parent;
|
|
1327
|
+
end = parent;
|
|
1328
|
+
}
|
|
1329
|
+
const lines = source.split("\n");
|
|
1330
|
+
const startLine = start.startPosition.row;
|
|
1331
|
+
const endLine = end.endPosition.column === 0 && end.endPosition.row > startLine ? end.endPosition.row - 1 : end.endPosition.row;
|
|
1332
|
+
return {
|
|
1333
|
+
text: lines.slice(startLine, endLine + 1).join("\n"),
|
|
1334
|
+
startLine: startLine + 1,
|
|
1335
|
+
endLine: endLine + 1
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// src/tree-sitter-resolver/resolver.ts
|
|
1340
|
+
var TREE_CACHE_LIMIT = 32;
|
|
1341
|
+
var TreeSitterResolver = class {
|
|
1342
|
+
name = "tree-sitter";
|
|
1343
|
+
grammars;
|
|
1344
|
+
loaded = /* @__PURE__ */ new Map();
|
|
1345
|
+
trees = /* @__PURE__ */ new Map();
|
|
1346
|
+
parser;
|
|
1347
|
+
initialized = false;
|
|
1348
|
+
/** Cache effectiveness, for tests and for the latency numbers. */
|
|
1349
|
+
stats = { parses: 0, cacheHits: 0 };
|
|
1350
|
+
constructor(options = {}) {
|
|
1351
|
+
this.grammars = options;
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* Loads the grammars these files need, once per language per process,
|
|
1355
|
+
* downloading each one on first use.
|
|
1356
|
+
*
|
|
1357
|
+
* A grammar that will not load is remembered as unavailable rather than
|
|
1358
|
+
* retried per anchor, and never throws: an unobtainable WASM is a finding.
|
|
1359
|
+
*/
|
|
1360
|
+
async prepare(files) {
|
|
1361
|
+
const wanted = /* @__PURE__ */ new Set();
|
|
1362
|
+
for (const file of files) {
|
|
1363
|
+
const language = languageForFile(file);
|
|
1364
|
+
if (language && !this.loaded.has(language)) wanted.add(language);
|
|
1365
|
+
}
|
|
1366
|
+
if (!wanted.size) return;
|
|
1367
|
+
if (!this.initialized) {
|
|
1368
|
+
try {
|
|
1369
|
+
await Parser.init();
|
|
1370
|
+
this.parser = new Parser();
|
|
1371
|
+
this.initialized = true;
|
|
1372
|
+
} catch {
|
|
1373
|
+
for (const language of wanted) this.loaded.set(language, null);
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
const languages = [...wanted];
|
|
1378
|
+
const loaded = await mapLimit(
|
|
1379
|
+
languages,
|
|
1380
|
+
Math.min(DEFAULT_IO_CONCURRENCY, languages.length),
|
|
1381
|
+
(language) => this.load(language)
|
|
1382
|
+
);
|
|
1383
|
+
languages.forEach(
|
|
1384
|
+
(language, at) => this.loaded.set(language, loaded[at] ?? null)
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1387
|
+
/**
|
|
1388
|
+
* An unobtainable grammar, one this runtime refuses, and a query that will
|
|
1389
|
+
* not compile are three faults with three repairs; all are reported through
|
|
1390
|
+
* the grammars module so every hint has one home.
|
|
1391
|
+
*/
|
|
1392
|
+
async load(language) {
|
|
1393
|
+
let pack2;
|
|
1394
|
+
try {
|
|
1395
|
+
pack2 = await ensureGrammar(language, this.grammars);
|
|
1396
|
+
} catch {
|
|
1397
|
+
return null;
|
|
1398
|
+
}
|
|
1399
|
+
if (!pack2?.query) return null;
|
|
1400
|
+
let grammar;
|
|
1401
|
+
try {
|
|
1402
|
+
grammar = await Language.load(pack2.wasm);
|
|
1403
|
+
} catch (error) {
|
|
1404
|
+
noteRejectedGrammar(language, why(error));
|
|
1405
|
+
return null;
|
|
1406
|
+
}
|
|
1407
|
+
try {
|
|
1408
|
+
return { language: grammar, query: new Query(grammar, pack2.query) };
|
|
1409
|
+
} catch (error) {
|
|
1410
|
+
noteUncompilableQuery(language, why(error));
|
|
1411
|
+
return null;
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
/**
|
|
1415
|
+
* Abstains on an extension with no grammar so the regex resolver gets a
|
|
1416
|
+
* turn; reports `resolver-unavailable` when the grammar exists in principle
|
|
1417
|
+
* but could not be loaded, because falling back there would silently trade a
|
|
1418
|
+
* precise span for a guessed one.
|
|
1419
|
+
*/
|
|
1420
|
+
attempt(source, symbol, file) {
|
|
1421
|
+
const language = file ? languageForFile(file) : void 0;
|
|
1422
|
+
if (!language) return { kind: "abstain" };
|
|
1423
|
+
if (!this.loaded.has(language)) return { kind: "abstain" };
|
|
1424
|
+
const loaded = this.loaded.get(language);
|
|
1425
|
+
if (!loaded) return { kind: "unresolved", reason: "resolver-unavailable" };
|
|
1426
|
+
const parsed = this.parse(language, loaded, source);
|
|
1427
|
+
if (!parsed) return { kind: "unresolved", reason: "resolver-unavailable" };
|
|
1428
|
+
const wanted = symbol.split(".").filter(Boolean);
|
|
1429
|
+
if (!wanted.length)
|
|
1430
|
+
return { kind: "unresolved", reason: "symbol-not-found" };
|
|
1431
|
+
const matches3 = select(parsed, wanted);
|
|
1432
|
+
if (!matches3.length)
|
|
1433
|
+
return { kind: "unresolved", reason: "symbol-not-found" };
|
|
1434
|
+
if (matches3.length > 1)
|
|
1435
|
+
return { kind: "unresolved", reason: "symbol-ambiguous" };
|
|
1436
|
+
return { kind: "resolved", span: spanOf(matches3[0], source) };
|
|
1437
|
+
}
|
|
1438
|
+
resolve(source, symbol, file) {
|
|
1439
|
+
const attempt = this.attempt(source, symbol, file);
|
|
1440
|
+
return attempt.kind === "resolved" ? attempt.span : null;
|
|
1441
|
+
}
|
|
1442
|
+
/** Parsed trees are keyed by content hash, so an unchanged file parses once. */
|
|
1443
|
+
parse(language, loaded, source) {
|
|
1444
|
+
const key = `${language}:${createHash2("sha256").update(source).digest("hex")}`;
|
|
1445
|
+
const cached2 = this.trees.get(key);
|
|
1446
|
+
if (cached2) {
|
|
1447
|
+
this.stats.cacheHits += 1;
|
|
1448
|
+
return cached2;
|
|
1449
|
+
}
|
|
1450
|
+
const parser = this.parser;
|
|
1451
|
+
if (!parser) return null;
|
|
1452
|
+
let parsed;
|
|
1453
|
+
try {
|
|
1454
|
+
parser.setLanguage(loaded.language);
|
|
1455
|
+
const tree = parser.parse(source);
|
|
1456
|
+
if (!tree) return null;
|
|
1457
|
+
parsed = index(tree, loaded.query);
|
|
1458
|
+
} catch {
|
|
1459
|
+
return null;
|
|
1460
|
+
}
|
|
1461
|
+
this.stats.parses += 1;
|
|
1462
|
+
if (this.trees.size >= TREE_CACHE_LIMIT) {
|
|
1463
|
+
const oldest = this.trees.keys().next();
|
|
1464
|
+
if (!oldest.done) {
|
|
1465
|
+
this.trees.get(oldest.value)?.tree.delete();
|
|
1466
|
+
this.trees.delete(oldest.value);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
this.trees.set(key, parsed);
|
|
1470
|
+
return parsed;
|
|
1471
|
+
}
|
|
1472
|
+
/** Drops cached trees. Grammars stay loaded — they are immutable. */
|
|
1473
|
+
reset() {
|
|
1474
|
+
for (const parsed of this.trees.values()) parsed.tree.delete();
|
|
1475
|
+
this.trees.clear();
|
|
1476
|
+
this.stats.parses = 0;
|
|
1477
|
+
this.stats.cacheHits = 0;
|
|
1478
|
+
}
|
|
1479
|
+
};
|
|
1480
|
+
function why(error) {
|
|
1481
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
1482
|
+
return text || "no reason given";
|
|
1483
|
+
}
|
|
1484
|
+
|
|
966
1485
|
// src/anchor-resolver/resolver.ts
|
|
967
|
-
import { createHash } from "crypto";
|
|
1486
|
+
import { createHash as createHash3 } from "crypto";
|
|
968
1487
|
var PARENT_SCOPE_LINES = 50;
|
|
969
1488
|
var CLEAN_STATE = { blockComment: false, template: false };
|
|
970
1489
|
function stripLine(line, state) {
|
|
971
1490
|
let out = "";
|
|
972
|
-
let
|
|
1491
|
+
let index2 = 0;
|
|
973
1492
|
let { blockComment, template } = state;
|
|
974
|
-
while (
|
|
975
|
-
const char = line[
|
|
976
|
-
const next = line[
|
|
1493
|
+
while (index2 < line.length) {
|
|
1494
|
+
const char = line[index2];
|
|
1495
|
+
const next = line[index2 + 1];
|
|
977
1496
|
if (blockComment) {
|
|
978
1497
|
if (char === "*" && next === "/") {
|
|
979
1498
|
blockComment = false;
|
|
980
|
-
|
|
1499
|
+
index2 += 2;
|
|
981
1500
|
continue;
|
|
982
1501
|
}
|
|
983
|
-
|
|
1502
|
+
index2 += 1;
|
|
984
1503
|
continue;
|
|
985
1504
|
}
|
|
986
1505
|
if (template) {
|
|
987
1506
|
if (char === "\\") {
|
|
988
|
-
|
|
1507
|
+
index2 += 2;
|
|
989
1508
|
continue;
|
|
990
1509
|
}
|
|
991
1510
|
if (char === "`") template = false;
|
|
992
|
-
|
|
1511
|
+
index2 += 1;
|
|
993
1512
|
continue;
|
|
994
1513
|
}
|
|
995
1514
|
if (char === "/" && next === "*") {
|
|
996
1515
|
blockComment = true;
|
|
997
|
-
|
|
1516
|
+
index2 += 2;
|
|
998
1517
|
continue;
|
|
999
1518
|
}
|
|
1000
1519
|
if (char === "/" && next === "/") break;
|
|
1001
1520
|
if (char === "`") {
|
|
1002
1521
|
template = true;
|
|
1003
|
-
|
|
1522
|
+
index2 += 1;
|
|
1004
1523
|
continue;
|
|
1005
1524
|
}
|
|
1006
1525
|
if (char === "'" || char === '"') {
|
|
1007
1526
|
const quote = char;
|
|
1008
|
-
|
|
1009
|
-
while (
|
|
1010
|
-
if (line[
|
|
1011
|
-
|
|
1527
|
+
index2 += 1;
|
|
1528
|
+
while (index2 < line.length) {
|
|
1529
|
+
if (line[index2] === "\\") {
|
|
1530
|
+
index2 += 2;
|
|
1012
1531
|
continue;
|
|
1013
1532
|
}
|
|
1014
|
-
if (line[
|
|
1015
|
-
|
|
1533
|
+
if (line[index2] === quote) {
|
|
1534
|
+
index2 += 1;
|
|
1016
1535
|
break;
|
|
1017
1536
|
}
|
|
1018
|
-
|
|
1537
|
+
index2 += 1;
|
|
1019
1538
|
}
|
|
1020
1539
|
continue;
|
|
1021
1540
|
}
|
|
1022
1541
|
out += char;
|
|
1023
|
-
|
|
1542
|
+
index2 += 1;
|
|
1024
1543
|
}
|
|
1025
1544
|
return { code: out, state: { blockComment, template } };
|
|
1026
1545
|
}
|
|
@@ -1035,8 +1554,8 @@ function captureBraceBlock(lines, matchLine) {
|
|
|
1035
1554
|
let depth = 0;
|
|
1036
1555
|
let opened = false;
|
|
1037
1556
|
let state = CLEAN_STATE;
|
|
1038
|
-
for (let
|
|
1039
|
-
const stripped = stripLine(lines[
|
|
1557
|
+
for (let index2 = matchLine; index2 < lines.length; index2++) {
|
|
1558
|
+
const stripped = stripLine(lines[index2] ?? "", state);
|
|
1040
1559
|
state = stripped.state;
|
|
1041
1560
|
for (const char of stripped.code) {
|
|
1042
1561
|
if (char === "{") {
|
|
@@ -1045,10 +1564,10 @@ function captureBraceBlock(lines, matchLine) {
|
|
|
1045
1564
|
} else if (char === "}") {
|
|
1046
1565
|
depth = Math.max(0, depth - 1);
|
|
1047
1566
|
} else if (char === ";" && !opened) {
|
|
1048
|
-
return span(lines, matchLine,
|
|
1567
|
+
return span(lines, matchLine, index2);
|
|
1049
1568
|
}
|
|
1050
1569
|
}
|
|
1051
|
-
if (opened && depth === 0) return span(lines, matchLine,
|
|
1570
|
+
if (opened && depth === 0) return span(lines, matchLine, index2);
|
|
1052
1571
|
}
|
|
1053
1572
|
return null;
|
|
1054
1573
|
}
|
|
@@ -1057,22 +1576,22 @@ function captureIndentedBlock(lines, matchLine) {
|
|
|
1057
1576
|
const header = lines[matchLine] ?? "";
|
|
1058
1577
|
const indent = header.length - header.trimStart().length;
|
|
1059
1578
|
let headerEnd = -1;
|
|
1060
|
-
for (let
|
|
1061
|
-
const code = stripLine(lines[
|
|
1579
|
+
for (let index2 = matchLine; index2 < lines.length && index2 <= matchLine + 20; index2++) {
|
|
1580
|
+
const code = stripLine(lines[index2] ?? "", CLEAN_STATE).code.trimEnd();
|
|
1062
1581
|
if (code.endsWith(":")) {
|
|
1063
|
-
headerEnd =
|
|
1582
|
+
headerEnd = index2;
|
|
1064
1583
|
break;
|
|
1065
1584
|
}
|
|
1066
|
-
if (code.includes(":")) return span(lines, matchLine,
|
|
1585
|
+
if (code.includes(":")) return span(lines, matchLine, index2);
|
|
1067
1586
|
}
|
|
1068
1587
|
if (headerEnd === -1) return null;
|
|
1069
1588
|
let end = headerEnd;
|
|
1070
|
-
for (let
|
|
1071
|
-
const line = lines[
|
|
1589
|
+
for (let index2 = headerEnd + 1; index2 < lines.length; index2++) {
|
|
1590
|
+
const line = lines[index2] ?? "";
|
|
1072
1591
|
if (line.trim() === "") continue;
|
|
1073
1592
|
const lineIndent = line.length - line.trimStart().length;
|
|
1074
1593
|
if (lineIndent <= indent) break;
|
|
1075
|
-
end =
|
|
1594
|
+
end = index2;
|
|
1076
1595
|
}
|
|
1077
1596
|
return end === headerEnd ? null : span(lines, matchLine, end);
|
|
1078
1597
|
}
|
|
@@ -1096,11 +1615,11 @@ var regexResolver = {
|
|
|
1096
1615
|
const lines = source.split("\n");
|
|
1097
1616
|
for (const tier of TIERS) {
|
|
1098
1617
|
const pattern = tier(escaped);
|
|
1099
|
-
let candidates = lines.map((line,
|
|
1618
|
+
let candidates = lines.map((line, index2) => ({ line, index: index2 })).filter((entry) => pattern.test(entry.line)).map((entry) => entry.index);
|
|
1100
1619
|
if (!candidates.length) continue;
|
|
1101
1620
|
if (parentPattern && candidates.length > 1) {
|
|
1102
1621
|
const distances = candidates.map(
|
|
1103
|
-
(
|
|
1622
|
+
(index2) => distanceToParent(lines, index2, parentPattern)
|
|
1104
1623
|
);
|
|
1105
1624
|
const nearest = Math.min(...distances);
|
|
1106
1625
|
if (Number.isFinite(nearest)) {
|
|
@@ -1117,34 +1636,79 @@ var regexResolver = {
|
|
|
1117
1636
|
function escapeRegExp(value) {
|
|
1118
1637
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1119
1638
|
}
|
|
1120
|
-
function distanceToParent(lines,
|
|
1121
|
-
const floor = Math.max(0,
|
|
1122
|
-
for (let at =
|
|
1123
|
-
if (parent.test(lines[at] ?? "")) return
|
|
1639
|
+
function distanceToParent(lines, index2, parent) {
|
|
1640
|
+
const floor = Math.max(0, index2 - PARENT_SCOPE_LINES);
|
|
1641
|
+
for (let at = index2; at >= floor; at--) {
|
|
1642
|
+
if (parent.test(lines[at] ?? "")) return index2 - at;
|
|
1124
1643
|
}
|
|
1125
1644
|
return Number.POSITIVE_INFINITY;
|
|
1126
1645
|
}
|
|
1127
1646
|
function hashAnchorText(text) {
|
|
1128
|
-
return `sha256:${
|
|
1647
|
+
return `sha256:${createHash3("sha256").update(text.replace(/\r\n/g, "\n")).digest("hex")}`;
|
|
1129
1648
|
}
|
|
1130
1649
|
function resolveAnchor(source, anchor, resolver = regexResolver) {
|
|
1650
|
+
const outcome = resolveAnchorSpan(source, anchor, [resolver]);
|
|
1651
|
+
return outcome.ok ? outcome.span : null;
|
|
1652
|
+
}
|
|
1653
|
+
function resolveAnchorSpan(source, anchor, resolvers = [regexResolver]) {
|
|
1131
1654
|
const normalized = source.replace(/\r\n/g, "\n");
|
|
1132
1655
|
if (!anchor.symbol) {
|
|
1133
1656
|
const lines = normalized.split("\n");
|
|
1134
1657
|
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
1135
1658
|
return {
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1659
|
+
ok: true,
|
|
1660
|
+
span: {
|
|
1661
|
+
text: normalized,
|
|
1662
|
+
startLine: 1,
|
|
1663
|
+
endLine: Math.max(1, lines.length)
|
|
1664
|
+
}
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
for (const resolver of resolvers) {
|
|
1668
|
+
const attempt = resolver.attempt ? resolver.attempt(normalized, anchor.symbol, anchor.file) : fromResolve(resolver, normalized, anchor.symbol, anchor.file);
|
|
1669
|
+
if (attempt.kind === "abstain") continue;
|
|
1670
|
+
if (attempt.kind === "unresolved") {
|
|
1671
|
+
if (attempt.reason === "symbol-not-found") continue;
|
|
1672
|
+
return { ok: false, reason: attempt.reason };
|
|
1673
|
+
}
|
|
1674
|
+
return {
|
|
1675
|
+
ok: true,
|
|
1676
|
+
span: attempt.span,
|
|
1677
|
+
...isResolverName(resolver.name) ? { resolver: resolver.name } : {}
|
|
1139
1678
|
};
|
|
1140
1679
|
}
|
|
1141
|
-
return
|
|
1680
|
+
return { ok: false, reason: "symbol-not-found" };
|
|
1681
|
+
}
|
|
1682
|
+
function fromResolve(resolver, source, symbol, file) {
|
|
1683
|
+
const span2 = resolver.resolve(source, symbol, file);
|
|
1684
|
+
return span2 ? { kind: "resolved", span: span2 } : { kind: "unresolved", reason: "symbol-not-found" };
|
|
1685
|
+
}
|
|
1686
|
+
function isResolverName(name) {
|
|
1687
|
+
return name === "tree-sitter" || name === "regex";
|
|
1688
|
+
}
|
|
1689
|
+
async function prepareResolvers(resolvers, files) {
|
|
1690
|
+
for (const resolver of resolvers) await resolver.prepare?.(files);
|
|
1691
|
+
}
|
|
1692
|
+
function defaultAnchorResolvers(grammars = {}) {
|
|
1693
|
+
return [new TreeSitterResolver(grammars), regexResolver];
|
|
1694
|
+
}
|
|
1695
|
+
function resolverChanged(source, anchor, produced) {
|
|
1696
|
+
const previous = anchor.resolver ?? "regex";
|
|
1697
|
+
if (!produced || !anchor.symbol || previous === produced) return false;
|
|
1698
|
+
if (previous !== "regex") return false;
|
|
1699
|
+
const before = regexResolver.resolve(
|
|
1700
|
+
source.replace(/\r\n/g, "\n"),
|
|
1701
|
+
anchor.symbol
|
|
1702
|
+
);
|
|
1703
|
+
return before !== null && hashAnchorText(before.text) === anchor.hash;
|
|
1142
1704
|
}
|
|
1143
1705
|
|
|
1144
1706
|
// src/anchor-resolver/drift.ts
|
|
1145
1707
|
async function detectAnchorDrift(records, options = {}) {
|
|
1146
1708
|
const repoRoot = options.repoRoot ?? process.cwd();
|
|
1147
|
-
const
|
|
1709
|
+
const resolvers = options.resolvers ?? (options.resolver ? [options.resolver] : defaultAnchorResolvers({
|
|
1710
|
+
offline: options.remote?.offline === true
|
|
1711
|
+
}));
|
|
1148
1712
|
const origin = new LazyOrigin(repoRoot);
|
|
1149
1713
|
const planned = /* @__PURE__ */ new Map();
|
|
1150
1714
|
let declaresRepo = false;
|
|
@@ -1182,12 +1746,16 @@ async function detectAnchorDrift(records, options = {}) {
|
|
|
1182
1746
|
),
|
|
1183
1747
|
(options.readRemote ?? readRemoteAnchors)(wants, options.remote ?? {})
|
|
1184
1748
|
]);
|
|
1749
|
+
await prepareResolvers(resolvers, [
|
|
1750
|
+
...files,
|
|
1751
|
+
...wants.map((want) => want.file)
|
|
1752
|
+
]);
|
|
1185
1753
|
const drift = /* @__PURE__ */ new Map();
|
|
1186
1754
|
for (const record of records) {
|
|
1187
1755
|
const entries = [];
|
|
1188
1756
|
for (const { anchor, foreign } of planned.get(record.conceptId) ?? []) {
|
|
1189
1757
|
entries.push(
|
|
1190
|
-
foreign ? remoteEntry(anchor, remote,
|
|
1758
|
+
foreign ? remoteEntry(anchor, remote, resolvers) : localEntry(anchor, reads.get(anchor.file), resolvers)
|
|
1191
1759
|
);
|
|
1192
1760
|
}
|
|
1193
1761
|
if (entries.length) drift.set(record.conceptId, entries);
|
|
@@ -1216,12 +1784,22 @@ function unresolved(anchor, reason, repo) {
|
|
|
1216
1784
|
...repo ? { repo } : {}
|
|
1217
1785
|
};
|
|
1218
1786
|
}
|
|
1219
|
-
function hashIn(source, anchor,
|
|
1220
|
-
const
|
|
1221
|
-
if (!
|
|
1787
|
+
function hashIn(source, anchor, resolvers) {
|
|
1788
|
+
const outcome = resolveAnchorSpan(source, anchor, resolvers);
|
|
1789
|
+
if (!outcome.ok) return { ok: false, reason: outcome.reason };
|
|
1790
|
+
return {
|
|
1791
|
+
ok: true,
|
|
1792
|
+
current: {
|
|
1793
|
+
hash: hashAnchorText(outcome.span.text),
|
|
1794
|
+
lines: outcome.span.endLine - outcome.span.startLine + 1,
|
|
1795
|
+
...outcome.resolver ? { resolver: outcome.resolver } : {}
|
|
1796
|
+
}
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
function resolverExtras(source, anchor, current) {
|
|
1222
1800
|
return {
|
|
1223
|
-
|
|
1224
|
-
|
|
1801
|
+
...current.resolver ? { resolver: current.resolver } : {},
|
|
1802
|
+
...current.hash !== anchor.hash && resolverChanged(source, anchor, current.resolver) ? { reason: "resolver-changed" } : {}
|
|
1225
1803
|
};
|
|
1226
1804
|
}
|
|
1227
1805
|
function compared(anchor, current, extra = {}) {
|
|
@@ -1233,30 +1811,48 @@ function compared(anchor, current, extra = {}) {
|
|
|
1233
1811
|
...extra
|
|
1234
1812
|
};
|
|
1235
1813
|
}
|
|
1236
|
-
function localEntry(anchor, read,
|
|
1814
|
+
function localEntry(anchor, read, resolvers) {
|
|
1237
1815
|
if (!read.ok) return unresolved(anchor, read.reason);
|
|
1238
|
-
const
|
|
1239
|
-
|
|
1816
|
+
const found = hashIn(read.source, anchor, resolvers);
|
|
1817
|
+
if (!found.ok) return unresolved(anchor, found.reason);
|
|
1818
|
+
return compared(
|
|
1819
|
+
anchor,
|
|
1820
|
+
found.current,
|
|
1821
|
+
resolverExtras(read.source, anchor, found.current)
|
|
1822
|
+
);
|
|
1240
1823
|
}
|
|
1241
|
-
function remoteEntry(anchor, remote,
|
|
1824
|
+
function remoteEntry(anchor, remote, resolvers) {
|
|
1242
1825
|
const repo = anchor.repo;
|
|
1243
1826
|
const key = normalizeRepoUrl(repo);
|
|
1244
1827
|
const atDefault = remote.get(wantKey(key, void 0, anchor.file));
|
|
1245
1828
|
const primary = anchor.ref ? remote.get(wantKey(key, anchor.ref, anchor.file)) : atDefault;
|
|
1246
1829
|
if (!primary) return unresolved(anchor, "remote-unreachable", repo);
|
|
1247
1830
|
if (!primary.ok) return unresolved(anchor, primary.reason, repo);
|
|
1248
|
-
const
|
|
1249
|
-
if (!
|
|
1250
|
-
|
|
1831
|
+
const found = hashIn(primary.source, anchor, resolvers);
|
|
1832
|
+
if (!found.ok) return unresolved(anchor, found.reason, repo);
|
|
1833
|
+
const current = found.current;
|
|
1834
|
+
const extras = resolverExtras(primary.source, anchor, current);
|
|
1835
|
+
if (!anchor.ref) return compared(anchor, current, { repo, ...extras });
|
|
1251
1836
|
if (current.hash !== anchor.hash) {
|
|
1252
|
-
return compared(anchor, current, {
|
|
1837
|
+
return compared(anchor, current, {
|
|
1838
|
+
repo,
|
|
1839
|
+
...extras,
|
|
1840
|
+
remoteState: "drifted-from-ref"
|
|
1841
|
+
});
|
|
1253
1842
|
}
|
|
1254
|
-
const head = atDefault?.ok ? hashIn(atDefault.source, anchor,
|
|
1255
|
-
return head && head.hash !== anchor.hash ? {
|
|
1256
|
-
...compared(anchor, head, {
|
|
1843
|
+
const head = atDefault?.ok ? hashIn(atDefault.source, anchor, resolvers) : null;
|
|
1844
|
+
return head?.ok && head.current.hash !== anchor.hash ? {
|
|
1845
|
+
...compared(anchor, head.current, {
|
|
1846
|
+
repo,
|
|
1847
|
+
...head.current.resolver ? { resolver: head.current.resolver } : {}
|
|
1848
|
+
}),
|
|
1257
1849
|
state: "drifted",
|
|
1258
1850
|
remoteState: "drifted-on-default"
|
|
1259
|
-
} : compared(anchor, current, {
|
|
1851
|
+
} : compared(anchor, current, {
|
|
1852
|
+
repo,
|
|
1853
|
+
...extras,
|
|
1854
|
+
remoteState: "matches-ref"
|
|
1855
|
+
});
|
|
1260
1856
|
}
|
|
1261
1857
|
|
|
1262
1858
|
// src/errors.ts
|
|
@@ -1463,9 +2059,9 @@ var KbStampDigestBaselineError = class extends BaseError {
|
|
|
1463
2059
|
// src/kb-pins/budgets.ts
|
|
1464
2060
|
function asBudgets(value) {
|
|
1465
2061
|
if (value === null || typeof value !== "object") return {};
|
|
1466
|
-
const
|
|
2062
|
+
const table2 = value;
|
|
1467
2063
|
const pick = (key, min) => {
|
|
1468
|
-
const raw =
|
|
2064
|
+
const raw = table2[key];
|
|
1469
2065
|
return typeof raw === "number" && Number.isInteger(raw) && raw >= min ? raw : void 0;
|
|
1470
2066
|
};
|
|
1471
2067
|
const budgetTokens = pick("budgetTokens", 1);
|
|
@@ -1476,9 +2072,9 @@ function asBudgets(value) {
|
|
|
1476
2072
|
};
|
|
1477
2073
|
}
|
|
1478
2074
|
function contextProfileBudgets(manifest, profile) {
|
|
1479
|
-
const
|
|
1480
|
-
if (
|
|
1481
|
-
const entries =
|
|
2075
|
+
const table2 = manifest.context;
|
|
2076
|
+
if (table2 === null || typeof table2 !== "object") return {};
|
|
2077
|
+
const entries = table2;
|
|
1482
2078
|
return {
|
|
1483
2079
|
...asBudgets(entries["default"]),
|
|
1484
2080
|
...profile ? asBudgets(entries[profile]) : {}
|
|
@@ -1509,15 +2105,15 @@ var KbBaseFrozenError = class extends Error {
|
|
|
1509
2105
|
};
|
|
1510
2106
|
|
|
1511
2107
|
// src/kb-pins/model.ts
|
|
1512
|
-
import { join as
|
|
1513
|
-
import { z as
|
|
1514
|
-
var PINS_FILE =
|
|
1515
|
-
var PINS_LOCAL_FILE =
|
|
2108
|
+
import { join as join4 } from "path";
|
|
2109
|
+
import { z as z5 } from "zod";
|
|
2110
|
+
var PINS_FILE = join4(".strauss", "kb-pins.json");
|
|
2111
|
+
var PINS_LOCAL_FILE = join4(".strauss", "kb-pins.local.json");
|
|
1516
2112
|
var PIN_LAYERS = ["project", "local", "user"];
|
|
1517
|
-
var pinSchema =
|
|
2113
|
+
var pinSchema = z5.object({
|
|
1518
2114
|
/** Relative to the manifest's root, so the file is committable. */
|
|
1519
|
-
path:
|
|
1520
|
-
pinnedAt:
|
|
2115
|
+
path: z5.string().min(1),
|
|
2116
|
+
pinnedAt: z5.string().min(1).optional(),
|
|
1521
2117
|
/**
|
|
1522
2118
|
* How `context` renders this base. `full` preloads the whole base into
|
|
1523
2119
|
* the block regardless of the full-under threshold — for a base whose
|
|
@@ -1527,7 +2123,7 @@ var pinSchema = z4.object({
|
|
|
1527
2123
|
* Absent: the profile's full-under threshold decides. Invalid values
|
|
1528
2124
|
* degrade to absent rather than failing the manifest.
|
|
1529
2125
|
*/
|
|
1530
|
-
mode:
|
|
2126
|
+
mode: z5.enum(["full", "index"]).optional().catch(void 0),
|
|
1531
2127
|
/**
|
|
1532
2128
|
* Context profiles this pin surfaces in (e.g. only at session-start,
|
|
1533
2129
|
* not per turn). Absent: every profile. A run without a profile sees
|
|
@@ -1535,17 +2131,17 @@ var pinSchema = z4.object({
|
|
|
1535
2131
|
* that skill at point of use than pinned at all — pins are what every
|
|
1536
2132
|
* session should see.
|
|
1537
2133
|
*/
|
|
1538
|
-
profiles:
|
|
2134
|
+
profiles: z5.array(z5.string()).optional().catch(void 0),
|
|
1539
2135
|
/**
|
|
1540
2136
|
* The base is concluded — a finished piece of research, a frozen ADR
|
|
1541
2137
|
* set. Write commands against it refuse while this workspace holds the
|
|
1542
2138
|
* pin, and `context` labels it read-only. Workspace policy, not base
|
|
1543
2139
|
* state: the base itself stays copyable and writable elsewhere.
|
|
1544
2140
|
*/
|
|
1545
|
-
frozen:
|
|
2141
|
+
frozen: z5.boolean().optional().catch(void 0)
|
|
1546
2142
|
}).passthrough();
|
|
1547
|
-
var pinsManifestSchema =
|
|
1548
|
-
pins:
|
|
2143
|
+
var pinsManifestSchema = z5.object({
|
|
2144
|
+
pins: z5.array(pinSchema).default([]),
|
|
1549
2145
|
/**
|
|
1550
2146
|
* Per-repo budgets for the `context` command, keyed by profile —
|
|
1551
2147
|
* `"session-start"`, `"compact"`, `"turn"`, or `"default"` for all of
|
|
@@ -1554,21 +2150,21 @@ var pinsManifestSchema = z4.object({
|
|
|
1554
2150
|
* the index at every session start. `contextProfileBudgets` does the
|
|
1555
2151
|
* tolerant read.
|
|
1556
2152
|
*/
|
|
1557
|
-
context:
|
|
2153
|
+
context: z5.unknown().optional()
|
|
1558
2154
|
}).passthrough();
|
|
1559
2155
|
|
|
1560
2156
|
// src/kb-pins/layers.ts
|
|
1561
|
-
import { mkdir as
|
|
1562
|
-
import { homedir as
|
|
1563
|
-
import { dirname, isAbsolute as isAbsolute2, join as
|
|
2157
|
+
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
|
|
2158
|
+
import { homedir as homedir3 } from "os";
|
|
2159
|
+
import { dirname as dirname3, isAbsolute as isAbsolute2, join as join5, relative as relative2, resolve as resolve2, sep as sep2 } from "path";
|
|
1564
2160
|
function userRoot() {
|
|
1565
|
-
return process.env.STRAUSS_KB_USER_ROOT ||
|
|
2161
|
+
return process.env.STRAUSS_KB_USER_ROOT || homedir3();
|
|
1566
2162
|
}
|
|
1567
2163
|
function layerRoot(workspaceDir, layer) {
|
|
1568
2164
|
return layer === "user" ? userRoot() : resolve2(workspaceDir);
|
|
1569
2165
|
}
|
|
1570
2166
|
function layerFile(workspaceDir, layer) {
|
|
1571
|
-
return
|
|
2167
|
+
return join5(
|
|
1572
2168
|
layerRoot(workspaceDir, layer),
|
|
1573
2169
|
layer === "local" ? PINS_LOCAL_FILE : PINS_FILE
|
|
1574
2170
|
);
|
|
@@ -1577,7 +2173,7 @@ async function readPinsLayer(workspaceDir, layer) {
|
|
|
1577
2173
|
const file = layerFile(workspaceDir, layer);
|
|
1578
2174
|
let raw;
|
|
1579
2175
|
try {
|
|
1580
|
-
raw = await
|
|
2176
|
+
raw = await readFile4(file, "utf8");
|
|
1581
2177
|
} catch {
|
|
1582
2178
|
return { pins: [] };
|
|
1583
2179
|
}
|
|
@@ -1601,8 +2197,8 @@ async function readPinsLayer(workspaceDir, layer) {
|
|
|
1601
2197
|
}
|
|
1602
2198
|
async function writePinsLayer(workspaceDir, layer, manifest) {
|
|
1603
2199
|
const file = layerFile(workspaceDir, layer);
|
|
1604
|
-
await
|
|
1605
|
-
await
|
|
2200
|
+
await mkdir3(dirname3(file), { recursive: true });
|
|
2201
|
+
await writeFile2(file, `${JSON.stringify(manifest, null, 2)}
|
|
1606
2202
|
`, "utf8");
|
|
1607
2203
|
}
|
|
1608
2204
|
function resolvePinPath(rootDir, path) {
|
|
@@ -1827,8 +2423,8 @@ function resolveHeads(from, byId) {
|
|
|
1827
2423
|
while (queue.length) {
|
|
1828
2424
|
const current = queue.shift();
|
|
1829
2425
|
const next = successors(current, byId);
|
|
1830
|
-
for (const
|
|
1831
|
-
warnings.push({ kind: "broken-chain", missing });
|
|
2426
|
+
for (const missing2 of next.missing) {
|
|
2427
|
+
warnings.push({ kind: "broken-chain", missing: missing2 });
|
|
1832
2428
|
}
|
|
1833
2429
|
if (!next.records.length) {
|
|
1834
2430
|
if (current.conceptId !== from.conceptId)
|
|
@@ -1859,13 +2455,13 @@ function successors(record, byId) {
|
|
|
1859
2455
|
}
|
|
1860
2456
|
}
|
|
1861
2457
|
const records = [];
|
|
1862
|
-
const
|
|
2458
|
+
const missing2 = [];
|
|
1863
2459
|
for (const id of ids) {
|
|
1864
2460
|
const found = byId.get(id);
|
|
1865
2461
|
if (found) records.push(found);
|
|
1866
|
-
else
|
|
2462
|
+
else missing2.push(id);
|
|
1867
2463
|
}
|
|
1868
|
-
return { records, missing };
|
|
2464
|
+
return { records, missing: missing2 };
|
|
1869
2465
|
}
|
|
1870
2466
|
|
|
1871
2467
|
// src/catalog.ts
|
|
@@ -1936,7 +2532,7 @@ function indexIsStale(stored, expected) {
|
|
|
1936
2532
|
}
|
|
1937
2533
|
|
|
1938
2534
|
// src/kb-context.ts
|
|
1939
|
-
import { readFile as
|
|
2535
|
+
import { readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
|
|
1940
2536
|
var HEADING2 = "## Knowledge bases (pinned)";
|
|
1941
2537
|
var DEFAULT_CONTEXT_BUDGET = 4e3;
|
|
1942
2538
|
var CONTEXT_PROFILES = {
|
|
@@ -2140,13 +2736,13 @@ function toHookJson(block, event) {
|
|
|
2140
2736
|
var CONTEXT_BEGIN = "<!-- strauss-kb:begin -->";
|
|
2141
2737
|
var CONTEXT_END = "<!-- strauss-kb:end -->";
|
|
2142
2738
|
async function syncInstructions(file, block) {
|
|
2143
|
-
const existing = await
|
|
2739
|
+
const existing = await readFile5(file, "utf8").catch(() => null);
|
|
2144
2740
|
const region = block ? `${CONTEXT_BEGIN}
|
|
2145
2741
|
${block.trim()}
|
|
2146
2742
|
${CONTEXT_END}` : null;
|
|
2147
2743
|
if (existing === null) {
|
|
2148
2744
|
if (!region) return { file, action: "unchanged" };
|
|
2149
|
-
await
|
|
2745
|
+
await writeFile3(file, `${region}
|
|
2150
2746
|
`, "utf8");
|
|
2151
2747
|
return { file, action: "created" };
|
|
2152
2748
|
}
|
|
@@ -2157,11 +2753,11 @@ ${CONTEXT_END}` : null;
|
|
|
2157
2753
|
const after = existing.slice(end + CONTEXT_END.length);
|
|
2158
2754
|
const next = region ? `${before}${region}${after}` : `${before.replace(/\n+$/, "\n")}${after.replace(/^\n+/, "\n")}`;
|
|
2159
2755
|
if (next === existing) return { file, action: "unchanged" };
|
|
2160
|
-
await
|
|
2756
|
+
await writeFile3(file, next, "utf8");
|
|
2161
2757
|
return { file, action: region ? "replaced" : "removed" };
|
|
2162
2758
|
}
|
|
2163
2759
|
if (!region) return { file, action: "unchanged" };
|
|
2164
|
-
await
|
|
2760
|
+
await writeFile3(
|
|
2165
2761
|
file,
|
|
2166
2762
|
`${existing.replace(/\n*$/, "\n\n")}${region}
|
|
2167
2763
|
`,
|
|
@@ -2365,6 +2961,18 @@ var CHECK_HEADLINES = {
|
|
|
2365
2961
|
unchecked: "an anchor in another repository nothing could reach"
|
|
2366
2962
|
};
|
|
2367
2963
|
var DAY_MS = 864e5;
|
|
2964
|
+
function anchorResolverCounts(bundle) {
|
|
2965
|
+
let treeSitter = 0;
|
|
2966
|
+
let regex = 0;
|
|
2967
|
+
for (const record of bundle) {
|
|
2968
|
+
for (const anchor of record.frontmatter.strauss_anchors ?? []) {
|
|
2969
|
+
if (!anchor.hash || !anchor.symbol) continue;
|
|
2970
|
+
if (anchor.resolver === "tree-sitter") treeSitter += 1;
|
|
2971
|
+
else regex += 1;
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
return { total: treeSitter + regex, treeSitter, regex };
|
|
2975
|
+
}
|
|
2368
2976
|
function doctor(bundle, options = {}) {
|
|
2369
2977
|
const thresholds = {
|
|
2370
2978
|
expiringDays: options.expiringDays ?? DEFAULT_EXPIRING_DAYS,
|
|
@@ -2400,6 +3008,7 @@ function doctor(bundle, options = {}) {
|
|
|
2400
3008
|
counts,
|
|
2401
3009
|
groups,
|
|
2402
3010
|
findingCount,
|
|
3011
|
+
anchorResolvers: anchorResolverCounts(bundle),
|
|
2403
3012
|
healthy: findingCount === 0
|
|
2404
3013
|
};
|
|
2405
3014
|
}
|
|
@@ -2646,9 +3255,9 @@ function ageInDays(record, now) {
|
|
|
2646
3255
|
}
|
|
2647
3256
|
|
|
2648
3257
|
// src/kb-log.ts
|
|
2649
|
-
import { z as
|
|
3258
|
+
import { z as z6 } from "zod";
|
|
2650
3259
|
var LOG_FILE = "log.jsonl";
|
|
2651
|
-
var kbLogEntrySchema =
|
|
3260
|
+
var kbLogEntrySchema = z6.object({
|
|
2652
3261
|
// Validated, not just `min(1)`: `at` is a sort key (see `parseLog`
|
|
2653
3262
|
// below), and a value that isn't actually chronological — a Unix
|
|
2654
3263
|
// timestamp, a human-typed date, garbage — would sort wrong without
|
|
@@ -2657,12 +3266,12 @@ var kbLogEntrySchema = z5.object({
|
|
|
2657
3266
|
// and rejects everything else, including a non-`Z` offset — so a
|
|
2658
3267
|
// malformed `at` is reported the same way a malformed line already is,
|
|
2659
3268
|
// rather than silently sorting into the wrong place.
|
|
2660
|
-
at:
|
|
2661
|
-
by:
|
|
2662
|
-
operation:
|
|
2663
|
-
conceptId:
|
|
3269
|
+
at: z6.iso.datetime(),
|
|
3270
|
+
by: z6.string().min(1),
|
|
3271
|
+
operation: z6.string().min(1),
|
|
3272
|
+
conceptId: z6.string().min(1),
|
|
2664
3273
|
/** Second concept id, where the operation relates two — supersession. */
|
|
2665
|
-
target:
|
|
3274
|
+
target: z6.string().min(1).optional()
|
|
2666
3275
|
}).strict();
|
|
2667
3276
|
function renderLogEntry(entry) {
|
|
2668
3277
|
return `${JSON.stringify(kbLogEntrySchema.parse(entry))}
|
|
@@ -2672,18 +3281,18 @@ function parseLog(raw) {
|
|
|
2672
3281
|
const entries = [];
|
|
2673
3282
|
const malformed = [];
|
|
2674
3283
|
const seen = /* @__PURE__ */ new Set();
|
|
2675
|
-
raw.split("\n").forEach((text,
|
|
3284
|
+
raw.split("\n").forEach((text, index2) => {
|
|
2676
3285
|
if (!text.trim()) return;
|
|
2677
3286
|
let value;
|
|
2678
3287
|
try {
|
|
2679
3288
|
value = JSON.parse(text);
|
|
2680
3289
|
} catch {
|
|
2681
|
-
malformed.push({ line:
|
|
3290
|
+
malformed.push({ line: index2 + 1, text });
|
|
2682
3291
|
return;
|
|
2683
3292
|
}
|
|
2684
3293
|
const parsed = kbLogEntrySchema.safeParse(value);
|
|
2685
3294
|
if (!parsed.success) {
|
|
2686
|
-
malformed.push({ line:
|
|
3295
|
+
malformed.push({ line: index2 + 1, text });
|
|
2687
3296
|
return;
|
|
2688
3297
|
}
|
|
2689
3298
|
const key = JSON.stringify(parsed.data);
|
|
@@ -2698,14 +3307,14 @@ function parseLog(raw) {
|
|
|
2698
3307
|
}
|
|
2699
3308
|
|
|
2700
3309
|
// src/json-schema.ts
|
|
2701
|
-
import { z as
|
|
3310
|
+
import { z as z7 } from "zod";
|
|
2702
3311
|
function kbJsonSchemas() {
|
|
2703
3312
|
return {
|
|
2704
|
-
recordFrontmatter:
|
|
3313
|
+
recordFrontmatter: z7.toJSONSchema(kbRecordFrontmatterSchema, {
|
|
2705
3314
|
io: "input"
|
|
2706
3315
|
}),
|
|
2707
|
-
composeInput:
|
|
2708
|
-
logEntry:
|
|
3316
|
+
composeInput: z7.toJSONSchema(composeInputSchema, { io: "input" }),
|
|
3317
|
+
logEntry: z7.toJSONSchema(kbLogEntrySchema, { io: "input" })
|
|
2709
3318
|
};
|
|
2710
3319
|
}
|
|
2711
3320
|
|
|
@@ -2758,13 +3367,13 @@ function byGeneratedAt(left, right) {
|
|
|
2758
3367
|
}
|
|
2759
3368
|
|
|
2760
3369
|
// src/commands/anchor-resolve.ts
|
|
2761
|
-
import { z as
|
|
3370
|
+
import { z as z9 } from "zod";
|
|
2762
3371
|
|
|
2763
3372
|
// src/commands/model.ts
|
|
2764
|
-
import { z as
|
|
2765
|
-
var bundlePath =
|
|
2766
|
-
var conceptId =
|
|
2767
|
-
var REPO_ROOT =
|
|
3373
|
+
import { z as z8 } from "zod";
|
|
3374
|
+
var bundlePath = z8.string().min(1).describe("Absolute path to the knowledge base directory.");
|
|
3375
|
+
var conceptId = z8.string().min(1).describe("e.g. decision.cursor-v2");
|
|
3376
|
+
var REPO_ROOT = z8.string().min(1).optional().describe(
|
|
2768
3377
|
"Where the anchored source lives, for the drift check. Defaults to the working directory."
|
|
2769
3378
|
);
|
|
2770
3379
|
function define(command) {
|
|
@@ -2787,22 +3396,30 @@ function argvFlag(argv, name) {
|
|
|
2787
3396
|
}
|
|
2788
3397
|
|
|
2789
3398
|
// src/commands/anchor-resolve.ts
|
|
3399
|
+
function resolverSummary(results) {
|
|
3400
|
+
const names = [
|
|
3401
|
+
...new Set(
|
|
3402
|
+
results.flatMap((entry) => entry.resolver ? [entry.resolver] : [])
|
|
3403
|
+
)
|
|
3404
|
+
].sort();
|
|
3405
|
+
return names.length ? `${names.join(" + ")} resolver` : "whole-file";
|
|
3406
|
+
}
|
|
2790
3407
|
var anchorResolveCommand = define({
|
|
2791
3408
|
name: "anchor-resolve",
|
|
2792
3409
|
tool: "kb_anchor_resolve",
|
|
2793
3410
|
usage: "anchor-resolve <concept-id> [--repo-root <path>] [--offline] [--rebaseline] [--restamp]",
|
|
2794
3411
|
description: "Resolve a record's anchors: stamp a hash onto anchors that lack one, report drift where the code moved. An anchor naming another repository is read from that remote through a bare cache; --offline uses the cache only. kb_verify's mechanical counterpart \u2014 reach for it when the question is whether the code still is what it was. Exits non-zero on drift.",
|
|
2795
|
-
input:
|
|
3412
|
+
input: z9.object({
|
|
2796
3413
|
bundlePath,
|
|
2797
3414
|
conceptId,
|
|
2798
|
-
repoRoot:
|
|
2799
|
-
offline:
|
|
3415
|
+
repoRoot: z9.string().min(1).optional(),
|
|
3416
|
+
offline: z9.boolean().optional().describe(
|
|
2800
3417
|
"Resolve foreign anchors from the local repo cache only, never fetching."
|
|
2801
3418
|
),
|
|
2802
|
-
rebaseline:
|
|
3419
|
+
rebaseline: z9.boolean().optional().describe(
|
|
2803
3420
|
"Accept the current code as the new baseline for anchors that drifted."
|
|
2804
3421
|
),
|
|
2805
|
-
restamp:
|
|
3422
|
+
restamp: z9.boolean().optional().describe(
|
|
2806
3423
|
"Refresh `resolved_at` on anchors that already match. Off by default, so a green run writes nothing."
|
|
2807
3424
|
)
|
|
2808
3425
|
}),
|
|
@@ -2831,6 +3448,11 @@ var anchorResolveCommand = define({
|
|
|
2831
3448
|
const updated = [];
|
|
2832
3449
|
let dirty = false;
|
|
2833
3450
|
const sources = await readSources(anchors, root, offline === true);
|
|
3451
|
+
const resolvers = defaultAnchorResolvers({ offline: offline === true });
|
|
3452
|
+
await prepareResolvers(
|
|
3453
|
+
resolvers,
|
|
3454
|
+
anchors.map((anchor) => anchor.file)
|
|
3455
|
+
);
|
|
2834
3456
|
for (const anchor of anchors) {
|
|
2835
3457
|
const base2 = {
|
|
2836
3458
|
file: anchor.file,
|
|
@@ -2847,27 +3469,35 @@ var anchorResolveCommand = define({
|
|
|
2847
3469
|
updated.push(anchor);
|
|
2848
3470
|
continue;
|
|
2849
3471
|
}
|
|
2850
|
-
const
|
|
2851
|
-
if (!
|
|
3472
|
+
const outcome = resolveAnchorSpan(source.source, anchor, resolvers);
|
|
3473
|
+
if (!outcome.ok) {
|
|
2852
3474
|
results.push({
|
|
2853
3475
|
...base2,
|
|
2854
3476
|
state: "unresolved",
|
|
2855
|
-
reason:
|
|
3477
|
+
reason: outcome.reason
|
|
2856
3478
|
});
|
|
2857
3479
|
updated.push(anchor);
|
|
2858
3480
|
continue;
|
|
2859
3481
|
}
|
|
3482
|
+
const resolved = outcome.span;
|
|
3483
|
+
const producedBy = outcome.resolver;
|
|
2860
3484
|
const currentHash = hashAnchorText(resolved.text);
|
|
2861
3485
|
const currentLines = resolved.endLine - resolved.startLine + 1;
|
|
2862
3486
|
const stamped = {
|
|
2863
3487
|
...anchor,
|
|
2864
3488
|
hash: currentHash,
|
|
2865
3489
|
lines: currentLines,
|
|
2866
|
-
resolved_at: now()
|
|
3490
|
+
resolved_at: now(),
|
|
3491
|
+
...producedBy ? { resolver: producedBy } : {}
|
|
2867
3492
|
};
|
|
2868
3493
|
const pinned = anchor.ref !== void 0 && source.repo !== void 0;
|
|
2869
3494
|
if (!anchor.hash) {
|
|
2870
|
-
results.push({
|
|
3495
|
+
results.push({
|
|
3496
|
+
...base2,
|
|
3497
|
+
state: "stamped",
|
|
3498
|
+
currentHash,
|
|
3499
|
+
...producedBy ? { resolver: producedBy } : {}
|
|
3500
|
+
});
|
|
2871
3501
|
updated.push(stamped);
|
|
2872
3502
|
dirty = true;
|
|
2873
3503
|
continue;
|
|
@@ -2878,6 +3508,10 @@ var anchorResolveCommand = define({
|
|
|
2878
3508
|
state: "drifted",
|
|
2879
3509
|
currentHash,
|
|
2880
3510
|
diffSize: lineDelta(anchor, currentLines),
|
|
3511
|
+
...producedBy ? { resolver: producedBy } : {},
|
|
3512
|
+
// A regex-stamped anchor re-read by tree-sitter drifts because the
|
|
3513
|
+
// resolver changed, not because the code did.
|
|
3514
|
+
...resolverChanged(source.source, anchor, producedBy) ? { reason: "resolver-changed" } : {},
|
|
2881
3515
|
...pinned ? { remoteState: "drifted-from-ref" } : {},
|
|
2882
3516
|
...rebaseline ? { rebaselined: true } : {}
|
|
2883
3517
|
});
|
|
@@ -2885,7 +3519,7 @@ var anchorResolveCommand = define({
|
|
|
2885
3519
|
if (rebaseline) dirty = true;
|
|
2886
3520
|
continue;
|
|
2887
3521
|
}
|
|
2888
|
-
const onDefault = pinned ? headHash(source, anchor) : void 0;
|
|
3522
|
+
const onDefault = pinned ? headHash(source, anchor, resolvers) : void 0;
|
|
2889
3523
|
if (onDefault && onDefault.hash !== anchor.hash) {
|
|
2890
3524
|
results.push({
|
|
2891
3525
|
...base2,
|
|
@@ -2901,6 +3535,7 @@ var anchorResolveCommand = define({
|
|
|
2901
3535
|
...base2,
|
|
2902
3536
|
state: "match",
|
|
2903
3537
|
currentHash,
|
|
3538
|
+
...producedBy ? { resolver: producedBy } : {},
|
|
2904
3539
|
...pinned ? { remoteState: "matches-ref" } : {}
|
|
2905
3540
|
});
|
|
2906
3541
|
const refresh = restamp || anchor.resolved_at === void 0;
|
|
@@ -2918,19 +3553,21 @@ var anchorResolveCommand = define({
|
|
|
2918
3553
|
if (!frozen) await store.updateAnchors(path, id, updated, actor);
|
|
2919
3554
|
}
|
|
2920
3555
|
const frozenNote = frozen ? { frozen: true, note: "base is frozen: nothing was stamped" } : {};
|
|
3556
|
+
const hints = grammarHints();
|
|
3557
|
+
const hintNote = hints.length ? { hints } : {};
|
|
2921
3558
|
const unreachable = results.filter(
|
|
2922
3559
|
(entry) => isUncheckedReason(entry.reason)
|
|
2923
3560
|
).length;
|
|
2924
3561
|
const checked = results.length - unreachable;
|
|
2925
|
-
const
|
|
2926
|
-
const note = `${
|
|
2927
|
-
const clean = checked > 0 &&
|
|
3562
|
+
const matches3 = results.filter((entry) => entry.state === "match").length;
|
|
3563
|
+
const note = `${matches3}/${checked} anchors match${unreachable ? `, ${unreachable} unreachable` : ""}`;
|
|
3564
|
+
const clean = checked > 0 && matches3 === checked && unreachable === 0;
|
|
2928
3565
|
if (clean) {
|
|
2929
3566
|
try {
|
|
2930
3567
|
await store.verify(
|
|
2931
3568
|
path,
|
|
2932
3569
|
id,
|
|
2933
|
-
`anchor-resolve: ${note} (
|
|
3570
|
+
`anchor-resolve: ${note} (${resolverSummary(results)})`,
|
|
2934
3571
|
actor,
|
|
2935
3572
|
now()
|
|
2936
3573
|
);
|
|
@@ -2941,17 +3578,25 @@ var anchorResolveCommand = define({
|
|
|
2941
3578
|
results,
|
|
2942
3579
|
verified: false,
|
|
2943
3580
|
verifyRefused: "self-verification",
|
|
2944
|
-
...frozenNote
|
|
3581
|
+
...frozenNote,
|
|
3582
|
+
...hintNote
|
|
2945
3583
|
};
|
|
2946
3584
|
}
|
|
2947
|
-
return {
|
|
3585
|
+
return {
|
|
3586
|
+
conceptId: id,
|
|
3587
|
+
results,
|
|
3588
|
+
verified: true,
|
|
3589
|
+
...frozenNote,
|
|
3590
|
+
...hintNote
|
|
3591
|
+
};
|
|
2948
3592
|
}
|
|
2949
3593
|
return {
|
|
2950
3594
|
conceptId: id,
|
|
2951
3595
|
results,
|
|
2952
3596
|
verified: false,
|
|
2953
3597
|
...unreachable ? { note } : {},
|
|
2954
|
-
...frozenNote
|
|
3598
|
+
...frozenNote,
|
|
3599
|
+
...hintNote
|
|
2955
3600
|
};
|
|
2956
3601
|
},
|
|
2957
3602
|
// A stored hash that no longer resolves is a broken anchor, not an absence:
|
|
@@ -2967,13 +3612,13 @@ var anchorResolveCommand = define({
|
|
|
2967
3612
|
function lineDelta(anchor, current) {
|
|
2968
3613
|
return anchor.lines === void 0 ? null : Math.abs(current - anchor.lines);
|
|
2969
3614
|
}
|
|
2970
|
-
function headHash(source, anchor) {
|
|
3615
|
+
function headHash(source, anchor, resolvers) {
|
|
2971
3616
|
if (source.head === void 0) return void 0;
|
|
2972
|
-
const
|
|
2973
|
-
if (!
|
|
3617
|
+
const outcome = resolveAnchorSpan(source.head, anchor, resolvers);
|
|
3618
|
+
if (!outcome.ok) return void 0;
|
|
2974
3619
|
return {
|
|
2975
|
-
hash: hashAnchorText(
|
|
2976
|
-
lines:
|
|
3620
|
+
hash: hashAnchorText(outcome.span.text),
|
|
3621
|
+
lines: outcome.span.endLine - outcome.span.startLine + 1
|
|
2977
3622
|
};
|
|
2978
3623
|
}
|
|
2979
3624
|
async function readSources(anchors, root, offline) {
|
|
@@ -3023,13 +3668,13 @@ async function readSources(anchors, root, offline) {
|
|
|
3023
3668
|
}
|
|
3024
3669
|
|
|
3025
3670
|
// src/commands/answer.ts
|
|
3026
|
-
import { z as
|
|
3671
|
+
import { z as z10 } from "zod";
|
|
3027
3672
|
var answerCommand = define({
|
|
3028
3673
|
name: "answer",
|
|
3029
3674
|
tool: "kb_answer",
|
|
3030
3675
|
usage: "answer <concept-id> <answer...>",
|
|
3031
3676
|
description: "Resolve an open question: set status, stamp who and when, append an Answer section. If the answer overturns a decision or assumption, supersede that record explicitly.",
|
|
3032
|
-
input:
|
|
3677
|
+
input: z10.object({ bundlePath, conceptId, answer: z10.string().min(1) }),
|
|
3033
3678
|
fromArgv: (argv, path) => ({
|
|
3034
3679
|
bundlePath: path,
|
|
3035
3680
|
conceptId: argv[1],
|
|
@@ -3043,27 +3688,27 @@ var answerCommand = define({
|
|
|
3043
3688
|
});
|
|
3044
3689
|
|
|
3045
3690
|
// src/commands/backlinks.ts
|
|
3046
|
-
import { z as
|
|
3691
|
+
import { z as z11 } from "zod";
|
|
3047
3692
|
var backlinksCommand = define({
|
|
3048
3693
|
name: "backlinks",
|
|
3049
3694
|
tool: "kb_backlinks",
|
|
3050
3695
|
usage: "backlinks <concept-id>",
|
|
3051
3696
|
description: "Who points at this record: every inbound typed causal link (`strauss_links`), one hop, every rel including `related_to`, each with its rel and the standing of the record that made it. Use it when you need the exact edges \u2014 reviewing or renaming a record.",
|
|
3052
|
-
input:
|
|
3697
|
+
input: z11.object({ bundlePath, conceptId }),
|
|
3053
3698
|
fromArgv: (argv, path) => ({ bundlePath: path, conceptId: argv[1] }),
|
|
3054
3699
|
run: async ({ store }, { bundlePath: path, conceptId: id }) => store.backlinks(path, id)
|
|
3055
3700
|
});
|
|
3056
3701
|
|
|
3057
3702
|
// src/commands/catalog.ts
|
|
3058
|
-
import { z as
|
|
3703
|
+
import { z as z12 } from "zod";
|
|
3059
3704
|
var catalogCommand = define({
|
|
3060
3705
|
name: "catalog",
|
|
3061
3706
|
tool: "kb_catalog",
|
|
3062
3707
|
usage: "catalog [type]",
|
|
3063
3708
|
description: "Lists every record as one line \u2014 concept id, type, title, standing, and a stale flag \u2014 at roughly thirty tokens each. Pick this over kb_load once kb_load refuses: kb_catalog never refuses. Superseded records show only their replacement; fetch bodies with kb_load, kb_pack, kb_query, or kb_trace.",
|
|
3064
|
-
input:
|
|
3709
|
+
input: z12.object({
|
|
3065
3710
|
bundlePath,
|
|
3066
|
-
type:
|
|
3711
|
+
type: z12.enum(KB_RECORD_TYPES).optional()
|
|
3067
3712
|
}),
|
|
3068
3713
|
fromArgv: (argv, path) => ({
|
|
3069
3714
|
bundlePath: path,
|
|
@@ -3118,26 +3763,26 @@ function count(value, noun) {
|
|
|
3118
3763
|
}
|
|
3119
3764
|
|
|
3120
3765
|
// src/commands/context.ts
|
|
3121
|
-
import { z as
|
|
3766
|
+
import { z as z13 } from "zod";
|
|
3122
3767
|
var contextCommand = define({
|
|
3123
3768
|
name: "context",
|
|
3124
3769
|
tool: "kb_context",
|
|
3125
3770
|
usage: "context [--profile NAME] [--budget N] [--full-under N] [--format json] [--event NAME]",
|
|
3126
3771
|
description: "Index block of pinned bases (ids, titles, standing) for injection at context birth. Takes no bundlePath \u2014 reads the workspace pin manifests. Empty when nothing is pinned; refuses over budget rather than truncating. Budget precedence: flags, then the manifest `context[profile]` over `context.default`, then the built-in profile, then package defaults.",
|
|
3127
|
-
input:
|
|
3128
|
-
budgetTokens:
|
|
3772
|
+
input: z13.object({
|
|
3773
|
+
budgetTokens: z13.number().int().positive().optional().describe(
|
|
3129
3774
|
"Ceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000."
|
|
3130
3775
|
),
|
|
3131
|
-
fullUnderTokens:
|
|
3776
|
+
fullUnderTokens: z13.number().int().positive().optional().describe(
|
|
3132
3777
|
"Per-base rendering threshold, applied before the budget: a base whose complete load fits under this arrives as full records instead of index lines, and the whole block still answers to budgetTokens. Off by default \u2014 index-only is the safe default at a context birth, because injected bodies outlive the qualifiers on them; the session-start profile opts tiny bases in at 1500."
|
|
3133
3778
|
),
|
|
3134
|
-
profile:
|
|
3779
|
+
profile: z13.string().optional().describe(
|
|
3135
3780
|
"Named budget set: built-ins are session-start (full-under 1500), compact and turn (budget 2500); the manifests' `context` tables override per repo. Unknown names fall through to defaults rather than failing."
|
|
3136
3781
|
),
|
|
3137
|
-
format:
|
|
3782
|
+
format: z13.enum(["markdown", "json"]).optional().describe(
|
|
3138
3783
|
"CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
|
|
3139
3784
|
),
|
|
3140
|
-
event:
|
|
3785
|
+
event: z13.string().optional().describe(
|
|
3141
3786
|
"hookEventName stamped into the JSON envelope. Only meaningful with format=json."
|
|
3142
3787
|
)
|
|
3143
3788
|
}),
|
|
@@ -3173,14 +3818,14 @@ var contextCommand = define({
|
|
|
3173
3818
|
});
|
|
3174
3819
|
|
|
3175
3820
|
// src/commands/doctor.ts
|
|
3176
|
-
import { z as
|
|
3177
|
-
var days = (what, fallback) =>
|
|
3821
|
+
import { z as z14 } from "zod";
|
|
3822
|
+
var days = (what, fallback) => z14.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
|
|
3178
3823
|
var doctorCommand = define({
|
|
3179
3824
|
name: "doctor",
|
|
3180
3825
|
tool: "kb_doctor",
|
|
3181
3826
|
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--offline] [--strict]",
|
|
3182
3827
|
description: "Read-only health sweep: expired, expiring, unverified, aging, orphaned, broken-supersession, superseded-but-cited, drifted and unchecked anchors. Every group is reported even when empty; nothing is written or re-stamped. Use it when picking up a base you have not touched in a while; kb_validate only checks that pointers between records agree.",
|
|
3183
|
-
input:
|
|
3828
|
+
input: z14.object({
|
|
3184
3829
|
bundlePath,
|
|
3185
3830
|
repoRoot: REPO_ROOT,
|
|
3186
3831
|
expiringDays: days(
|
|
@@ -3195,10 +3840,10 @@ var doctorCommand = define({
|
|
|
3195
3840
|
"How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
|
|
3196
3841
|
DEFAULT_AGING_DAYS
|
|
3197
3842
|
),
|
|
3198
|
-
offline:
|
|
3843
|
+
offline: z14.boolean().optional().describe(
|
|
3199
3844
|
"Read foreign anchors from the local repo cache only, never fetching."
|
|
3200
3845
|
),
|
|
3201
|
-
strict:
|
|
3846
|
+
strict: z14.boolean().optional().describe(
|
|
3202
3847
|
"Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
|
|
3203
3848
|
)
|
|
3204
3849
|
}),
|
|
@@ -3241,7 +3886,13 @@ var doctorCommand = define({
|
|
|
3241
3886
|
...anchorDrift !== void 0 ? { anchorDrift } : {},
|
|
3242
3887
|
now: new Date(checkedAt)
|
|
3243
3888
|
});
|
|
3244
|
-
|
|
3889
|
+
const hints = grammarHints();
|
|
3890
|
+
return {
|
|
3891
|
+
bundlePath: path,
|
|
3892
|
+
checkedAt,
|
|
3893
|
+
...report,
|
|
3894
|
+
...hints.length ? { hints } : {}
|
|
3895
|
+
};
|
|
3245
3896
|
},
|
|
3246
3897
|
render: (result) => render2(result),
|
|
3247
3898
|
// Only expiry, and only under --strict. The other seven checks report debt a
|
|
@@ -3259,12 +3910,15 @@ function render2(result) {
|
|
|
3259
3910
|
`records: ${result.recordCount}`,
|
|
3260
3911
|
`thresholds: expiring within ${thresholds.expiringDays}d, unverified over ${thresholds.unverifiedDays}d, aging over ${thresholds.agingDays}d`,
|
|
3261
3912
|
`checked: ${result.checkedAt}`,
|
|
3913
|
+
...result.anchorResolvers.total ? [
|
|
3914
|
+
`anchors: ${result.anchorResolvers.total} hashed \u2014 ${result.anchorResolvers.treeSitter} tree-sitter, ${result.anchorResolvers.regex} regex`
|
|
3915
|
+
] : [],
|
|
3262
3916
|
""
|
|
3263
3917
|
];
|
|
3264
|
-
const
|
|
3918
|
+
const width2 = Math.max(...result.groups.map((group2) => group2.check.length));
|
|
3265
3919
|
for (const group2 of result.groups) {
|
|
3266
3920
|
lines.push(
|
|
3267
|
-
` ${group2.check.padEnd(
|
|
3921
|
+
` ${group2.check.padEnd(width2)} ${String(group2.count).padStart(3)} ${group2.headline}`
|
|
3268
3922
|
);
|
|
3269
3923
|
}
|
|
3270
3924
|
for (const group2 of result.groups) {
|
|
@@ -3276,6 +3930,7 @@ function render2(result) {
|
|
|
3276
3930
|
);
|
|
3277
3931
|
}
|
|
3278
3932
|
}
|
|
3933
|
+
for (const hint of result.hints ?? []) lines.push("", hint);
|
|
3279
3934
|
lines.push(
|
|
3280
3935
|
"",
|
|
3281
3936
|
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.`
|
|
@@ -3284,19 +3939,19 @@ function render2(result) {
|
|
|
3284
3939
|
}
|
|
3285
3940
|
|
|
3286
3941
|
// src/commands/impact.ts
|
|
3287
|
-
import { z as
|
|
3942
|
+
import { z as z15 } from "zod";
|
|
3288
3943
|
var impactCommand = define({
|
|
3289
3944
|
name: "impact",
|
|
3290
3945
|
tool: "kb_impact",
|
|
3291
3946
|
usage: "impact <concept-id> [--depth N] [--rels a,b]",
|
|
3292
3947
|
description: "What breaks if this record changes: its transitive set of dependants, each with its standing. Each rel declares which of its ends depends on the other, and the walk follows each rel in its own direction. Naming `related_to` or an unknown rel in `rels` is an error. kb_backlinks gives one flat hop.",
|
|
3293
|
-
input:
|
|
3948
|
+
input: z15.object({
|
|
3294
3949
|
bundlePath,
|
|
3295
3950
|
conceptId,
|
|
3296
|
-
depth:
|
|
3951
|
+
depth: z15.number().int().positive().optional().describe(
|
|
3297
3952
|
"Hops out from the record. Unbounded when omitted; a walk this cuts reports truncated: true."
|
|
3298
3953
|
),
|
|
3299
|
-
rels:
|
|
3954
|
+
rels: z15.array(z15.enum(KB_CAUSAL_LINK_RELS)).optional().describe(
|
|
3300
3955
|
"Narrow which rels the walk follows. Defaults to every rel that carries a dependence \u2014 all but related_to."
|
|
3301
3956
|
)
|
|
3302
3957
|
}),
|
|
@@ -3317,13 +3972,13 @@ var impactCommand = define({
|
|
|
3317
3972
|
});
|
|
3318
3973
|
|
|
3319
3974
|
// src/commands/list.ts
|
|
3320
|
-
import { z as
|
|
3975
|
+
import { z as z16 } from "zod";
|
|
3321
3976
|
var listCommand = define({
|
|
3322
3977
|
name: "list",
|
|
3323
3978
|
tool: "kb_list",
|
|
3324
3979
|
usage: "list [type]",
|
|
3325
3980
|
description: "Every record, optionally one type. For enumerating; use kb_query for a question.",
|
|
3326
|
-
input:
|
|
3981
|
+
input: z16.object({ bundlePath, type: z16.enum(KB_RECORD_TYPES).optional() }),
|
|
3327
3982
|
fromArgv: (argv, path) => ({ bundlePath: path, type: argv[1] }),
|
|
3328
3983
|
run: async ({ store }, { bundlePath: path, type }) => (await store.list(path, type)).map((record) => ({
|
|
3329
3984
|
conceptId: record.conceptId,
|
|
@@ -3335,17 +3990,17 @@ var listCommand = define({
|
|
|
3335
3990
|
});
|
|
3336
3991
|
|
|
3337
3992
|
// src/commands/load.ts
|
|
3338
|
-
import { z as
|
|
3993
|
+
import { z as z17 } from "zod";
|
|
3339
3994
|
var loadCommand = define({
|
|
3340
3995
|
name: "load",
|
|
3341
3996
|
tool: "kb_load",
|
|
3342
3997
|
usage: "load [type] [--budget N | --all] [--repo-root PATH]",
|
|
3343
3998
|
description: "Load the whole base, each record with its standing \u2014 call it first, at the point of use, since compaction drops it. Superseded records arrive as stubs; kb_trace has the history. Over budget it refuses: kb_catalog, then kb_pack, or narrow with `type`; `all` bypasses. Never read record files directly \u2014 only kb_* tools resolve supersession. `digest` stamps the base's content, so hooks know when to reload.",
|
|
3344
|
-
input:
|
|
3999
|
+
input: z17.object({
|
|
3345
4000
|
bundlePath,
|
|
3346
|
-
type:
|
|
3347
|
-
budgetTokens:
|
|
3348
|
-
all:
|
|
4001
|
+
type: z17.enum(KB_RECORD_TYPES).optional(),
|
|
4002
|
+
budgetTokens: z17.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
|
|
4003
|
+
all: z17.boolean().optional().describe(
|
|
3349
4004
|
"Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
|
|
3350
4005
|
),
|
|
3351
4006
|
repoRoot: REPO_ROOT
|
|
@@ -3387,25 +4042,25 @@ var loadCommand = define({
|
|
|
3387
4042
|
});
|
|
3388
4043
|
|
|
3389
4044
|
// src/commands/log.ts
|
|
3390
|
-
import { z as
|
|
4045
|
+
import { z as z18 } from "zod";
|
|
3391
4046
|
var logCommand = define({
|
|
3392
4047
|
name: "log",
|
|
3393
4048
|
tool: "kb_log",
|
|
3394
4049
|
usage: "log",
|
|
3395
4050
|
description: "Who touched what, and when. Append-only; malformed lines are reported, never repaired.",
|
|
3396
|
-
input:
|
|
4051
|
+
input: z18.object({ bundlePath }),
|
|
3397
4052
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
3398
4053
|
run: ({ store }, { bundlePath: path }) => store.readLog(path)
|
|
3399
4054
|
});
|
|
3400
4055
|
|
|
3401
4056
|
// src/commands/no-decision.ts
|
|
3402
|
-
import { z as
|
|
4057
|
+
import { z as z19 } from "zod";
|
|
3403
4058
|
var noDecisionCommand = define({
|
|
3404
4059
|
name: "no-decision",
|
|
3405
4060
|
tool: "kb_no_decision",
|
|
3406
4061
|
usage: "no-decision <reason...>",
|
|
3407
4062
|
description: "Record in one sentence that a piece of work had nothing to decide. Idempotent.",
|
|
3408
|
-
input:
|
|
4063
|
+
input: z19.object({ bundlePath, reason: z19.string().min(1) }),
|
|
3409
4064
|
fromArgv: (argv, path) => ({
|
|
3410
4065
|
bundlePath: path,
|
|
3411
4066
|
reason: argv.slice(1).join(" ").trim()
|
|
@@ -3422,20 +4077,20 @@ var noDecisionCommand = define({
|
|
|
3422
4077
|
});
|
|
3423
4078
|
|
|
3424
4079
|
// src/commands/pack.ts
|
|
3425
|
-
import { z as
|
|
4080
|
+
import { z as z20 } from "zod";
|
|
3426
4081
|
var packCommand = define({
|
|
3427
4082
|
name: "pack",
|
|
3428
4083
|
tool: "kb_pack",
|
|
3429
4084
|
usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
|
|
3430
4085
|
description: "Bounded neighbourhood around one record: within `hops`, ranked, cut to `maxNodes`, with every cut record named under Excluded. Use when the base is over kb_load's budget and the work centres on a record you can name. Refuses over budget rather than truncating. Everything below the header is byte-stable across runs. Resolves supersession like kb_load.",
|
|
3431
|
-
input:
|
|
4086
|
+
input: z20.object({
|
|
3432
4087
|
bundlePath,
|
|
3433
4088
|
conceptId,
|
|
3434
|
-
hops:
|
|
3435
|
-
maxNodes:
|
|
4089
|
+
hops: z20.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
|
|
4090
|
+
maxNodes: z20.number().int().positive().optional().describe(
|
|
3436
4091
|
"How many records the pack may hold, root included. Defaults to 20."
|
|
3437
4092
|
),
|
|
3438
|
-
budgetTokens:
|
|
4093
|
+
budgetTokens: z20.number().int().positive().optional().describe(
|
|
3439
4094
|
"Approximate token ceiling over what is actually emitted. Defaults to 25000."
|
|
3440
4095
|
)
|
|
3441
4096
|
}),
|
|
@@ -3522,22 +4177,22 @@ function warningLabel(warning) {
|
|
|
3522
4177
|
}
|
|
3523
4178
|
|
|
3524
4179
|
// src/commands/pin.ts
|
|
3525
|
-
import { z as
|
|
4180
|
+
import { z as z21 } from "zod";
|
|
3526
4181
|
var pinCommand = define({
|
|
3527
4182
|
name: "pin",
|
|
3528
4183
|
tool: "kb_pin",
|
|
3529
4184
|
usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
|
|
3530
4185
|
description: "Pin a base into a workspace manifest so kb_context surfaces it. Layers, nearest wins: project `.strauss/kb-pins.json` (default), `--local` (personal, gitignored), `--user` (`~/.strauss`). Idempotent; `--mode full|index`, `--profiles`, `--frozen`/`--unfreeze` update only those fields. A path with no records pins with a warning. Never touches the base itself.",
|
|
3531
|
-
input:
|
|
4186
|
+
input: z21.object({
|
|
3532
4187
|
bundlePath,
|
|
3533
|
-
mode:
|
|
4188
|
+
mode: z21.enum(["full", "index"]).optional().describe(
|
|
3534
4189
|
"full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
|
|
3535
4190
|
),
|
|
3536
|
-
profiles:
|
|
3537
|
-
layer:
|
|
4191
|
+
profiles: z21.array(z21.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
|
|
4192
|
+
layer: z21.enum(["project", "local", "user"]).optional().describe(
|
|
3538
4193
|
"Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
|
|
3539
4194
|
),
|
|
3540
|
-
frozen:
|
|
4195
|
+
frozen: z21.boolean().optional().describe(
|
|
3541
4196
|
"true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
|
|
3542
4197
|
)
|
|
3543
4198
|
}),
|
|
@@ -3566,29 +4221,29 @@ var pinCommand = define({
|
|
|
3566
4221
|
});
|
|
3567
4222
|
|
|
3568
4223
|
// src/commands/pins.ts
|
|
3569
|
-
import { z as
|
|
4224
|
+
import { z as z22 } from "zod";
|
|
3570
4225
|
var pinsCommand = define({
|
|
3571
4226
|
name: "pins",
|
|
3572
4227
|
tool: "kb_pins",
|
|
3573
4228
|
usage: "pins",
|
|
3574
4229
|
description: "Every pinned base across the manifest layers, with its layer and whether it resolves to records. Takes no bundlePath.",
|
|
3575
|
-
input:
|
|
4230
|
+
input: z22.object({}),
|
|
3576
4231
|
fromArgv: () => ({}),
|
|
3577
4232
|
run: ({ store }) => listPins(store, process.cwd())
|
|
3578
4233
|
});
|
|
3579
4234
|
|
|
3580
4235
|
// src/commands/query.ts
|
|
3581
|
-
import { z as
|
|
4236
|
+
import { z as z23 } from "zod";
|
|
3582
4237
|
var queryCommand = define({
|
|
3583
4238
|
name: "query",
|
|
3584
4239
|
tool: "kb_query",
|
|
3585
4240
|
usage: "query <text...> [--repo-root PATH]",
|
|
3586
4241
|
description: "Search; every hit carries its standing. Flagged, never filtered: a superseded hit returns with its replacement, a rejected one is marked. Prefer kb_load when the base fits its budget \u2014 a full read beats search. Results are volatile: place them at the tail, not the cached prefix. Never read record files directly.",
|
|
3587
|
-
input:
|
|
4242
|
+
input: z23.object({
|
|
3588
4243
|
bundlePath,
|
|
3589
|
-
text:
|
|
3590
|
-
type:
|
|
3591
|
-
includeNonCurrent:
|
|
4244
|
+
text: z23.string().optional(),
|
|
4245
|
+
type: z23.enum(KB_RECORD_TYPES).optional(),
|
|
4246
|
+
includeNonCurrent: z23.boolean().optional(),
|
|
3592
4247
|
repoRoot: REPO_ROOT
|
|
3593
4248
|
}),
|
|
3594
4249
|
// `--repo-root` is a flag, so its value must not fall into the search text.
|
|
@@ -3620,43 +4275,43 @@ var queryCommand = define({
|
|
|
3620
4275
|
});
|
|
3621
4276
|
|
|
3622
4277
|
// src/commands/read-index.ts
|
|
3623
|
-
import { z as
|
|
4278
|
+
import { z as z24 } from "zod";
|
|
3624
4279
|
var readIndexCommand = define({
|
|
3625
4280
|
name: "index",
|
|
3626
4281
|
tool: "kb_index",
|
|
3627
4282
|
usage: "index",
|
|
3628
4283
|
description: "The index \u2014 title, type, status, description per record \u2014 rebuilt if stale. Cheapest re-orientation after compaction: call it (or kb_context) first, then kb_load or fetch by id.",
|
|
3629
|
-
input:
|
|
4284
|
+
input: z24.object({ bundlePath }),
|
|
3630
4285
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
3631
4286
|
run: ({ store }, { bundlePath: path }) => store.readIndex(path)
|
|
3632
4287
|
});
|
|
3633
4288
|
|
|
3634
4289
|
// src/commands/schema.ts
|
|
3635
|
-
import { z as
|
|
4290
|
+
import { z as z25 } from "zod";
|
|
3636
4291
|
var schemaCommand = define({
|
|
3637
4292
|
name: "schema",
|
|
3638
4293
|
tool: "kb_schema",
|
|
3639
4294
|
usage: "schema",
|
|
3640
4295
|
description: "JSON Schema for frontmatter, write input, and log entries, generated from the enforcing code.",
|
|
3641
|
-
input:
|
|
4296
|
+
input: z25.object({}),
|
|
3642
4297
|
fromArgv: () => ({}),
|
|
3643
4298
|
run: () => Promise.resolve(kbJsonSchemas())
|
|
3644
4299
|
});
|
|
3645
4300
|
|
|
3646
4301
|
// src/commands/stamp.ts
|
|
3647
|
-
import { readFile as
|
|
3648
|
-
import { z as
|
|
4302
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
4303
|
+
import { z as z26 } from "zod";
|
|
3649
4304
|
var DIGEST = /^[0-9a-f]{64}$/;
|
|
3650
4305
|
var stampCommand = define({
|
|
3651
4306
|
name: "stamp",
|
|
3652
4307
|
tool: "kb_stamp",
|
|
3653
4308
|
usage: "stamp [--bundle PATH] [--since DIGEST|FILE]",
|
|
3654
4309
|
description: "Content stamp of a base \u2014 `load`'s digest, record counts, per-record digests \u2014 without any bodies. Takes no bundlePath to stamp every pinned base. With `since`, reports only the bases that moved, naming the changed ids when the baseline is a prior stamp; silent when nothing changed. Reads, never writes.",
|
|
3655
|
-
input:
|
|
3656
|
-
bundlePath:
|
|
4310
|
+
input: z26.object({
|
|
4311
|
+
bundlePath: z26.string().min(1).optional().describe(
|
|
3657
4312
|
"Absolute path to one knowledge base. Omit to stamp every pinned base."
|
|
3658
4313
|
),
|
|
3659
|
-
since:
|
|
4314
|
+
since: z26.string().min(1).optional().describe(
|
|
3660
4315
|
"Prior digest, or path to a prior `stamp --json`; only moved bases return, with changed ids when the baseline is a file."
|
|
3661
4316
|
)
|
|
3662
4317
|
}),
|
|
@@ -3718,7 +4373,7 @@ async function readBaseline(since) {
|
|
|
3718
4373
|
if (DIGEST.test(since)) return { digest: since, byPath: /* @__PURE__ */ new Map() };
|
|
3719
4374
|
let parsed;
|
|
3720
4375
|
try {
|
|
3721
|
-
parsed = JSON.parse(await
|
|
4376
|
+
parsed = JSON.parse(await readFile6(since, "utf8"));
|
|
3722
4377
|
} catch {
|
|
3723
4378
|
throw new KbStampBaselineError(since);
|
|
3724
4379
|
}
|
|
@@ -3742,16 +4397,16 @@ async function readBaseline(since) {
|
|
|
3742
4397
|
}
|
|
3743
4398
|
|
|
3744
4399
|
// src/commands/status.ts
|
|
3745
|
-
import { z as
|
|
4400
|
+
import { z as z27 } from "zod";
|
|
3746
4401
|
var statusCommand = define({
|
|
3747
4402
|
name: "status",
|
|
3748
4403
|
tool: "kb_status",
|
|
3749
4404
|
usage: "status <concept-id> <status>",
|
|
3750
4405
|
description: "Move a record's status. Compare-and-swap: a concurrent change fails instead of being overwritten.",
|
|
3751
|
-
input:
|
|
4406
|
+
input: z27.object({
|
|
3752
4407
|
bundlePath,
|
|
3753
4408
|
conceptId,
|
|
3754
|
-
status:
|
|
4409
|
+
status: z27.enum(KB_RECORD_STATUSES)
|
|
3755
4410
|
}),
|
|
3756
4411
|
fromArgv: (argv, path) => ({
|
|
3757
4412
|
bundlePath: path,
|
|
@@ -3766,13 +4421,13 @@ var statusCommand = define({
|
|
|
3766
4421
|
});
|
|
3767
4422
|
|
|
3768
4423
|
// src/commands/supersede.ts
|
|
3769
|
-
import { z as
|
|
4424
|
+
import { z as z28 } from "zod";
|
|
3770
4425
|
var supersedeCommand = define({
|
|
3771
4426
|
name: "supersede",
|
|
3772
4427
|
tool: "kb_supersede",
|
|
3773
4428
|
usage: "supersede <concept-id> <replacement-id>",
|
|
3774
4429
|
description: "Mark a record superseded by another, linked in both directions. Use instead of editing a record whose meaning changed.",
|
|
3775
|
-
input:
|
|
4430
|
+
input: z28.object({ bundlePath, conceptId, replacementId: conceptId }),
|
|
3776
4431
|
fromArgv: (argv, path) => ({
|
|
3777
4432
|
bundlePath: path,
|
|
3778
4433
|
conceptId: argv[1],
|
|
@@ -3786,16 +4441,16 @@ var supersedeCommand = define({
|
|
|
3786
4441
|
});
|
|
3787
4442
|
|
|
3788
4443
|
// src/commands/sync-instructions.ts
|
|
3789
|
-
import { z as
|
|
4444
|
+
import { z as z29 } from "zod";
|
|
3790
4445
|
var syncInstructionsCommand = define({
|
|
3791
4446
|
name: "sync-instructions",
|
|
3792
4447
|
usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
|
|
3793
4448
|
description: "CLI-only: plant the kb_context block between sentinel comments in AGENTS.md or CLAUDE.md, idempotently.",
|
|
3794
|
-
input:
|
|
3795
|
-
file:
|
|
3796
|
-
budgetTokens:
|
|
3797
|
-
fullUnderTokens:
|
|
3798
|
-
profile:
|
|
4449
|
+
input: z29.object({
|
|
4450
|
+
file: z29.string().min(1).describe("The instruction file to edit in place."),
|
|
4451
|
+
budgetTokens: z29.number().int().positive().optional(),
|
|
4452
|
+
fullUnderTokens: z29.number().int().positive().optional(),
|
|
4453
|
+
profile: z29.string().optional()
|
|
3799
4454
|
}),
|
|
3800
4455
|
fromArgv: (argv) => {
|
|
3801
4456
|
const budget = argvFlag(argv, "--budget");
|
|
@@ -3821,17 +4476,17 @@ var syncInstructionsCommand = define({
|
|
|
3821
4476
|
});
|
|
3822
4477
|
|
|
3823
4478
|
// src/commands/trace.ts
|
|
3824
|
-
import { z as
|
|
4479
|
+
import { z as z30 } from "zod";
|
|
3825
4480
|
var traceCommand = define({
|
|
3826
4481
|
name: "trace",
|
|
3827
4482
|
tool: "kb_trace",
|
|
3828
4483
|
usage: "trace <concept-id> [edges...]",
|
|
3829
4484
|
description: 'Timeline of how a position was reached, ordered by write time, following supersession, shared anchors and shared sources. Includes rejected, draft and superseded records \u2014 in a history they are the content. For "why is it like this"; kb_load answers "what holds now".',
|
|
3830
|
-
input:
|
|
4485
|
+
input: z30.object({
|
|
3831
4486
|
bundlePath,
|
|
3832
4487
|
conceptId,
|
|
3833
|
-
edges:
|
|
3834
|
-
depth:
|
|
4488
|
+
edges: z30.array(z30.enum(TRACE_EDGES)).optional(),
|
|
4489
|
+
depth: z30.number().int().positive().optional()
|
|
3835
4490
|
}),
|
|
3836
4491
|
fromArgv: (argv, path) => ({
|
|
3837
4492
|
bundlePath: path,
|
|
@@ -3853,37 +4508,37 @@ var traceCommand = define({
|
|
|
3853
4508
|
});
|
|
3854
4509
|
|
|
3855
4510
|
// src/commands/types.ts
|
|
3856
|
-
import { z as
|
|
4511
|
+
import { z as z31 } from "zod";
|
|
3857
4512
|
var typesCommand = define({
|
|
3858
4513
|
name: "types",
|
|
3859
4514
|
tool: "kb_types",
|
|
3860
4515
|
usage: "types",
|
|
3861
4516
|
description: "The twelve record types with their purpose, body sections, and starting status. Read this before writing rather than guessing headings \u2014 a section the type does not define is rejected.",
|
|
3862
|
-
input:
|
|
4517
|
+
input: z31.object({}),
|
|
3863
4518
|
fromArgv: () => ({}),
|
|
3864
4519
|
run: () => Promise.resolve(RECORD_TYPES)
|
|
3865
4520
|
});
|
|
3866
4521
|
|
|
3867
4522
|
// src/commands/unpin.ts
|
|
3868
|
-
import { z as
|
|
4523
|
+
import { z as z32 } from "zod";
|
|
3869
4524
|
var unpinCommand = define({
|
|
3870
4525
|
name: "unpin",
|
|
3871
4526
|
tool: "kb_unpin",
|
|
3872
4527
|
usage: "unpin [bundle-path]",
|
|
3873
4528
|
description: "Remove a base from every manifest layer that holds it. Reports the layers touched.",
|
|
3874
|
-
input:
|
|
4529
|
+
input: z32.object({ bundlePath }),
|
|
3875
4530
|
fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
|
|
3876
4531
|
run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
|
|
3877
4532
|
});
|
|
3878
4533
|
|
|
3879
4534
|
// src/commands/validate.ts
|
|
3880
|
-
import { z as
|
|
4535
|
+
import { z as z33 } from "zod";
|
|
3881
4536
|
var validateCommand = define({
|
|
3882
4537
|
name: "validate",
|
|
3883
4538
|
tool: "kb_validate",
|
|
3884
4539
|
usage: "validate",
|
|
3885
4540
|
description: "Check pointers no single record can see: supersession links that disagree between the two records, typed causal links, and assumptions that cite sources. Each finding carries a severity: errors fail the exit code, warnings do not.",
|
|
3886
|
-
input:
|
|
4541
|
+
input: z33.object({ bundlePath }),
|
|
3887
4542
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
3888
4543
|
run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
|
|
3889
4544
|
// Warnings never fail the exit code; every other severity does.
|
|
@@ -3893,16 +4548,16 @@ var validateCommand = define({
|
|
|
3893
4548
|
});
|
|
3894
4549
|
|
|
3895
4550
|
// src/commands/verify.ts
|
|
3896
|
-
import { z as
|
|
4551
|
+
import { z as z34 } from "zod";
|
|
3897
4552
|
var verifyCommand = define({
|
|
3898
4553
|
name: "verify",
|
|
3899
4554
|
tool: "kb_verify",
|
|
3900
4555
|
usage: "verify <concept-id> --note <text>",
|
|
3901
4556
|
description: "Append a verified[] event: who checked, when, and what was found. Append-only. A record's own generator is refused unless the actor is `human:`-prefixed.",
|
|
3902
|
-
input:
|
|
4557
|
+
input: z34.object({
|
|
3903
4558
|
bundlePath,
|
|
3904
4559
|
conceptId,
|
|
3905
|
-
note:
|
|
4560
|
+
note: z34.string().refine((s) => s.trim().length > 0, {
|
|
3906
4561
|
message: "note must say what the check found"
|
|
3907
4562
|
})
|
|
3908
4563
|
}),
|
|
@@ -3922,15 +4577,15 @@ var verifyCommand = define({
|
|
|
3922
4577
|
});
|
|
3923
4578
|
|
|
3924
4579
|
// src/commands/write.ts
|
|
3925
|
-
import { z as
|
|
4580
|
+
import { z as z35 } from "zod";
|
|
3926
4581
|
var writeCommand = define({
|
|
3927
4582
|
name: "write",
|
|
3928
4583
|
tool: "kb_write",
|
|
3929
4584
|
usage: "write <type> < record.json",
|
|
3930
4585
|
description: "Write one record. Search first \u2014 a duplicate concept id is rejected, not overwritten; kb_types lists each type's sections. An unsourced claim is an `assumption` with assumption: true, never a vague `fact`. Conflicting records get a `risk`, `open-question`, or superseding `decision`. Prefer a new short record over overloading one. Never delete; supersede.",
|
|
3931
|
-
input:
|
|
4586
|
+
input: z35.object({
|
|
3932
4587
|
bundlePath,
|
|
3933
|
-
type:
|
|
4588
|
+
type: z35.enum(KB_RECORD_TYPES),
|
|
3934
4589
|
input: composeInputSchema
|
|
3935
4590
|
}),
|
|
3936
4591
|
fromArgv: async (argv, path, stdin) => ({
|
|
@@ -3954,13 +4609,13 @@ var writeCommand = define({
|
|
|
3954
4609
|
});
|
|
3955
4610
|
|
|
3956
4611
|
// src/commands/write-decision.ts
|
|
3957
|
-
import { z as
|
|
4612
|
+
import { z as z36 } from "zod";
|
|
3958
4613
|
var writeDecisionCommand = define({
|
|
3959
4614
|
name: "write-decision",
|
|
3960
4615
|
tool: "kb_write_decision",
|
|
3961
4616
|
usage: "write-decision < decision.json",
|
|
3962
4617
|
description: "Write a decision, with `alternative` (what was rejected and why) and `impact` as fields. Record one when a later reader would otherwise simplify the constraint away; skip when the diff already answers it. `sources` for material read, `anchors` for code, `relatedConceptIds` for records.",
|
|
3963
|
-
input:
|
|
4618
|
+
input: z36.object({ bundlePath, input: decisionInputSchema }),
|
|
3964
4619
|
fromArgv: async (_argv, path, stdin) => ({
|
|
3965
4620
|
bundlePath: path,
|
|
3966
4621
|
input: JSON.parse(await stdin())
|
|
@@ -4042,7 +4697,7 @@ function parseMarkdownWithFrontmatter(text, schema) {
|
|
|
4042
4697
|
|
|
4043
4698
|
// src/search-index.ts
|
|
4044
4699
|
import { stat as stat2 } from "fs/promises";
|
|
4045
|
-
import { join as
|
|
4700
|
+
import { join as join6 } from "path";
|
|
4046
4701
|
var SEARCH_INDEX_FILE = ".index.sqlite";
|
|
4047
4702
|
var COLLECTION = "kb";
|
|
4048
4703
|
async function searchBase(bundlePath2, query, options = {}) {
|
|
@@ -4051,7 +4706,7 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
4051
4706
|
let store = null;
|
|
4052
4707
|
try {
|
|
4053
4708
|
store = await qmd.createStore({
|
|
4054
|
-
dbPath:
|
|
4709
|
+
dbPath: join6(bundlePath2, SEARCH_INDEX_FILE),
|
|
4055
4710
|
config: {
|
|
4056
4711
|
collections: {
|
|
4057
4712
|
[COLLECTION]: {
|
|
@@ -4086,7 +4741,7 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
4086
4741
|
}
|
|
4087
4742
|
}
|
|
4088
4743
|
async function isStale(bundlePath2) {
|
|
4089
|
-
const indexAt = await stat2(
|
|
4744
|
+
const indexAt = await stat2(join6(bundlePath2, SEARCH_INDEX_FILE)).then((s) => s.mtimeMs).catch(() => 0);
|
|
4090
4745
|
if (!indexAt) return true;
|
|
4091
4746
|
const { readdir: readdir2 } = await import("fs/promises");
|
|
4092
4747
|
const names = (await readdir2(bundlePath2).catch(() => [])).filter(
|
|
@@ -4095,7 +4750,7 @@ async function isStale(bundlePath2) {
|
|
|
4095
4750
|
let stale = false;
|
|
4096
4751
|
await mapLimit(names, DEFAULT_IO_CONCURRENCY, async (name) => {
|
|
4097
4752
|
if (stale) return;
|
|
4098
|
-
const at = await stat2(
|
|
4753
|
+
const at = await stat2(join6(bundlePath2, name)).then((s) => s.mtimeMs).catch(() => 0);
|
|
4099
4754
|
if (at > indexAt) stale = true;
|
|
4100
4755
|
});
|
|
4101
4756
|
return stale;
|
|
@@ -4133,25 +4788,25 @@ async function loadQmd(logger) {
|
|
|
4133
4788
|
import {
|
|
4134
4789
|
appendFile,
|
|
4135
4790
|
link,
|
|
4136
|
-
mkdir as
|
|
4791
|
+
mkdir as mkdir4,
|
|
4137
4792
|
readdir,
|
|
4138
|
-
readFile as
|
|
4139
|
-
rename,
|
|
4793
|
+
readFile as readFile7,
|
|
4794
|
+
rename as rename2,
|
|
4140
4795
|
unlink,
|
|
4141
|
-
writeFile as
|
|
4796
|
+
writeFile as writeFile4
|
|
4142
4797
|
} from "fs/promises";
|
|
4143
|
-
import { join as
|
|
4798
|
+
import { join as join7, resolve as resolve5, sep as sep3 } from "path";
|
|
4144
4799
|
|
|
4145
4800
|
// src/kb-stamp.ts
|
|
4146
|
-
import { createHash as
|
|
4147
|
-
function
|
|
4148
|
-
return
|
|
4801
|
+
import { createHash as createHash4 } from "crypto";
|
|
4802
|
+
function sha2563(contents) {
|
|
4803
|
+
return createHash4("sha256").update(contents).digest("hex");
|
|
4149
4804
|
}
|
|
4150
4805
|
function bundleStamp(records, superseded) {
|
|
4151
4806
|
const entries = [
|
|
4152
4807
|
...records.map((hit) => ({
|
|
4153
4808
|
conceptId: hit.record.conceptId,
|
|
4154
|
-
digest: `current:${
|
|
4809
|
+
digest: `current:${sha2563(
|
|
4155
4810
|
stringifyMarkdownWithFrontmatter(
|
|
4156
4811
|
hit.record.body,
|
|
4157
4812
|
hit.record.frontmatter
|
|
@@ -4160,11 +4815,11 @@ function bundleStamp(records, superseded) {
|
|
|
4160
4815
|
})),
|
|
4161
4816
|
...superseded.map((entry) => ({
|
|
4162
4817
|
conceptId: entry.conceptId,
|
|
4163
|
-
digest: `superseded:${
|
|
4818
|
+
digest: `superseded:${sha2563(JSON.stringify(entry))}`
|
|
4164
4819
|
}))
|
|
4165
4820
|
].sort((a, b) => a.conceptId < b.conceptId ? -1 : 1);
|
|
4166
4821
|
return {
|
|
4167
|
-
digest:
|
|
4822
|
+
digest: sha2563(
|
|
4168
4823
|
entries.map((entry) => `${entry.conceptId}:${entry.digest}`).join("\n")
|
|
4169
4824
|
),
|
|
4170
4825
|
records: entries
|
|
@@ -4334,7 +4989,7 @@ function appendUnionMergeLine(contents) {
|
|
|
4334
4989
|
}
|
|
4335
4990
|
|
|
4336
4991
|
// src/kb-store.ts
|
|
4337
|
-
var KB_DIR =
|
|
4992
|
+
var KB_DIR = join7(".strauss", "kb");
|
|
4338
4993
|
var STORE_OWNED = /* @__PURE__ */ new Set([INDEX_FILE, LOG_FILE, SEARCH_INDEX_FILE]);
|
|
4339
4994
|
var DEFAULT_LOAD_BUDGET = 25e3;
|
|
4340
4995
|
var KbStore = class {
|
|
@@ -4365,7 +5020,7 @@ var KbStore = class {
|
|
|
4365
5020
|
const conceptId2 = `${input.type}.${input.slug}`;
|
|
4366
5021
|
const root = this.root(bundlePath2);
|
|
4367
5022
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
4368
|
-
await
|
|
5023
|
+
await mkdir4(root, { recursive: true });
|
|
4369
5024
|
await this.publish(
|
|
4370
5025
|
target,
|
|
4371
5026
|
stringifyMarkdownWithFrontmatter(input.body, frontmatter),
|
|
@@ -4404,7 +5059,7 @@ var KbStore = class {
|
|
|
4404
5059
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
4405
5060
|
let raw;
|
|
4406
5061
|
try {
|
|
4407
|
-
raw = await
|
|
5062
|
+
raw = await readFile7(target, "utf8");
|
|
4408
5063
|
} catch {
|
|
4409
5064
|
return null;
|
|
4410
5065
|
}
|
|
@@ -4429,7 +5084,7 @@ var KbStore = class {
|
|
|
4429
5084
|
const records = await mapLimit(
|
|
4430
5085
|
wanted,
|
|
4431
5086
|
DEFAULT_IO_CONCURRENCY,
|
|
4432
|
-
async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await
|
|
5087
|
+
async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await readFile7(join7(root, name), "utf8"))
|
|
4433
5088
|
);
|
|
4434
5089
|
return records.filter((record) => record !== null);
|
|
4435
5090
|
}
|
|
@@ -4586,7 +5241,7 @@ ${answer}
|
|
|
4586
5241
|
if (found.length) return found;
|
|
4587
5242
|
}
|
|
4588
5243
|
const lowered = needle.toLowerCase();
|
|
4589
|
-
return bundle.filter((record) =>
|
|
5244
|
+
return bundle.filter((record) => matches2(record, lowered));
|
|
4590
5245
|
}
|
|
4591
5246
|
/**
|
|
4592
5247
|
* Anchor drift over the records about to be handed back. Like the search
|
|
@@ -4756,11 +5411,11 @@ ${answer}
|
|
|
4756
5411
|
async readIndex(bundlePath2) {
|
|
4757
5412
|
const root = this.root(bundlePath2);
|
|
4758
5413
|
const expected = renderIndex(await this.list(bundlePath2));
|
|
4759
|
-
const stored = await
|
|
5414
|
+
const stored = await readFile7(join7(root, INDEX_FILE), "utf8").catch(
|
|
4760
5415
|
() => null
|
|
4761
5416
|
);
|
|
4762
5417
|
if (indexIsStale(stored, expected)) {
|
|
4763
|
-
await this.publish(
|
|
5418
|
+
await this.publish(join7(root, INDEX_FILE), expected, true, INDEX_FILE);
|
|
4764
5419
|
this.logger.info?.({
|
|
4765
5420
|
operation: "kb.index.repair",
|
|
4766
5421
|
bundlePath: root,
|
|
@@ -4777,8 +5432,8 @@ ${answer}
|
|
|
4777
5432
|
* knows which agent touched what. So a bad line is surfaced and left alone.
|
|
4778
5433
|
*/
|
|
4779
5434
|
async readLog(bundlePath2) {
|
|
4780
|
-
const raw = await
|
|
4781
|
-
|
|
5435
|
+
const raw = await readFile7(
|
|
5436
|
+
join7(this.root(bundlePath2), LOG_FILE),
|
|
4782
5437
|
"utf8"
|
|
4783
5438
|
).catch(() => "");
|
|
4784
5439
|
const result = parseLog(raw);
|
|
@@ -4829,15 +5484,15 @@ ${answer}
|
|
|
4829
5484
|
}
|
|
4830
5485
|
async mutate(bundlePath2, conceptId2, change, entry, changeBody = (body) => body) {
|
|
4831
5486
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
4832
|
-
const before = await
|
|
5487
|
+
const before = await readFile7(target, "utf8").catch(() => null);
|
|
4833
5488
|
if (before === null) throw new KbRecordNotFoundError(conceptId2);
|
|
4834
5489
|
const parsed = this.parse(conceptId2, before);
|
|
4835
5490
|
if (!parsed) throw new KbRecordNotFoundError(conceptId2);
|
|
4836
5491
|
const frontmatter = change(parsed.frontmatter);
|
|
4837
5492
|
const body = changeBody(parsed.body);
|
|
4838
5493
|
const contents = stringifyMarkdownWithFrontmatter(body, frontmatter);
|
|
4839
|
-
const witness = await
|
|
4840
|
-
if (witness === null ||
|
|
5494
|
+
const witness = await readFile7(target, "utf8").catch(() => null);
|
|
5495
|
+
if (witness === null || sha2563(witness) !== sha2563(before)) {
|
|
4841
5496
|
throw new KbWriteConflictError(conceptId2);
|
|
4842
5497
|
}
|
|
4843
5498
|
await this.publish(target, contents, true, conceptId2);
|
|
@@ -4862,10 +5517,10 @@ ${answer}
|
|
|
4862
5517
|
*/
|
|
4863
5518
|
async publish(target, contents, overwrite, conceptId2) {
|
|
4864
5519
|
const staging = `${target}.${process.pid}.tmp`;
|
|
4865
|
-
await
|
|
5520
|
+
await writeFile4(staging, contents, "utf8");
|
|
4866
5521
|
try {
|
|
4867
5522
|
if (overwrite) {
|
|
4868
|
-
await
|
|
5523
|
+
await rename2(staging, target);
|
|
4869
5524
|
return;
|
|
4870
5525
|
}
|
|
4871
5526
|
await link(staging, target);
|
|
@@ -4919,18 +5574,18 @@ ${answer}
|
|
|
4919
5574
|
* file must not fail the mutation it guards.
|
|
4920
5575
|
*/
|
|
4921
5576
|
async ensureGitattributes(root) {
|
|
4922
|
-
const target =
|
|
5577
|
+
const target = join7(root, GITATTRIBUTES_FILE);
|
|
4923
5578
|
try {
|
|
4924
5579
|
let existing;
|
|
4925
5580
|
try {
|
|
4926
|
-
existing = await
|
|
5581
|
+
existing = await readFile7(target, "utf8");
|
|
4927
5582
|
} catch (error) {
|
|
4928
5583
|
if (error.code !== "ENOENT") throw error;
|
|
4929
5584
|
existing = null;
|
|
4930
5585
|
}
|
|
4931
5586
|
if (existing === null) {
|
|
4932
5587
|
try {
|
|
4933
|
-
await
|
|
5588
|
+
await writeFile4(target, appendUnionMergeLine(""), {
|
|
4934
5589
|
encoding: "utf8",
|
|
4935
5590
|
flag: "wx"
|
|
4936
5591
|
});
|
|
@@ -4970,7 +5625,7 @@ ${answer}
|
|
|
4970
5625
|
async record(root, entry) {
|
|
4971
5626
|
await this.ensureGitattributes(root);
|
|
4972
5627
|
const line = renderLogEntry({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
|
|
4973
|
-
await appendFile(
|
|
5628
|
+
await appendFile(join7(root, LOG_FILE), line, "utf8").catch((error) => {
|
|
4974
5629
|
this.logger.warn?.({
|
|
4975
5630
|
operation: "kb.log.append",
|
|
4976
5631
|
outcome: "failed",
|
|
@@ -5007,7 +5662,7 @@ ${answer}
|
|
|
5007
5662
|
{ conceptId: conceptId2 }
|
|
5008
5663
|
);
|
|
5009
5664
|
}
|
|
5010
|
-
return
|
|
5665
|
+
return join7(this.root(bundlePath2), `${conceptId2}.md`);
|
|
5011
5666
|
}
|
|
5012
5667
|
};
|
|
5013
5668
|
function estimateTokens(record) {
|
|
@@ -5034,7 +5689,7 @@ function stub(hit) {
|
|
|
5034
5689
|
at: hit.record.frontmatter.generated?.at ?? null
|
|
5035
5690
|
};
|
|
5036
5691
|
}
|
|
5037
|
-
function
|
|
5692
|
+
function matches2(record, needle) {
|
|
5038
5693
|
const { title, description } = record.frontmatter;
|
|
5039
5694
|
return [record.conceptId, title, description, record.body].some(
|
|
5040
5695
|
(field) => field?.toLowerCase().includes(needle)
|
|
@@ -5126,12 +5781,12 @@ function byRank(left, right) {
|
|
|
5126
5781
|
) || left.record.conceptId.localeCompare(right.record.conceptId);
|
|
5127
5782
|
}
|
|
5128
5783
|
function typeRank(record) {
|
|
5129
|
-
const
|
|
5130
|
-
return
|
|
5784
|
+
const index2 = TYPE_PRIORITY.indexOf(record.frontmatter.type);
|
|
5785
|
+
return index2 === -1 ? TYPE_PRIORITY.length : index2;
|
|
5131
5786
|
}
|
|
5132
5787
|
|
|
5133
5788
|
// src/version.ts
|
|
5134
|
-
var VERSION = true ? "0.1.
|
|
5789
|
+
var VERSION = true ? "0.1.17" : "0.0.0-dev";
|
|
5135
5790
|
|
|
5136
5791
|
export {
|
|
5137
5792
|
kbSourceSchema,
|
|
@@ -5167,9 +5822,19 @@ export {
|
|
|
5167
5822
|
repoCacheDir,
|
|
5168
5823
|
readRemoteAnchors,
|
|
5169
5824
|
anchorFilePath,
|
|
5825
|
+
grammarsCacheRoot,
|
|
5826
|
+
grammarManifest,
|
|
5827
|
+
ensureGrammar,
|
|
5828
|
+
grammarHints,
|
|
5829
|
+
languageForFile,
|
|
5830
|
+
treeSitterLanguages,
|
|
5831
|
+
TreeSitterResolver,
|
|
5170
5832
|
regexResolver,
|
|
5171
5833
|
hashAnchorText,
|
|
5172
5834
|
resolveAnchor,
|
|
5835
|
+
resolveAnchorSpan,
|
|
5836
|
+
prepareResolvers,
|
|
5837
|
+
defaultAnchorResolvers,
|
|
5173
5838
|
detectAnchorDrift,
|
|
5174
5839
|
Fault,
|
|
5175
5840
|
ErrorTypes,
|
|
@@ -5247,4 +5912,4 @@ export {
|
|
|
5247
5912
|
KbStore,
|
|
5248
5913
|
VERSION
|
|
5249
5914
|
};
|
|
5250
|
-
//# sourceMappingURL=chunk-
|
|
5915
|
+
//# sourceMappingURL=chunk-ZKIQOBHT.js.map
|