@saasontools/strauss-kb 0.1.15 → 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 +31 -15
- package/dist/{chunk-MK7GU4DX.js → chunk-RMJUGTAQ.js} +15 -6
- package/dist/chunk-RMJUGTAQ.js.map +1 -0
- package/dist/{chunk-LACCRB2Y.js → chunk-SA3A2SPY.js} +2 -2
- package/dist/{chunk-KNIUBCZY.js → chunk-ZKIQOBHT.js} +1122 -292
- package/dist/chunk-ZKIQOBHT.js.map +1 -0
- package/dist/cli-main.cjs +1138 -310
- package/dist/cli-main.cjs.map +1 -1
- package/dist/cli-main.js +2 -2
- package/dist/index.cjs +1193 -336
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +244 -7
- package/dist/index.d.ts +244 -7
- package/dist/index.js +23 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp-main.cjs +1122 -303
- 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-KNIUBCZY.js.map +0 -1
- package/dist/chunk-MK7GU4DX.js.map +0 -1
- /package/dist/{chunk-LACCRB2Y.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
|
+
}
|
|
1139
1665
|
};
|
|
1140
1666
|
}
|
|
1141
|
-
|
|
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 } : {}
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
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 };
|
|
1222
1790
|
return {
|
|
1223
|
-
|
|
1224
|
-
|
|
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) {
|
|
1800
|
+
return {
|
|
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
|
|
@@ -1273,6 +1869,8 @@ var ErrorTypes = /* @__PURE__ */ ((ErrorTypes2) => {
|
|
|
1273
1869
|
ErrorTypes2["KbPackBudgetExceeded"] = "KbPackBudgetExceeded";
|
|
1274
1870
|
ErrorTypes2["KbRecordNotFound"] = "KbRecordNotFound";
|
|
1275
1871
|
ErrorTypes2["KbSelfVerification"] = "KbSelfVerification";
|
|
1872
|
+
ErrorTypes2["KbStampBaselineUnreadable"] = "KbStampBaselineUnreadable";
|
|
1873
|
+
ErrorTypes2["KbStampDigestBaselineAmbiguous"] = "KbStampDigestBaselineAmbiguous";
|
|
1276
1874
|
ErrorTypes2["KbUnknownLinkRel"] = "KbUnknownLinkRel";
|
|
1277
1875
|
ErrorTypes2["KbWriteConflict"] = "KbWriteConflict";
|
|
1278
1876
|
return ErrorTypes2;
|
|
@@ -1427,13 +2025,43 @@ var KbInvalidConceptIdError = class extends BaseError {
|
|
|
1427
2025
|
});
|
|
1428
2026
|
}
|
|
1429
2027
|
};
|
|
2028
|
+
var KbStampBaselineError = class extends BaseError {
|
|
2029
|
+
constructor(since) {
|
|
2030
|
+
super({
|
|
2031
|
+
message: `kb: --since ${since} is neither a 64-character digest nor a readable stamp file`,
|
|
2032
|
+
errorType: "KbStampBaselineUnreadable" /* KbStampBaselineUnreadable */,
|
|
2033
|
+
code: 400,
|
|
2034
|
+
fault: "User" /* User */,
|
|
2035
|
+
retriable: false,
|
|
2036
|
+
reportToUser: true,
|
|
2037
|
+
details: { since }
|
|
2038
|
+
});
|
|
2039
|
+
this.since = since;
|
|
2040
|
+
}
|
|
2041
|
+
since;
|
|
2042
|
+
};
|
|
2043
|
+
var KbStampDigestBaselineError = class extends BaseError {
|
|
2044
|
+
constructor(since) {
|
|
2045
|
+
super({
|
|
2046
|
+
message: `kb: --since ${since} is a digest, which needs --bundle (one base) \u2014 a file baseline works for many`,
|
|
2047
|
+
errorType: "KbStampDigestBaselineAmbiguous" /* KbStampDigestBaselineAmbiguous */,
|
|
2048
|
+
code: 400,
|
|
2049
|
+
fault: "User" /* User */,
|
|
2050
|
+
retriable: false,
|
|
2051
|
+
reportToUser: true,
|
|
2052
|
+
details: { since }
|
|
2053
|
+
});
|
|
2054
|
+
this.since = since;
|
|
2055
|
+
}
|
|
2056
|
+
since;
|
|
2057
|
+
};
|
|
1430
2058
|
|
|
1431
2059
|
// src/kb-pins/budgets.ts
|
|
1432
2060
|
function asBudgets(value) {
|
|
1433
2061
|
if (value === null || typeof value !== "object") return {};
|
|
1434
|
-
const
|
|
2062
|
+
const table2 = value;
|
|
1435
2063
|
const pick = (key, min) => {
|
|
1436
|
-
const raw =
|
|
2064
|
+
const raw = table2[key];
|
|
1437
2065
|
return typeof raw === "number" && Number.isInteger(raw) && raw >= min ? raw : void 0;
|
|
1438
2066
|
};
|
|
1439
2067
|
const budgetTokens = pick("budgetTokens", 1);
|
|
@@ -1444,9 +2072,9 @@ function asBudgets(value) {
|
|
|
1444
2072
|
};
|
|
1445
2073
|
}
|
|
1446
2074
|
function contextProfileBudgets(manifest, profile) {
|
|
1447
|
-
const
|
|
1448
|
-
if (
|
|
1449
|
-
const entries =
|
|
2075
|
+
const table2 = manifest.context;
|
|
2076
|
+
if (table2 === null || typeof table2 !== "object") return {};
|
|
2077
|
+
const entries = table2;
|
|
1450
2078
|
return {
|
|
1451
2079
|
...asBudgets(entries["default"]),
|
|
1452
2080
|
...profile ? asBudgets(entries[profile]) : {}
|
|
@@ -1477,15 +2105,15 @@ var KbBaseFrozenError = class extends Error {
|
|
|
1477
2105
|
};
|
|
1478
2106
|
|
|
1479
2107
|
// src/kb-pins/model.ts
|
|
1480
|
-
import { join as
|
|
1481
|
-
import { z as
|
|
1482
|
-
var PINS_FILE =
|
|
1483
|
-
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");
|
|
1484
2112
|
var PIN_LAYERS = ["project", "local", "user"];
|
|
1485
|
-
var pinSchema =
|
|
2113
|
+
var pinSchema = z5.object({
|
|
1486
2114
|
/** Relative to the manifest's root, so the file is committable. */
|
|
1487
|
-
path:
|
|
1488
|
-
pinnedAt:
|
|
2115
|
+
path: z5.string().min(1),
|
|
2116
|
+
pinnedAt: z5.string().min(1).optional(),
|
|
1489
2117
|
/**
|
|
1490
2118
|
* How `context` renders this base. `full` preloads the whole base into
|
|
1491
2119
|
* the block regardless of the full-under threshold — for a base whose
|
|
@@ -1495,7 +2123,7 @@ var pinSchema = z4.object({
|
|
|
1495
2123
|
* Absent: the profile's full-under threshold decides. Invalid values
|
|
1496
2124
|
* degrade to absent rather than failing the manifest.
|
|
1497
2125
|
*/
|
|
1498
|
-
mode:
|
|
2126
|
+
mode: z5.enum(["full", "index"]).optional().catch(void 0),
|
|
1499
2127
|
/**
|
|
1500
2128
|
* Context profiles this pin surfaces in (e.g. only at session-start,
|
|
1501
2129
|
* not per turn). Absent: every profile. A run without a profile sees
|
|
@@ -1503,17 +2131,17 @@ var pinSchema = z4.object({
|
|
|
1503
2131
|
* that skill at point of use than pinned at all — pins are what every
|
|
1504
2132
|
* session should see.
|
|
1505
2133
|
*/
|
|
1506
|
-
profiles:
|
|
2134
|
+
profiles: z5.array(z5.string()).optional().catch(void 0),
|
|
1507
2135
|
/**
|
|
1508
2136
|
* The base is concluded — a finished piece of research, a frozen ADR
|
|
1509
2137
|
* set. Write commands against it refuse while this workspace holds the
|
|
1510
2138
|
* pin, and `context` labels it read-only. Workspace policy, not base
|
|
1511
2139
|
* state: the base itself stays copyable and writable elsewhere.
|
|
1512
2140
|
*/
|
|
1513
|
-
frozen:
|
|
2141
|
+
frozen: z5.boolean().optional().catch(void 0)
|
|
1514
2142
|
}).passthrough();
|
|
1515
|
-
var pinsManifestSchema =
|
|
1516
|
-
pins:
|
|
2143
|
+
var pinsManifestSchema = z5.object({
|
|
2144
|
+
pins: z5.array(pinSchema).default([]),
|
|
1517
2145
|
/**
|
|
1518
2146
|
* Per-repo budgets for the `context` command, keyed by profile —
|
|
1519
2147
|
* `"session-start"`, `"compact"`, `"turn"`, or `"default"` for all of
|
|
@@ -1522,21 +2150,21 @@ var pinsManifestSchema = z4.object({
|
|
|
1522
2150
|
* the index at every session start. `contextProfileBudgets` does the
|
|
1523
2151
|
* tolerant read.
|
|
1524
2152
|
*/
|
|
1525
|
-
context:
|
|
2153
|
+
context: z5.unknown().optional()
|
|
1526
2154
|
}).passthrough();
|
|
1527
2155
|
|
|
1528
2156
|
// src/kb-pins/layers.ts
|
|
1529
|
-
import { mkdir as
|
|
1530
|
-
import { homedir as
|
|
1531
|
-
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";
|
|
1532
2160
|
function userRoot() {
|
|
1533
|
-
return process.env.STRAUSS_KB_USER_ROOT ||
|
|
2161
|
+
return process.env.STRAUSS_KB_USER_ROOT || homedir3();
|
|
1534
2162
|
}
|
|
1535
2163
|
function layerRoot(workspaceDir, layer) {
|
|
1536
2164
|
return layer === "user" ? userRoot() : resolve2(workspaceDir);
|
|
1537
2165
|
}
|
|
1538
2166
|
function layerFile(workspaceDir, layer) {
|
|
1539
|
-
return
|
|
2167
|
+
return join5(
|
|
1540
2168
|
layerRoot(workspaceDir, layer),
|
|
1541
2169
|
layer === "local" ? PINS_LOCAL_FILE : PINS_FILE
|
|
1542
2170
|
);
|
|
@@ -1545,7 +2173,7 @@ async function readPinsLayer(workspaceDir, layer) {
|
|
|
1545
2173
|
const file = layerFile(workspaceDir, layer);
|
|
1546
2174
|
let raw;
|
|
1547
2175
|
try {
|
|
1548
|
-
raw = await
|
|
2176
|
+
raw = await readFile4(file, "utf8");
|
|
1549
2177
|
} catch {
|
|
1550
2178
|
return { pins: [] };
|
|
1551
2179
|
}
|
|
@@ -1569,8 +2197,8 @@ async function readPinsLayer(workspaceDir, layer) {
|
|
|
1569
2197
|
}
|
|
1570
2198
|
async function writePinsLayer(workspaceDir, layer, manifest) {
|
|
1571
2199
|
const file = layerFile(workspaceDir, layer);
|
|
1572
|
-
await
|
|
1573
|
-
await
|
|
2200
|
+
await mkdir3(dirname3(file), { recursive: true });
|
|
2201
|
+
await writeFile2(file, `${JSON.stringify(manifest, null, 2)}
|
|
1574
2202
|
`, "utf8");
|
|
1575
2203
|
}
|
|
1576
2204
|
function resolvePinPath(rootDir, path) {
|
|
@@ -1795,8 +2423,8 @@ function resolveHeads(from, byId) {
|
|
|
1795
2423
|
while (queue.length) {
|
|
1796
2424
|
const current = queue.shift();
|
|
1797
2425
|
const next = successors(current, byId);
|
|
1798
|
-
for (const
|
|
1799
|
-
warnings.push({ kind: "broken-chain", missing });
|
|
2426
|
+
for (const missing2 of next.missing) {
|
|
2427
|
+
warnings.push({ kind: "broken-chain", missing: missing2 });
|
|
1800
2428
|
}
|
|
1801
2429
|
if (!next.records.length) {
|
|
1802
2430
|
if (current.conceptId !== from.conceptId)
|
|
@@ -1827,13 +2455,13 @@ function successors(record, byId) {
|
|
|
1827
2455
|
}
|
|
1828
2456
|
}
|
|
1829
2457
|
const records = [];
|
|
1830
|
-
const
|
|
2458
|
+
const missing2 = [];
|
|
1831
2459
|
for (const id of ids) {
|
|
1832
2460
|
const found = byId.get(id);
|
|
1833
2461
|
if (found) records.push(found);
|
|
1834
|
-
else
|
|
2462
|
+
else missing2.push(id);
|
|
1835
2463
|
}
|
|
1836
|
-
return { records, missing };
|
|
2464
|
+
return { records, missing: missing2 };
|
|
1837
2465
|
}
|
|
1838
2466
|
|
|
1839
2467
|
// src/catalog.ts
|
|
@@ -1904,7 +2532,7 @@ function indexIsStale(stored, expected) {
|
|
|
1904
2532
|
}
|
|
1905
2533
|
|
|
1906
2534
|
// src/kb-context.ts
|
|
1907
|
-
import { readFile as
|
|
2535
|
+
import { readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
|
|
1908
2536
|
var HEADING2 = "## Knowledge bases (pinned)";
|
|
1909
2537
|
var DEFAULT_CONTEXT_BUDGET = 4e3;
|
|
1910
2538
|
var CONTEXT_PROFILES = {
|
|
@@ -2108,13 +2736,13 @@ function toHookJson(block, event) {
|
|
|
2108
2736
|
var CONTEXT_BEGIN = "<!-- strauss-kb:begin -->";
|
|
2109
2737
|
var CONTEXT_END = "<!-- strauss-kb:end -->";
|
|
2110
2738
|
async function syncInstructions(file, block) {
|
|
2111
|
-
const existing = await
|
|
2739
|
+
const existing = await readFile5(file, "utf8").catch(() => null);
|
|
2112
2740
|
const region = block ? `${CONTEXT_BEGIN}
|
|
2113
2741
|
${block.trim()}
|
|
2114
2742
|
${CONTEXT_END}` : null;
|
|
2115
2743
|
if (existing === null) {
|
|
2116
2744
|
if (!region) return { file, action: "unchanged" };
|
|
2117
|
-
await
|
|
2745
|
+
await writeFile3(file, `${region}
|
|
2118
2746
|
`, "utf8");
|
|
2119
2747
|
return { file, action: "created" };
|
|
2120
2748
|
}
|
|
@@ -2125,11 +2753,11 @@ ${CONTEXT_END}` : null;
|
|
|
2125
2753
|
const after = existing.slice(end + CONTEXT_END.length);
|
|
2126
2754
|
const next = region ? `${before}${region}${after}` : `${before.replace(/\n+$/, "\n")}${after.replace(/^\n+/, "\n")}`;
|
|
2127
2755
|
if (next === existing) return { file, action: "unchanged" };
|
|
2128
|
-
await
|
|
2756
|
+
await writeFile3(file, next, "utf8");
|
|
2129
2757
|
return { file, action: region ? "replaced" : "removed" };
|
|
2130
2758
|
}
|
|
2131
2759
|
if (!region) return { file, action: "unchanged" };
|
|
2132
|
-
await
|
|
2760
|
+
await writeFile3(
|
|
2133
2761
|
file,
|
|
2134
2762
|
`${existing.replace(/\n*$/, "\n\n")}${region}
|
|
2135
2763
|
`,
|
|
@@ -2333,6 +2961,18 @@ var CHECK_HEADLINES = {
|
|
|
2333
2961
|
unchecked: "an anchor in another repository nothing could reach"
|
|
2334
2962
|
};
|
|
2335
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
|
+
}
|
|
2336
2976
|
function doctor(bundle, options = {}) {
|
|
2337
2977
|
const thresholds = {
|
|
2338
2978
|
expiringDays: options.expiringDays ?? DEFAULT_EXPIRING_DAYS,
|
|
@@ -2368,6 +3008,7 @@ function doctor(bundle, options = {}) {
|
|
|
2368
3008
|
counts,
|
|
2369
3009
|
groups,
|
|
2370
3010
|
findingCount,
|
|
3011
|
+
anchorResolvers: anchorResolverCounts(bundle),
|
|
2371
3012
|
healthy: findingCount === 0
|
|
2372
3013
|
};
|
|
2373
3014
|
}
|
|
@@ -2614,9 +3255,9 @@ function ageInDays(record, now) {
|
|
|
2614
3255
|
}
|
|
2615
3256
|
|
|
2616
3257
|
// src/kb-log.ts
|
|
2617
|
-
import { z as
|
|
3258
|
+
import { z as z6 } from "zod";
|
|
2618
3259
|
var LOG_FILE = "log.jsonl";
|
|
2619
|
-
var kbLogEntrySchema =
|
|
3260
|
+
var kbLogEntrySchema = z6.object({
|
|
2620
3261
|
// Validated, not just `min(1)`: `at` is a sort key (see `parseLog`
|
|
2621
3262
|
// below), and a value that isn't actually chronological — a Unix
|
|
2622
3263
|
// timestamp, a human-typed date, garbage — would sort wrong without
|
|
@@ -2625,12 +3266,12 @@ var kbLogEntrySchema = z5.object({
|
|
|
2625
3266
|
// and rejects everything else, including a non-`Z` offset — so a
|
|
2626
3267
|
// malformed `at` is reported the same way a malformed line already is,
|
|
2627
3268
|
// rather than silently sorting into the wrong place.
|
|
2628
|
-
at:
|
|
2629
|
-
by:
|
|
2630
|
-
operation:
|
|
2631
|
-
conceptId:
|
|
3269
|
+
at: z6.iso.datetime(),
|
|
3270
|
+
by: z6.string().min(1),
|
|
3271
|
+
operation: z6.string().min(1),
|
|
3272
|
+
conceptId: z6.string().min(1),
|
|
2632
3273
|
/** Second concept id, where the operation relates two — supersession. */
|
|
2633
|
-
target:
|
|
3274
|
+
target: z6.string().min(1).optional()
|
|
2634
3275
|
}).strict();
|
|
2635
3276
|
function renderLogEntry(entry) {
|
|
2636
3277
|
return `${JSON.stringify(kbLogEntrySchema.parse(entry))}
|
|
@@ -2640,18 +3281,18 @@ function parseLog(raw) {
|
|
|
2640
3281
|
const entries = [];
|
|
2641
3282
|
const malformed = [];
|
|
2642
3283
|
const seen = /* @__PURE__ */ new Set();
|
|
2643
|
-
raw.split("\n").forEach((text,
|
|
3284
|
+
raw.split("\n").forEach((text, index2) => {
|
|
2644
3285
|
if (!text.trim()) return;
|
|
2645
3286
|
let value;
|
|
2646
3287
|
try {
|
|
2647
3288
|
value = JSON.parse(text);
|
|
2648
3289
|
} catch {
|
|
2649
|
-
malformed.push({ line:
|
|
3290
|
+
malformed.push({ line: index2 + 1, text });
|
|
2650
3291
|
return;
|
|
2651
3292
|
}
|
|
2652
3293
|
const parsed = kbLogEntrySchema.safeParse(value);
|
|
2653
3294
|
if (!parsed.success) {
|
|
2654
|
-
malformed.push({ line:
|
|
3295
|
+
malformed.push({ line: index2 + 1, text });
|
|
2655
3296
|
return;
|
|
2656
3297
|
}
|
|
2657
3298
|
const key = JSON.stringify(parsed.data);
|
|
@@ -2666,14 +3307,14 @@ function parseLog(raw) {
|
|
|
2666
3307
|
}
|
|
2667
3308
|
|
|
2668
3309
|
// src/json-schema.ts
|
|
2669
|
-
import { z as
|
|
3310
|
+
import { z as z7 } from "zod";
|
|
2670
3311
|
function kbJsonSchemas() {
|
|
2671
3312
|
return {
|
|
2672
|
-
recordFrontmatter:
|
|
3313
|
+
recordFrontmatter: z7.toJSONSchema(kbRecordFrontmatterSchema, {
|
|
2673
3314
|
io: "input"
|
|
2674
3315
|
}),
|
|
2675
|
-
composeInput:
|
|
2676
|
-
logEntry:
|
|
3316
|
+
composeInput: z7.toJSONSchema(composeInputSchema, { io: "input" }),
|
|
3317
|
+
logEntry: z7.toJSONSchema(kbLogEntrySchema, { io: "input" })
|
|
2677
3318
|
};
|
|
2678
3319
|
}
|
|
2679
3320
|
|
|
@@ -2726,13 +3367,13 @@ function byGeneratedAt(left, right) {
|
|
|
2726
3367
|
}
|
|
2727
3368
|
|
|
2728
3369
|
// src/commands/anchor-resolve.ts
|
|
2729
|
-
import { z as
|
|
3370
|
+
import { z as z9 } from "zod";
|
|
2730
3371
|
|
|
2731
3372
|
// src/commands/model.ts
|
|
2732
|
-
import { z as
|
|
2733
|
-
var bundlePath =
|
|
2734
|
-
var conceptId =
|
|
2735
|
-
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(
|
|
2736
3377
|
"Where the anchored source lives, for the drift check. Defaults to the working directory."
|
|
2737
3378
|
);
|
|
2738
3379
|
function define(command) {
|
|
@@ -2755,22 +3396,30 @@ function argvFlag(argv, name) {
|
|
|
2755
3396
|
}
|
|
2756
3397
|
|
|
2757
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
|
+
}
|
|
2758
3407
|
var anchorResolveCommand = define({
|
|
2759
3408
|
name: "anchor-resolve",
|
|
2760
3409
|
tool: "kb_anchor_resolve",
|
|
2761
3410
|
usage: "anchor-resolve <concept-id> [--repo-root <path>] [--offline] [--rebaseline] [--restamp]",
|
|
2762
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.",
|
|
2763
|
-
input:
|
|
3412
|
+
input: z9.object({
|
|
2764
3413
|
bundlePath,
|
|
2765
3414
|
conceptId,
|
|
2766
|
-
repoRoot:
|
|
2767
|
-
offline:
|
|
3415
|
+
repoRoot: z9.string().min(1).optional(),
|
|
3416
|
+
offline: z9.boolean().optional().describe(
|
|
2768
3417
|
"Resolve foreign anchors from the local repo cache only, never fetching."
|
|
2769
3418
|
),
|
|
2770
|
-
rebaseline:
|
|
3419
|
+
rebaseline: z9.boolean().optional().describe(
|
|
2771
3420
|
"Accept the current code as the new baseline for anchors that drifted."
|
|
2772
3421
|
),
|
|
2773
|
-
restamp:
|
|
3422
|
+
restamp: z9.boolean().optional().describe(
|
|
2774
3423
|
"Refresh `resolved_at` on anchors that already match. Off by default, so a green run writes nothing."
|
|
2775
3424
|
)
|
|
2776
3425
|
}),
|
|
@@ -2799,6 +3448,11 @@ var anchorResolveCommand = define({
|
|
|
2799
3448
|
const updated = [];
|
|
2800
3449
|
let dirty = false;
|
|
2801
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
|
+
);
|
|
2802
3456
|
for (const anchor of anchors) {
|
|
2803
3457
|
const base2 = {
|
|
2804
3458
|
file: anchor.file,
|
|
@@ -2815,27 +3469,35 @@ var anchorResolveCommand = define({
|
|
|
2815
3469
|
updated.push(anchor);
|
|
2816
3470
|
continue;
|
|
2817
3471
|
}
|
|
2818
|
-
const
|
|
2819
|
-
if (!
|
|
3472
|
+
const outcome = resolveAnchorSpan(source.source, anchor, resolvers);
|
|
3473
|
+
if (!outcome.ok) {
|
|
2820
3474
|
results.push({
|
|
2821
3475
|
...base2,
|
|
2822
3476
|
state: "unresolved",
|
|
2823
|
-
reason:
|
|
3477
|
+
reason: outcome.reason
|
|
2824
3478
|
});
|
|
2825
3479
|
updated.push(anchor);
|
|
2826
3480
|
continue;
|
|
2827
3481
|
}
|
|
3482
|
+
const resolved = outcome.span;
|
|
3483
|
+
const producedBy = outcome.resolver;
|
|
2828
3484
|
const currentHash = hashAnchorText(resolved.text);
|
|
2829
3485
|
const currentLines = resolved.endLine - resolved.startLine + 1;
|
|
2830
3486
|
const stamped = {
|
|
2831
3487
|
...anchor,
|
|
2832
3488
|
hash: currentHash,
|
|
2833
3489
|
lines: currentLines,
|
|
2834
|
-
resolved_at: now()
|
|
3490
|
+
resolved_at: now(),
|
|
3491
|
+
...producedBy ? { resolver: producedBy } : {}
|
|
2835
3492
|
};
|
|
2836
3493
|
const pinned = anchor.ref !== void 0 && source.repo !== void 0;
|
|
2837
3494
|
if (!anchor.hash) {
|
|
2838
|
-
results.push({
|
|
3495
|
+
results.push({
|
|
3496
|
+
...base2,
|
|
3497
|
+
state: "stamped",
|
|
3498
|
+
currentHash,
|
|
3499
|
+
...producedBy ? { resolver: producedBy } : {}
|
|
3500
|
+
});
|
|
2839
3501
|
updated.push(stamped);
|
|
2840
3502
|
dirty = true;
|
|
2841
3503
|
continue;
|
|
@@ -2846,6 +3508,10 @@ var anchorResolveCommand = define({
|
|
|
2846
3508
|
state: "drifted",
|
|
2847
3509
|
currentHash,
|
|
2848
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" } : {},
|
|
2849
3515
|
...pinned ? { remoteState: "drifted-from-ref" } : {},
|
|
2850
3516
|
...rebaseline ? { rebaselined: true } : {}
|
|
2851
3517
|
});
|
|
@@ -2853,7 +3519,7 @@ var anchorResolveCommand = define({
|
|
|
2853
3519
|
if (rebaseline) dirty = true;
|
|
2854
3520
|
continue;
|
|
2855
3521
|
}
|
|
2856
|
-
const onDefault = pinned ? headHash(source, anchor) : void 0;
|
|
3522
|
+
const onDefault = pinned ? headHash(source, anchor, resolvers) : void 0;
|
|
2857
3523
|
if (onDefault && onDefault.hash !== anchor.hash) {
|
|
2858
3524
|
results.push({
|
|
2859
3525
|
...base2,
|
|
@@ -2869,6 +3535,7 @@ var anchorResolveCommand = define({
|
|
|
2869
3535
|
...base2,
|
|
2870
3536
|
state: "match",
|
|
2871
3537
|
currentHash,
|
|
3538
|
+
...producedBy ? { resolver: producedBy } : {},
|
|
2872
3539
|
...pinned ? { remoteState: "matches-ref" } : {}
|
|
2873
3540
|
});
|
|
2874
3541
|
const refresh = restamp || anchor.resolved_at === void 0;
|
|
@@ -2886,19 +3553,21 @@ var anchorResolveCommand = define({
|
|
|
2886
3553
|
if (!frozen) await store.updateAnchors(path, id, updated, actor);
|
|
2887
3554
|
}
|
|
2888
3555
|
const frozenNote = frozen ? { frozen: true, note: "base is frozen: nothing was stamped" } : {};
|
|
3556
|
+
const hints = grammarHints();
|
|
3557
|
+
const hintNote = hints.length ? { hints } : {};
|
|
2889
3558
|
const unreachable = results.filter(
|
|
2890
3559
|
(entry) => isUncheckedReason(entry.reason)
|
|
2891
3560
|
).length;
|
|
2892
3561
|
const checked = results.length - unreachable;
|
|
2893
|
-
const
|
|
2894
|
-
const note = `${
|
|
2895
|
-
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;
|
|
2896
3565
|
if (clean) {
|
|
2897
3566
|
try {
|
|
2898
3567
|
await store.verify(
|
|
2899
3568
|
path,
|
|
2900
3569
|
id,
|
|
2901
|
-
`anchor-resolve: ${note} (
|
|
3570
|
+
`anchor-resolve: ${note} (${resolverSummary(results)})`,
|
|
2902
3571
|
actor,
|
|
2903
3572
|
now()
|
|
2904
3573
|
);
|
|
@@ -2909,17 +3578,25 @@ var anchorResolveCommand = define({
|
|
|
2909
3578
|
results,
|
|
2910
3579
|
verified: false,
|
|
2911
3580
|
verifyRefused: "self-verification",
|
|
2912
|
-
...frozenNote
|
|
3581
|
+
...frozenNote,
|
|
3582
|
+
...hintNote
|
|
2913
3583
|
};
|
|
2914
3584
|
}
|
|
2915
|
-
return {
|
|
3585
|
+
return {
|
|
3586
|
+
conceptId: id,
|
|
3587
|
+
results,
|
|
3588
|
+
verified: true,
|
|
3589
|
+
...frozenNote,
|
|
3590
|
+
...hintNote
|
|
3591
|
+
};
|
|
2916
3592
|
}
|
|
2917
3593
|
return {
|
|
2918
3594
|
conceptId: id,
|
|
2919
3595
|
results,
|
|
2920
3596
|
verified: false,
|
|
2921
3597
|
...unreachable ? { note } : {},
|
|
2922
|
-
...frozenNote
|
|
3598
|
+
...frozenNote,
|
|
3599
|
+
...hintNote
|
|
2923
3600
|
};
|
|
2924
3601
|
},
|
|
2925
3602
|
// A stored hash that no longer resolves is a broken anchor, not an absence:
|
|
@@ -2935,13 +3612,13 @@ var anchorResolveCommand = define({
|
|
|
2935
3612
|
function lineDelta(anchor, current) {
|
|
2936
3613
|
return anchor.lines === void 0 ? null : Math.abs(current - anchor.lines);
|
|
2937
3614
|
}
|
|
2938
|
-
function headHash(source, anchor) {
|
|
3615
|
+
function headHash(source, anchor, resolvers) {
|
|
2939
3616
|
if (source.head === void 0) return void 0;
|
|
2940
|
-
const
|
|
2941
|
-
if (!
|
|
3617
|
+
const outcome = resolveAnchorSpan(source.head, anchor, resolvers);
|
|
3618
|
+
if (!outcome.ok) return void 0;
|
|
2942
3619
|
return {
|
|
2943
|
-
hash: hashAnchorText(
|
|
2944
|
-
lines:
|
|
3620
|
+
hash: hashAnchorText(outcome.span.text),
|
|
3621
|
+
lines: outcome.span.endLine - outcome.span.startLine + 1
|
|
2945
3622
|
};
|
|
2946
3623
|
}
|
|
2947
3624
|
async function readSources(anchors, root, offline) {
|
|
@@ -2991,13 +3668,13 @@ async function readSources(anchors, root, offline) {
|
|
|
2991
3668
|
}
|
|
2992
3669
|
|
|
2993
3670
|
// src/commands/answer.ts
|
|
2994
|
-
import { z as
|
|
3671
|
+
import { z as z10 } from "zod";
|
|
2995
3672
|
var answerCommand = define({
|
|
2996
3673
|
name: "answer",
|
|
2997
3674
|
tool: "kb_answer",
|
|
2998
3675
|
usage: "answer <concept-id> <answer...>",
|
|
2999
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.",
|
|
3000
|
-
input:
|
|
3677
|
+
input: z10.object({ bundlePath, conceptId, answer: z10.string().min(1) }),
|
|
3001
3678
|
fromArgv: (argv, path) => ({
|
|
3002
3679
|
bundlePath: path,
|
|
3003
3680
|
conceptId: argv[1],
|
|
@@ -3011,27 +3688,27 @@ var answerCommand = define({
|
|
|
3011
3688
|
});
|
|
3012
3689
|
|
|
3013
3690
|
// src/commands/backlinks.ts
|
|
3014
|
-
import { z as
|
|
3691
|
+
import { z as z11 } from "zod";
|
|
3015
3692
|
var backlinksCommand = define({
|
|
3016
3693
|
name: "backlinks",
|
|
3017
3694
|
tool: "kb_backlinks",
|
|
3018
3695
|
usage: "backlinks <concept-id>",
|
|
3019
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.",
|
|
3020
|
-
input:
|
|
3697
|
+
input: z11.object({ bundlePath, conceptId }),
|
|
3021
3698
|
fromArgv: (argv, path) => ({ bundlePath: path, conceptId: argv[1] }),
|
|
3022
3699
|
run: async ({ store }, { bundlePath: path, conceptId: id }) => store.backlinks(path, id)
|
|
3023
3700
|
});
|
|
3024
3701
|
|
|
3025
3702
|
// src/commands/catalog.ts
|
|
3026
|
-
import { z as
|
|
3703
|
+
import { z as z12 } from "zod";
|
|
3027
3704
|
var catalogCommand = define({
|
|
3028
3705
|
name: "catalog",
|
|
3029
3706
|
tool: "kb_catalog",
|
|
3030
3707
|
usage: "catalog [type]",
|
|
3031
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.",
|
|
3032
|
-
input:
|
|
3709
|
+
input: z12.object({
|
|
3033
3710
|
bundlePath,
|
|
3034
|
-
type:
|
|
3711
|
+
type: z12.enum(KB_RECORD_TYPES).optional()
|
|
3035
3712
|
}),
|
|
3036
3713
|
fromArgv: (argv, path) => ({
|
|
3037
3714
|
bundlePath: path,
|
|
@@ -3086,26 +3763,26 @@ function count(value, noun) {
|
|
|
3086
3763
|
}
|
|
3087
3764
|
|
|
3088
3765
|
// src/commands/context.ts
|
|
3089
|
-
import { z as
|
|
3766
|
+
import { z as z13 } from "zod";
|
|
3090
3767
|
var contextCommand = define({
|
|
3091
3768
|
name: "context",
|
|
3092
3769
|
tool: "kb_context",
|
|
3093
3770
|
usage: "context [--profile NAME] [--budget N] [--full-under N] [--format json] [--event NAME]",
|
|
3094
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.",
|
|
3095
|
-
input:
|
|
3096
|
-
budgetTokens:
|
|
3772
|
+
input: z13.object({
|
|
3773
|
+
budgetTokens: z13.number().int().positive().optional().describe(
|
|
3097
3774
|
"Ceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000."
|
|
3098
3775
|
),
|
|
3099
|
-
fullUnderTokens:
|
|
3776
|
+
fullUnderTokens: z13.number().int().positive().optional().describe(
|
|
3100
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."
|
|
3101
3778
|
),
|
|
3102
|
-
profile:
|
|
3779
|
+
profile: z13.string().optional().describe(
|
|
3103
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."
|
|
3104
3781
|
),
|
|
3105
|
-
format:
|
|
3782
|
+
format: z13.enum(["markdown", "json"]).optional().describe(
|
|
3106
3783
|
"CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
|
|
3107
3784
|
),
|
|
3108
|
-
event:
|
|
3785
|
+
event: z13.string().optional().describe(
|
|
3109
3786
|
"hookEventName stamped into the JSON envelope. Only meaningful with format=json."
|
|
3110
3787
|
)
|
|
3111
3788
|
}),
|
|
@@ -3141,14 +3818,14 @@ var contextCommand = define({
|
|
|
3141
3818
|
});
|
|
3142
3819
|
|
|
3143
3820
|
// src/commands/doctor.ts
|
|
3144
|
-
import { z as
|
|
3145
|
-
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}.`);
|
|
3146
3823
|
var doctorCommand = define({
|
|
3147
3824
|
name: "doctor",
|
|
3148
3825
|
tool: "kb_doctor",
|
|
3149
3826
|
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--offline] [--strict]",
|
|
3150
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.",
|
|
3151
|
-
input:
|
|
3828
|
+
input: z14.object({
|
|
3152
3829
|
bundlePath,
|
|
3153
3830
|
repoRoot: REPO_ROOT,
|
|
3154
3831
|
expiringDays: days(
|
|
@@ -3163,10 +3840,10 @@ var doctorCommand = define({
|
|
|
3163
3840
|
"How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
|
|
3164
3841
|
DEFAULT_AGING_DAYS
|
|
3165
3842
|
),
|
|
3166
|
-
offline:
|
|
3843
|
+
offline: z14.boolean().optional().describe(
|
|
3167
3844
|
"Read foreign anchors from the local repo cache only, never fetching."
|
|
3168
3845
|
),
|
|
3169
|
-
strict:
|
|
3846
|
+
strict: z14.boolean().optional().describe(
|
|
3170
3847
|
"Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
|
|
3171
3848
|
)
|
|
3172
3849
|
}),
|
|
@@ -3209,7 +3886,13 @@ var doctorCommand = define({
|
|
|
3209
3886
|
...anchorDrift !== void 0 ? { anchorDrift } : {},
|
|
3210
3887
|
now: new Date(checkedAt)
|
|
3211
3888
|
});
|
|
3212
|
-
|
|
3889
|
+
const hints = grammarHints();
|
|
3890
|
+
return {
|
|
3891
|
+
bundlePath: path,
|
|
3892
|
+
checkedAt,
|
|
3893
|
+
...report,
|
|
3894
|
+
...hints.length ? { hints } : {}
|
|
3895
|
+
};
|
|
3213
3896
|
},
|
|
3214
3897
|
render: (result) => render2(result),
|
|
3215
3898
|
// Only expiry, and only under --strict. The other seven checks report debt a
|
|
@@ -3227,12 +3910,15 @@ function render2(result) {
|
|
|
3227
3910
|
`records: ${result.recordCount}`,
|
|
3228
3911
|
`thresholds: expiring within ${thresholds.expiringDays}d, unverified over ${thresholds.unverifiedDays}d, aging over ${thresholds.agingDays}d`,
|
|
3229
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
|
+
] : [],
|
|
3230
3916
|
""
|
|
3231
3917
|
];
|
|
3232
|
-
const
|
|
3918
|
+
const width2 = Math.max(...result.groups.map((group2) => group2.check.length));
|
|
3233
3919
|
for (const group2 of result.groups) {
|
|
3234
3920
|
lines.push(
|
|
3235
|
-
` ${group2.check.padEnd(
|
|
3921
|
+
` ${group2.check.padEnd(width2)} ${String(group2.count).padStart(3)} ${group2.headline}`
|
|
3236
3922
|
);
|
|
3237
3923
|
}
|
|
3238
3924
|
for (const group2 of result.groups) {
|
|
@@ -3244,6 +3930,7 @@ function render2(result) {
|
|
|
3244
3930
|
);
|
|
3245
3931
|
}
|
|
3246
3932
|
}
|
|
3933
|
+
for (const hint of result.hints ?? []) lines.push("", hint);
|
|
3247
3934
|
lines.push(
|
|
3248
3935
|
"",
|
|
3249
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.`
|
|
@@ -3252,19 +3939,19 @@ function render2(result) {
|
|
|
3252
3939
|
}
|
|
3253
3940
|
|
|
3254
3941
|
// src/commands/impact.ts
|
|
3255
|
-
import { z as
|
|
3942
|
+
import { z as z15 } from "zod";
|
|
3256
3943
|
var impactCommand = define({
|
|
3257
3944
|
name: "impact",
|
|
3258
3945
|
tool: "kb_impact",
|
|
3259
3946
|
usage: "impact <concept-id> [--depth N] [--rels a,b]",
|
|
3260
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.",
|
|
3261
|
-
input:
|
|
3948
|
+
input: z15.object({
|
|
3262
3949
|
bundlePath,
|
|
3263
3950
|
conceptId,
|
|
3264
|
-
depth:
|
|
3951
|
+
depth: z15.number().int().positive().optional().describe(
|
|
3265
3952
|
"Hops out from the record. Unbounded when omitted; a walk this cuts reports truncated: true."
|
|
3266
3953
|
),
|
|
3267
|
-
rels:
|
|
3954
|
+
rels: z15.array(z15.enum(KB_CAUSAL_LINK_RELS)).optional().describe(
|
|
3268
3955
|
"Narrow which rels the walk follows. Defaults to every rel that carries a dependence \u2014 all but related_to."
|
|
3269
3956
|
)
|
|
3270
3957
|
}),
|
|
@@ -3285,13 +3972,13 @@ var impactCommand = define({
|
|
|
3285
3972
|
});
|
|
3286
3973
|
|
|
3287
3974
|
// src/commands/list.ts
|
|
3288
|
-
import { z as
|
|
3975
|
+
import { z as z16 } from "zod";
|
|
3289
3976
|
var listCommand = define({
|
|
3290
3977
|
name: "list",
|
|
3291
3978
|
tool: "kb_list",
|
|
3292
3979
|
usage: "list [type]",
|
|
3293
3980
|
description: "Every record, optionally one type. For enumerating; use kb_query for a question.",
|
|
3294
|
-
input:
|
|
3981
|
+
input: z16.object({ bundlePath, type: z16.enum(KB_RECORD_TYPES).optional() }),
|
|
3295
3982
|
fromArgv: (argv, path) => ({ bundlePath: path, type: argv[1] }),
|
|
3296
3983
|
run: async ({ store }, { bundlePath: path, type }) => (await store.list(path, type)).map((record) => ({
|
|
3297
3984
|
conceptId: record.conceptId,
|
|
@@ -3303,17 +3990,17 @@ var listCommand = define({
|
|
|
3303
3990
|
});
|
|
3304
3991
|
|
|
3305
3992
|
// src/commands/load.ts
|
|
3306
|
-
import { z as
|
|
3993
|
+
import { z as z17 } from "zod";
|
|
3307
3994
|
var loadCommand = define({
|
|
3308
3995
|
name: "load",
|
|
3309
3996
|
tool: "kb_load",
|
|
3310
3997
|
usage: "load [type] [--budget N | --all] [--repo-root PATH]",
|
|
3311
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.",
|
|
3312
|
-
input:
|
|
3999
|
+
input: z17.object({
|
|
3313
4000
|
bundlePath,
|
|
3314
|
-
type:
|
|
3315
|
-
budgetTokens:
|
|
3316
|
-
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(
|
|
3317
4004
|
"Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
|
|
3318
4005
|
),
|
|
3319
4006
|
repoRoot: REPO_ROOT
|
|
@@ -3355,25 +4042,25 @@ var loadCommand = define({
|
|
|
3355
4042
|
});
|
|
3356
4043
|
|
|
3357
4044
|
// src/commands/log.ts
|
|
3358
|
-
import { z as
|
|
4045
|
+
import { z as z18 } from "zod";
|
|
3359
4046
|
var logCommand = define({
|
|
3360
4047
|
name: "log",
|
|
3361
4048
|
tool: "kb_log",
|
|
3362
4049
|
usage: "log",
|
|
3363
4050
|
description: "Who touched what, and when. Append-only; malformed lines are reported, never repaired.",
|
|
3364
|
-
input:
|
|
4051
|
+
input: z18.object({ bundlePath }),
|
|
3365
4052
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
3366
4053
|
run: ({ store }, { bundlePath: path }) => store.readLog(path)
|
|
3367
4054
|
});
|
|
3368
4055
|
|
|
3369
4056
|
// src/commands/no-decision.ts
|
|
3370
|
-
import { z as
|
|
4057
|
+
import { z as z19 } from "zod";
|
|
3371
4058
|
var noDecisionCommand = define({
|
|
3372
4059
|
name: "no-decision",
|
|
3373
4060
|
tool: "kb_no_decision",
|
|
3374
4061
|
usage: "no-decision <reason...>",
|
|
3375
4062
|
description: "Record in one sentence that a piece of work had nothing to decide. Idempotent.",
|
|
3376
|
-
input:
|
|
4063
|
+
input: z19.object({ bundlePath, reason: z19.string().min(1) }),
|
|
3377
4064
|
fromArgv: (argv, path) => ({
|
|
3378
4065
|
bundlePath: path,
|
|
3379
4066
|
reason: argv.slice(1).join(" ").trim()
|
|
@@ -3390,20 +4077,20 @@ var noDecisionCommand = define({
|
|
|
3390
4077
|
});
|
|
3391
4078
|
|
|
3392
4079
|
// src/commands/pack.ts
|
|
3393
|
-
import { z as
|
|
4080
|
+
import { z as z20 } from "zod";
|
|
3394
4081
|
var packCommand = define({
|
|
3395
4082
|
name: "pack",
|
|
3396
4083
|
tool: "kb_pack",
|
|
3397
4084
|
usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
|
|
3398
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.",
|
|
3399
|
-
input:
|
|
4086
|
+
input: z20.object({
|
|
3400
4087
|
bundlePath,
|
|
3401
4088
|
conceptId,
|
|
3402
|
-
hops:
|
|
3403
|
-
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(
|
|
3404
4091
|
"How many records the pack may hold, root included. Defaults to 20."
|
|
3405
4092
|
),
|
|
3406
|
-
budgetTokens:
|
|
4093
|
+
budgetTokens: z20.number().int().positive().optional().describe(
|
|
3407
4094
|
"Approximate token ceiling over what is actually emitted. Defaults to 25000."
|
|
3408
4095
|
)
|
|
3409
4096
|
}),
|
|
@@ -3490,22 +4177,22 @@ function warningLabel(warning) {
|
|
|
3490
4177
|
}
|
|
3491
4178
|
|
|
3492
4179
|
// src/commands/pin.ts
|
|
3493
|
-
import { z as
|
|
4180
|
+
import { z as z21 } from "zod";
|
|
3494
4181
|
var pinCommand = define({
|
|
3495
4182
|
name: "pin",
|
|
3496
4183
|
tool: "kb_pin",
|
|
3497
4184
|
usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
|
|
3498
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.",
|
|
3499
|
-
input:
|
|
4186
|
+
input: z21.object({
|
|
3500
4187
|
bundlePath,
|
|
3501
|
-
mode:
|
|
4188
|
+
mode: z21.enum(["full", "index"]).optional().describe(
|
|
3502
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."
|
|
3503
4190
|
),
|
|
3504
|
-
profiles:
|
|
3505
|
-
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(
|
|
3506
4193
|
"Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
|
|
3507
4194
|
),
|
|
3508
|
-
frozen:
|
|
4195
|
+
frozen: z21.boolean().optional().describe(
|
|
3509
4196
|
"true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
|
|
3510
4197
|
)
|
|
3511
4198
|
}),
|
|
@@ -3534,29 +4221,29 @@ var pinCommand = define({
|
|
|
3534
4221
|
});
|
|
3535
4222
|
|
|
3536
4223
|
// src/commands/pins.ts
|
|
3537
|
-
import { z as
|
|
4224
|
+
import { z as z22 } from "zod";
|
|
3538
4225
|
var pinsCommand = define({
|
|
3539
4226
|
name: "pins",
|
|
3540
4227
|
tool: "kb_pins",
|
|
3541
4228
|
usage: "pins",
|
|
3542
4229
|
description: "Every pinned base across the manifest layers, with its layer and whether it resolves to records. Takes no bundlePath.",
|
|
3543
|
-
input:
|
|
4230
|
+
input: z22.object({}),
|
|
3544
4231
|
fromArgv: () => ({}),
|
|
3545
4232
|
run: ({ store }) => listPins(store, process.cwd())
|
|
3546
4233
|
});
|
|
3547
4234
|
|
|
3548
4235
|
// src/commands/query.ts
|
|
3549
|
-
import { z as
|
|
4236
|
+
import { z as z23 } from "zod";
|
|
3550
4237
|
var queryCommand = define({
|
|
3551
4238
|
name: "query",
|
|
3552
4239
|
tool: "kb_query",
|
|
3553
4240
|
usage: "query <text...> [--repo-root PATH]",
|
|
3554
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.",
|
|
3555
|
-
input:
|
|
4242
|
+
input: z23.object({
|
|
3556
4243
|
bundlePath,
|
|
3557
|
-
text:
|
|
3558
|
-
type:
|
|
3559
|
-
includeNonCurrent:
|
|
4244
|
+
text: z23.string().optional(),
|
|
4245
|
+
type: z23.enum(KB_RECORD_TYPES).optional(),
|
|
4246
|
+
includeNonCurrent: z23.boolean().optional(),
|
|
3560
4247
|
repoRoot: REPO_ROOT
|
|
3561
4248
|
}),
|
|
3562
4249
|
// `--repo-root` is a flag, so its value must not fall into the search text.
|
|
@@ -3588,40 +4275,138 @@ var queryCommand = define({
|
|
|
3588
4275
|
});
|
|
3589
4276
|
|
|
3590
4277
|
// src/commands/read-index.ts
|
|
3591
|
-
import { z as
|
|
4278
|
+
import { z as z24 } from "zod";
|
|
3592
4279
|
var readIndexCommand = define({
|
|
3593
4280
|
name: "index",
|
|
3594
4281
|
tool: "kb_index",
|
|
3595
4282
|
usage: "index",
|
|
3596
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.",
|
|
3597
|
-
input:
|
|
4284
|
+
input: z24.object({ bundlePath }),
|
|
3598
4285
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
3599
4286
|
run: ({ store }, { bundlePath: path }) => store.readIndex(path)
|
|
3600
4287
|
});
|
|
3601
4288
|
|
|
3602
4289
|
// src/commands/schema.ts
|
|
3603
|
-
import { z as
|
|
4290
|
+
import { z as z25 } from "zod";
|
|
3604
4291
|
var schemaCommand = define({
|
|
3605
4292
|
name: "schema",
|
|
3606
4293
|
tool: "kb_schema",
|
|
3607
4294
|
usage: "schema",
|
|
3608
4295
|
description: "JSON Schema for frontmatter, write input, and log entries, generated from the enforcing code.",
|
|
3609
|
-
input:
|
|
4296
|
+
input: z25.object({}),
|
|
3610
4297
|
fromArgv: () => ({}),
|
|
3611
4298
|
run: () => Promise.resolve(kbJsonSchemas())
|
|
3612
4299
|
});
|
|
3613
4300
|
|
|
4301
|
+
// src/commands/stamp.ts
|
|
4302
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
4303
|
+
import { z as z26 } from "zod";
|
|
4304
|
+
var DIGEST = /^[0-9a-f]{64}$/;
|
|
4305
|
+
var stampCommand = define({
|
|
4306
|
+
name: "stamp",
|
|
4307
|
+
tool: "kb_stamp",
|
|
4308
|
+
usage: "stamp [--bundle PATH] [--since DIGEST|FILE]",
|
|
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.",
|
|
4310
|
+
input: z26.object({
|
|
4311
|
+
bundlePath: z26.string().min(1).optional().describe(
|
|
4312
|
+
"Absolute path to one knowledge base. Omit to stamp every pinned base."
|
|
4313
|
+
),
|
|
4314
|
+
since: z26.string().min(1).optional().describe(
|
|
4315
|
+
"Prior digest, or path to a prior `stamp --json`; only moved bases return, with changed ids when the baseline is a file."
|
|
4316
|
+
)
|
|
4317
|
+
}),
|
|
4318
|
+
fromArgv: (argv, path, _stdin, bundleExplicit) => {
|
|
4319
|
+
const since = argvFlag(argv, "--since");
|
|
4320
|
+
return {
|
|
4321
|
+
...bundleExplicit ? { bundlePath: path } : {},
|
|
4322
|
+
...since !== void 0 ? { since } : {}
|
|
4323
|
+
};
|
|
4324
|
+
},
|
|
4325
|
+
run: async ({ store }, { bundlePath: bundlePath2, since }) => {
|
|
4326
|
+
const targets = bundlePath2 ? [bundlePath2] : (await readMergedPins(process.cwd())).pins.map(
|
|
4327
|
+
(pin) => pin.absolutePath
|
|
4328
|
+
);
|
|
4329
|
+
if (since !== void 0 && DIGEST.test(since) && targets.length > 1) {
|
|
4330
|
+
throw new KbStampDigestBaselineError(since);
|
|
4331
|
+
}
|
|
4332
|
+
const stamps = await Promise.all(
|
|
4333
|
+
targets.map((target) => store.stamp(target))
|
|
4334
|
+
);
|
|
4335
|
+
if (since === void 0) {
|
|
4336
|
+
return stamps.map((stamp) => ({ ...stamp, changed: null }));
|
|
4337
|
+
}
|
|
4338
|
+
const baseline = await readBaseline(since);
|
|
4339
|
+
const reports = [];
|
|
4340
|
+
for (const stamp of stamps) {
|
|
4341
|
+
const before = baseline.byPath.get(stamp.path);
|
|
4342
|
+
if (baseline.digest !== null) {
|
|
4343
|
+
if (baseline.digest === stamp.digest) continue;
|
|
4344
|
+
reports.push({ ...stamp, changed: null });
|
|
4345
|
+
continue;
|
|
4346
|
+
}
|
|
4347
|
+
if (before && before.digest === stamp.digest) continue;
|
|
4348
|
+
reports.push({ ...stamp, changed: changedIds(before?.records, stamp) });
|
|
4349
|
+
}
|
|
4350
|
+
return reports;
|
|
4351
|
+
},
|
|
4352
|
+
render: (result) => result.map((report) => {
|
|
4353
|
+
const counts = `${report.recordCount} record(s), ${report.superseded} superseded`;
|
|
4354
|
+
const head = `${report.path} ${report.digest} ${counts}${report.newestAt ? ` newest ${report.newestAt}` : ""}`;
|
|
4355
|
+
return report.changed?.length ? `${head}
|
|
4356
|
+
changed: ${report.changed.join(", ")}` : head;
|
|
4357
|
+
}).join("\n")
|
|
4358
|
+
});
|
|
4359
|
+
function changedIds(before, stamp) {
|
|
4360
|
+
const now = new Map(
|
|
4361
|
+
stamp.records.map((record) => [record.conceptId, record.digest])
|
|
4362
|
+
);
|
|
4363
|
+
const ids = /* @__PURE__ */ new Set();
|
|
4364
|
+
for (const [conceptId2, digest] of now) {
|
|
4365
|
+
if (before?.get(conceptId2) !== digest) ids.add(conceptId2);
|
|
4366
|
+
}
|
|
4367
|
+
for (const conceptId2 of before?.keys() ?? []) {
|
|
4368
|
+
if (!now.has(conceptId2)) ids.add(conceptId2);
|
|
4369
|
+
}
|
|
4370
|
+
return [...ids].sort();
|
|
4371
|
+
}
|
|
4372
|
+
async function readBaseline(since) {
|
|
4373
|
+
if (DIGEST.test(since)) return { digest: since, byPath: /* @__PURE__ */ new Map() };
|
|
4374
|
+
let parsed;
|
|
4375
|
+
try {
|
|
4376
|
+
parsed = JSON.parse(await readFile6(since, "utf8"));
|
|
4377
|
+
} catch {
|
|
4378
|
+
throw new KbStampBaselineError(since);
|
|
4379
|
+
}
|
|
4380
|
+
const entries = Array.isArray(parsed) ? parsed : parsed?.stamps ?? [];
|
|
4381
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
4382
|
+
for (const entry of entries) {
|
|
4383
|
+
if (typeof entry?.path !== "string" || typeof entry?.digest !== "string") {
|
|
4384
|
+
continue;
|
|
4385
|
+
}
|
|
4386
|
+
byPath.set(entry.path, {
|
|
4387
|
+
digest: entry.digest,
|
|
4388
|
+
records: new Map(
|
|
4389
|
+
(entry.records ?? []).map((record) => [
|
|
4390
|
+
record.conceptId,
|
|
4391
|
+
record.digest
|
|
4392
|
+
])
|
|
4393
|
+
)
|
|
4394
|
+
});
|
|
4395
|
+
}
|
|
4396
|
+
return { digest: null, byPath };
|
|
4397
|
+
}
|
|
4398
|
+
|
|
3614
4399
|
// src/commands/status.ts
|
|
3615
|
-
import { z as
|
|
4400
|
+
import { z as z27 } from "zod";
|
|
3616
4401
|
var statusCommand = define({
|
|
3617
4402
|
name: "status",
|
|
3618
4403
|
tool: "kb_status",
|
|
3619
4404
|
usage: "status <concept-id> <status>",
|
|
3620
4405
|
description: "Move a record's status. Compare-and-swap: a concurrent change fails instead of being overwritten.",
|
|
3621
|
-
input:
|
|
4406
|
+
input: z27.object({
|
|
3622
4407
|
bundlePath,
|
|
3623
4408
|
conceptId,
|
|
3624
|
-
status:
|
|
4409
|
+
status: z27.enum(KB_RECORD_STATUSES)
|
|
3625
4410
|
}),
|
|
3626
4411
|
fromArgv: (argv, path) => ({
|
|
3627
4412
|
bundlePath: path,
|
|
@@ -3636,13 +4421,13 @@ var statusCommand = define({
|
|
|
3636
4421
|
});
|
|
3637
4422
|
|
|
3638
4423
|
// src/commands/supersede.ts
|
|
3639
|
-
import { z as
|
|
4424
|
+
import { z as z28 } from "zod";
|
|
3640
4425
|
var supersedeCommand = define({
|
|
3641
4426
|
name: "supersede",
|
|
3642
4427
|
tool: "kb_supersede",
|
|
3643
4428
|
usage: "supersede <concept-id> <replacement-id>",
|
|
3644
4429
|
description: "Mark a record superseded by another, linked in both directions. Use instead of editing a record whose meaning changed.",
|
|
3645
|
-
input:
|
|
4430
|
+
input: z28.object({ bundlePath, conceptId, replacementId: conceptId }),
|
|
3646
4431
|
fromArgv: (argv, path) => ({
|
|
3647
4432
|
bundlePath: path,
|
|
3648
4433
|
conceptId: argv[1],
|
|
@@ -3656,16 +4441,16 @@ var supersedeCommand = define({
|
|
|
3656
4441
|
});
|
|
3657
4442
|
|
|
3658
4443
|
// src/commands/sync-instructions.ts
|
|
3659
|
-
import { z as
|
|
4444
|
+
import { z as z29 } from "zod";
|
|
3660
4445
|
var syncInstructionsCommand = define({
|
|
3661
4446
|
name: "sync-instructions",
|
|
3662
4447
|
usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
|
|
3663
4448
|
description: "CLI-only: plant the kb_context block between sentinel comments in AGENTS.md or CLAUDE.md, idempotently.",
|
|
3664
|
-
input:
|
|
3665
|
-
file:
|
|
3666
|
-
budgetTokens:
|
|
3667
|
-
fullUnderTokens:
|
|
3668
|
-
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()
|
|
3669
4454
|
}),
|
|
3670
4455
|
fromArgv: (argv) => {
|
|
3671
4456
|
const budget = argvFlag(argv, "--budget");
|
|
@@ -3691,17 +4476,17 @@ var syncInstructionsCommand = define({
|
|
|
3691
4476
|
});
|
|
3692
4477
|
|
|
3693
4478
|
// src/commands/trace.ts
|
|
3694
|
-
import { z as
|
|
4479
|
+
import { z as z30 } from "zod";
|
|
3695
4480
|
var traceCommand = define({
|
|
3696
4481
|
name: "trace",
|
|
3697
4482
|
tool: "kb_trace",
|
|
3698
4483
|
usage: "trace <concept-id> [edges...]",
|
|
3699
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".',
|
|
3700
|
-
input:
|
|
4485
|
+
input: z30.object({
|
|
3701
4486
|
bundlePath,
|
|
3702
4487
|
conceptId,
|
|
3703
|
-
edges:
|
|
3704
|
-
depth:
|
|
4488
|
+
edges: z30.array(z30.enum(TRACE_EDGES)).optional(),
|
|
4489
|
+
depth: z30.number().int().positive().optional()
|
|
3705
4490
|
}),
|
|
3706
4491
|
fromArgv: (argv, path) => ({
|
|
3707
4492
|
bundlePath: path,
|
|
@@ -3723,37 +4508,37 @@ var traceCommand = define({
|
|
|
3723
4508
|
});
|
|
3724
4509
|
|
|
3725
4510
|
// src/commands/types.ts
|
|
3726
|
-
import { z as
|
|
4511
|
+
import { z as z31 } from "zod";
|
|
3727
4512
|
var typesCommand = define({
|
|
3728
4513
|
name: "types",
|
|
3729
4514
|
tool: "kb_types",
|
|
3730
4515
|
usage: "types",
|
|
3731
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.",
|
|
3732
|
-
input:
|
|
4517
|
+
input: z31.object({}),
|
|
3733
4518
|
fromArgv: () => ({}),
|
|
3734
4519
|
run: () => Promise.resolve(RECORD_TYPES)
|
|
3735
4520
|
});
|
|
3736
4521
|
|
|
3737
4522
|
// src/commands/unpin.ts
|
|
3738
|
-
import { z as
|
|
4523
|
+
import { z as z32 } from "zod";
|
|
3739
4524
|
var unpinCommand = define({
|
|
3740
4525
|
name: "unpin",
|
|
3741
4526
|
tool: "kb_unpin",
|
|
3742
4527
|
usage: "unpin [bundle-path]",
|
|
3743
4528
|
description: "Remove a base from every manifest layer that holds it. Reports the layers touched.",
|
|
3744
|
-
input:
|
|
4529
|
+
input: z32.object({ bundlePath }),
|
|
3745
4530
|
fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
|
|
3746
4531
|
run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
|
|
3747
4532
|
});
|
|
3748
4533
|
|
|
3749
4534
|
// src/commands/validate.ts
|
|
3750
|
-
import { z as
|
|
4535
|
+
import { z as z33 } from "zod";
|
|
3751
4536
|
var validateCommand = define({
|
|
3752
4537
|
name: "validate",
|
|
3753
4538
|
tool: "kb_validate",
|
|
3754
4539
|
usage: "validate",
|
|
3755
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.",
|
|
3756
|
-
input:
|
|
4541
|
+
input: z33.object({ bundlePath }),
|
|
3757
4542
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
3758
4543
|
run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
|
|
3759
4544
|
// Warnings never fail the exit code; every other severity does.
|
|
@@ -3763,16 +4548,16 @@ var validateCommand = define({
|
|
|
3763
4548
|
});
|
|
3764
4549
|
|
|
3765
4550
|
// src/commands/verify.ts
|
|
3766
|
-
import { z as
|
|
4551
|
+
import { z as z34 } from "zod";
|
|
3767
4552
|
var verifyCommand = define({
|
|
3768
4553
|
name: "verify",
|
|
3769
4554
|
tool: "kb_verify",
|
|
3770
4555
|
usage: "verify <concept-id> --note <text>",
|
|
3771
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.",
|
|
3772
|
-
input:
|
|
4557
|
+
input: z34.object({
|
|
3773
4558
|
bundlePath,
|
|
3774
4559
|
conceptId,
|
|
3775
|
-
note:
|
|
4560
|
+
note: z34.string().refine((s) => s.trim().length > 0, {
|
|
3776
4561
|
message: "note must say what the check found"
|
|
3777
4562
|
})
|
|
3778
4563
|
}),
|
|
@@ -3792,15 +4577,15 @@ var verifyCommand = define({
|
|
|
3792
4577
|
});
|
|
3793
4578
|
|
|
3794
4579
|
// src/commands/write.ts
|
|
3795
|
-
import { z as
|
|
4580
|
+
import { z as z35 } from "zod";
|
|
3796
4581
|
var writeCommand = define({
|
|
3797
4582
|
name: "write",
|
|
3798
4583
|
tool: "kb_write",
|
|
3799
4584
|
usage: "write <type> < record.json",
|
|
3800
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.",
|
|
3801
|
-
input:
|
|
4586
|
+
input: z35.object({
|
|
3802
4587
|
bundlePath,
|
|
3803
|
-
type:
|
|
4588
|
+
type: z35.enum(KB_RECORD_TYPES),
|
|
3804
4589
|
input: composeInputSchema
|
|
3805
4590
|
}),
|
|
3806
4591
|
fromArgv: async (argv, path, stdin) => ({
|
|
@@ -3824,13 +4609,13 @@ var writeCommand = define({
|
|
|
3824
4609
|
});
|
|
3825
4610
|
|
|
3826
4611
|
// src/commands/write-decision.ts
|
|
3827
|
-
import { z as
|
|
4612
|
+
import { z as z36 } from "zod";
|
|
3828
4613
|
var writeDecisionCommand = define({
|
|
3829
4614
|
name: "write-decision",
|
|
3830
4615
|
tool: "kb_write_decision",
|
|
3831
4616
|
usage: "write-decision < decision.json",
|
|
3832
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.",
|
|
3833
|
-
input:
|
|
4618
|
+
input: z36.object({ bundlePath, input: decisionInputSchema }),
|
|
3834
4619
|
fromArgv: async (_argv, path, stdin) => ({
|
|
3835
4620
|
bundlePath: path,
|
|
3836
4621
|
input: JSON.parse(await stdin())
|
|
@@ -3870,6 +4655,7 @@ var KB_COMMANDS = [
|
|
|
3870
4655
|
listCommand,
|
|
3871
4656
|
readIndexCommand,
|
|
3872
4657
|
logCommand,
|
|
4658
|
+
stampCommand,
|
|
3873
4659
|
validateCommand,
|
|
3874
4660
|
doctorCommand,
|
|
3875
4661
|
schemaCommand,
|
|
@@ -3911,7 +4697,7 @@ function parseMarkdownWithFrontmatter(text, schema) {
|
|
|
3911
4697
|
|
|
3912
4698
|
// src/search-index.ts
|
|
3913
4699
|
import { stat as stat2 } from "fs/promises";
|
|
3914
|
-
import { join as
|
|
4700
|
+
import { join as join6 } from "path";
|
|
3915
4701
|
var SEARCH_INDEX_FILE = ".index.sqlite";
|
|
3916
4702
|
var COLLECTION = "kb";
|
|
3917
4703
|
async function searchBase(bundlePath2, query, options = {}) {
|
|
@@ -3920,7 +4706,7 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
3920
4706
|
let store = null;
|
|
3921
4707
|
try {
|
|
3922
4708
|
store = await qmd.createStore({
|
|
3923
|
-
dbPath:
|
|
4709
|
+
dbPath: join6(bundlePath2, SEARCH_INDEX_FILE),
|
|
3924
4710
|
config: {
|
|
3925
4711
|
collections: {
|
|
3926
4712
|
[COLLECTION]: {
|
|
@@ -3955,7 +4741,7 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
3955
4741
|
}
|
|
3956
4742
|
}
|
|
3957
4743
|
async function isStale(bundlePath2) {
|
|
3958
|
-
const indexAt = await stat2(
|
|
4744
|
+
const indexAt = await stat2(join6(bundlePath2, SEARCH_INDEX_FILE)).then((s) => s.mtimeMs).catch(() => 0);
|
|
3959
4745
|
if (!indexAt) return true;
|
|
3960
4746
|
const { readdir: readdir2 } = await import("fs/promises");
|
|
3961
4747
|
const names = (await readdir2(bundlePath2).catch(() => [])).filter(
|
|
@@ -3964,7 +4750,7 @@ async function isStale(bundlePath2) {
|
|
|
3964
4750
|
let stale = false;
|
|
3965
4751
|
await mapLimit(names, DEFAULT_IO_CONCURRENCY, async (name) => {
|
|
3966
4752
|
if (stale) return;
|
|
3967
|
-
const at = await stat2(
|
|
4753
|
+
const at = await stat2(join6(bundlePath2, name)).then((s) => s.mtimeMs).catch(() => 0);
|
|
3968
4754
|
if (at > indexAt) stale = true;
|
|
3969
4755
|
});
|
|
3970
4756
|
return stale;
|
|
@@ -3999,18 +4785,49 @@ async function loadQmd(logger) {
|
|
|
3999
4785
|
}
|
|
4000
4786
|
|
|
4001
4787
|
// src/kb-store.ts
|
|
4002
|
-
import { createHash as createHash2 } from "crypto";
|
|
4003
4788
|
import {
|
|
4004
4789
|
appendFile,
|
|
4005
4790
|
link,
|
|
4006
|
-
mkdir as
|
|
4791
|
+
mkdir as mkdir4,
|
|
4007
4792
|
readdir,
|
|
4008
|
-
readFile as
|
|
4009
|
-
rename,
|
|
4793
|
+
readFile as readFile7,
|
|
4794
|
+
rename as rename2,
|
|
4010
4795
|
unlink,
|
|
4011
|
-
writeFile as
|
|
4796
|
+
writeFile as writeFile4
|
|
4012
4797
|
} from "fs/promises";
|
|
4013
|
-
import { join as
|
|
4798
|
+
import { join as join7, resolve as resolve5, sep as sep3 } from "path";
|
|
4799
|
+
|
|
4800
|
+
// src/kb-stamp.ts
|
|
4801
|
+
import { createHash as createHash4 } from "crypto";
|
|
4802
|
+
function sha2563(contents) {
|
|
4803
|
+
return createHash4("sha256").update(contents).digest("hex");
|
|
4804
|
+
}
|
|
4805
|
+
function bundleStamp(records, superseded) {
|
|
4806
|
+
const entries = [
|
|
4807
|
+
...records.map((hit) => ({
|
|
4808
|
+
conceptId: hit.record.conceptId,
|
|
4809
|
+
digest: `current:${sha2563(
|
|
4810
|
+
stringifyMarkdownWithFrontmatter(
|
|
4811
|
+
hit.record.body,
|
|
4812
|
+
hit.record.frontmatter
|
|
4813
|
+
)
|
|
4814
|
+
)}`
|
|
4815
|
+
})),
|
|
4816
|
+
...superseded.map((entry) => ({
|
|
4817
|
+
conceptId: entry.conceptId,
|
|
4818
|
+
digest: `superseded:${sha2563(JSON.stringify(entry))}`
|
|
4819
|
+
}))
|
|
4820
|
+
].sort((a, b) => a.conceptId < b.conceptId ? -1 : 1);
|
|
4821
|
+
return {
|
|
4822
|
+
digest: sha2563(
|
|
4823
|
+
entries.map((entry) => `${entry.conceptId}:${entry.digest}`).join("\n")
|
|
4824
|
+
),
|
|
4825
|
+
records: entries
|
|
4826
|
+
};
|
|
4827
|
+
}
|
|
4828
|
+
function bundleDigest(records, superseded) {
|
|
4829
|
+
return bundleStamp(records, superseded).digest;
|
|
4830
|
+
}
|
|
4014
4831
|
|
|
4015
4832
|
// src/kb-links/inbound.ts
|
|
4016
4833
|
function inboundIndex(bundle) {
|
|
@@ -4172,7 +4989,7 @@ function appendUnionMergeLine(contents) {
|
|
|
4172
4989
|
}
|
|
4173
4990
|
|
|
4174
4991
|
// src/kb-store.ts
|
|
4175
|
-
var KB_DIR =
|
|
4992
|
+
var KB_DIR = join7(".strauss", "kb");
|
|
4176
4993
|
var STORE_OWNED = /* @__PURE__ */ new Set([INDEX_FILE, LOG_FILE, SEARCH_INDEX_FILE]);
|
|
4177
4994
|
var DEFAULT_LOAD_BUDGET = 25e3;
|
|
4178
4995
|
var KbStore = class {
|
|
@@ -4203,7 +5020,7 @@ var KbStore = class {
|
|
|
4203
5020
|
const conceptId2 = `${input.type}.${input.slug}`;
|
|
4204
5021
|
const root = this.root(bundlePath2);
|
|
4205
5022
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
4206
|
-
await
|
|
5023
|
+
await mkdir4(root, { recursive: true });
|
|
4207
5024
|
await this.publish(
|
|
4208
5025
|
target,
|
|
4209
5026
|
stringifyMarkdownWithFrontmatter(input.body, frontmatter),
|
|
@@ -4242,7 +5059,7 @@ var KbStore = class {
|
|
|
4242
5059
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
4243
5060
|
let raw;
|
|
4244
5061
|
try {
|
|
4245
|
-
raw = await
|
|
5062
|
+
raw = await readFile7(target, "utf8");
|
|
4246
5063
|
} catch {
|
|
4247
5064
|
return null;
|
|
4248
5065
|
}
|
|
@@ -4267,7 +5084,7 @@ var KbStore = class {
|
|
|
4267
5084
|
const records = await mapLimit(
|
|
4268
5085
|
wanted,
|
|
4269
5086
|
DEFAULT_IO_CONCURRENCY,
|
|
4270
|
-
async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await
|
|
5087
|
+
async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await readFile7(join7(root, name), "utf8"))
|
|
4271
5088
|
);
|
|
4272
5089
|
return records.filter((record) => record !== null);
|
|
4273
5090
|
}
|
|
@@ -4424,7 +5241,7 @@ ${answer}
|
|
|
4424
5241
|
if (found.length) return found;
|
|
4425
5242
|
}
|
|
4426
5243
|
const lowered = needle.toLowerCase();
|
|
4427
|
-
return bundle.filter((record) =>
|
|
5244
|
+
return bundle.filter((record) => matches2(record, lowered));
|
|
4428
5245
|
}
|
|
4429
5246
|
/**
|
|
4430
5247
|
* Anchor drift over the records about to be handed back. Like the search
|
|
@@ -4542,6 +5359,28 @@ ${answer}
|
|
|
4542
5359
|
digest: bundleDigestValue
|
|
4543
5360
|
};
|
|
4544
5361
|
}
|
|
5362
|
+
/**
|
|
5363
|
+
* `load`'s digest without `load`'s bodies — the same records, adjudicated
|
|
5364
|
+
* the same way, handed back as a stamp. Skips the anchor drift pass, which
|
|
5365
|
+
* reads source files and only ever adds warnings: no warning reaches the
|
|
5366
|
+
* digest, so the value is identical to the one `load` returns.
|
|
5367
|
+
*/
|
|
5368
|
+
async stamp(bundlePath2) {
|
|
5369
|
+
const bundle = await this.list(bundlePath2);
|
|
5370
|
+
const adjudicated = adjudicate(bundle, bundle, /* @__PURE__ */ new Date());
|
|
5371
|
+
const current = adjudicated.filter((hit) => hit.standing !== "superseded");
|
|
5372
|
+
const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(stub);
|
|
5373
|
+
const stamped = bundleStamp(current, superseded);
|
|
5374
|
+
const dates = bundle.map((record) => record.frontmatter.generated?.at ?? null).filter((at) => typeof at === "string").sort();
|
|
5375
|
+
return {
|
|
5376
|
+
path: bundlePath2,
|
|
5377
|
+
digest: stamped.digest,
|
|
5378
|
+
recordCount: bundle.length,
|
|
5379
|
+
superseded: superseded.length,
|
|
5380
|
+
newestAt: dates.at(-1) ?? null,
|
|
5381
|
+
records: stamped.records
|
|
5382
|
+
};
|
|
5383
|
+
}
|
|
4545
5384
|
/** How a position was arrived at, as a timeline. See `trace.ts`. */
|
|
4546
5385
|
async trace(bundlePath2, seedId, options = {}) {
|
|
4547
5386
|
return trace(seedId, await this.list(bundlePath2), options);
|
|
@@ -4572,11 +5411,11 @@ ${answer}
|
|
|
4572
5411
|
async readIndex(bundlePath2) {
|
|
4573
5412
|
const root = this.root(bundlePath2);
|
|
4574
5413
|
const expected = renderIndex(await this.list(bundlePath2));
|
|
4575
|
-
const stored = await
|
|
5414
|
+
const stored = await readFile7(join7(root, INDEX_FILE), "utf8").catch(
|
|
4576
5415
|
() => null
|
|
4577
5416
|
);
|
|
4578
5417
|
if (indexIsStale(stored, expected)) {
|
|
4579
|
-
await this.publish(
|
|
5418
|
+
await this.publish(join7(root, INDEX_FILE), expected, true, INDEX_FILE);
|
|
4580
5419
|
this.logger.info?.({
|
|
4581
5420
|
operation: "kb.index.repair",
|
|
4582
5421
|
bundlePath: root,
|
|
@@ -4593,8 +5432,8 @@ ${answer}
|
|
|
4593
5432
|
* knows which agent touched what. So a bad line is surfaced and left alone.
|
|
4594
5433
|
*/
|
|
4595
5434
|
async readLog(bundlePath2) {
|
|
4596
|
-
const raw = await
|
|
4597
|
-
|
|
5435
|
+
const raw = await readFile7(
|
|
5436
|
+
join7(this.root(bundlePath2), LOG_FILE),
|
|
4598
5437
|
"utf8"
|
|
4599
5438
|
).catch(() => "");
|
|
4600
5439
|
const result = parseLog(raw);
|
|
@@ -4645,15 +5484,15 @@ ${answer}
|
|
|
4645
5484
|
}
|
|
4646
5485
|
async mutate(bundlePath2, conceptId2, change, entry, changeBody = (body) => body) {
|
|
4647
5486
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
4648
|
-
const before = await
|
|
5487
|
+
const before = await readFile7(target, "utf8").catch(() => null);
|
|
4649
5488
|
if (before === null) throw new KbRecordNotFoundError(conceptId2);
|
|
4650
5489
|
const parsed = this.parse(conceptId2, before);
|
|
4651
5490
|
if (!parsed) throw new KbRecordNotFoundError(conceptId2);
|
|
4652
5491
|
const frontmatter = change(parsed.frontmatter);
|
|
4653
5492
|
const body = changeBody(parsed.body);
|
|
4654
5493
|
const contents = stringifyMarkdownWithFrontmatter(body, frontmatter);
|
|
4655
|
-
const witness = await
|
|
4656
|
-
if (witness === null ||
|
|
5494
|
+
const witness = await readFile7(target, "utf8").catch(() => null);
|
|
5495
|
+
if (witness === null || sha2563(witness) !== sha2563(before)) {
|
|
4657
5496
|
throw new KbWriteConflictError(conceptId2);
|
|
4658
5497
|
}
|
|
4659
5498
|
await this.publish(target, contents, true, conceptId2);
|
|
@@ -4678,10 +5517,10 @@ ${answer}
|
|
|
4678
5517
|
*/
|
|
4679
5518
|
async publish(target, contents, overwrite, conceptId2) {
|
|
4680
5519
|
const staging = `${target}.${process.pid}.tmp`;
|
|
4681
|
-
await
|
|
5520
|
+
await writeFile4(staging, contents, "utf8");
|
|
4682
5521
|
try {
|
|
4683
5522
|
if (overwrite) {
|
|
4684
|
-
await
|
|
5523
|
+
await rename2(staging, target);
|
|
4685
5524
|
return;
|
|
4686
5525
|
}
|
|
4687
5526
|
await link(staging, target);
|
|
@@ -4735,18 +5574,18 @@ ${answer}
|
|
|
4735
5574
|
* file must not fail the mutation it guards.
|
|
4736
5575
|
*/
|
|
4737
5576
|
async ensureGitattributes(root) {
|
|
4738
|
-
const target =
|
|
5577
|
+
const target = join7(root, GITATTRIBUTES_FILE);
|
|
4739
5578
|
try {
|
|
4740
5579
|
let existing;
|
|
4741
5580
|
try {
|
|
4742
|
-
existing = await
|
|
5581
|
+
existing = await readFile7(target, "utf8");
|
|
4743
5582
|
} catch (error) {
|
|
4744
5583
|
if (error.code !== "ENOENT") throw error;
|
|
4745
5584
|
existing = null;
|
|
4746
5585
|
}
|
|
4747
5586
|
if (existing === null) {
|
|
4748
5587
|
try {
|
|
4749
|
-
await
|
|
5588
|
+
await writeFile4(target, appendUnionMergeLine(""), {
|
|
4750
5589
|
encoding: "utf8",
|
|
4751
5590
|
flag: "wx"
|
|
4752
5591
|
});
|
|
@@ -4786,7 +5625,7 @@ ${answer}
|
|
|
4786
5625
|
async record(root, entry) {
|
|
4787
5626
|
await this.ensureGitattributes(root);
|
|
4788
5627
|
const line = renderLogEntry({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
|
|
4789
|
-
await appendFile(
|
|
5628
|
+
await appendFile(join7(root, LOG_FILE), line, "utf8").catch((error) => {
|
|
4790
5629
|
this.logger.warn?.({
|
|
4791
5630
|
operation: "kb.log.append",
|
|
4792
5631
|
outcome: "failed",
|
|
@@ -4823,7 +5662,7 @@ ${answer}
|
|
|
4823
5662
|
{ conceptId: conceptId2 }
|
|
4824
5663
|
);
|
|
4825
5664
|
}
|
|
4826
|
-
return
|
|
5665
|
+
return join7(this.root(bundlePath2), `${conceptId2}.md`);
|
|
4827
5666
|
}
|
|
4828
5667
|
};
|
|
4829
5668
|
function estimateTokens(record) {
|
|
@@ -4850,7 +5689,7 @@ function stub(hit) {
|
|
|
4850
5689
|
at: hit.record.frontmatter.generated?.at ?? null
|
|
4851
5690
|
};
|
|
4852
5691
|
}
|
|
4853
|
-
function
|
|
5692
|
+
function matches2(record, needle) {
|
|
4854
5693
|
const { title, description } = record.frontmatter;
|
|
4855
5694
|
return [record.conceptId, title, description, record.body].some(
|
|
4856
5695
|
(field) => field?.toLowerCase().includes(needle)
|
|
@@ -4861,25 +5700,6 @@ function normalizeActor(id) {
|
|
|
4861
5700
|
if (colon === -1) return id.toLowerCase();
|
|
4862
5701
|
return id.slice(0, colon + 1).toLowerCase() + id.slice(colon + 1);
|
|
4863
5702
|
}
|
|
4864
|
-
function digest(contents) {
|
|
4865
|
-
return createHash2("sha256").update(contents).digest("hex");
|
|
4866
|
-
}
|
|
4867
|
-
function bundleDigest(records, superseded) {
|
|
4868
|
-
const entries = [
|
|
4869
|
-
...records.map(
|
|
4870
|
-
(hit) => `${hit.record.conceptId}:current:${digest(
|
|
4871
|
-
stringifyMarkdownWithFrontmatter(
|
|
4872
|
-
hit.record.body,
|
|
4873
|
-
hit.record.frontmatter
|
|
4874
|
-
)
|
|
4875
|
-
)}`
|
|
4876
|
-
),
|
|
4877
|
-
...superseded.map(
|
|
4878
|
-
(entry) => `${entry.conceptId}:superseded:${digest(JSON.stringify(entry))}`
|
|
4879
|
-
)
|
|
4880
|
-
].sort();
|
|
4881
|
-
return digest(entries.join("\n"));
|
|
4882
|
-
}
|
|
4883
5703
|
|
|
4884
5704
|
// src/pack.ts
|
|
4885
5705
|
var DEFAULT_PACK_HOPS = 2;
|
|
@@ -4961,12 +5781,12 @@ function byRank(left, right) {
|
|
|
4961
5781
|
) || left.record.conceptId.localeCompare(right.record.conceptId);
|
|
4962
5782
|
}
|
|
4963
5783
|
function typeRank(record) {
|
|
4964
|
-
const
|
|
4965
|
-
return
|
|
5784
|
+
const index2 = TYPE_PRIORITY.indexOf(record.frontmatter.type);
|
|
5785
|
+
return index2 === -1 ? TYPE_PRIORITY.length : index2;
|
|
4966
5786
|
}
|
|
4967
5787
|
|
|
4968
5788
|
// src/version.ts
|
|
4969
|
-
var VERSION = true ? "0.1.
|
|
5789
|
+
var VERSION = true ? "0.1.17" : "0.0.0-dev";
|
|
4970
5790
|
|
|
4971
5791
|
export {
|
|
4972
5792
|
kbSourceSchema,
|
|
@@ -5002,9 +5822,19 @@ export {
|
|
|
5002
5822
|
repoCacheDir,
|
|
5003
5823
|
readRemoteAnchors,
|
|
5004
5824
|
anchorFilePath,
|
|
5825
|
+
grammarsCacheRoot,
|
|
5826
|
+
grammarManifest,
|
|
5827
|
+
ensureGrammar,
|
|
5828
|
+
grammarHints,
|
|
5829
|
+
languageForFile,
|
|
5830
|
+
treeSitterLanguages,
|
|
5831
|
+
TreeSitterResolver,
|
|
5005
5832
|
regexResolver,
|
|
5006
5833
|
hashAnchorText,
|
|
5007
5834
|
resolveAnchor,
|
|
5835
|
+
resolveAnchorSpan,
|
|
5836
|
+
prepareResolvers,
|
|
5837
|
+
defaultAnchorResolvers,
|
|
5008
5838
|
detectAnchorDrift,
|
|
5009
5839
|
Fault,
|
|
5010
5840
|
ErrorTypes,
|
|
@@ -5082,4 +5912,4 @@ export {
|
|
|
5082
5912
|
KbStore,
|
|
5083
5913
|
VERSION
|
|
5084
5914
|
};
|
|
5085
|
-
//# sourceMappingURL=chunk-
|
|
5915
|
+
//# sourceMappingURL=chunk-ZKIQOBHT.js.map
|