@saasontools/strauss-kb 0.1.16 → 0.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +22 -5
- package/README.md +14 -4
- package/dist/{chunk-RINBOAQZ.js → chunk-RMJUGTAQ.js} +2 -2
- package/dist/{chunk-MEZCF646.js → chunk-SA3A2SPY.js} +2 -2
- package/dist/{chunk-H5W53NVU.js → chunk-ZKIQOBHT.js} +949 -284
- package/dist/chunk-ZKIQOBHT.js.map +1 -0
- package/dist/cli-main.cjs +955 -299
- package/dist/cli-main.cjs.map +1 -1
- package/dist/cli-main.js +2 -2
- package/dist/index.cjs +1000 -317
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +214 -5
- package/dist/index.d.ts +214 -5
- package/dist/index.js +23 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp-main.cjs +951 -295
- package/dist/mcp-main.cjs.map +1 -1
- package/dist/mcp-main.js +2 -2
- package/grammars/README.md +78 -0
- package/grammars/manifest.json +595 -0
- package/grammars/packs.json +127 -0
- package/package.json +7 -2
- package/dist/chunk-H5W53NVU.js.map +0 -1
- /package/dist/{chunk-RINBOAQZ.js.map → chunk-RMJUGTAQ.js.map} +0 -0
- /package/dist/{chunk-MEZCF646.js.map → chunk-SA3A2SPY.js.map} +0 -0
package/dist/cli-main.cjs
CHANGED
|
@@ -23,8 +23,12 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
23
|
mod
|
|
24
24
|
));
|
|
25
25
|
|
|
26
|
+
// ../../node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.26_supports-color@8.1.1_typescript@6.0.3_yaml@2.9.0/node_modules/tsup/assets/cjs_shims.js
|
|
27
|
+
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
28
|
+
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
29
|
+
|
|
26
30
|
// src/cli.ts
|
|
27
|
-
var
|
|
31
|
+
var import_node_path12 = require("path");
|
|
28
32
|
|
|
29
33
|
// src/decision-record.ts
|
|
30
34
|
var import_zod3 = require("zod");
|
|
@@ -77,7 +81,13 @@ var kbAnchorSchema = import_zod.z.object({
|
|
|
77
81
|
/** ISO 8601 timestamp of the last successful resolution. */
|
|
78
82
|
resolved_at: import_zod.z.string().min(1).optional(),
|
|
79
83
|
/** Line count of the text the hash was taken over. */
|
|
80
|
-
lines: import_zod.z.number().int().positive().optional()
|
|
84
|
+
lines: import_zod.z.number().int().positive().optional(),
|
|
85
|
+
/**
|
|
86
|
+
* Which resolver produced the hashed span. Absent means an anchor stamped
|
|
87
|
+
* before resolvers were named, which is read as `regex` — the only one
|
|
88
|
+
* there was. A hash from a different resolver is drift, not a match.
|
|
89
|
+
*/
|
|
90
|
+
resolver: import_zod.z.enum(["tree-sitter", "regex"]).optional()
|
|
81
91
|
}).strict();
|
|
82
92
|
var kbLinkSchema = import_zod.z.object({
|
|
83
93
|
target: import_zod.z.string().min(1),
|
|
@@ -441,7 +451,7 @@ function composeNoDecisionRecord(reason, writtenBy, writtenAt) {
|
|
|
441
451
|
}
|
|
442
452
|
|
|
443
453
|
// src/commands/anchor-resolve.ts
|
|
444
|
-
var
|
|
454
|
+
var import_zod7 = require("zod");
|
|
445
455
|
|
|
446
456
|
// src/concurrency.ts
|
|
447
457
|
var DEFAULT_IO_CONCURRENCY = 16;
|
|
@@ -738,18 +748,18 @@ async function readOneRepo(repo, url, declared, context) {
|
|
|
738
748
|
if (!repoUrlIsSafe(url)) return all({ ok: false, reason: "repo-invalid" });
|
|
739
749
|
const cache = cachePathFor(repo, context.cacheDir);
|
|
740
750
|
if (!cache) return all({ ok: false, reason: "remote-unreachable" });
|
|
741
|
-
const
|
|
751
|
+
const rejected2 = /* @__PURE__ */ new Map();
|
|
742
752
|
const usable = [];
|
|
743
753
|
for (const want of wants) {
|
|
744
754
|
const reason = wantReason(want);
|
|
745
755
|
if (reason)
|
|
746
|
-
|
|
756
|
+
rejected2.set(wantKey(repo, want.ref, want.file), { ok: false, reason });
|
|
747
757
|
else usable.push(want);
|
|
748
758
|
}
|
|
749
|
-
if (!usable.length) return
|
|
759
|
+
if (!usable.length) return rejected2;
|
|
750
760
|
wants = usable;
|
|
751
761
|
const opened = await openCache(cache, url, context);
|
|
752
|
-
if (opened) return new Map([...
|
|
762
|
+
if (opened) return new Map([...rejected2, ...all(opened)]);
|
|
753
763
|
const wantsDefault = wants.some((want) => want.ref === void 0);
|
|
754
764
|
const branch = wantsDefault ? await defaultBranch(cache, context) : {};
|
|
755
765
|
const revs = /* @__PURE__ */ new Map();
|
|
@@ -776,7 +786,7 @@ async function readOneRepo(repo, url, declared, context) {
|
|
|
776
786
|
}
|
|
777
787
|
);
|
|
778
788
|
return new Map([
|
|
779
|
-
...
|
|
789
|
+
...rejected2,
|
|
780
790
|
...wants.map(
|
|
781
791
|
(want, at) => [wantKey(repo, want.ref, want.file), reads[at]]
|
|
782
792
|
)
|
|
@@ -828,13 +838,13 @@ async function defaultBranch(cache, context) {
|
|
|
828
838
|
if (!listed.ok) {
|
|
829
839
|
const reason = transportReason(listed.stderr);
|
|
830
840
|
if (reason !== "ref-not-found") {
|
|
831
|
-
const
|
|
832
|
-
return
|
|
841
|
+
const cached3 = await cachedBranch(cache);
|
|
842
|
+
return cached3 ? { name: cached3 } : { reason };
|
|
833
843
|
}
|
|
834
844
|
}
|
|
835
845
|
}
|
|
836
|
-
const
|
|
837
|
-
if (
|
|
846
|
+
const cached2 = await cachedBranch(cache);
|
|
847
|
+
if (cached2) return { name: cached2 };
|
|
838
848
|
return {
|
|
839
849
|
reason: context.offline ? "remote-unreachable" : "default-branch-unknown"
|
|
840
850
|
};
|
|
@@ -861,8 +871,8 @@ async function ensureRev(cache, rev, context) {
|
|
|
861
871
|
cwd: cache
|
|
862
872
|
}
|
|
863
873
|
);
|
|
864
|
-
const
|
|
865
|
-
if (
|
|
874
|
+
const cached2 = have.ok && have.stdout.trim().length > 0;
|
|
875
|
+
if (cached2 && (context.offline || IMMUTABLE_REV.test(rev))) return void 0;
|
|
866
876
|
if (context.offline) return { ok: false, reason: "remote-unreachable" };
|
|
867
877
|
const fetched = await git(
|
|
868
878
|
[
|
|
@@ -878,7 +888,7 @@ async function ensureRev(cache, rev, context) {
|
|
|
878
888
|
);
|
|
879
889
|
if (!fetched.ok) {
|
|
880
890
|
const reason = transportReason(fetched.stderr);
|
|
881
|
-
if (
|
|
891
|
+
if (cached2 && reason !== "ref-not-found") return void 0;
|
|
882
892
|
return { ok: false, reason };
|
|
883
893
|
}
|
|
884
894
|
const head = await git(["rev-parse", "FETCH_HEAD"], { cwd: cache });
|
|
@@ -993,63 +1003,577 @@ async function readAnchorFiles(files, read, concurrency = DEFAULT_IO_CONCURRENCY
|
|
|
993
1003
|
}
|
|
994
1004
|
|
|
995
1005
|
// src/anchor-resolver/resolver.ts
|
|
1006
|
+
var import_node_crypto3 = require("crypto");
|
|
1007
|
+
|
|
1008
|
+
// src/tree-sitter-resolver/languages.ts
|
|
1009
|
+
var import_node_path5 = require("path");
|
|
1010
|
+
|
|
1011
|
+
// src/grammars/index.ts
|
|
1012
|
+
var import_promises4 = require("fs/promises");
|
|
1013
|
+
|
|
1014
|
+
// src/grammars/store.ts
|
|
996
1015
|
var import_node_crypto = require("crypto");
|
|
1016
|
+
var import_promises3 = require("fs/promises");
|
|
1017
|
+
var import_node_os2 = require("os");
|
|
1018
|
+
var import_node_path3 = require("path");
|
|
1019
|
+
function grammarsCacheRoot(override) {
|
|
1020
|
+
return override ?? process.env["STRAUSS_KB_GRAMMARS_DIR"] ?? (0, import_node_path3.join)((0, import_node_os2.homedir)(), ".strauss", "grammars");
|
|
1021
|
+
}
|
|
1022
|
+
function grammarCachePath(root, language, sha2564, extension = "wasm") {
|
|
1023
|
+
return (0, import_node_path3.join)(root, language, `${sha2564.slice(0, 12)}.${extension}`);
|
|
1024
|
+
}
|
|
1025
|
+
function sha256(bytes) {
|
|
1026
|
+
return (0, import_node_crypto.createHash)("sha256").update(bytes).digest("hex");
|
|
1027
|
+
}
|
|
1028
|
+
function matches(bytes, entry) {
|
|
1029
|
+
if (entry.bytes !== void 0 && bytes.byteLength !== entry.bytes)
|
|
1030
|
+
return false;
|
|
1031
|
+
return sha256(bytes) === entry.sha256;
|
|
1032
|
+
}
|
|
1033
|
+
async function verifyCached(path, entry) {
|
|
1034
|
+
let bytes;
|
|
1035
|
+
try {
|
|
1036
|
+
bytes = await (0, import_promises3.readFile)(path);
|
|
1037
|
+
} catch {
|
|
1038
|
+
return false;
|
|
1039
|
+
}
|
|
1040
|
+
if (matches(bytes, entry)) return true;
|
|
1041
|
+
await (0, import_promises3.rm)(path, { force: true });
|
|
1042
|
+
return false;
|
|
1043
|
+
}
|
|
1044
|
+
async function writeCached(path, bytes) {
|
|
1045
|
+
await (0, import_promises3.mkdir)((0, import_node_path3.dirname)(path), { recursive: true });
|
|
1046
|
+
const temporary = `${path}.${process.pid}.${(0, import_node_crypto.randomBytes)(4).toString("hex")}.tmp`;
|
|
1047
|
+
try {
|
|
1048
|
+
await (0, import_promises3.writeFile)(temporary, bytes);
|
|
1049
|
+
await (0, import_promises3.rename)(temporary, path);
|
|
1050
|
+
} catch (error) {
|
|
1051
|
+
await (0, import_promises3.rm)(temporary, { force: true });
|
|
1052
|
+
throw error;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// src/grammars/fetch.ts
|
|
1057
|
+
var ATTEMPTS = 3;
|
|
1058
|
+
var BACKOFF_MS = 250;
|
|
1059
|
+
function grammarUrl(url, override) {
|
|
1060
|
+
const base2 = grammarsBaseUrl(override);
|
|
1061
|
+
if (!base2) return url;
|
|
1062
|
+
let root = base2;
|
|
1063
|
+
while (root.endsWith("/")) root = root.slice(0, -1);
|
|
1064
|
+
const pinned = new URL(url);
|
|
1065
|
+
return `${root}${pinned.pathname}${pinned.search}`;
|
|
1066
|
+
}
|
|
1067
|
+
function grammarsBaseUrl(override) {
|
|
1068
|
+
return override ?? process.env["STRAUSS_KB_GRAMMARS_URL"];
|
|
1069
|
+
}
|
|
1070
|
+
async function downloadPart(url, name, entry, options = {}) {
|
|
1071
|
+
const log = options.log ?? ((line) => void process.stderr.write(line));
|
|
1072
|
+
const weight = entry.bytes === void 0 ? "" : ` (${size(entry.bytes)} from manifest)`;
|
|
1073
|
+
log(`strauss-kb: downloading ${name}${weight} from ${url}
|
|
1074
|
+
`);
|
|
1075
|
+
let cause = "";
|
|
1076
|
+
for (let attempt = 1; attempt <= ATTEMPTS; attempt++) {
|
|
1077
|
+
const outcome = await attemptDownload(url, entry, options.fetchTimeoutMs);
|
|
1078
|
+
if ("bytes" in outcome) return outcome;
|
|
1079
|
+
cause = outcome.cause;
|
|
1080
|
+
log(
|
|
1081
|
+
`strauss-kb: ${name} attempt ${attempt}/${ATTEMPTS} failed: ${cause}
|
|
1082
|
+
`
|
|
1083
|
+
);
|
|
1084
|
+
if (!outcome.retry) break;
|
|
1085
|
+
if (attempt < ATTEMPTS) await pause(BACKOFF_MS * attempt);
|
|
1086
|
+
}
|
|
1087
|
+
log(`strauss-kb: ${name} not downloaded: ${cause}
|
|
1088
|
+
`);
|
|
1089
|
+
return { cause };
|
|
1090
|
+
}
|
|
1091
|
+
async function attemptDownload(url, entry, timeoutMs) {
|
|
1092
|
+
try {
|
|
1093
|
+
const response = await fetch(url, {
|
|
1094
|
+
signal: AbortSignal.timeout(fetchTimeoutMs(timeoutMs))
|
|
1095
|
+
});
|
|
1096
|
+
if (!response.ok) {
|
|
1097
|
+
return {
|
|
1098
|
+
cause: `HTTP ${response.status}`,
|
|
1099
|
+
retry: response.status >= 500 || response.status === 429
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
1103
|
+
if (!matches(bytes, entry))
|
|
1104
|
+
return { cause: "sha256 mismatch", retry: false };
|
|
1105
|
+
return { bytes };
|
|
1106
|
+
} catch (error) {
|
|
1107
|
+
const timedOut = error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
|
|
1108
|
+
return { cause: timedOut ? "timeout" : "network error", retry: true };
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
function size(bytes) {
|
|
1112
|
+
return bytes >= 1024 * 1024 ? `${(bytes / (1024 * 1024)).toFixed(1)} MB` : `${Math.round(bytes / 1024)} KB`;
|
|
1113
|
+
}
|
|
1114
|
+
function pause(ms) {
|
|
1115
|
+
return new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// src/grammars/manifest.ts
|
|
1119
|
+
var import_node_fs = require("fs");
|
|
1120
|
+
var import_node_path4 = require("path");
|
|
1121
|
+
var import_node_url = require("url");
|
|
1122
|
+
|
|
1123
|
+
// src/grammars/model.ts
|
|
1124
|
+
var import_zod4 = require("zod");
|
|
1125
|
+
var sha2562 = import_zod4.z.string().regex(/^[0-9a-f]{64}$/);
|
|
1126
|
+
var grammarWasmSchema = import_zod4.z.object({
|
|
1127
|
+
url: import_zod4.z.string().min(1),
|
|
1128
|
+
sha256: sha2562,
|
|
1129
|
+
bytes: import_zod4.z.number().int().positive()
|
|
1130
|
+
});
|
|
1131
|
+
var grammarTagsSchema = import_zod4.z.object({ url: import_zod4.z.string().min(1), sha256: sha2562 });
|
|
1132
|
+
var grammarPackSchema = import_zod4.z.object({
|
|
1133
|
+
package: import_zod4.z.string().min(1),
|
|
1134
|
+
wasm: grammarWasmSchema,
|
|
1135
|
+
tags: import_zod4.z.array(grammarTagsSchema),
|
|
1136
|
+
license: import_zod4.z.string().min(1),
|
|
1137
|
+
extensions: import_zod4.z.array(import_zod4.z.string().min(1))
|
|
1138
|
+
});
|
|
1139
|
+
var grammarManifestSchema = import_zod4.z.object({
|
|
1140
|
+
/** The runtime the packs were proved against. */
|
|
1141
|
+
webTreeSitter: import_zod4.z.string().min(1),
|
|
1142
|
+
linguist: import_zod4.z.object({ tag: import_zod4.z.string().min(1), commit: import_zod4.z.string().min(1) }),
|
|
1143
|
+
packs: import_zod4.z.record(import_zod4.z.string().min(1), grammarPackSchema)
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
// src/grammars/manifest.ts
|
|
1147
|
+
var cached;
|
|
1148
|
+
function grammarManifest() {
|
|
1149
|
+
cached ??= grammarManifestSchema.parse(
|
|
1150
|
+
JSON.parse((0, import_node_fs.readFileSync)(grammarsDataPath("manifest.json"), "utf8"))
|
|
1151
|
+
);
|
|
1152
|
+
return cached;
|
|
1153
|
+
}
|
|
1154
|
+
function grammarsDataPath(...segments) {
|
|
1155
|
+
let dir = (0, import_node_path4.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
1156
|
+
for (let up = 0; up < 5; up++) {
|
|
1157
|
+
const candidate = (0, import_node_path4.join)(dir, "grammars");
|
|
1158
|
+
if ((0, import_node_fs.existsSync)((0, import_node_path4.join)(candidate, "manifest.json")))
|
|
1159
|
+
return (0, import_node_path4.join)(candidate, ...segments);
|
|
1160
|
+
dir = (0, import_node_path4.dirname)(dir);
|
|
1161
|
+
}
|
|
1162
|
+
throw new Error("grammars/manifest.json is missing from the package");
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// src/grammars/index.ts
|
|
1166
|
+
var inFlight = /* @__PURE__ */ new Map();
|
|
1167
|
+
var missing = /* @__PURE__ */ new Map();
|
|
1168
|
+
var uncompilable = /* @__PURE__ */ new Map();
|
|
1169
|
+
var rejected = /* @__PURE__ */ new Map();
|
|
1170
|
+
function grammarsDownloadDisabled() {
|
|
1171
|
+
return process.env["STRAUSS_KB_GRAMMARS"] === "off";
|
|
1172
|
+
}
|
|
1173
|
+
async function ensureGrammar(language, options = {}) {
|
|
1174
|
+
const pack2 = grammarManifest().packs[language];
|
|
1175
|
+
if (!pack2) return null;
|
|
1176
|
+
const root = grammarsCacheRoot(options.cacheRoot);
|
|
1177
|
+
const wasm = grammarCachePath(root, language, pack2.wasm.sha256);
|
|
1178
|
+
const key = `${wasm} ${grammarsBaseUrl(options.baseUrl) ?? ""}`;
|
|
1179
|
+
const existing = inFlight.get(key);
|
|
1180
|
+
if (existing) return existing;
|
|
1181
|
+
const pending = (async () => {
|
|
1182
|
+
const grammar = await ensurePart(
|
|
1183
|
+
wasm,
|
|
1184
|
+
`tree-sitter-${language}`,
|
|
1185
|
+
pack2.wasm,
|
|
1186
|
+
options
|
|
1187
|
+
);
|
|
1188
|
+
if (grammar !== true)
|
|
1189
|
+
return miss(language, `grammar tree-sitter-${language}`, grammar);
|
|
1190
|
+
const parts = [];
|
|
1191
|
+
const total = pack2.tags.length;
|
|
1192
|
+
for (const [at, part] of pack2.tags.entries()) {
|
|
1193
|
+
const name = `${language} tags${total > 1 ? ` part ${at + 1}/${total}` : ""}`;
|
|
1194
|
+
const path = grammarCachePath(root, language, part.sha256, "scm");
|
|
1195
|
+
const held = await ensurePart(path, name, part, options);
|
|
1196
|
+
if (held !== true) return miss(language, name, held);
|
|
1197
|
+
parts.push(`; ${part.url}
|
|
1198
|
+
${lf(await (0, import_promises4.readFile)(path, "utf8"))}`);
|
|
1199
|
+
}
|
|
1200
|
+
missing.delete(language);
|
|
1201
|
+
return { wasm, query: total ? parts.join("\n") : void 0 };
|
|
1202
|
+
})();
|
|
1203
|
+
inFlight.set(key, pending);
|
|
1204
|
+
const result = await pending;
|
|
1205
|
+
if (result === null) inFlight.delete(key);
|
|
1206
|
+
return result;
|
|
1207
|
+
}
|
|
1208
|
+
async function ensurePart(path, name, entry, options) {
|
|
1209
|
+
if (await verifyCached(path, entry)) return true;
|
|
1210
|
+
if (options.offline === true || grammarsDownloadDisabled()) return {};
|
|
1211
|
+
const download = await downloadPart(
|
|
1212
|
+
grammarUrl(entry.url, options.baseUrl),
|
|
1213
|
+
name,
|
|
1214
|
+
entry,
|
|
1215
|
+
options
|
|
1216
|
+
);
|
|
1217
|
+
if ("cause" in download) return { cause: download.cause };
|
|
1218
|
+
await writeCached(path, download.bytes).catch(() => null);
|
|
1219
|
+
return true;
|
|
1220
|
+
}
|
|
1221
|
+
function miss(language, subject, failure) {
|
|
1222
|
+
missing.set(language, { subject, ...failure });
|
|
1223
|
+
return null;
|
|
1224
|
+
}
|
|
1225
|
+
function lf(body) {
|
|
1226
|
+
return body.replace(/\r\n/g, "\n");
|
|
1227
|
+
}
|
|
1228
|
+
function noteUncompilableQuery(language, cause) {
|
|
1229
|
+
uncompilable.set(language, cause);
|
|
1230
|
+
}
|
|
1231
|
+
function noteRejectedGrammar(language, cause) {
|
|
1232
|
+
rejected.set(language, cause);
|
|
1233
|
+
}
|
|
1234
|
+
function grammarHints() {
|
|
1235
|
+
const manifest = grammarManifest();
|
|
1236
|
+
const packs = manifest.packs;
|
|
1237
|
+
const lines = /* @__PURE__ */ new Map();
|
|
1238
|
+
for (const [language, { subject, cause }] of missing)
|
|
1239
|
+
lines.set(
|
|
1240
|
+
language,
|
|
1241
|
+
`${subject} not cached${cause ? ` (${cause})` : ""}; run online once, or set STRAUSS_KB_GRAMMARS_DIR`
|
|
1242
|
+
);
|
|
1243
|
+
for (const [language, cause] of rejected)
|
|
1244
|
+
lines.set(
|
|
1245
|
+
language,
|
|
1246
|
+
`${packs[language]?.package ?? `tree-sitter-${language}`} rejected by web-tree-sitter ${manifest.webTreeSitter}${cause ? `: ${cause}` : ""}; re-pin with pnpm grammars pin ${language}`
|
|
1247
|
+
);
|
|
1248
|
+
for (const [language, cause] of uncompilable)
|
|
1249
|
+
lines.set(
|
|
1250
|
+
language,
|
|
1251
|
+
`tags query for ${language} does not compile against ${packs[language]?.package ?? `tree-sitter-${language}`}: ${cause}; re-pin with pnpm grammars pin ${language}`
|
|
1252
|
+
);
|
|
1253
|
+
return [...lines].sort(([a], [b]) => a.localeCompare(b)).map(([, line]) => line);
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// src/tree-sitter-resolver/languages.ts
|
|
1257
|
+
var table;
|
|
1258
|
+
function extensionTable() {
|
|
1259
|
+
const manifest = grammarManifest();
|
|
1260
|
+
if (table?.of !== manifest)
|
|
1261
|
+
table = {
|
|
1262
|
+
of: manifest,
|
|
1263
|
+
extensions: Object.fromEntries(
|
|
1264
|
+
Object.entries(manifest.packs).flatMap(
|
|
1265
|
+
([language, pack2]) => pack2.extensions.map((extension) => [extension, language])
|
|
1266
|
+
)
|
|
1267
|
+
)
|
|
1268
|
+
};
|
|
1269
|
+
return table.extensions;
|
|
1270
|
+
}
|
|
1271
|
+
function hasQuery(language) {
|
|
1272
|
+
return (grammarManifest().packs[language]?.tags.length ?? 0) > 0;
|
|
1273
|
+
}
|
|
1274
|
+
function languageForFile(file) {
|
|
1275
|
+
const language = extensionTable()[(0, import_node_path5.extname)(file).toLowerCase()];
|
|
1276
|
+
return language && hasQuery(language) ? language : void 0;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// src/tree-sitter-resolver/resolver.ts
|
|
1280
|
+
var import_node_crypto2 = require("crypto");
|
|
1281
|
+
var import_web_tree_sitter = require("web-tree-sitter");
|
|
1282
|
+
|
|
1283
|
+
// src/tree-sitter-resolver/definitions.ts
|
|
1284
|
+
var SCOPE_ONLY = "reference.implementation";
|
|
1285
|
+
function index(tree, query) {
|
|
1286
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1287
|
+
for (const match of query.matches(tree.rootNode)) {
|
|
1288
|
+
const nameNode = match.captures.find((capture) => capture.name === "name");
|
|
1289
|
+
const defNode = match.captures.find(
|
|
1290
|
+
(capture) => capture.name.startsWith("definition.") || capture.name === SCOPE_ONLY
|
|
1291
|
+
);
|
|
1292
|
+
if (!nameNode || !defNode) continue;
|
|
1293
|
+
const candidate = {
|
|
1294
|
+
node: defNode.node,
|
|
1295
|
+
name: nameNode.node.text,
|
|
1296
|
+
target: defNode.name !== SCOPE_ONLY
|
|
1297
|
+
};
|
|
1298
|
+
const existing = byName.get(nameNode.node.id);
|
|
1299
|
+
if (existing && width(existing.node) <= width(candidate.node)) continue;
|
|
1300
|
+
byName.set(nameNode.node.id, candidate);
|
|
1301
|
+
}
|
|
1302
|
+
const definitions = [...byName.values()];
|
|
1303
|
+
return {
|
|
1304
|
+
tree,
|
|
1305
|
+
byNodeId: new Map(
|
|
1306
|
+
definitions.map((definition) => [definition.node.id, definition])
|
|
1307
|
+
),
|
|
1308
|
+
definitions
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
function select(parsed, wanted) {
|
|
1312
|
+
const matches3 = parsed.definitions.filter(
|
|
1313
|
+
(definition) => definition.target && endsWith(chainOf(definition, parsed.byNodeId), wanted)
|
|
1314
|
+
);
|
|
1315
|
+
if (matches3.length < 2) return matches3;
|
|
1316
|
+
const bodied = matches3.filter(
|
|
1317
|
+
(definition) => definition.node.childForFieldName("body") !== null
|
|
1318
|
+
);
|
|
1319
|
+
return bodied.length === 1 ? bodied : matches3;
|
|
1320
|
+
}
|
|
1321
|
+
function chainOf(definition, byNodeId) {
|
|
1322
|
+
const chain = [definition.name];
|
|
1323
|
+
const receiver = definition.node.childForFieldName("receiver");
|
|
1324
|
+
const type = receiver && typeNameIn(receiver);
|
|
1325
|
+
if (type) chain.unshift(type);
|
|
1326
|
+
for (let node = definition.node.parent; node; node = node.parent) {
|
|
1327
|
+
const enclosing = byNodeId.get(node.id);
|
|
1328
|
+
if (enclosing && enclosing.node !== definition.node)
|
|
1329
|
+
chain.unshift(enclosing.name);
|
|
1330
|
+
}
|
|
1331
|
+
return chain;
|
|
1332
|
+
}
|
|
1333
|
+
function typeNameIn(receiver) {
|
|
1334
|
+
const stack = [receiver];
|
|
1335
|
+
while (stack.length) {
|
|
1336
|
+
const node = stack.pop();
|
|
1337
|
+
if (node.type === "type_identifier") return node.text;
|
|
1338
|
+
for (let at = 0; at < node.childCount; at++) {
|
|
1339
|
+
const child = node.child(at);
|
|
1340
|
+
if (child) stack.push(child);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
return void 0;
|
|
1344
|
+
}
|
|
1345
|
+
function endsWith(chain, wanted) {
|
|
1346
|
+
if (wanted.length > chain.length) return false;
|
|
1347
|
+
const offset = chain.length - wanted.length;
|
|
1348
|
+
return wanted.every((segment, at) => chain[offset + at] === segment);
|
|
1349
|
+
}
|
|
1350
|
+
function width(node) {
|
|
1351
|
+
return node.endIndex - node.startIndex;
|
|
1352
|
+
}
|
|
1353
|
+
function spanOf(definition, source) {
|
|
1354
|
+
let start = definition.node;
|
|
1355
|
+
let end = definition.node;
|
|
1356
|
+
for (let sibling = start.previousSibling; sibling?.type === "decorator"; sibling = sibling.previousSibling) {
|
|
1357
|
+
start = sibling;
|
|
1358
|
+
}
|
|
1359
|
+
const parent = end.parent;
|
|
1360
|
+
if (parent?.type === "export_statement" && parent.childForFieldName("declaration")?.id === end.id) {
|
|
1361
|
+
start = parent;
|
|
1362
|
+
end = parent;
|
|
1363
|
+
}
|
|
1364
|
+
const lines = source.split("\n");
|
|
1365
|
+
const startLine = start.startPosition.row;
|
|
1366
|
+
const endLine = end.endPosition.column === 0 && end.endPosition.row > startLine ? end.endPosition.row - 1 : end.endPosition.row;
|
|
1367
|
+
return {
|
|
1368
|
+
text: lines.slice(startLine, endLine + 1).join("\n"),
|
|
1369
|
+
startLine: startLine + 1,
|
|
1370
|
+
endLine: endLine + 1
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// src/tree-sitter-resolver/resolver.ts
|
|
1375
|
+
var TREE_CACHE_LIMIT = 32;
|
|
1376
|
+
var TreeSitterResolver = class {
|
|
1377
|
+
name = "tree-sitter";
|
|
1378
|
+
grammars;
|
|
1379
|
+
loaded = /* @__PURE__ */ new Map();
|
|
1380
|
+
trees = /* @__PURE__ */ new Map();
|
|
1381
|
+
parser;
|
|
1382
|
+
initialized = false;
|
|
1383
|
+
/** Cache effectiveness, for tests and for the latency numbers. */
|
|
1384
|
+
stats = { parses: 0, cacheHits: 0 };
|
|
1385
|
+
constructor(options = {}) {
|
|
1386
|
+
this.grammars = options;
|
|
1387
|
+
}
|
|
1388
|
+
/**
|
|
1389
|
+
* Loads the grammars these files need, once per language per process,
|
|
1390
|
+
* downloading each one on first use.
|
|
1391
|
+
*
|
|
1392
|
+
* A grammar that will not load is remembered as unavailable rather than
|
|
1393
|
+
* retried per anchor, and never throws: an unobtainable WASM is a finding.
|
|
1394
|
+
*/
|
|
1395
|
+
async prepare(files) {
|
|
1396
|
+
const wanted = /* @__PURE__ */ new Set();
|
|
1397
|
+
for (const file of files) {
|
|
1398
|
+
const language = languageForFile(file);
|
|
1399
|
+
if (language && !this.loaded.has(language)) wanted.add(language);
|
|
1400
|
+
}
|
|
1401
|
+
if (!wanted.size) return;
|
|
1402
|
+
if (!this.initialized) {
|
|
1403
|
+
try {
|
|
1404
|
+
await import_web_tree_sitter.Parser.init();
|
|
1405
|
+
this.parser = new import_web_tree_sitter.Parser();
|
|
1406
|
+
this.initialized = true;
|
|
1407
|
+
} catch {
|
|
1408
|
+
for (const language of wanted) this.loaded.set(language, null);
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
const languages = [...wanted];
|
|
1413
|
+
const loaded = await mapLimit(
|
|
1414
|
+
languages,
|
|
1415
|
+
Math.min(DEFAULT_IO_CONCURRENCY, languages.length),
|
|
1416
|
+
(language) => this.load(language)
|
|
1417
|
+
);
|
|
1418
|
+
languages.forEach(
|
|
1419
|
+
(language, at) => this.loaded.set(language, loaded[at] ?? null)
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* An unobtainable grammar, one this runtime refuses, and a query that will
|
|
1424
|
+
* not compile are three faults with three repairs; all are reported through
|
|
1425
|
+
* the grammars module so every hint has one home.
|
|
1426
|
+
*/
|
|
1427
|
+
async load(language) {
|
|
1428
|
+
let pack2;
|
|
1429
|
+
try {
|
|
1430
|
+
pack2 = await ensureGrammar(language, this.grammars);
|
|
1431
|
+
} catch {
|
|
1432
|
+
return null;
|
|
1433
|
+
}
|
|
1434
|
+
if (!pack2?.query) return null;
|
|
1435
|
+
let grammar;
|
|
1436
|
+
try {
|
|
1437
|
+
grammar = await import_web_tree_sitter.Language.load(pack2.wasm);
|
|
1438
|
+
} catch (error) {
|
|
1439
|
+
noteRejectedGrammar(language, why(error));
|
|
1440
|
+
return null;
|
|
1441
|
+
}
|
|
1442
|
+
try {
|
|
1443
|
+
return { language: grammar, query: new import_web_tree_sitter.Query(grammar, pack2.query) };
|
|
1444
|
+
} catch (error) {
|
|
1445
|
+
noteUncompilableQuery(language, why(error));
|
|
1446
|
+
return null;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* Abstains on an extension with no grammar so the regex resolver gets a
|
|
1451
|
+
* turn; reports `resolver-unavailable` when the grammar exists in principle
|
|
1452
|
+
* but could not be loaded, because falling back there would silently trade a
|
|
1453
|
+
* precise span for a guessed one.
|
|
1454
|
+
*/
|
|
1455
|
+
attempt(source, symbol, file) {
|
|
1456
|
+
const language = file ? languageForFile(file) : void 0;
|
|
1457
|
+
if (!language) return { kind: "abstain" };
|
|
1458
|
+
if (!this.loaded.has(language)) return { kind: "abstain" };
|
|
1459
|
+
const loaded = this.loaded.get(language);
|
|
1460
|
+
if (!loaded) return { kind: "unresolved", reason: "resolver-unavailable" };
|
|
1461
|
+
const parsed = this.parse(language, loaded, source);
|
|
1462
|
+
if (!parsed) return { kind: "unresolved", reason: "resolver-unavailable" };
|
|
1463
|
+
const wanted = symbol.split(".").filter(Boolean);
|
|
1464
|
+
if (!wanted.length)
|
|
1465
|
+
return { kind: "unresolved", reason: "symbol-not-found" };
|
|
1466
|
+
const matches3 = select(parsed, wanted);
|
|
1467
|
+
if (!matches3.length)
|
|
1468
|
+
return { kind: "unresolved", reason: "symbol-not-found" };
|
|
1469
|
+
if (matches3.length > 1)
|
|
1470
|
+
return { kind: "unresolved", reason: "symbol-ambiguous" };
|
|
1471
|
+
return { kind: "resolved", span: spanOf(matches3[0], source) };
|
|
1472
|
+
}
|
|
1473
|
+
resolve(source, symbol, file) {
|
|
1474
|
+
const attempt = this.attempt(source, symbol, file);
|
|
1475
|
+
return attempt.kind === "resolved" ? attempt.span : null;
|
|
1476
|
+
}
|
|
1477
|
+
/** Parsed trees are keyed by content hash, so an unchanged file parses once. */
|
|
1478
|
+
parse(language, loaded, source) {
|
|
1479
|
+
const key = `${language}:${(0, import_node_crypto2.createHash)("sha256").update(source).digest("hex")}`;
|
|
1480
|
+
const cached2 = this.trees.get(key);
|
|
1481
|
+
if (cached2) {
|
|
1482
|
+
this.stats.cacheHits += 1;
|
|
1483
|
+
return cached2;
|
|
1484
|
+
}
|
|
1485
|
+
const parser = this.parser;
|
|
1486
|
+
if (!parser) return null;
|
|
1487
|
+
let parsed;
|
|
1488
|
+
try {
|
|
1489
|
+
parser.setLanguage(loaded.language);
|
|
1490
|
+
const tree = parser.parse(source);
|
|
1491
|
+
if (!tree) return null;
|
|
1492
|
+
parsed = index(tree, loaded.query);
|
|
1493
|
+
} catch {
|
|
1494
|
+
return null;
|
|
1495
|
+
}
|
|
1496
|
+
this.stats.parses += 1;
|
|
1497
|
+
if (this.trees.size >= TREE_CACHE_LIMIT) {
|
|
1498
|
+
const oldest = this.trees.keys().next();
|
|
1499
|
+
if (!oldest.done) {
|
|
1500
|
+
this.trees.get(oldest.value)?.tree.delete();
|
|
1501
|
+
this.trees.delete(oldest.value);
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
this.trees.set(key, parsed);
|
|
1505
|
+
return parsed;
|
|
1506
|
+
}
|
|
1507
|
+
/** Drops cached trees. Grammars stay loaded — they are immutable. */
|
|
1508
|
+
reset() {
|
|
1509
|
+
for (const parsed of this.trees.values()) parsed.tree.delete();
|
|
1510
|
+
this.trees.clear();
|
|
1511
|
+
this.stats.parses = 0;
|
|
1512
|
+
this.stats.cacheHits = 0;
|
|
1513
|
+
}
|
|
1514
|
+
};
|
|
1515
|
+
function why(error) {
|
|
1516
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
1517
|
+
return text || "no reason given";
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
// src/anchor-resolver/resolver.ts
|
|
997
1521
|
var PARENT_SCOPE_LINES = 50;
|
|
998
1522
|
var CLEAN_STATE = { blockComment: false, template: false };
|
|
999
1523
|
function stripLine(line, state) {
|
|
1000
1524
|
let out = "";
|
|
1001
|
-
let
|
|
1525
|
+
let index2 = 0;
|
|
1002
1526
|
let { blockComment, template } = state;
|
|
1003
|
-
while (
|
|
1004
|
-
const char = line[
|
|
1005
|
-
const next = line[
|
|
1527
|
+
while (index2 < line.length) {
|
|
1528
|
+
const char = line[index2];
|
|
1529
|
+
const next = line[index2 + 1];
|
|
1006
1530
|
if (blockComment) {
|
|
1007
1531
|
if (char === "*" && next === "/") {
|
|
1008
1532
|
blockComment = false;
|
|
1009
|
-
|
|
1533
|
+
index2 += 2;
|
|
1010
1534
|
continue;
|
|
1011
1535
|
}
|
|
1012
|
-
|
|
1536
|
+
index2 += 1;
|
|
1013
1537
|
continue;
|
|
1014
1538
|
}
|
|
1015
1539
|
if (template) {
|
|
1016
1540
|
if (char === "\\") {
|
|
1017
|
-
|
|
1541
|
+
index2 += 2;
|
|
1018
1542
|
continue;
|
|
1019
1543
|
}
|
|
1020
1544
|
if (char === "`") template = false;
|
|
1021
|
-
|
|
1545
|
+
index2 += 1;
|
|
1022
1546
|
continue;
|
|
1023
1547
|
}
|
|
1024
1548
|
if (char === "/" && next === "*") {
|
|
1025
1549
|
blockComment = true;
|
|
1026
|
-
|
|
1550
|
+
index2 += 2;
|
|
1027
1551
|
continue;
|
|
1028
1552
|
}
|
|
1029
1553
|
if (char === "/" && next === "/") break;
|
|
1030
1554
|
if (char === "`") {
|
|
1031
1555
|
template = true;
|
|
1032
|
-
|
|
1556
|
+
index2 += 1;
|
|
1033
1557
|
continue;
|
|
1034
1558
|
}
|
|
1035
1559
|
if (char === "'" || char === '"') {
|
|
1036
1560
|
const quote = char;
|
|
1037
|
-
|
|
1038
|
-
while (
|
|
1039
|
-
if (line[
|
|
1040
|
-
|
|
1561
|
+
index2 += 1;
|
|
1562
|
+
while (index2 < line.length) {
|
|
1563
|
+
if (line[index2] === "\\") {
|
|
1564
|
+
index2 += 2;
|
|
1041
1565
|
continue;
|
|
1042
1566
|
}
|
|
1043
|
-
if (line[
|
|
1044
|
-
|
|
1567
|
+
if (line[index2] === quote) {
|
|
1568
|
+
index2 += 1;
|
|
1045
1569
|
break;
|
|
1046
1570
|
}
|
|
1047
|
-
|
|
1571
|
+
index2 += 1;
|
|
1048
1572
|
}
|
|
1049
1573
|
continue;
|
|
1050
1574
|
}
|
|
1051
1575
|
out += char;
|
|
1052
|
-
|
|
1576
|
+
index2 += 1;
|
|
1053
1577
|
}
|
|
1054
1578
|
return { code: out, state: { blockComment, template } };
|
|
1055
1579
|
}
|
|
@@ -1064,8 +1588,8 @@ function captureBraceBlock(lines, matchLine) {
|
|
|
1064
1588
|
let depth = 0;
|
|
1065
1589
|
let opened = false;
|
|
1066
1590
|
let state = CLEAN_STATE;
|
|
1067
|
-
for (let
|
|
1068
|
-
const stripped = stripLine(lines[
|
|
1591
|
+
for (let index2 = matchLine; index2 < lines.length; index2++) {
|
|
1592
|
+
const stripped = stripLine(lines[index2] ?? "", state);
|
|
1069
1593
|
state = stripped.state;
|
|
1070
1594
|
for (const char of stripped.code) {
|
|
1071
1595
|
if (char === "{") {
|
|
@@ -1074,10 +1598,10 @@ function captureBraceBlock(lines, matchLine) {
|
|
|
1074
1598
|
} else if (char === "}") {
|
|
1075
1599
|
depth = Math.max(0, depth - 1);
|
|
1076
1600
|
} else if (char === ";" && !opened) {
|
|
1077
|
-
return span(lines, matchLine,
|
|
1601
|
+
return span(lines, matchLine, index2);
|
|
1078
1602
|
}
|
|
1079
1603
|
}
|
|
1080
|
-
if (opened && depth === 0) return span(lines, matchLine,
|
|
1604
|
+
if (opened && depth === 0) return span(lines, matchLine, index2);
|
|
1081
1605
|
}
|
|
1082
1606
|
return null;
|
|
1083
1607
|
}
|
|
@@ -1086,22 +1610,22 @@ function captureIndentedBlock(lines, matchLine) {
|
|
|
1086
1610
|
const header = lines[matchLine] ?? "";
|
|
1087
1611
|
const indent = header.length - header.trimStart().length;
|
|
1088
1612
|
let headerEnd = -1;
|
|
1089
|
-
for (let
|
|
1090
|
-
const code = stripLine(lines[
|
|
1613
|
+
for (let index2 = matchLine; index2 < lines.length && index2 <= matchLine + 20; index2++) {
|
|
1614
|
+
const code = stripLine(lines[index2] ?? "", CLEAN_STATE).code.trimEnd();
|
|
1091
1615
|
if (code.endsWith(":")) {
|
|
1092
|
-
headerEnd =
|
|
1616
|
+
headerEnd = index2;
|
|
1093
1617
|
break;
|
|
1094
1618
|
}
|
|
1095
|
-
if (code.includes(":")) return span(lines, matchLine,
|
|
1619
|
+
if (code.includes(":")) return span(lines, matchLine, index2);
|
|
1096
1620
|
}
|
|
1097
1621
|
if (headerEnd === -1) return null;
|
|
1098
1622
|
let end = headerEnd;
|
|
1099
|
-
for (let
|
|
1100
|
-
const line = lines[
|
|
1623
|
+
for (let index2 = headerEnd + 1; index2 < lines.length; index2++) {
|
|
1624
|
+
const line = lines[index2] ?? "";
|
|
1101
1625
|
if (line.trim() === "") continue;
|
|
1102
1626
|
const lineIndent = line.length - line.trimStart().length;
|
|
1103
1627
|
if (lineIndent <= indent) break;
|
|
1104
|
-
end =
|
|
1628
|
+
end = index2;
|
|
1105
1629
|
}
|
|
1106
1630
|
return end === headerEnd ? null : span(lines, matchLine, end);
|
|
1107
1631
|
}
|
|
@@ -1125,11 +1649,11 @@ var regexResolver = {
|
|
|
1125
1649
|
const lines = source.split("\n");
|
|
1126
1650
|
for (const tier of TIERS) {
|
|
1127
1651
|
const pattern = tier(escaped);
|
|
1128
|
-
let candidates = lines.map((line,
|
|
1652
|
+
let candidates = lines.map((line, index2) => ({ line, index: index2 })).filter((entry) => pattern.test(entry.line)).map((entry) => entry.index);
|
|
1129
1653
|
if (!candidates.length) continue;
|
|
1130
1654
|
if (parentPattern && candidates.length > 1) {
|
|
1131
1655
|
const distances = candidates.map(
|
|
1132
|
-
(
|
|
1656
|
+
(index2) => distanceToParent(lines, index2, parentPattern)
|
|
1133
1657
|
);
|
|
1134
1658
|
const nearest = Math.min(...distances);
|
|
1135
1659
|
if (Number.isFinite(nearest)) {
|
|
@@ -1146,34 +1670,75 @@ var regexResolver = {
|
|
|
1146
1670
|
function escapeRegExp(value) {
|
|
1147
1671
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1148
1672
|
}
|
|
1149
|
-
function distanceToParent(lines,
|
|
1150
|
-
const floor = Math.max(0,
|
|
1151
|
-
for (let at =
|
|
1152
|
-
if (parent.test(lines[at] ?? "")) return
|
|
1673
|
+
function distanceToParent(lines, index2, parent) {
|
|
1674
|
+
const floor = Math.max(0, index2 - PARENT_SCOPE_LINES);
|
|
1675
|
+
for (let at = index2; at >= floor; at--) {
|
|
1676
|
+
if (parent.test(lines[at] ?? "")) return index2 - at;
|
|
1153
1677
|
}
|
|
1154
1678
|
return Number.POSITIVE_INFINITY;
|
|
1155
1679
|
}
|
|
1156
1680
|
function hashAnchorText(text) {
|
|
1157
|
-
return `sha256:${(0,
|
|
1681
|
+
return `sha256:${(0, import_node_crypto3.createHash)("sha256").update(text.replace(/\r\n/g, "\n")).digest("hex")}`;
|
|
1158
1682
|
}
|
|
1159
|
-
function
|
|
1683
|
+
function resolveAnchorSpan(source, anchor, resolvers = [regexResolver]) {
|
|
1160
1684
|
const normalized = source.replace(/\r\n/g, "\n");
|
|
1161
1685
|
if (!anchor.symbol) {
|
|
1162
1686
|
const lines = normalized.split("\n");
|
|
1163
1687
|
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
1164
1688
|
return {
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1689
|
+
ok: true,
|
|
1690
|
+
span: {
|
|
1691
|
+
text: normalized,
|
|
1692
|
+
startLine: 1,
|
|
1693
|
+
endLine: Math.max(1, lines.length)
|
|
1694
|
+
}
|
|
1695
|
+
};
|
|
1696
|
+
}
|
|
1697
|
+
for (const resolver of resolvers) {
|
|
1698
|
+
const attempt = resolver.attempt ? resolver.attempt(normalized, anchor.symbol, anchor.file) : fromResolve(resolver, normalized, anchor.symbol, anchor.file);
|
|
1699
|
+
if (attempt.kind === "abstain") continue;
|
|
1700
|
+
if (attempt.kind === "unresolved") {
|
|
1701
|
+
if (attempt.reason === "symbol-not-found") continue;
|
|
1702
|
+
return { ok: false, reason: attempt.reason };
|
|
1703
|
+
}
|
|
1704
|
+
return {
|
|
1705
|
+
ok: true,
|
|
1706
|
+
span: attempt.span,
|
|
1707
|
+
...isResolverName(resolver.name) ? { resolver: resolver.name } : {}
|
|
1168
1708
|
};
|
|
1169
1709
|
}
|
|
1170
|
-
return
|
|
1710
|
+
return { ok: false, reason: "symbol-not-found" };
|
|
1711
|
+
}
|
|
1712
|
+
function fromResolve(resolver, source, symbol, file) {
|
|
1713
|
+
const span2 = resolver.resolve(source, symbol, file);
|
|
1714
|
+
return span2 ? { kind: "resolved", span: span2 } : { kind: "unresolved", reason: "symbol-not-found" };
|
|
1715
|
+
}
|
|
1716
|
+
function isResolverName(name) {
|
|
1717
|
+
return name === "tree-sitter" || name === "regex";
|
|
1718
|
+
}
|
|
1719
|
+
async function prepareResolvers(resolvers, files) {
|
|
1720
|
+
for (const resolver of resolvers) await resolver.prepare?.(files);
|
|
1721
|
+
}
|
|
1722
|
+
function defaultAnchorResolvers(grammars = {}) {
|
|
1723
|
+
return [new TreeSitterResolver(grammars), regexResolver];
|
|
1724
|
+
}
|
|
1725
|
+
function resolverChanged(source, anchor, produced) {
|
|
1726
|
+
const previous = anchor.resolver ?? "regex";
|
|
1727
|
+
if (!produced || !anchor.symbol || previous === produced) return false;
|
|
1728
|
+
if (previous !== "regex") return false;
|
|
1729
|
+
const before = regexResolver.resolve(
|
|
1730
|
+
source.replace(/\r\n/g, "\n"),
|
|
1731
|
+
anchor.symbol
|
|
1732
|
+
);
|
|
1733
|
+
return before !== null && hashAnchorText(before.text) === anchor.hash;
|
|
1171
1734
|
}
|
|
1172
1735
|
|
|
1173
1736
|
// src/anchor-resolver/drift.ts
|
|
1174
1737
|
async function detectAnchorDrift(records, options = {}) {
|
|
1175
1738
|
const repoRoot = options.repoRoot ?? process.cwd();
|
|
1176
|
-
const
|
|
1739
|
+
const resolvers = options.resolvers ?? (options.resolver ? [options.resolver] : defaultAnchorResolvers({
|
|
1740
|
+
offline: options.remote?.offline === true
|
|
1741
|
+
}));
|
|
1177
1742
|
const origin = new LazyOrigin(repoRoot);
|
|
1178
1743
|
const planned = /* @__PURE__ */ new Map();
|
|
1179
1744
|
let declaresRepo = false;
|
|
@@ -1211,12 +1776,16 @@ async function detectAnchorDrift(records, options = {}) {
|
|
|
1211
1776
|
),
|
|
1212
1777
|
(options.readRemote ?? readRemoteAnchors)(wants, options.remote ?? {})
|
|
1213
1778
|
]);
|
|
1779
|
+
await prepareResolvers(resolvers, [
|
|
1780
|
+
...files,
|
|
1781
|
+
...wants.map((want) => want.file)
|
|
1782
|
+
]);
|
|
1214
1783
|
const drift = /* @__PURE__ */ new Map();
|
|
1215
1784
|
for (const record of records) {
|
|
1216
1785
|
const entries = [];
|
|
1217
1786
|
for (const { anchor, foreign } of planned.get(record.conceptId) ?? []) {
|
|
1218
1787
|
entries.push(
|
|
1219
|
-
foreign ? remoteEntry(anchor, remote,
|
|
1788
|
+
foreign ? remoteEntry(anchor, remote, resolvers) : localEntry(anchor, reads.get(anchor.file), resolvers)
|
|
1220
1789
|
);
|
|
1221
1790
|
}
|
|
1222
1791
|
if (entries.length) drift.set(record.conceptId, entries);
|
|
@@ -1245,12 +1814,22 @@ function unresolved(anchor, reason, repo) {
|
|
|
1245
1814
|
...repo ? { repo } : {}
|
|
1246
1815
|
};
|
|
1247
1816
|
}
|
|
1248
|
-
function hashIn(source, anchor,
|
|
1249
|
-
const
|
|
1250
|
-
if (!
|
|
1817
|
+
function hashIn(source, anchor, resolvers) {
|
|
1818
|
+
const outcome = resolveAnchorSpan(source, anchor, resolvers);
|
|
1819
|
+
if (!outcome.ok) return { ok: false, reason: outcome.reason };
|
|
1820
|
+
return {
|
|
1821
|
+
ok: true,
|
|
1822
|
+
current: {
|
|
1823
|
+
hash: hashAnchorText(outcome.span.text),
|
|
1824
|
+
lines: outcome.span.endLine - outcome.span.startLine + 1,
|
|
1825
|
+
...outcome.resolver ? { resolver: outcome.resolver } : {}
|
|
1826
|
+
}
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1829
|
+
function resolverExtras(source, anchor, current) {
|
|
1251
1830
|
return {
|
|
1252
|
-
|
|
1253
|
-
|
|
1831
|
+
...current.resolver ? { resolver: current.resolver } : {},
|
|
1832
|
+
...current.hash !== anchor.hash && resolverChanged(source, anchor, current.resolver) ? { reason: "resolver-changed" } : {}
|
|
1254
1833
|
};
|
|
1255
1834
|
}
|
|
1256
1835
|
function compared(anchor, current, extra = {}) {
|
|
@@ -1262,30 +1841,48 @@ function compared(anchor, current, extra = {}) {
|
|
|
1262
1841
|
...extra
|
|
1263
1842
|
};
|
|
1264
1843
|
}
|
|
1265
|
-
function localEntry(anchor, read,
|
|
1844
|
+
function localEntry(anchor, read, resolvers) {
|
|
1266
1845
|
if (!read.ok) return unresolved(anchor, read.reason);
|
|
1267
|
-
const
|
|
1268
|
-
|
|
1846
|
+
const found = hashIn(read.source, anchor, resolvers);
|
|
1847
|
+
if (!found.ok) return unresolved(anchor, found.reason);
|
|
1848
|
+
return compared(
|
|
1849
|
+
anchor,
|
|
1850
|
+
found.current,
|
|
1851
|
+
resolverExtras(read.source, anchor, found.current)
|
|
1852
|
+
);
|
|
1269
1853
|
}
|
|
1270
|
-
function remoteEntry(anchor, remote,
|
|
1854
|
+
function remoteEntry(anchor, remote, resolvers) {
|
|
1271
1855
|
const repo = anchor.repo;
|
|
1272
1856
|
const key = normalizeRepoUrl(repo);
|
|
1273
1857
|
const atDefault = remote.get(wantKey(key, void 0, anchor.file));
|
|
1274
1858
|
const primary = anchor.ref ? remote.get(wantKey(key, anchor.ref, anchor.file)) : atDefault;
|
|
1275
1859
|
if (!primary) return unresolved(anchor, "remote-unreachable", repo);
|
|
1276
1860
|
if (!primary.ok) return unresolved(anchor, primary.reason, repo);
|
|
1277
|
-
const
|
|
1278
|
-
if (!
|
|
1279
|
-
|
|
1861
|
+
const found = hashIn(primary.source, anchor, resolvers);
|
|
1862
|
+
if (!found.ok) return unresolved(anchor, found.reason, repo);
|
|
1863
|
+
const current = found.current;
|
|
1864
|
+
const extras = resolverExtras(primary.source, anchor, current);
|
|
1865
|
+
if (!anchor.ref) return compared(anchor, current, { repo, ...extras });
|
|
1280
1866
|
if (current.hash !== anchor.hash) {
|
|
1281
|
-
return compared(anchor, current, {
|
|
1867
|
+
return compared(anchor, current, {
|
|
1868
|
+
repo,
|
|
1869
|
+
...extras,
|
|
1870
|
+
remoteState: "drifted-from-ref"
|
|
1871
|
+
});
|
|
1282
1872
|
}
|
|
1283
|
-
const head = atDefault?.ok ? hashIn(atDefault.source, anchor,
|
|
1284
|
-
return head && head.hash !== anchor.hash ? {
|
|
1285
|
-
...compared(anchor, head, {
|
|
1873
|
+
const head = atDefault?.ok ? hashIn(atDefault.source, anchor, resolvers) : null;
|
|
1874
|
+
return head?.ok && head.current.hash !== anchor.hash ? {
|
|
1875
|
+
...compared(anchor, head.current, {
|
|
1876
|
+
repo,
|
|
1877
|
+
...head.current.resolver ? { resolver: head.current.resolver } : {}
|
|
1878
|
+
}),
|
|
1286
1879
|
state: "drifted",
|
|
1287
1880
|
remoteState: "drifted-on-default"
|
|
1288
|
-
} : compared(anchor, current, {
|
|
1881
|
+
} : compared(anchor, current, {
|
|
1882
|
+
repo,
|
|
1883
|
+
...extras,
|
|
1884
|
+
remoteState: "matches-ref"
|
|
1885
|
+
});
|
|
1289
1886
|
}
|
|
1290
1887
|
|
|
1291
1888
|
// src/errors.ts
|
|
@@ -1473,9 +2070,9 @@ var KbStampDigestBaselineError = class extends BaseError {
|
|
|
1473
2070
|
// src/kb-pins/budgets.ts
|
|
1474
2071
|
function asBudgets(value) {
|
|
1475
2072
|
if (value === null || typeof value !== "object") return {};
|
|
1476
|
-
const
|
|
2073
|
+
const table2 = value;
|
|
1477
2074
|
const pick = (key, min) => {
|
|
1478
|
-
const raw =
|
|
2075
|
+
const raw = table2[key];
|
|
1479
2076
|
return typeof raw === "number" && Number.isInteger(raw) && raw >= min ? raw : void 0;
|
|
1480
2077
|
};
|
|
1481
2078
|
const budgetTokens = pick("budgetTokens", 1);
|
|
@@ -1486,9 +2083,9 @@ function asBudgets(value) {
|
|
|
1486
2083
|
};
|
|
1487
2084
|
}
|
|
1488
2085
|
function contextProfileBudgets(manifest, profile) {
|
|
1489
|
-
const
|
|
1490
|
-
if (
|
|
1491
|
-
const entries =
|
|
2086
|
+
const table2 = manifest.context;
|
|
2087
|
+
if (table2 === null || typeof table2 !== "object") return {};
|
|
2088
|
+
const entries = table2;
|
|
1492
2089
|
return {
|
|
1493
2090
|
...asBudgets(entries["default"]),
|
|
1494
2091
|
...profile ? asBudgets(entries[profile]) : {}
|
|
@@ -1519,23 +2116,23 @@ var KbBaseFrozenError = class extends Error {
|
|
|
1519
2116
|
};
|
|
1520
2117
|
|
|
1521
2118
|
// src/kb-pins/frozen.ts
|
|
1522
|
-
var
|
|
2119
|
+
var import_node_path8 = require("path");
|
|
1523
2120
|
|
|
1524
2121
|
// src/kb-pins/layers.ts
|
|
1525
|
-
var
|
|
1526
|
-
var
|
|
1527
|
-
var
|
|
2122
|
+
var import_promises5 = require("fs/promises");
|
|
2123
|
+
var import_node_os3 = require("os");
|
|
2124
|
+
var import_node_path7 = require("path");
|
|
1528
2125
|
|
|
1529
2126
|
// src/kb-pins/model.ts
|
|
1530
|
-
var
|
|
1531
|
-
var
|
|
1532
|
-
var PINS_FILE = (0,
|
|
1533
|
-
var PINS_LOCAL_FILE = (0,
|
|
2127
|
+
var import_node_path6 = require("path");
|
|
2128
|
+
var import_zod5 = require("zod");
|
|
2129
|
+
var PINS_FILE = (0, import_node_path6.join)(".strauss", "kb-pins.json");
|
|
2130
|
+
var PINS_LOCAL_FILE = (0, import_node_path6.join)(".strauss", "kb-pins.local.json");
|
|
1534
2131
|
var PIN_LAYERS = ["project", "local", "user"];
|
|
1535
|
-
var pinSchema =
|
|
2132
|
+
var pinSchema = import_zod5.z.object({
|
|
1536
2133
|
/** Relative to the manifest's root, so the file is committable. */
|
|
1537
|
-
path:
|
|
1538
|
-
pinnedAt:
|
|
2134
|
+
path: import_zod5.z.string().min(1),
|
|
2135
|
+
pinnedAt: import_zod5.z.string().min(1).optional(),
|
|
1539
2136
|
/**
|
|
1540
2137
|
* How `context` renders this base. `full` preloads the whole base into
|
|
1541
2138
|
* the block regardless of the full-under threshold — for a base whose
|
|
@@ -1545,7 +2142,7 @@ var pinSchema = import_zod4.z.object({
|
|
|
1545
2142
|
* Absent: the profile's full-under threshold decides. Invalid values
|
|
1546
2143
|
* degrade to absent rather than failing the manifest.
|
|
1547
2144
|
*/
|
|
1548
|
-
mode:
|
|
2145
|
+
mode: import_zod5.z.enum(["full", "index"]).optional().catch(void 0),
|
|
1549
2146
|
/**
|
|
1550
2147
|
* Context profiles this pin surfaces in (e.g. only at session-start,
|
|
1551
2148
|
* not per turn). Absent: every profile. A run without a profile sees
|
|
@@ -1553,17 +2150,17 @@ var pinSchema = import_zod4.z.object({
|
|
|
1553
2150
|
* that skill at point of use than pinned at all — pins are what every
|
|
1554
2151
|
* session should see.
|
|
1555
2152
|
*/
|
|
1556
|
-
profiles:
|
|
2153
|
+
profiles: import_zod5.z.array(import_zod5.z.string()).optional().catch(void 0),
|
|
1557
2154
|
/**
|
|
1558
2155
|
* The base is concluded — a finished piece of research, a frozen ADR
|
|
1559
2156
|
* set. Write commands against it refuse while this workspace holds the
|
|
1560
2157
|
* pin, and `context` labels it read-only. Workspace policy, not base
|
|
1561
2158
|
* state: the base itself stays copyable and writable elsewhere.
|
|
1562
2159
|
*/
|
|
1563
|
-
frozen:
|
|
2160
|
+
frozen: import_zod5.z.boolean().optional().catch(void 0)
|
|
1564
2161
|
}).passthrough();
|
|
1565
|
-
var pinsManifestSchema =
|
|
1566
|
-
pins:
|
|
2162
|
+
var pinsManifestSchema = import_zod5.z.object({
|
|
2163
|
+
pins: import_zod5.z.array(pinSchema).default([]),
|
|
1567
2164
|
/**
|
|
1568
2165
|
* Per-repo budgets for the `context` command, keyed by profile —
|
|
1569
2166
|
* `"session-start"`, `"compact"`, `"turn"`, or `"default"` for all of
|
|
@@ -1572,18 +2169,18 @@ var pinsManifestSchema = import_zod4.z.object({
|
|
|
1572
2169
|
* the index at every session start. `contextProfileBudgets` does the
|
|
1573
2170
|
* tolerant read.
|
|
1574
2171
|
*/
|
|
1575
|
-
context:
|
|
2172
|
+
context: import_zod5.z.unknown().optional()
|
|
1576
2173
|
}).passthrough();
|
|
1577
2174
|
|
|
1578
2175
|
// src/kb-pins/layers.ts
|
|
1579
2176
|
function userRoot() {
|
|
1580
|
-
return process.env.STRAUSS_KB_USER_ROOT || (0,
|
|
2177
|
+
return process.env.STRAUSS_KB_USER_ROOT || (0, import_node_os3.homedir)();
|
|
1581
2178
|
}
|
|
1582
2179
|
function layerRoot(workspaceDir, layer) {
|
|
1583
|
-
return layer === "user" ? userRoot() : (0,
|
|
2180
|
+
return layer === "user" ? userRoot() : (0, import_node_path7.resolve)(workspaceDir);
|
|
1584
2181
|
}
|
|
1585
2182
|
function layerFile(workspaceDir, layer) {
|
|
1586
|
-
return (0,
|
|
2183
|
+
return (0, import_node_path7.join)(
|
|
1587
2184
|
layerRoot(workspaceDir, layer),
|
|
1588
2185
|
layer === "local" ? PINS_LOCAL_FILE : PINS_FILE
|
|
1589
2186
|
);
|
|
@@ -1592,7 +2189,7 @@ async function readPinsLayer(workspaceDir, layer) {
|
|
|
1592
2189
|
const file = layerFile(workspaceDir, layer);
|
|
1593
2190
|
let raw;
|
|
1594
2191
|
try {
|
|
1595
|
-
raw = await (0,
|
|
2192
|
+
raw = await (0, import_promises5.readFile)(file, "utf8");
|
|
1596
2193
|
} catch {
|
|
1597
2194
|
return { pins: [] };
|
|
1598
2195
|
}
|
|
@@ -1616,16 +2213,16 @@ async function readPinsLayer(workspaceDir, layer) {
|
|
|
1616
2213
|
}
|
|
1617
2214
|
async function writePinsLayer(workspaceDir, layer, manifest) {
|
|
1618
2215
|
const file = layerFile(workspaceDir, layer);
|
|
1619
|
-
await (0,
|
|
1620
|
-
await (0,
|
|
2216
|
+
await (0, import_promises5.mkdir)((0, import_node_path7.dirname)(file), { recursive: true });
|
|
2217
|
+
await (0, import_promises5.writeFile)(file, `${JSON.stringify(manifest, null, 2)}
|
|
1621
2218
|
`, "utf8");
|
|
1622
2219
|
}
|
|
1623
2220
|
function resolvePinPath(rootDir, path) {
|
|
1624
|
-
return (0,
|
|
2221
|
+
return (0, import_node_path7.isAbsolute)(path) ? (0, import_node_path7.resolve)(path) : (0, import_node_path7.resolve)(rootDir, path.split("/").join(import_node_path7.sep));
|
|
1625
2222
|
}
|
|
1626
2223
|
function storablePath(rootDir, bundlePath2) {
|
|
1627
|
-
const rel = (0,
|
|
1628
|
-
return (rel === "" ? "." : rel).split(
|
|
2224
|
+
const rel = (0, import_node_path7.relative)((0, import_node_path7.resolve)(rootDir), (0, import_node_path7.resolve)(bundlePath2));
|
|
2225
|
+
return (rel === "" ? "." : rel).split(import_node_path7.sep).join("/");
|
|
1629
2226
|
}
|
|
1630
2227
|
async function readMergedPins(workspaceDir) {
|
|
1631
2228
|
const manifests = {};
|
|
@@ -1653,7 +2250,7 @@ async function readMergedPins(workspaceDir) {
|
|
|
1653
2250
|
// src/kb-pins/frozen.ts
|
|
1654
2251
|
async function assertBaseNotFrozen(workspaceDir, bundlePath2) {
|
|
1655
2252
|
const merged = await readMergedPins(workspaceDir);
|
|
1656
|
-
const absolute = (0,
|
|
2253
|
+
const absolute = (0, import_node_path8.resolve)(bundlePath2);
|
|
1657
2254
|
const pin = merged.pins.find((entry) => entry.absolutePath === absolute);
|
|
1658
2255
|
if (pin?.frozen === true) {
|
|
1659
2256
|
throw new KbBaseFrozenError(pin.path, pin.layer);
|
|
@@ -1738,7 +2335,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
|
|
|
1738
2335
|
}
|
|
1739
2336
|
|
|
1740
2337
|
// src/kb-pins/unpin.ts
|
|
1741
|
-
var
|
|
2338
|
+
var import_node_path9 = require("path");
|
|
1742
2339
|
async function unpinBase(workspaceDir, bundlePath2) {
|
|
1743
2340
|
const layers = [];
|
|
1744
2341
|
for (const layer of PIN_LAYERS) {
|
|
@@ -1759,17 +2356,17 @@ async function unpinBase(workspaceDir, bundlePath2) {
|
|
|
1759
2356
|
}
|
|
1760
2357
|
}
|
|
1761
2358
|
return {
|
|
1762
|
-
path: storablePath((0,
|
|
2359
|
+
path: storablePath((0, import_node_path9.resolve)(workspaceDir), bundlePath2),
|
|
1763
2360
|
removed: layers.length > 0,
|
|
1764
2361
|
layers
|
|
1765
2362
|
};
|
|
1766
2363
|
}
|
|
1767
2364
|
|
|
1768
2365
|
// src/commands/model.ts
|
|
1769
|
-
var
|
|
1770
|
-
var bundlePath =
|
|
1771
|
-
var conceptId =
|
|
1772
|
-
var REPO_ROOT =
|
|
2366
|
+
var import_zod6 = require("zod");
|
|
2367
|
+
var bundlePath = import_zod6.z.string().min(1).describe("Absolute path to the knowledge base directory.");
|
|
2368
|
+
var conceptId = import_zod6.z.string().min(1).describe("e.g. decision.cursor-v2");
|
|
2369
|
+
var REPO_ROOT = import_zod6.z.string().min(1).optional().describe(
|
|
1773
2370
|
"Where the anchored source lives, for the drift check. Defaults to the working directory."
|
|
1774
2371
|
);
|
|
1775
2372
|
function define(command) {
|
|
@@ -1792,22 +2389,30 @@ function argvFlag(argv, name) {
|
|
|
1792
2389
|
}
|
|
1793
2390
|
|
|
1794
2391
|
// src/commands/anchor-resolve.ts
|
|
2392
|
+
function resolverSummary(results) {
|
|
2393
|
+
const names = [
|
|
2394
|
+
...new Set(
|
|
2395
|
+
results.flatMap((entry) => entry.resolver ? [entry.resolver] : [])
|
|
2396
|
+
)
|
|
2397
|
+
].sort();
|
|
2398
|
+
return names.length ? `${names.join(" + ")} resolver` : "whole-file";
|
|
2399
|
+
}
|
|
1795
2400
|
var anchorResolveCommand = define({
|
|
1796
2401
|
name: "anchor-resolve",
|
|
1797
2402
|
tool: "kb_anchor_resolve",
|
|
1798
2403
|
usage: "anchor-resolve <concept-id> [--repo-root <path>] [--offline] [--rebaseline] [--restamp]",
|
|
1799
2404
|
description: "Resolve a record's anchors: stamp a hash onto anchors that lack one, report drift where the code moved. An anchor naming another repository is read from that remote through a bare cache; --offline uses the cache only. kb_verify's mechanical counterpart \u2014 reach for it when the question is whether the code still is what it was. Exits non-zero on drift.",
|
|
1800
|
-
input:
|
|
2405
|
+
input: import_zod7.z.object({
|
|
1801
2406
|
bundlePath,
|
|
1802
2407
|
conceptId,
|
|
1803
|
-
repoRoot:
|
|
1804
|
-
offline:
|
|
2408
|
+
repoRoot: import_zod7.z.string().min(1).optional(),
|
|
2409
|
+
offline: import_zod7.z.boolean().optional().describe(
|
|
1805
2410
|
"Resolve foreign anchors from the local repo cache only, never fetching."
|
|
1806
2411
|
),
|
|
1807
|
-
rebaseline:
|
|
2412
|
+
rebaseline: import_zod7.z.boolean().optional().describe(
|
|
1808
2413
|
"Accept the current code as the new baseline for anchors that drifted."
|
|
1809
2414
|
),
|
|
1810
|
-
restamp:
|
|
2415
|
+
restamp: import_zod7.z.boolean().optional().describe(
|
|
1811
2416
|
"Refresh `resolved_at` on anchors that already match. Off by default, so a green run writes nothing."
|
|
1812
2417
|
)
|
|
1813
2418
|
}),
|
|
@@ -1836,6 +2441,11 @@ var anchorResolveCommand = define({
|
|
|
1836
2441
|
const updated = [];
|
|
1837
2442
|
let dirty = false;
|
|
1838
2443
|
const sources = await readSources(anchors, root, offline === true);
|
|
2444
|
+
const resolvers = defaultAnchorResolvers({ offline: offline === true });
|
|
2445
|
+
await prepareResolvers(
|
|
2446
|
+
resolvers,
|
|
2447
|
+
anchors.map((anchor) => anchor.file)
|
|
2448
|
+
);
|
|
1839
2449
|
for (const anchor of anchors) {
|
|
1840
2450
|
const base2 = {
|
|
1841
2451
|
file: anchor.file,
|
|
@@ -1852,27 +2462,35 @@ var anchorResolveCommand = define({
|
|
|
1852
2462
|
updated.push(anchor);
|
|
1853
2463
|
continue;
|
|
1854
2464
|
}
|
|
1855
|
-
const
|
|
1856
|
-
if (!
|
|
2465
|
+
const outcome = resolveAnchorSpan(source.source, anchor, resolvers);
|
|
2466
|
+
if (!outcome.ok) {
|
|
1857
2467
|
results.push({
|
|
1858
2468
|
...base2,
|
|
1859
2469
|
state: "unresolved",
|
|
1860
|
-
reason:
|
|
2470
|
+
reason: outcome.reason
|
|
1861
2471
|
});
|
|
1862
2472
|
updated.push(anchor);
|
|
1863
2473
|
continue;
|
|
1864
2474
|
}
|
|
2475
|
+
const resolved = outcome.span;
|
|
2476
|
+
const producedBy = outcome.resolver;
|
|
1865
2477
|
const currentHash = hashAnchorText(resolved.text);
|
|
1866
2478
|
const currentLines = resolved.endLine - resolved.startLine + 1;
|
|
1867
2479
|
const stamped = {
|
|
1868
2480
|
...anchor,
|
|
1869
2481
|
hash: currentHash,
|
|
1870
2482
|
lines: currentLines,
|
|
1871
|
-
resolved_at: now()
|
|
2483
|
+
resolved_at: now(),
|
|
2484
|
+
...producedBy ? { resolver: producedBy } : {}
|
|
1872
2485
|
};
|
|
1873
2486
|
const pinned = anchor.ref !== void 0 && source.repo !== void 0;
|
|
1874
2487
|
if (!anchor.hash) {
|
|
1875
|
-
results.push({
|
|
2488
|
+
results.push({
|
|
2489
|
+
...base2,
|
|
2490
|
+
state: "stamped",
|
|
2491
|
+
currentHash,
|
|
2492
|
+
...producedBy ? { resolver: producedBy } : {}
|
|
2493
|
+
});
|
|
1876
2494
|
updated.push(stamped);
|
|
1877
2495
|
dirty = true;
|
|
1878
2496
|
continue;
|
|
@@ -1883,6 +2501,10 @@ var anchorResolveCommand = define({
|
|
|
1883
2501
|
state: "drifted",
|
|
1884
2502
|
currentHash,
|
|
1885
2503
|
diffSize: lineDelta(anchor, currentLines),
|
|
2504
|
+
...producedBy ? { resolver: producedBy } : {},
|
|
2505
|
+
// A regex-stamped anchor re-read by tree-sitter drifts because the
|
|
2506
|
+
// resolver changed, not because the code did.
|
|
2507
|
+
...resolverChanged(source.source, anchor, producedBy) ? { reason: "resolver-changed" } : {},
|
|
1886
2508
|
...pinned ? { remoteState: "drifted-from-ref" } : {},
|
|
1887
2509
|
...rebaseline ? { rebaselined: true } : {}
|
|
1888
2510
|
});
|
|
@@ -1890,7 +2512,7 @@ var anchorResolveCommand = define({
|
|
|
1890
2512
|
if (rebaseline) dirty = true;
|
|
1891
2513
|
continue;
|
|
1892
2514
|
}
|
|
1893
|
-
const onDefault = pinned ? headHash(source, anchor) : void 0;
|
|
2515
|
+
const onDefault = pinned ? headHash(source, anchor, resolvers) : void 0;
|
|
1894
2516
|
if (onDefault && onDefault.hash !== anchor.hash) {
|
|
1895
2517
|
results.push({
|
|
1896
2518
|
...base2,
|
|
@@ -1906,6 +2528,7 @@ var anchorResolveCommand = define({
|
|
|
1906
2528
|
...base2,
|
|
1907
2529
|
state: "match",
|
|
1908
2530
|
currentHash,
|
|
2531
|
+
...producedBy ? { resolver: producedBy } : {},
|
|
1909
2532
|
...pinned ? { remoteState: "matches-ref" } : {}
|
|
1910
2533
|
});
|
|
1911
2534
|
const refresh = restamp || anchor.resolved_at === void 0;
|
|
@@ -1923,19 +2546,21 @@ var anchorResolveCommand = define({
|
|
|
1923
2546
|
if (!frozen) await store.updateAnchors(path, id, updated, actor);
|
|
1924
2547
|
}
|
|
1925
2548
|
const frozenNote = frozen ? { frozen: true, note: "base is frozen: nothing was stamped" } : {};
|
|
2549
|
+
const hints = grammarHints();
|
|
2550
|
+
const hintNote = hints.length ? { hints } : {};
|
|
1926
2551
|
const unreachable = results.filter(
|
|
1927
2552
|
(entry) => isUncheckedReason(entry.reason)
|
|
1928
2553
|
).length;
|
|
1929
2554
|
const checked = results.length - unreachable;
|
|
1930
|
-
const
|
|
1931
|
-
const note = `${
|
|
1932
|
-
const clean = checked > 0 &&
|
|
2555
|
+
const matches3 = results.filter((entry) => entry.state === "match").length;
|
|
2556
|
+
const note = `${matches3}/${checked} anchors match${unreachable ? `, ${unreachable} unreachable` : ""}`;
|
|
2557
|
+
const clean = checked > 0 && matches3 === checked && unreachable === 0;
|
|
1933
2558
|
if (clean) {
|
|
1934
2559
|
try {
|
|
1935
2560
|
await store.verify(
|
|
1936
2561
|
path,
|
|
1937
2562
|
id,
|
|
1938
|
-
`anchor-resolve: ${note} (
|
|
2563
|
+
`anchor-resolve: ${note} (${resolverSummary(results)})`,
|
|
1939
2564
|
actor,
|
|
1940
2565
|
now()
|
|
1941
2566
|
);
|
|
@@ -1946,17 +2571,25 @@ var anchorResolveCommand = define({
|
|
|
1946
2571
|
results,
|
|
1947
2572
|
verified: false,
|
|
1948
2573
|
verifyRefused: "self-verification",
|
|
1949
|
-
...frozenNote
|
|
2574
|
+
...frozenNote,
|
|
2575
|
+
...hintNote
|
|
1950
2576
|
};
|
|
1951
2577
|
}
|
|
1952
|
-
return {
|
|
2578
|
+
return {
|
|
2579
|
+
conceptId: id,
|
|
2580
|
+
results,
|
|
2581
|
+
verified: true,
|
|
2582
|
+
...frozenNote,
|
|
2583
|
+
...hintNote
|
|
2584
|
+
};
|
|
1953
2585
|
}
|
|
1954
2586
|
return {
|
|
1955
2587
|
conceptId: id,
|
|
1956
2588
|
results,
|
|
1957
2589
|
verified: false,
|
|
1958
2590
|
...unreachable ? { note } : {},
|
|
1959
|
-
...frozenNote
|
|
2591
|
+
...frozenNote,
|
|
2592
|
+
...hintNote
|
|
1960
2593
|
};
|
|
1961
2594
|
},
|
|
1962
2595
|
// A stored hash that no longer resolves is a broken anchor, not an absence:
|
|
@@ -1972,13 +2605,13 @@ var anchorResolveCommand = define({
|
|
|
1972
2605
|
function lineDelta(anchor, current) {
|
|
1973
2606
|
return anchor.lines === void 0 ? null : Math.abs(current - anchor.lines);
|
|
1974
2607
|
}
|
|
1975
|
-
function headHash(source, anchor) {
|
|
2608
|
+
function headHash(source, anchor, resolvers) {
|
|
1976
2609
|
if (source.head === void 0) return void 0;
|
|
1977
|
-
const
|
|
1978
|
-
if (!
|
|
2610
|
+
const outcome = resolveAnchorSpan(source.head, anchor, resolvers);
|
|
2611
|
+
if (!outcome.ok) return void 0;
|
|
1979
2612
|
return {
|
|
1980
|
-
hash: hashAnchorText(
|
|
1981
|
-
lines:
|
|
2613
|
+
hash: hashAnchorText(outcome.span.text),
|
|
2614
|
+
lines: outcome.span.endLine - outcome.span.startLine + 1
|
|
1982
2615
|
};
|
|
1983
2616
|
}
|
|
1984
2617
|
async function readSources(anchors, root, offline) {
|
|
@@ -2028,13 +2661,13 @@ async function readSources(anchors, root, offline) {
|
|
|
2028
2661
|
}
|
|
2029
2662
|
|
|
2030
2663
|
// src/commands/answer.ts
|
|
2031
|
-
var
|
|
2664
|
+
var import_zod8 = require("zod");
|
|
2032
2665
|
var answerCommand = define({
|
|
2033
2666
|
name: "answer",
|
|
2034
2667
|
tool: "kb_answer",
|
|
2035
2668
|
usage: "answer <concept-id> <answer...>",
|
|
2036
2669
|
description: "Resolve an open question: set status, stamp who and when, append an Answer section. If the answer overturns a decision or assumption, supersede that record explicitly.",
|
|
2037
|
-
input:
|
|
2670
|
+
input: import_zod8.z.object({ bundlePath, conceptId, answer: import_zod8.z.string().min(1) }),
|
|
2038
2671
|
fromArgv: (argv, path) => ({
|
|
2039
2672
|
bundlePath: path,
|
|
2040
2673
|
conceptId: argv[1],
|
|
@@ -2048,19 +2681,19 @@ var answerCommand = define({
|
|
|
2048
2681
|
});
|
|
2049
2682
|
|
|
2050
2683
|
// src/commands/backlinks.ts
|
|
2051
|
-
var
|
|
2684
|
+
var import_zod9 = require("zod");
|
|
2052
2685
|
var backlinksCommand = define({
|
|
2053
2686
|
name: "backlinks",
|
|
2054
2687
|
tool: "kb_backlinks",
|
|
2055
2688
|
usage: "backlinks <concept-id>",
|
|
2056
2689
|
description: "Who points at this record: every inbound typed causal link (`strauss_links`), one hop, every rel including `related_to`, each with its rel and the standing of the record that made it. Use it when you need the exact edges \u2014 reviewing or renaming a record.",
|
|
2057
|
-
input:
|
|
2690
|
+
input: import_zod9.z.object({ bundlePath, conceptId }),
|
|
2058
2691
|
fromArgv: (argv, path) => ({ bundlePath: path, conceptId: argv[1] }),
|
|
2059
2692
|
run: async ({ store }, { bundlePath: path, conceptId: id }) => store.backlinks(path, id)
|
|
2060
2693
|
});
|
|
2061
2694
|
|
|
2062
2695
|
// src/commands/catalog.ts
|
|
2063
|
-
var
|
|
2696
|
+
var import_zod10 = require("zod");
|
|
2064
2697
|
|
|
2065
2698
|
// src/adjudicate.ts
|
|
2066
2699
|
var STANDING = {
|
|
@@ -2138,8 +2771,8 @@ function resolveHeads(from, byId) {
|
|
|
2138
2771
|
while (queue.length) {
|
|
2139
2772
|
const current = queue.shift();
|
|
2140
2773
|
const next = successors(current, byId);
|
|
2141
|
-
for (const
|
|
2142
|
-
warnings.push({ kind: "broken-chain", missing });
|
|
2774
|
+
for (const missing2 of next.missing) {
|
|
2775
|
+
warnings.push({ kind: "broken-chain", missing: missing2 });
|
|
2143
2776
|
}
|
|
2144
2777
|
if (!next.records.length) {
|
|
2145
2778
|
if (current.conceptId !== from.conceptId)
|
|
@@ -2170,13 +2803,13 @@ function successors(record, byId) {
|
|
|
2170
2803
|
}
|
|
2171
2804
|
}
|
|
2172
2805
|
const records = [];
|
|
2173
|
-
const
|
|
2806
|
+
const missing2 = [];
|
|
2174
2807
|
for (const id of ids) {
|
|
2175
2808
|
const found = byId.get(id);
|
|
2176
2809
|
if (found) records.push(found);
|
|
2177
|
-
else
|
|
2810
|
+
else missing2.push(id);
|
|
2178
2811
|
}
|
|
2179
|
-
return { records, missing };
|
|
2812
|
+
return { records, missing: missing2 };
|
|
2180
2813
|
}
|
|
2181
2814
|
|
|
2182
2815
|
// src/catalog.ts
|
|
@@ -2231,9 +2864,9 @@ var catalogCommand = define({
|
|
|
2231
2864
|
tool: "kb_catalog",
|
|
2232
2865
|
usage: "catalog [type]",
|
|
2233
2866
|
description: "Lists every record as one line \u2014 concept id, type, title, standing, and a stale flag \u2014 at roughly thirty tokens each. Pick this over kb_load once kb_load refuses: kb_catalog never refuses. Superseded records show only their replacement; fetch bodies with kb_load, kb_pack, kb_query, or kb_trace.",
|
|
2234
|
-
input:
|
|
2867
|
+
input: import_zod10.z.object({
|
|
2235
2868
|
bundlePath,
|
|
2236
|
-
type:
|
|
2869
|
+
type: import_zod10.z.enum(KB_RECORD_TYPES).optional()
|
|
2237
2870
|
}),
|
|
2238
2871
|
fromArgv: (argv, path) => ({
|
|
2239
2872
|
bundlePath: path,
|
|
@@ -2288,10 +2921,10 @@ function count(value, noun) {
|
|
|
2288
2921
|
}
|
|
2289
2922
|
|
|
2290
2923
|
// src/commands/context.ts
|
|
2291
|
-
var
|
|
2924
|
+
var import_zod11 = require("zod");
|
|
2292
2925
|
|
|
2293
2926
|
// src/kb-context.ts
|
|
2294
|
-
var
|
|
2927
|
+
var import_promises6 = require("fs/promises");
|
|
2295
2928
|
|
|
2296
2929
|
// src/kb-index.ts
|
|
2297
2930
|
var INDEX_FILE = "INDEX.md";
|
|
@@ -2518,13 +3151,13 @@ function toHookJson(block, event) {
|
|
|
2518
3151
|
var CONTEXT_BEGIN = "<!-- strauss-kb:begin -->";
|
|
2519
3152
|
var CONTEXT_END = "<!-- strauss-kb:end -->";
|
|
2520
3153
|
async function syncInstructions(file, block) {
|
|
2521
|
-
const existing = await (0,
|
|
3154
|
+
const existing = await (0, import_promises6.readFile)(file, "utf8").catch(() => null);
|
|
2522
3155
|
const region = block ? `${CONTEXT_BEGIN}
|
|
2523
3156
|
${block.trim()}
|
|
2524
3157
|
${CONTEXT_END}` : null;
|
|
2525
3158
|
if (existing === null) {
|
|
2526
3159
|
if (!region) return { file, action: "unchanged" };
|
|
2527
|
-
await (0,
|
|
3160
|
+
await (0, import_promises6.writeFile)(file, `${region}
|
|
2528
3161
|
`, "utf8");
|
|
2529
3162
|
return { file, action: "created" };
|
|
2530
3163
|
}
|
|
@@ -2535,11 +3168,11 @@ ${CONTEXT_END}` : null;
|
|
|
2535
3168
|
const after = existing.slice(end + CONTEXT_END.length);
|
|
2536
3169
|
const next = region ? `${before}${region}${after}` : `${before.replace(/\n+$/, "\n")}${after.replace(/^\n+/, "\n")}`;
|
|
2537
3170
|
if (next === existing) return { file, action: "unchanged" };
|
|
2538
|
-
await (0,
|
|
3171
|
+
await (0, import_promises6.writeFile)(file, next, "utf8");
|
|
2539
3172
|
return { file, action: region ? "replaced" : "removed" };
|
|
2540
3173
|
}
|
|
2541
3174
|
if (!region) return { file, action: "unchanged" };
|
|
2542
|
-
await (0,
|
|
3175
|
+
await (0, import_promises6.writeFile)(
|
|
2543
3176
|
file,
|
|
2544
3177
|
`${existing.replace(/\n*$/, "\n\n")}${region}
|
|
2545
3178
|
`,
|
|
@@ -2554,20 +3187,20 @@ var contextCommand = define({
|
|
|
2554
3187
|
tool: "kb_context",
|
|
2555
3188
|
usage: "context [--profile NAME] [--budget N] [--full-under N] [--format json] [--event NAME]",
|
|
2556
3189
|
description: "Index block of pinned bases (ids, titles, standing) for injection at context birth. Takes no bundlePath \u2014 reads the workspace pin manifests. Empty when nothing is pinned; refuses over budget rather than truncating. Budget precedence: flags, then the manifest `context[profile]` over `context.default`, then the built-in profile, then package defaults.",
|
|
2557
|
-
input:
|
|
2558
|
-
budgetTokens:
|
|
3190
|
+
input: import_zod11.z.object({
|
|
3191
|
+
budgetTokens: import_zod11.z.number().int().positive().optional().describe(
|
|
2559
3192
|
"Ceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000."
|
|
2560
3193
|
),
|
|
2561
|
-
fullUnderTokens:
|
|
3194
|
+
fullUnderTokens: import_zod11.z.number().int().positive().optional().describe(
|
|
2562
3195
|
"Per-base rendering threshold, applied before the budget: a base whose complete load fits under this arrives as full records instead of index lines, and the whole block still answers to budgetTokens. Off by default \u2014 index-only is the safe default at a context birth, because injected bodies outlive the qualifiers on them; the session-start profile opts tiny bases in at 1500."
|
|
2563
3196
|
),
|
|
2564
|
-
profile:
|
|
3197
|
+
profile: import_zod11.z.string().optional().describe(
|
|
2565
3198
|
"Named budget set: built-ins are session-start (full-under 1500), compact and turn (budget 2500); the manifests' `context` tables override per repo. Unknown names fall through to defaults rather than failing."
|
|
2566
3199
|
),
|
|
2567
|
-
format:
|
|
3200
|
+
format: import_zod11.z.enum(["markdown", "json"]).optional().describe(
|
|
2568
3201
|
"CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
|
|
2569
3202
|
),
|
|
2570
|
-
event:
|
|
3203
|
+
event: import_zod11.z.string().optional().describe(
|
|
2571
3204
|
"hookEventName stamped into the JSON envelope. Only meaningful with format=json."
|
|
2572
3205
|
)
|
|
2573
3206
|
}),
|
|
@@ -2603,7 +3236,7 @@ var contextCommand = define({
|
|
|
2603
3236
|
});
|
|
2604
3237
|
|
|
2605
3238
|
// src/commands/doctor.ts
|
|
2606
|
-
var
|
|
3239
|
+
var import_zod12 = require("zod");
|
|
2607
3240
|
|
|
2608
3241
|
// src/kb-edges.ts
|
|
2609
3242
|
var KB_EDGE_KINDS = [
|
|
@@ -2789,6 +3422,18 @@ var CHECK_HEADLINES = {
|
|
|
2789
3422
|
unchecked: "an anchor in another repository nothing could reach"
|
|
2790
3423
|
};
|
|
2791
3424
|
var DAY_MS = 864e5;
|
|
3425
|
+
function anchorResolverCounts(bundle) {
|
|
3426
|
+
let treeSitter = 0;
|
|
3427
|
+
let regex = 0;
|
|
3428
|
+
for (const record of bundle) {
|
|
3429
|
+
for (const anchor of record.frontmatter.strauss_anchors ?? []) {
|
|
3430
|
+
if (!anchor.hash || !anchor.symbol) continue;
|
|
3431
|
+
if (anchor.resolver === "tree-sitter") treeSitter += 1;
|
|
3432
|
+
else regex += 1;
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
return { total: treeSitter + regex, treeSitter, regex };
|
|
3436
|
+
}
|
|
2792
3437
|
function doctor(bundle, options = {}) {
|
|
2793
3438
|
const thresholds = {
|
|
2794
3439
|
expiringDays: options.expiringDays ?? DEFAULT_EXPIRING_DAYS,
|
|
@@ -2824,6 +3469,7 @@ function doctor(bundle, options = {}) {
|
|
|
2824
3469
|
counts,
|
|
2825
3470
|
groups,
|
|
2826
3471
|
findingCount,
|
|
3472
|
+
anchorResolvers: anchorResolverCounts(bundle),
|
|
2827
3473
|
healthy: findingCount === 0
|
|
2828
3474
|
};
|
|
2829
3475
|
}
|
|
@@ -3070,13 +3716,13 @@ function ageInDays(record, now) {
|
|
|
3070
3716
|
}
|
|
3071
3717
|
|
|
3072
3718
|
// src/commands/doctor.ts
|
|
3073
|
-
var days = (what, fallback) =>
|
|
3719
|
+
var days = (what, fallback) => import_zod12.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
|
|
3074
3720
|
var doctorCommand = define({
|
|
3075
3721
|
name: "doctor",
|
|
3076
3722
|
tool: "kb_doctor",
|
|
3077
3723
|
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--offline] [--strict]",
|
|
3078
3724
|
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.",
|
|
3079
|
-
input:
|
|
3725
|
+
input: import_zod12.z.object({
|
|
3080
3726
|
bundlePath,
|
|
3081
3727
|
repoRoot: REPO_ROOT,
|
|
3082
3728
|
expiringDays: days(
|
|
@@ -3091,10 +3737,10 @@ var doctorCommand = define({
|
|
|
3091
3737
|
"How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
|
|
3092
3738
|
DEFAULT_AGING_DAYS
|
|
3093
3739
|
),
|
|
3094
|
-
offline:
|
|
3740
|
+
offline: import_zod12.z.boolean().optional().describe(
|
|
3095
3741
|
"Read foreign anchors from the local repo cache only, never fetching."
|
|
3096
3742
|
),
|
|
3097
|
-
strict:
|
|
3743
|
+
strict: import_zod12.z.boolean().optional().describe(
|
|
3098
3744
|
"Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
|
|
3099
3745
|
)
|
|
3100
3746
|
}),
|
|
@@ -3137,7 +3783,13 @@ var doctorCommand = define({
|
|
|
3137
3783
|
...anchorDrift !== void 0 ? { anchorDrift } : {},
|
|
3138
3784
|
now: new Date(checkedAt)
|
|
3139
3785
|
});
|
|
3140
|
-
|
|
3786
|
+
const hints = grammarHints();
|
|
3787
|
+
return {
|
|
3788
|
+
bundlePath: path,
|
|
3789
|
+
checkedAt,
|
|
3790
|
+
...report,
|
|
3791
|
+
...hints.length ? { hints } : {}
|
|
3792
|
+
};
|
|
3141
3793
|
},
|
|
3142
3794
|
render: (result) => render2(result),
|
|
3143
3795
|
// Only expiry, and only under --strict. The other seven checks report debt a
|
|
@@ -3155,12 +3807,15 @@ function render2(result) {
|
|
|
3155
3807
|
`records: ${result.recordCount}`,
|
|
3156
3808
|
`thresholds: expiring within ${thresholds.expiringDays}d, unverified over ${thresholds.unverifiedDays}d, aging over ${thresholds.agingDays}d`,
|
|
3157
3809
|
`checked: ${result.checkedAt}`,
|
|
3810
|
+
...result.anchorResolvers.total ? [
|
|
3811
|
+
`anchors: ${result.anchorResolvers.total} hashed \u2014 ${result.anchorResolvers.treeSitter} tree-sitter, ${result.anchorResolvers.regex} regex`
|
|
3812
|
+
] : [],
|
|
3158
3813
|
""
|
|
3159
3814
|
];
|
|
3160
|
-
const
|
|
3815
|
+
const width2 = Math.max(...result.groups.map((group2) => group2.check.length));
|
|
3161
3816
|
for (const group2 of result.groups) {
|
|
3162
3817
|
lines.push(
|
|
3163
|
-
` ${group2.check.padEnd(
|
|
3818
|
+
` ${group2.check.padEnd(width2)} ${String(group2.count).padStart(3)} ${group2.headline}`
|
|
3164
3819
|
);
|
|
3165
3820
|
}
|
|
3166
3821
|
for (const group2 of result.groups) {
|
|
@@ -3172,6 +3827,7 @@ function render2(result) {
|
|
|
3172
3827
|
);
|
|
3173
3828
|
}
|
|
3174
3829
|
}
|
|
3830
|
+
for (const hint of result.hints ?? []) lines.push("", hint);
|
|
3175
3831
|
lines.push(
|
|
3176
3832
|
"",
|
|
3177
3833
|
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.`
|
|
@@ -3180,19 +3836,19 @@ function render2(result) {
|
|
|
3180
3836
|
}
|
|
3181
3837
|
|
|
3182
3838
|
// src/commands/impact.ts
|
|
3183
|
-
var
|
|
3839
|
+
var import_zod13 = require("zod");
|
|
3184
3840
|
var impactCommand = define({
|
|
3185
3841
|
name: "impact",
|
|
3186
3842
|
tool: "kb_impact",
|
|
3187
3843
|
usage: "impact <concept-id> [--depth N] [--rels a,b]",
|
|
3188
3844
|
description: "What breaks if this record changes: its transitive set of dependants, each with its standing. Each rel declares which of its ends depends on the other, and the walk follows each rel in its own direction. Naming `related_to` or an unknown rel in `rels` is an error. kb_backlinks gives one flat hop.",
|
|
3189
|
-
input:
|
|
3845
|
+
input: import_zod13.z.object({
|
|
3190
3846
|
bundlePath,
|
|
3191
3847
|
conceptId,
|
|
3192
|
-
depth:
|
|
3848
|
+
depth: import_zod13.z.number().int().positive().optional().describe(
|
|
3193
3849
|
"Hops out from the record. Unbounded when omitted; a walk this cuts reports truncated: true."
|
|
3194
3850
|
),
|
|
3195
|
-
rels:
|
|
3851
|
+
rels: import_zod13.z.array(import_zod13.z.enum(KB_CAUSAL_LINK_RELS)).optional().describe(
|
|
3196
3852
|
"Narrow which rels the walk follows. Defaults to every rel that carries a dependence \u2014 all but related_to."
|
|
3197
3853
|
)
|
|
3198
3854
|
}),
|
|
@@ -3213,13 +3869,13 @@ var impactCommand = define({
|
|
|
3213
3869
|
});
|
|
3214
3870
|
|
|
3215
3871
|
// src/commands/list.ts
|
|
3216
|
-
var
|
|
3872
|
+
var import_zod14 = require("zod");
|
|
3217
3873
|
var listCommand = define({
|
|
3218
3874
|
name: "list",
|
|
3219
3875
|
tool: "kb_list",
|
|
3220
3876
|
usage: "list [type]",
|
|
3221
3877
|
description: "Every record, optionally one type. For enumerating; use kb_query for a question.",
|
|
3222
|
-
input:
|
|
3878
|
+
input: import_zod14.z.object({ bundlePath, type: import_zod14.z.enum(KB_RECORD_TYPES).optional() }),
|
|
3223
3879
|
fromArgv: (argv, path) => ({ bundlePath: path, type: argv[1] }),
|
|
3224
3880
|
run: async ({ store }, { bundlePath: path, type }) => (await store.list(path, type)).map((record) => ({
|
|
3225
3881
|
conceptId: record.conceptId,
|
|
@@ -3231,17 +3887,17 @@ var listCommand = define({
|
|
|
3231
3887
|
});
|
|
3232
3888
|
|
|
3233
3889
|
// src/commands/load.ts
|
|
3234
|
-
var
|
|
3890
|
+
var import_zod15 = require("zod");
|
|
3235
3891
|
var loadCommand = define({
|
|
3236
3892
|
name: "load",
|
|
3237
3893
|
tool: "kb_load",
|
|
3238
3894
|
usage: "load [type] [--budget N | --all] [--repo-root PATH]",
|
|
3239
3895
|
description: "Load the whole base, each record with its standing \u2014 call it first, at the point of use, since compaction drops it. Superseded records arrive as stubs; kb_trace has the history. Over budget it refuses: kb_catalog, then kb_pack, or narrow with `type`; `all` bypasses. Never read record files directly \u2014 only kb_* tools resolve supersession. `digest` stamps the base's content, so hooks know when to reload.",
|
|
3240
|
-
input:
|
|
3896
|
+
input: import_zod15.z.object({
|
|
3241
3897
|
bundlePath,
|
|
3242
|
-
type:
|
|
3243
|
-
budgetTokens:
|
|
3244
|
-
all:
|
|
3898
|
+
type: import_zod15.z.enum(KB_RECORD_TYPES).optional(),
|
|
3899
|
+
budgetTokens: import_zod15.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
|
|
3900
|
+
all: import_zod15.z.boolean().optional().describe(
|
|
3245
3901
|
"Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
|
|
3246
3902
|
),
|
|
3247
3903
|
repoRoot: REPO_ROOT
|
|
@@ -3283,25 +3939,25 @@ var loadCommand = define({
|
|
|
3283
3939
|
});
|
|
3284
3940
|
|
|
3285
3941
|
// src/commands/log.ts
|
|
3286
|
-
var
|
|
3942
|
+
var import_zod16 = require("zod");
|
|
3287
3943
|
var logCommand = define({
|
|
3288
3944
|
name: "log",
|
|
3289
3945
|
tool: "kb_log",
|
|
3290
3946
|
usage: "log",
|
|
3291
3947
|
description: "Who touched what, and when. Append-only; malformed lines are reported, never repaired.",
|
|
3292
|
-
input:
|
|
3948
|
+
input: import_zod16.z.object({ bundlePath }),
|
|
3293
3949
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
3294
3950
|
run: ({ store }, { bundlePath: path }) => store.readLog(path)
|
|
3295
3951
|
});
|
|
3296
3952
|
|
|
3297
3953
|
// src/commands/no-decision.ts
|
|
3298
|
-
var
|
|
3954
|
+
var import_zod17 = require("zod");
|
|
3299
3955
|
var noDecisionCommand = define({
|
|
3300
3956
|
name: "no-decision",
|
|
3301
3957
|
tool: "kb_no_decision",
|
|
3302
3958
|
usage: "no-decision <reason...>",
|
|
3303
3959
|
description: "Record in one sentence that a piece of work had nothing to decide. Idempotent.",
|
|
3304
|
-
input:
|
|
3960
|
+
input: import_zod17.z.object({ bundlePath, reason: import_zod17.z.string().min(1) }),
|
|
3305
3961
|
fromArgv: (argv, path) => ({
|
|
3306
3962
|
bundlePath: path,
|
|
3307
3963
|
reason: argv.slice(1).join(" ").trim()
|
|
@@ -3318,20 +3974,20 @@ var noDecisionCommand = define({
|
|
|
3318
3974
|
});
|
|
3319
3975
|
|
|
3320
3976
|
// src/commands/pack.ts
|
|
3321
|
-
var
|
|
3977
|
+
var import_zod18 = require("zod");
|
|
3322
3978
|
var packCommand = define({
|
|
3323
3979
|
name: "pack",
|
|
3324
3980
|
tool: "kb_pack",
|
|
3325
3981
|
usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
|
|
3326
3982
|
description: "Bounded neighbourhood around one record: within `hops`, ranked, cut to `maxNodes`, with every cut record named under Excluded. Use when the base is over kb_load's budget and the work centres on a record you can name. Refuses over budget rather than truncating. Everything below the header is byte-stable across runs. Resolves supersession like kb_load.",
|
|
3327
|
-
input:
|
|
3983
|
+
input: import_zod18.z.object({
|
|
3328
3984
|
bundlePath,
|
|
3329
3985
|
conceptId,
|
|
3330
|
-
hops:
|
|
3331
|
-
maxNodes:
|
|
3986
|
+
hops: import_zod18.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
|
|
3987
|
+
maxNodes: import_zod18.z.number().int().positive().optional().describe(
|
|
3332
3988
|
"How many records the pack may hold, root included. Defaults to 20."
|
|
3333
3989
|
),
|
|
3334
|
-
budgetTokens:
|
|
3990
|
+
budgetTokens: import_zod18.z.number().int().positive().optional().describe(
|
|
3335
3991
|
"Approximate token ceiling over what is actually emitted. Defaults to 25000."
|
|
3336
3992
|
)
|
|
3337
3993
|
}),
|
|
@@ -3418,22 +4074,22 @@ function warningLabel(warning) {
|
|
|
3418
4074
|
}
|
|
3419
4075
|
|
|
3420
4076
|
// src/commands/pin.ts
|
|
3421
|
-
var
|
|
4077
|
+
var import_zod19 = require("zod");
|
|
3422
4078
|
var pinCommand = define({
|
|
3423
4079
|
name: "pin",
|
|
3424
4080
|
tool: "kb_pin",
|
|
3425
4081
|
usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
|
|
3426
4082
|
description: "Pin a base into a workspace manifest so kb_context surfaces it. Layers, nearest wins: project `.strauss/kb-pins.json` (default), `--local` (personal, gitignored), `--user` (`~/.strauss`). Idempotent; `--mode full|index`, `--profiles`, `--frozen`/`--unfreeze` update only those fields. A path with no records pins with a warning. Never touches the base itself.",
|
|
3427
|
-
input:
|
|
4083
|
+
input: import_zod19.z.object({
|
|
3428
4084
|
bundlePath,
|
|
3429
|
-
mode:
|
|
4085
|
+
mode: import_zod19.z.enum(["full", "index"]).optional().describe(
|
|
3430
4086
|
"full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
|
|
3431
4087
|
),
|
|
3432
|
-
profiles:
|
|
3433
|
-
layer:
|
|
4088
|
+
profiles: import_zod19.z.array(import_zod19.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
|
|
4089
|
+
layer: import_zod19.z.enum(["project", "local", "user"]).optional().describe(
|
|
3434
4090
|
"Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
|
|
3435
4091
|
),
|
|
3436
|
-
frozen:
|
|
4092
|
+
frozen: import_zod19.z.boolean().optional().describe(
|
|
3437
4093
|
"true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
|
|
3438
4094
|
)
|
|
3439
4095
|
}),
|
|
@@ -3462,29 +4118,29 @@ var pinCommand = define({
|
|
|
3462
4118
|
});
|
|
3463
4119
|
|
|
3464
4120
|
// src/commands/pins.ts
|
|
3465
|
-
var
|
|
4121
|
+
var import_zod20 = require("zod");
|
|
3466
4122
|
var pinsCommand = define({
|
|
3467
4123
|
name: "pins",
|
|
3468
4124
|
tool: "kb_pins",
|
|
3469
4125
|
usage: "pins",
|
|
3470
4126
|
description: "Every pinned base across the manifest layers, with its layer and whether it resolves to records. Takes no bundlePath.",
|
|
3471
|
-
input:
|
|
4127
|
+
input: import_zod20.z.object({}),
|
|
3472
4128
|
fromArgv: () => ({}),
|
|
3473
4129
|
run: ({ store }) => listPins(store, process.cwd())
|
|
3474
4130
|
});
|
|
3475
4131
|
|
|
3476
4132
|
// src/commands/query.ts
|
|
3477
|
-
var
|
|
4133
|
+
var import_zod21 = require("zod");
|
|
3478
4134
|
var queryCommand = define({
|
|
3479
4135
|
name: "query",
|
|
3480
4136
|
tool: "kb_query",
|
|
3481
4137
|
usage: "query <text...> [--repo-root PATH]",
|
|
3482
4138
|
description: "Search; every hit carries its standing. Flagged, never filtered: a superseded hit returns with its replacement, a rejected one is marked. Prefer kb_load when the base fits its budget \u2014 a full read beats search. Results are volatile: place them at the tail, not the cached prefix. Never read record files directly.",
|
|
3483
|
-
input:
|
|
4139
|
+
input: import_zod21.z.object({
|
|
3484
4140
|
bundlePath,
|
|
3485
|
-
text:
|
|
3486
|
-
type:
|
|
3487
|
-
includeNonCurrent:
|
|
4141
|
+
text: import_zod21.z.string().optional(),
|
|
4142
|
+
type: import_zod21.z.enum(KB_RECORD_TYPES).optional(),
|
|
4143
|
+
includeNonCurrent: import_zod21.z.boolean().optional(),
|
|
3488
4144
|
repoRoot: REPO_ROOT
|
|
3489
4145
|
}),
|
|
3490
4146
|
// `--repo-root` is a flag, so its value must not fall into the search text.
|
|
@@ -3516,27 +4172,27 @@ var queryCommand = define({
|
|
|
3516
4172
|
});
|
|
3517
4173
|
|
|
3518
4174
|
// src/commands/read-index.ts
|
|
3519
|
-
var
|
|
4175
|
+
var import_zod22 = require("zod");
|
|
3520
4176
|
var readIndexCommand = define({
|
|
3521
4177
|
name: "index",
|
|
3522
4178
|
tool: "kb_index",
|
|
3523
4179
|
usage: "index",
|
|
3524
4180
|
description: "The index \u2014 title, type, status, description per record \u2014 rebuilt if stale. Cheapest re-orientation after compaction: call it (or kb_context) first, then kb_load or fetch by id.",
|
|
3525
|
-
input:
|
|
4181
|
+
input: import_zod22.z.object({ bundlePath }),
|
|
3526
4182
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
3527
4183
|
run: ({ store }, { bundlePath: path }) => store.readIndex(path)
|
|
3528
4184
|
});
|
|
3529
4185
|
|
|
3530
4186
|
// src/commands/schema.ts
|
|
3531
|
-
var
|
|
4187
|
+
var import_zod25 = require("zod");
|
|
3532
4188
|
|
|
3533
4189
|
// src/json-schema.ts
|
|
3534
|
-
var
|
|
4190
|
+
var import_zod24 = require("zod");
|
|
3535
4191
|
|
|
3536
4192
|
// src/kb-log.ts
|
|
3537
|
-
var
|
|
4193
|
+
var import_zod23 = require("zod");
|
|
3538
4194
|
var LOG_FILE = "log.jsonl";
|
|
3539
|
-
var kbLogEntrySchema =
|
|
4195
|
+
var kbLogEntrySchema = import_zod23.z.object({
|
|
3540
4196
|
// Validated, not just `min(1)`: `at` is a sort key (see `parseLog`
|
|
3541
4197
|
// below), and a value that isn't actually chronological — a Unix
|
|
3542
4198
|
// timestamp, a human-typed date, garbage — would sort wrong without
|
|
@@ -3545,12 +4201,12 @@ var kbLogEntrySchema = import_zod22.z.object({
|
|
|
3545
4201
|
// and rejects everything else, including a non-`Z` offset — so a
|
|
3546
4202
|
// malformed `at` is reported the same way a malformed line already is,
|
|
3547
4203
|
// rather than silently sorting into the wrong place.
|
|
3548
|
-
at:
|
|
3549
|
-
by:
|
|
3550
|
-
operation:
|
|
3551
|
-
conceptId:
|
|
4204
|
+
at: import_zod23.z.iso.datetime(),
|
|
4205
|
+
by: import_zod23.z.string().min(1),
|
|
4206
|
+
operation: import_zod23.z.string().min(1),
|
|
4207
|
+
conceptId: import_zod23.z.string().min(1),
|
|
3552
4208
|
/** Second concept id, where the operation relates two — supersession. */
|
|
3553
|
-
target:
|
|
4209
|
+
target: import_zod23.z.string().min(1).optional()
|
|
3554
4210
|
}).strict();
|
|
3555
4211
|
function renderLogEntry(entry) {
|
|
3556
4212
|
return `${JSON.stringify(kbLogEntrySchema.parse(entry))}
|
|
@@ -3560,18 +4216,18 @@ function parseLog(raw) {
|
|
|
3560
4216
|
const entries = [];
|
|
3561
4217
|
const malformed = [];
|
|
3562
4218
|
const seen = /* @__PURE__ */ new Set();
|
|
3563
|
-
raw.split("\n").forEach((text,
|
|
4219
|
+
raw.split("\n").forEach((text, index2) => {
|
|
3564
4220
|
if (!text.trim()) return;
|
|
3565
4221
|
let value;
|
|
3566
4222
|
try {
|
|
3567
4223
|
value = JSON.parse(text);
|
|
3568
4224
|
} catch {
|
|
3569
|
-
malformed.push({ line:
|
|
4225
|
+
malformed.push({ line: index2 + 1, text });
|
|
3570
4226
|
return;
|
|
3571
4227
|
}
|
|
3572
4228
|
const parsed = kbLogEntrySchema.safeParse(value);
|
|
3573
4229
|
if (!parsed.success) {
|
|
3574
|
-
malformed.push({ line:
|
|
4230
|
+
malformed.push({ line: index2 + 1, text });
|
|
3575
4231
|
return;
|
|
3576
4232
|
}
|
|
3577
4233
|
const key = JSON.stringify(parsed.data);
|
|
@@ -3588,11 +4244,11 @@ function parseLog(raw) {
|
|
|
3588
4244
|
// src/json-schema.ts
|
|
3589
4245
|
function kbJsonSchemas() {
|
|
3590
4246
|
return {
|
|
3591
|
-
recordFrontmatter:
|
|
4247
|
+
recordFrontmatter: import_zod24.z.toJSONSchema(kbRecordFrontmatterSchema, {
|
|
3592
4248
|
io: "input"
|
|
3593
4249
|
}),
|
|
3594
|
-
composeInput:
|
|
3595
|
-
logEntry:
|
|
4250
|
+
composeInput: import_zod24.z.toJSONSchema(composeInputSchema, { io: "input" }),
|
|
4251
|
+
logEntry: import_zod24.z.toJSONSchema(kbLogEntrySchema, { io: "input" })
|
|
3596
4252
|
};
|
|
3597
4253
|
}
|
|
3598
4254
|
|
|
@@ -3602,25 +4258,25 @@ var schemaCommand = define({
|
|
|
3602
4258
|
tool: "kb_schema",
|
|
3603
4259
|
usage: "schema",
|
|
3604
4260
|
description: "JSON Schema for frontmatter, write input, and log entries, generated from the enforcing code.",
|
|
3605
|
-
input:
|
|
4261
|
+
input: import_zod25.z.object({}),
|
|
3606
4262
|
fromArgv: () => ({}),
|
|
3607
4263
|
run: () => Promise.resolve(kbJsonSchemas())
|
|
3608
4264
|
});
|
|
3609
4265
|
|
|
3610
4266
|
// src/commands/stamp.ts
|
|
3611
|
-
var
|
|
3612
|
-
var
|
|
4267
|
+
var import_promises7 = require("fs/promises");
|
|
4268
|
+
var import_zod26 = require("zod");
|
|
3613
4269
|
var DIGEST = /^[0-9a-f]{64}$/;
|
|
3614
4270
|
var stampCommand = define({
|
|
3615
4271
|
name: "stamp",
|
|
3616
4272
|
tool: "kb_stamp",
|
|
3617
4273
|
usage: "stamp [--bundle PATH] [--since DIGEST|FILE]",
|
|
3618
4274
|
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.",
|
|
3619
|
-
input:
|
|
3620
|
-
bundlePath:
|
|
4275
|
+
input: import_zod26.z.object({
|
|
4276
|
+
bundlePath: import_zod26.z.string().min(1).optional().describe(
|
|
3621
4277
|
"Absolute path to one knowledge base. Omit to stamp every pinned base."
|
|
3622
4278
|
),
|
|
3623
|
-
since:
|
|
4279
|
+
since: import_zod26.z.string().min(1).optional().describe(
|
|
3624
4280
|
"Prior digest, or path to a prior `stamp --json`; only moved bases return, with changed ids when the baseline is a file."
|
|
3625
4281
|
)
|
|
3626
4282
|
}),
|
|
@@ -3682,7 +4338,7 @@ async function readBaseline(since) {
|
|
|
3682
4338
|
if (DIGEST.test(since)) return { digest: since, byPath: /* @__PURE__ */ new Map() };
|
|
3683
4339
|
let parsed;
|
|
3684
4340
|
try {
|
|
3685
|
-
parsed = JSON.parse(await (0,
|
|
4341
|
+
parsed = JSON.parse(await (0, import_promises7.readFile)(since, "utf8"));
|
|
3686
4342
|
} catch {
|
|
3687
4343
|
throw new KbStampBaselineError(since);
|
|
3688
4344
|
}
|
|
@@ -3706,16 +4362,16 @@ async function readBaseline(since) {
|
|
|
3706
4362
|
}
|
|
3707
4363
|
|
|
3708
4364
|
// src/commands/status.ts
|
|
3709
|
-
var
|
|
4365
|
+
var import_zod27 = require("zod");
|
|
3710
4366
|
var statusCommand = define({
|
|
3711
4367
|
name: "status",
|
|
3712
4368
|
tool: "kb_status",
|
|
3713
4369
|
usage: "status <concept-id> <status>",
|
|
3714
4370
|
description: "Move a record's status. Compare-and-swap: a concurrent change fails instead of being overwritten.",
|
|
3715
|
-
input:
|
|
4371
|
+
input: import_zod27.z.object({
|
|
3716
4372
|
bundlePath,
|
|
3717
4373
|
conceptId,
|
|
3718
|
-
status:
|
|
4374
|
+
status: import_zod27.z.enum(KB_RECORD_STATUSES)
|
|
3719
4375
|
}),
|
|
3720
4376
|
fromArgv: (argv, path) => ({
|
|
3721
4377
|
bundlePath: path,
|
|
@@ -3730,13 +4386,13 @@ var statusCommand = define({
|
|
|
3730
4386
|
});
|
|
3731
4387
|
|
|
3732
4388
|
// src/commands/supersede.ts
|
|
3733
|
-
var
|
|
4389
|
+
var import_zod28 = require("zod");
|
|
3734
4390
|
var supersedeCommand = define({
|
|
3735
4391
|
name: "supersede",
|
|
3736
4392
|
tool: "kb_supersede",
|
|
3737
4393
|
usage: "supersede <concept-id> <replacement-id>",
|
|
3738
4394
|
description: "Mark a record superseded by another, linked in both directions. Use instead of editing a record whose meaning changed.",
|
|
3739
|
-
input:
|
|
4395
|
+
input: import_zod28.z.object({ bundlePath, conceptId, replacementId: conceptId }),
|
|
3740
4396
|
fromArgv: (argv, path) => ({
|
|
3741
4397
|
bundlePath: path,
|
|
3742
4398
|
conceptId: argv[1],
|
|
@@ -3750,16 +4406,16 @@ var supersedeCommand = define({
|
|
|
3750
4406
|
});
|
|
3751
4407
|
|
|
3752
4408
|
// src/commands/sync-instructions.ts
|
|
3753
|
-
var
|
|
4409
|
+
var import_zod29 = require("zod");
|
|
3754
4410
|
var syncInstructionsCommand = define({
|
|
3755
4411
|
name: "sync-instructions",
|
|
3756
4412
|
usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
|
|
3757
4413
|
description: "CLI-only: plant the kb_context block between sentinel comments in AGENTS.md or CLAUDE.md, idempotently.",
|
|
3758
|
-
input:
|
|
3759
|
-
file:
|
|
3760
|
-
budgetTokens:
|
|
3761
|
-
fullUnderTokens:
|
|
3762
|
-
profile:
|
|
4414
|
+
input: import_zod29.z.object({
|
|
4415
|
+
file: import_zod29.z.string().min(1).describe("The instruction file to edit in place."),
|
|
4416
|
+
budgetTokens: import_zod29.z.number().int().positive().optional(),
|
|
4417
|
+
fullUnderTokens: import_zod29.z.number().int().positive().optional(),
|
|
4418
|
+
profile: import_zod29.z.string().optional()
|
|
3763
4419
|
}),
|
|
3764
4420
|
fromArgv: (argv) => {
|
|
3765
4421
|
const budget = argvFlag(argv, "--budget");
|
|
@@ -3785,7 +4441,7 @@ var syncInstructionsCommand = define({
|
|
|
3785
4441
|
});
|
|
3786
4442
|
|
|
3787
4443
|
// src/commands/trace.ts
|
|
3788
|
-
var
|
|
4444
|
+
var import_zod30 = require("zod");
|
|
3789
4445
|
|
|
3790
4446
|
// src/trace.ts
|
|
3791
4447
|
var TRACE_EDGES = [
|
|
@@ -3841,11 +4497,11 @@ var traceCommand = define({
|
|
|
3841
4497
|
tool: "kb_trace",
|
|
3842
4498
|
usage: "trace <concept-id> [edges...]",
|
|
3843
4499
|
description: 'Timeline of how a position was reached, ordered by write time, following supersession, shared anchors and shared sources. Includes rejected, draft and superseded records \u2014 in a history they are the content. For "why is it like this"; kb_load answers "what holds now".',
|
|
3844
|
-
input:
|
|
4500
|
+
input: import_zod30.z.object({
|
|
3845
4501
|
bundlePath,
|
|
3846
4502
|
conceptId,
|
|
3847
|
-
edges:
|
|
3848
|
-
depth:
|
|
4503
|
+
edges: import_zod30.z.array(import_zod30.z.enum(TRACE_EDGES)).optional(),
|
|
4504
|
+
depth: import_zod30.z.number().int().positive().optional()
|
|
3849
4505
|
}),
|
|
3850
4506
|
fromArgv: (argv, path) => ({
|
|
3851
4507
|
bundlePath: path,
|
|
@@ -3867,37 +4523,37 @@ var traceCommand = define({
|
|
|
3867
4523
|
});
|
|
3868
4524
|
|
|
3869
4525
|
// src/commands/types.ts
|
|
3870
|
-
var
|
|
4526
|
+
var import_zod31 = require("zod");
|
|
3871
4527
|
var typesCommand = define({
|
|
3872
4528
|
name: "types",
|
|
3873
4529
|
tool: "kb_types",
|
|
3874
4530
|
usage: "types",
|
|
3875
4531
|
description: "The twelve record types with their purpose, body sections, and starting status. Read this before writing rather than guessing headings \u2014 a section the type does not define is rejected.",
|
|
3876
|
-
input:
|
|
4532
|
+
input: import_zod31.z.object({}),
|
|
3877
4533
|
fromArgv: () => ({}),
|
|
3878
4534
|
run: () => Promise.resolve(RECORD_TYPES)
|
|
3879
4535
|
});
|
|
3880
4536
|
|
|
3881
4537
|
// src/commands/unpin.ts
|
|
3882
|
-
var
|
|
4538
|
+
var import_zod32 = require("zod");
|
|
3883
4539
|
var unpinCommand = define({
|
|
3884
4540
|
name: "unpin",
|
|
3885
4541
|
tool: "kb_unpin",
|
|
3886
4542
|
usage: "unpin [bundle-path]",
|
|
3887
4543
|
description: "Remove a base from every manifest layer that holds it. Reports the layers touched.",
|
|
3888
|
-
input:
|
|
4544
|
+
input: import_zod32.z.object({ bundlePath }),
|
|
3889
4545
|
fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
|
|
3890
4546
|
run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
|
|
3891
4547
|
});
|
|
3892
4548
|
|
|
3893
4549
|
// src/commands/validate.ts
|
|
3894
|
-
var
|
|
4550
|
+
var import_zod33 = require("zod");
|
|
3895
4551
|
var validateCommand = define({
|
|
3896
4552
|
name: "validate",
|
|
3897
4553
|
tool: "kb_validate",
|
|
3898
4554
|
usage: "validate",
|
|
3899
4555
|
description: "Check pointers no single record can see: supersession links that disagree between the two records, typed causal links, and assumptions that cite sources. Each finding carries a severity: errors fail the exit code, warnings do not.",
|
|
3900
|
-
input:
|
|
4556
|
+
input: import_zod33.z.object({ bundlePath }),
|
|
3901
4557
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
3902
4558
|
run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
|
|
3903
4559
|
// Warnings never fail the exit code; every other severity does.
|
|
@@ -3907,16 +4563,16 @@ var validateCommand = define({
|
|
|
3907
4563
|
});
|
|
3908
4564
|
|
|
3909
4565
|
// src/commands/verify.ts
|
|
3910
|
-
var
|
|
4566
|
+
var import_zod34 = require("zod");
|
|
3911
4567
|
var verifyCommand = define({
|
|
3912
4568
|
name: "verify",
|
|
3913
4569
|
tool: "kb_verify",
|
|
3914
4570
|
usage: "verify <concept-id> --note <text>",
|
|
3915
4571
|
description: "Append a verified[] event: who checked, when, and what was found. Append-only. A record's own generator is refused unless the actor is `human:`-prefixed.",
|
|
3916
|
-
input:
|
|
4572
|
+
input: import_zod34.z.object({
|
|
3917
4573
|
bundlePath,
|
|
3918
4574
|
conceptId,
|
|
3919
|
-
note:
|
|
4575
|
+
note: import_zod34.z.string().refine((s) => s.trim().length > 0, {
|
|
3920
4576
|
message: "note must say what the check found"
|
|
3921
4577
|
})
|
|
3922
4578
|
}),
|
|
@@ -3936,15 +4592,15 @@ var verifyCommand = define({
|
|
|
3936
4592
|
});
|
|
3937
4593
|
|
|
3938
4594
|
// src/commands/write.ts
|
|
3939
|
-
var
|
|
4595
|
+
var import_zod35 = require("zod");
|
|
3940
4596
|
var writeCommand = define({
|
|
3941
4597
|
name: "write",
|
|
3942
4598
|
tool: "kb_write",
|
|
3943
4599
|
usage: "write <type> < record.json",
|
|
3944
4600
|
description: "Write one record. Search first \u2014 a duplicate concept id is rejected, not overwritten; kb_types lists each type's sections. An unsourced claim is an `assumption` with assumption: true, never a vague `fact`. Conflicting records get a `risk`, `open-question`, or superseding `decision`. Prefer a new short record over overloading one. Never delete; supersede.",
|
|
3945
|
-
input:
|
|
4601
|
+
input: import_zod35.z.object({
|
|
3946
4602
|
bundlePath,
|
|
3947
|
-
type:
|
|
4603
|
+
type: import_zod35.z.enum(KB_RECORD_TYPES),
|
|
3948
4604
|
input: composeInputSchema
|
|
3949
4605
|
}),
|
|
3950
4606
|
fromArgv: async (argv, path, stdin) => ({
|
|
@@ -3968,13 +4624,13 @@ var writeCommand = define({
|
|
|
3968
4624
|
});
|
|
3969
4625
|
|
|
3970
4626
|
// src/commands/write-decision.ts
|
|
3971
|
-
var
|
|
4627
|
+
var import_zod36 = require("zod");
|
|
3972
4628
|
var writeDecisionCommand = define({
|
|
3973
4629
|
name: "write-decision",
|
|
3974
4630
|
tool: "kb_write_decision",
|
|
3975
4631
|
usage: "write-decision < decision.json",
|
|
3976
4632
|
description: "Write a decision, with `alternative` (what was rejected and why) and `impact` as fields. Record one when a later reader would otherwise simplify the constraint away; skip when the diff already answers it. `sources` for material read, `anchors` for code, `relatedConceptIds` for records.",
|
|
3977
|
-
input:
|
|
4633
|
+
input: import_zod36.z.object({ bundlePath, input: decisionInputSchema }),
|
|
3978
4634
|
fromArgv: async (_argv, path, stdin) => ({
|
|
3979
4635
|
bundlePath: path,
|
|
3980
4636
|
input: JSON.parse(await stdin())
|
|
@@ -4030,8 +4686,8 @@ var KB_COMMANDS_BY_NAME = new Map(
|
|
|
4030
4686
|
);
|
|
4031
4687
|
|
|
4032
4688
|
// src/kb-store.ts
|
|
4033
|
-
var
|
|
4034
|
-
var
|
|
4689
|
+
var import_promises9 = require("fs/promises");
|
|
4690
|
+
var import_node_path11 = require("path");
|
|
4035
4691
|
|
|
4036
4692
|
// src/markdown.ts
|
|
4037
4693
|
var import_gray_matter = __toESM(require("gray-matter"), 1);
|
|
@@ -4059,15 +4715,15 @@ function parseMarkdownWithFrontmatter(text, schema) {
|
|
|
4059
4715
|
}
|
|
4060
4716
|
|
|
4061
4717
|
// src/kb-stamp.ts
|
|
4062
|
-
var
|
|
4063
|
-
function
|
|
4064
|
-
return (0,
|
|
4718
|
+
var import_node_crypto4 = require("crypto");
|
|
4719
|
+
function sha2563(contents) {
|
|
4720
|
+
return (0, import_node_crypto4.createHash)("sha256").update(contents).digest("hex");
|
|
4065
4721
|
}
|
|
4066
4722
|
function bundleStamp(records, superseded) {
|
|
4067
4723
|
const entries = [
|
|
4068
4724
|
...records.map((hit) => ({
|
|
4069
4725
|
conceptId: hit.record.conceptId,
|
|
4070
|
-
digest: `current:${
|
|
4726
|
+
digest: `current:${sha2563(
|
|
4071
4727
|
stringifyMarkdownWithFrontmatter(
|
|
4072
4728
|
hit.record.body,
|
|
4073
4729
|
hit.record.frontmatter
|
|
@@ -4076,11 +4732,11 @@ function bundleStamp(records, superseded) {
|
|
|
4076
4732
|
})),
|
|
4077
4733
|
...superseded.map((entry) => ({
|
|
4078
4734
|
conceptId: entry.conceptId,
|
|
4079
|
-
digest: `superseded:${
|
|
4735
|
+
digest: `superseded:${sha2563(JSON.stringify(entry))}`
|
|
4080
4736
|
}))
|
|
4081
4737
|
].sort((a, b) => a.conceptId < b.conceptId ? -1 : 1);
|
|
4082
4738
|
return {
|
|
4083
|
-
digest:
|
|
4739
|
+
digest: sha2563(
|
|
4084
4740
|
entries.map((entry) => `${entry.conceptId}:${entry.digest}`).join("\n")
|
|
4085
4741
|
),
|
|
4086
4742
|
records: entries
|
|
@@ -4091,8 +4747,8 @@ function bundleDigest(records, superseded) {
|
|
|
4091
4747
|
}
|
|
4092
4748
|
|
|
4093
4749
|
// src/search-index.ts
|
|
4094
|
-
var
|
|
4095
|
-
var
|
|
4750
|
+
var import_promises8 = require("fs/promises");
|
|
4751
|
+
var import_node_path10 = require("path");
|
|
4096
4752
|
var SEARCH_INDEX_FILE = ".index.sqlite";
|
|
4097
4753
|
var COLLECTION = "kb";
|
|
4098
4754
|
async function searchBase(bundlePath2, query, options = {}) {
|
|
@@ -4101,7 +4757,7 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
4101
4757
|
let store = null;
|
|
4102
4758
|
try {
|
|
4103
4759
|
store = await qmd.createStore({
|
|
4104
|
-
dbPath: (0,
|
|
4760
|
+
dbPath: (0, import_node_path10.join)(bundlePath2, SEARCH_INDEX_FILE),
|
|
4105
4761
|
config: {
|
|
4106
4762
|
collections: {
|
|
4107
4763
|
[COLLECTION]: {
|
|
@@ -4136,7 +4792,7 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
4136
4792
|
}
|
|
4137
4793
|
}
|
|
4138
4794
|
async function isStale(bundlePath2) {
|
|
4139
|
-
const indexAt = await (0,
|
|
4795
|
+
const indexAt = await (0, import_promises8.stat)((0, import_node_path10.join)(bundlePath2, SEARCH_INDEX_FILE)).then((s) => s.mtimeMs).catch(() => 0);
|
|
4140
4796
|
if (!indexAt) return true;
|
|
4141
4797
|
const { readdir: readdir2 } = await import("fs/promises");
|
|
4142
4798
|
const names = (await readdir2(bundlePath2).catch(() => [])).filter(
|
|
@@ -4145,7 +4801,7 @@ async function isStale(bundlePath2) {
|
|
|
4145
4801
|
let stale = false;
|
|
4146
4802
|
await mapLimit(names, DEFAULT_IO_CONCURRENCY, async (name) => {
|
|
4147
4803
|
if (stale) return;
|
|
4148
|
-
const at = await (0,
|
|
4804
|
+
const at = await (0, import_promises8.stat)((0, import_node_path10.join)(bundlePath2, name)).then((s) => s.mtimeMs).catch(() => 0);
|
|
4149
4805
|
if (at > indexAt) stale = true;
|
|
4150
4806
|
});
|
|
4151
4807
|
return stale;
|
|
@@ -4259,8 +4915,8 @@ function byRank(left, right) {
|
|
|
4259
4915
|
) || left.record.conceptId.localeCompare(right.record.conceptId);
|
|
4260
4916
|
}
|
|
4261
4917
|
function typeRank(record) {
|
|
4262
|
-
const
|
|
4263
|
-
return
|
|
4918
|
+
const index2 = TYPE_PRIORITY.indexOf(record.frontmatter.type);
|
|
4919
|
+
return index2 === -1 ? TYPE_PRIORITY.length : index2;
|
|
4264
4920
|
}
|
|
4265
4921
|
|
|
4266
4922
|
// src/kb-links/inbound.ts
|
|
@@ -4423,7 +5079,7 @@ function appendUnionMergeLine(contents) {
|
|
|
4423
5079
|
}
|
|
4424
5080
|
|
|
4425
5081
|
// src/kb-store.ts
|
|
4426
|
-
var KB_DIR = (0,
|
|
5082
|
+
var KB_DIR = (0, import_node_path11.join)(".strauss", "kb");
|
|
4427
5083
|
var STORE_OWNED = /* @__PURE__ */ new Set([INDEX_FILE, LOG_FILE, SEARCH_INDEX_FILE]);
|
|
4428
5084
|
var DEFAULT_LOAD_BUDGET = 25e3;
|
|
4429
5085
|
var KbStore = class {
|
|
@@ -4454,7 +5110,7 @@ var KbStore = class {
|
|
|
4454
5110
|
const conceptId2 = `${input.type}.${input.slug}`;
|
|
4455
5111
|
const root = this.root(bundlePath2);
|
|
4456
5112
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
4457
|
-
await (0,
|
|
5113
|
+
await (0, import_promises9.mkdir)(root, { recursive: true });
|
|
4458
5114
|
await this.publish(
|
|
4459
5115
|
target,
|
|
4460
5116
|
stringifyMarkdownWithFrontmatter(input.body, frontmatter),
|
|
@@ -4493,7 +5149,7 @@ var KbStore = class {
|
|
|
4493
5149
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
4494
5150
|
let raw;
|
|
4495
5151
|
try {
|
|
4496
|
-
raw = await (0,
|
|
5152
|
+
raw = await (0, import_promises9.readFile)(target, "utf8");
|
|
4497
5153
|
} catch {
|
|
4498
5154
|
return null;
|
|
4499
5155
|
}
|
|
@@ -4510,7 +5166,7 @@ var KbStore = class {
|
|
|
4510
5166
|
const root = this.root(bundlePath2);
|
|
4511
5167
|
let names;
|
|
4512
5168
|
try {
|
|
4513
|
-
names = await (0,
|
|
5169
|
+
names = await (0, import_promises9.readdir)(root);
|
|
4514
5170
|
} catch {
|
|
4515
5171
|
return [];
|
|
4516
5172
|
}
|
|
@@ -4518,7 +5174,7 @@ var KbStore = class {
|
|
|
4518
5174
|
const records = await mapLimit(
|
|
4519
5175
|
wanted,
|
|
4520
5176
|
DEFAULT_IO_CONCURRENCY,
|
|
4521
|
-
async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await (0,
|
|
5177
|
+
async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await (0, import_promises9.readFile)((0, import_node_path11.join)(root, name), "utf8"))
|
|
4522
5178
|
);
|
|
4523
5179
|
return records.filter((record) => record !== null);
|
|
4524
5180
|
}
|
|
@@ -4675,7 +5331,7 @@ ${answer}
|
|
|
4675
5331
|
if (found.length) return found;
|
|
4676
5332
|
}
|
|
4677
5333
|
const lowered = needle.toLowerCase();
|
|
4678
|
-
return bundle.filter((record) =>
|
|
5334
|
+
return bundle.filter((record) => matches2(record, lowered));
|
|
4679
5335
|
}
|
|
4680
5336
|
/**
|
|
4681
5337
|
* Anchor drift over the records about to be handed back. Like the search
|
|
@@ -4845,11 +5501,11 @@ ${answer}
|
|
|
4845
5501
|
async readIndex(bundlePath2) {
|
|
4846
5502
|
const root = this.root(bundlePath2);
|
|
4847
5503
|
const expected = renderIndex(await this.list(bundlePath2));
|
|
4848
|
-
const stored = await (0,
|
|
5504
|
+
const stored = await (0, import_promises9.readFile)((0, import_node_path11.join)(root, INDEX_FILE), "utf8").catch(
|
|
4849
5505
|
() => null
|
|
4850
5506
|
);
|
|
4851
5507
|
if (indexIsStale(stored, expected)) {
|
|
4852
|
-
await this.publish((0,
|
|
5508
|
+
await this.publish((0, import_node_path11.join)(root, INDEX_FILE), expected, true, INDEX_FILE);
|
|
4853
5509
|
this.logger.info?.({
|
|
4854
5510
|
operation: "kb.index.repair",
|
|
4855
5511
|
bundlePath: root,
|
|
@@ -4866,8 +5522,8 @@ ${answer}
|
|
|
4866
5522
|
* knows which agent touched what. So a bad line is surfaced and left alone.
|
|
4867
5523
|
*/
|
|
4868
5524
|
async readLog(bundlePath2) {
|
|
4869
|
-
const raw = await (0,
|
|
4870
|
-
(0,
|
|
5525
|
+
const raw = await (0, import_promises9.readFile)(
|
|
5526
|
+
(0, import_node_path11.join)(this.root(bundlePath2), LOG_FILE),
|
|
4871
5527
|
"utf8"
|
|
4872
5528
|
).catch(() => "");
|
|
4873
5529
|
const result = parseLog(raw);
|
|
@@ -4918,15 +5574,15 @@ ${answer}
|
|
|
4918
5574
|
}
|
|
4919
5575
|
async mutate(bundlePath2, conceptId2, change, entry, changeBody = (body) => body) {
|
|
4920
5576
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
4921
|
-
const before = await (0,
|
|
5577
|
+
const before = await (0, import_promises9.readFile)(target, "utf8").catch(() => null);
|
|
4922
5578
|
if (before === null) throw new KbRecordNotFoundError(conceptId2);
|
|
4923
5579
|
const parsed = this.parse(conceptId2, before);
|
|
4924
5580
|
if (!parsed) throw new KbRecordNotFoundError(conceptId2);
|
|
4925
5581
|
const frontmatter = change(parsed.frontmatter);
|
|
4926
5582
|
const body = changeBody(parsed.body);
|
|
4927
5583
|
const contents = stringifyMarkdownWithFrontmatter(body, frontmatter);
|
|
4928
|
-
const witness = await (0,
|
|
4929
|
-
if (witness === null ||
|
|
5584
|
+
const witness = await (0, import_promises9.readFile)(target, "utf8").catch(() => null);
|
|
5585
|
+
if (witness === null || sha2563(witness) !== sha2563(before)) {
|
|
4930
5586
|
throw new KbWriteConflictError(conceptId2);
|
|
4931
5587
|
}
|
|
4932
5588
|
await this.publish(target, contents, true, conceptId2);
|
|
@@ -4951,20 +5607,20 @@ ${answer}
|
|
|
4951
5607
|
*/
|
|
4952
5608
|
async publish(target, contents, overwrite, conceptId2) {
|
|
4953
5609
|
const staging = `${target}.${process.pid}.tmp`;
|
|
4954
|
-
await (0,
|
|
5610
|
+
await (0, import_promises9.writeFile)(staging, contents, "utf8");
|
|
4955
5611
|
try {
|
|
4956
5612
|
if (overwrite) {
|
|
4957
|
-
await (0,
|
|
5613
|
+
await (0, import_promises9.rename)(staging, target);
|
|
4958
5614
|
return;
|
|
4959
5615
|
}
|
|
4960
|
-
await (0,
|
|
5616
|
+
await (0, import_promises9.link)(staging, target);
|
|
4961
5617
|
} catch (error) {
|
|
4962
5618
|
if (error.code === "EEXIST") {
|
|
4963
5619
|
throw new KbRecordAlreadyExistsError(conceptId2);
|
|
4964
5620
|
}
|
|
4965
5621
|
throw error;
|
|
4966
5622
|
} finally {
|
|
4967
|
-
await (0,
|
|
5623
|
+
await (0, import_promises9.unlink)(staging).catch(() => void 0);
|
|
4968
5624
|
}
|
|
4969
5625
|
}
|
|
4970
5626
|
/**
|
|
@@ -5008,18 +5664,18 @@ ${answer}
|
|
|
5008
5664
|
* file must not fail the mutation it guards.
|
|
5009
5665
|
*/
|
|
5010
5666
|
async ensureGitattributes(root) {
|
|
5011
|
-
const target = (0,
|
|
5667
|
+
const target = (0, import_node_path11.join)(root, GITATTRIBUTES_FILE);
|
|
5012
5668
|
try {
|
|
5013
5669
|
let existing;
|
|
5014
5670
|
try {
|
|
5015
|
-
existing = await (0,
|
|
5671
|
+
existing = await (0, import_promises9.readFile)(target, "utf8");
|
|
5016
5672
|
} catch (error) {
|
|
5017
5673
|
if (error.code !== "ENOENT") throw error;
|
|
5018
5674
|
existing = null;
|
|
5019
5675
|
}
|
|
5020
5676
|
if (existing === null) {
|
|
5021
5677
|
try {
|
|
5022
|
-
await (0,
|
|
5678
|
+
await (0, import_promises9.writeFile)(target, appendUnionMergeLine(""), {
|
|
5023
5679
|
encoding: "utf8",
|
|
5024
5680
|
flag: "wx"
|
|
5025
5681
|
});
|
|
@@ -5040,7 +5696,7 @@ ${answer}
|
|
|
5040
5696
|
return;
|
|
5041
5697
|
}
|
|
5042
5698
|
if (!hasMergeDeclaration(existing)) {
|
|
5043
|
-
await (0,
|
|
5699
|
+
await (0, import_promises9.appendFile)(target, appendUnionMergeLine(existing), "utf8");
|
|
5044
5700
|
this.logger.info?.({
|
|
5045
5701
|
operation: "kb.gitattributes.ensure",
|
|
5046
5702
|
bundlePath: root,
|
|
@@ -5059,7 +5715,7 @@ ${answer}
|
|
|
5059
5715
|
async record(root, entry) {
|
|
5060
5716
|
await this.ensureGitattributes(root);
|
|
5061
5717
|
const line = renderLogEntry({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
|
|
5062
|
-
await (0,
|
|
5718
|
+
await (0, import_promises9.appendFile)((0, import_node_path11.join)(root, LOG_FILE), line, "utf8").catch((error) => {
|
|
5063
5719
|
this.logger.warn?.({
|
|
5064
5720
|
operation: "kb.log.append",
|
|
5065
5721
|
outcome: "failed",
|
|
@@ -5085,18 +5741,18 @@ ${answer}
|
|
|
5085
5741
|
};
|
|
5086
5742
|
}
|
|
5087
5743
|
root(bundlePath2) {
|
|
5088
|
-
return (0,
|
|
5744
|
+
return (0, import_node_path11.resolve)(bundlePath2);
|
|
5089
5745
|
}
|
|
5090
5746
|
// Concept ids are `<type>.<slug>` and map to a single file directly under the
|
|
5091
5747
|
// bundle root; anything carrying a separator would escape it.
|
|
5092
5748
|
recordPath(bundlePath2, conceptId2) {
|
|
5093
|
-
if (conceptId2.includes(
|
|
5749
|
+
if (conceptId2.includes(import_node_path11.sep) || conceptId2.includes("/")) {
|
|
5094
5750
|
throw new KbInvalidConceptIdError(
|
|
5095
5751
|
"concept id must not contain a path separator",
|
|
5096
5752
|
{ conceptId: conceptId2 }
|
|
5097
5753
|
);
|
|
5098
5754
|
}
|
|
5099
|
-
return (0,
|
|
5755
|
+
return (0, import_node_path11.join)(this.root(bundlePath2), `${conceptId2}.md`);
|
|
5100
5756
|
}
|
|
5101
5757
|
};
|
|
5102
5758
|
function estimateTokens(record) {
|
|
@@ -5123,7 +5779,7 @@ function stub(hit) {
|
|
|
5123
5779
|
at: hit.record.frontmatter.generated?.at ?? null
|
|
5124
5780
|
};
|
|
5125
5781
|
}
|
|
5126
|
-
function
|
|
5782
|
+
function matches2(record, needle) {
|
|
5127
5783
|
const { title, description } = record.frontmatter;
|
|
5128
5784
|
return [record.conceptId, title, description, record.body].some(
|
|
5129
5785
|
(field) => field?.toLowerCase().includes(needle)
|
|
@@ -5136,7 +5792,7 @@ function normalizeActor(id) {
|
|
|
5136
5792
|
}
|
|
5137
5793
|
|
|
5138
5794
|
// src/version.ts
|
|
5139
|
-
var VERSION = true ? "0.1.
|
|
5795
|
+
var VERSION = true ? "0.1.17" : "0.0.0-dev";
|
|
5140
5796
|
|
|
5141
5797
|
// src/cli.ts
|
|
5142
5798
|
async function runKbCli(argv) {
|
|
@@ -5200,7 +5856,7 @@ function takeLiteral(argv) {
|
|
|
5200
5856
|
function takeBundle(argv) {
|
|
5201
5857
|
const at = argv.indexOf("--bundle");
|
|
5202
5858
|
if (at === -1) {
|
|
5203
|
-
return { bundle: (0,
|
|
5859
|
+
return { bundle: (0, import_node_path12.join)(process.cwd(), KB_DIR), explicit: false, rest: argv };
|
|
5204
5860
|
}
|
|
5205
5861
|
const bundle = argv[at + 1];
|
|
5206
5862
|
if (!bundle) die("--bundle requires a path");
|
|
@@ -5230,14 +5886,14 @@ function summarise(description) {
|
|
|
5230
5886
|
return sentence.length > 78 ? `${sentence.slice(0, 75)}\u2026` : sentence;
|
|
5231
5887
|
}
|
|
5232
5888
|
function usage() {
|
|
5233
|
-
const
|
|
5889
|
+
const width2 = Math.max(...KB_COMMANDS.map((command) => command.usage.length));
|
|
5234
5890
|
return [
|
|
5235
5891
|
"strauss-kb \u2014 knowledge base commands",
|
|
5236
5892
|
"",
|
|
5237
5893
|
"Usage: strauss-kb [--bundle PATH] <command> [args]",
|
|
5238
5894
|
"",
|
|
5239
5895
|
...KB_COMMANDS.map(
|
|
5240
|
-
(command) => ` ${command.usage.padEnd(
|
|
5896
|
+
(command) => ` ${command.usage.padEnd(width2)} ${summarise(command.description)}`
|
|
5241
5897
|
),
|
|
5242
5898
|
"",
|
|
5243
5899
|
` --bundle PATH defaults to ./${KB_DIR}`,
|