@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/index.cjs
CHANGED
|
@@ -78,6 +78,7 @@ __export(index_exports, {
|
|
|
78
78
|
RECORD_TYPES: () => RECORD_TYPES,
|
|
79
79
|
SEARCH_INDEX_FILE: () => SEARCH_INDEX_FILE,
|
|
80
80
|
TRACE_EDGES: () => TRACE_EDGES,
|
|
81
|
+
TreeSitterResolver: () => TreeSitterResolver,
|
|
81
82
|
adjudicate: () => adjudicate,
|
|
82
83
|
anchorFilePath: () => anchorFilePath,
|
|
83
84
|
assertBaseNotFrozen: () => assertBaseNotFrozen,
|
|
@@ -92,9 +93,14 @@ __export(index_exports, {
|
|
|
92
93
|
contextProfileBudgets: () => contextProfileBudgets,
|
|
93
94
|
createKbMcpServer: () => createKbMcpServer,
|
|
94
95
|
decisionInputSchema: () => decisionInputSchema,
|
|
96
|
+
defaultAnchorResolvers: () => defaultAnchorResolvers,
|
|
95
97
|
detectAnchorDrift: () => detectAnchorDrift,
|
|
96
98
|
doctor: () => doctor,
|
|
97
99
|
edgeNeighbours: () => edgeNeighbours,
|
|
100
|
+
ensureGrammar: () => ensureGrammar,
|
|
101
|
+
grammarHints: () => grammarHints,
|
|
102
|
+
grammarManifest: () => grammarManifest,
|
|
103
|
+
grammarsCacheRoot: () => grammarsCacheRoot,
|
|
98
104
|
hashAnchorText: () => hashAnchorText,
|
|
99
105
|
impact: () => impact,
|
|
100
106
|
inboundIndex: () => inboundIndex,
|
|
@@ -112,6 +118,7 @@ __export(index_exports, {
|
|
|
112
118
|
kbRecordFrontmatterSchema: () => kbRecordFrontmatterSchema,
|
|
113
119
|
kbSourceSchema: () => kbSourceSchema,
|
|
114
120
|
kbVerifiedEventSchema: () => kbVerifiedEventSchema,
|
|
121
|
+
languageForFile: () => languageForFile,
|
|
115
122
|
listPins: () => listPins,
|
|
116
123
|
loadQmd: () => loadQmd,
|
|
117
124
|
matchToDiff: () => matchToDiff,
|
|
@@ -121,6 +128,7 @@ __export(index_exports, {
|
|
|
121
128
|
parseLog: () => parseLog,
|
|
122
129
|
parseMarkdownWithFrontmatter: () => parseMarkdownWithFrontmatter,
|
|
123
130
|
pinBase: () => pinBase,
|
|
131
|
+
prepareResolvers: () => prepareResolvers,
|
|
124
132
|
readMergedPins: () => readMergedPins,
|
|
125
133
|
readPinsLayer: () => readPinsLayer,
|
|
126
134
|
readRemoteAnchors: () => readRemoteAnchors,
|
|
@@ -131,6 +139,7 @@ __export(index_exports, {
|
|
|
131
139
|
renderLogEntry: () => renderLogEntry,
|
|
132
140
|
repoCacheDir: () => repoCacheDir,
|
|
133
141
|
resolveAnchor: () => resolveAnchor,
|
|
142
|
+
resolveAnchorSpan: () => resolveAnchorSpan,
|
|
134
143
|
resolveHeads: () => resolveHeads,
|
|
135
144
|
resolveHits: () => resolveHits,
|
|
136
145
|
resolvePinPath: () => resolvePinPath,
|
|
@@ -143,14 +152,19 @@ __export(index_exports, {
|
|
|
143
152
|
syncInstructions: () => syncInstructions,
|
|
144
153
|
toHookJson: () => toHookJson,
|
|
145
154
|
trace: () => trace,
|
|
155
|
+
treeSitterLanguages: () => treeSitterLanguages,
|
|
146
156
|
unpinBase: () => unpinBase,
|
|
147
157
|
validateBundle: () => validateBundle
|
|
148
158
|
});
|
|
149
159
|
module.exports = __toCommonJS(index_exports);
|
|
150
160
|
|
|
161
|
+
// ../../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
|
|
162
|
+
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;
|
|
163
|
+
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
164
|
+
|
|
151
165
|
// src/kb-store.ts
|
|
152
|
-
var
|
|
153
|
-
var
|
|
166
|
+
var import_promises6 = require("fs/promises");
|
|
167
|
+
var import_node_path7 = require("path");
|
|
154
168
|
|
|
155
169
|
// src/concurrency.ts
|
|
156
170
|
var DEFAULT_IO_CONCURRENCY = 16;
|
|
@@ -251,7 +265,13 @@ var kbAnchorSchema = import_zod.z.object({
|
|
|
251
265
|
/** ISO 8601 timestamp of the last successful resolution. */
|
|
252
266
|
resolved_at: import_zod.z.string().min(1).optional(),
|
|
253
267
|
/** Line count of the text the hash was taken over. */
|
|
254
|
-
lines: import_zod.z.number().int().positive().optional()
|
|
268
|
+
lines: import_zod.z.number().int().positive().optional(),
|
|
269
|
+
/**
|
|
270
|
+
* Which resolver produced the hashed span. Absent means an anchor stamped
|
|
271
|
+
* before resolvers were named, which is read as `regex` — the only one
|
|
272
|
+
* there was. A hash from a different resolver is drift, not a match.
|
|
273
|
+
*/
|
|
274
|
+
resolver: import_zod.z.enum(["tree-sitter", "regex"]).optional()
|
|
255
275
|
}).strict();
|
|
256
276
|
var kbLinkSchema = import_zod.z.object({
|
|
257
277
|
target: import_zod.z.string().min(1),
|
|
@@ -818,18 +838,18 @@ async function readOneRepo(repo, url, declared, context) {
|
|
|
818
838
|
if (!repoUrlIsSafe(url)) return all({ ok: false, reason: "repo-invalid" });
|
|
819
839
|
const cache = cachePathFor(repo, context.cacheDir);
|
|
820
840
|
if (!cache) return all({ ok: false, reason: "remote-unreachable" });
|
|
821
|
-
const
|
|
841
|
+
const rejected2 = /* @__PURE__ */ new Map();
|
|
822
842
|
const usable = [];
|
|
823
843
|
for (const want of wants) {
|
|
824
844
|
const reason = wantReason(want);
|
|
825
845
|
if (reason)
|
|
826
|
-
|
|
846
|
+
rejected2.set(wantKey(repo, want.ref, want.file), { ok: false, reason });
|
|
827
847
|
else usable.push(want);
|
|
828
848
|
}
|
|
829
|
-
if (!usable.length) return
|
|
849
|
+
if (!usable.length) return rejected2;
|
|
830
850
|
wants = usable;
|
|
831
851
|
const opened = await openCache(cache, url, context);
|
|
832
|
-
if (opened) return new Map([...
|
|
852
|
+
if (opened) return new Map([...rejected2, ...all(opened)]);
|
|
833
853
|
const wantsDefault = wants.some((want) => want.ref === void 0);
|
|
834
854
|
const branch = wantsDefault ? await defaultBranch(cache, context) : {};
|
|
835
855
|
const revs = /* @__PURE__ */ new Map();
|
|
@@ -856,7 +876,7 @@ async function readOneRepo(repo, url, declared, context) {
|
|
|
856
876
|
}
|
|
857
877
|
);
|
|
858
878
|
return new Map([
|
|
859
|
-
...
|
|
879
|
+
...rejected2,
|
|
860
880
|
...wants.map(
|
|
861
881
|
(want, at) => [wantKey(repo, want.ref, want.file), reads[at]]
|
|
862
882
|
)
|
|
@@ -908,13 +928,13 @@ async function defaultBranch(cache, context) {
|
|
|
908
928
|
if (!listed.ok) {
|
|
909
929
|
const reason = transportReason(listed.stderr);
|
|
910
930
|
if (reason !== "ref-not-found") {
|
|
911
|
-
const
|
|
912
|
-
return
|
|
931
|
+
const cached3 = await cachedBranch(cache);
|
|
932
|
+
return cached3 ? { name: cached3 } : { reason };
|
|
913
933
|
}
|
|
914
934
|
}
|
|
915
935
|
}
|
|
916
|
-
const
|
|
917
|
-
if (
|
|
936
|
+
const cached2 = await cachedBranch(cache);
|
|
937
|
+
if (cached2) return { name: cached2 };
|
|
918
938
|
return {
|
|
919
939
|
reason: context.offline ? "remote-unreachable" : "default-branch-unknown"
|
|
920
940
|
};
|
|
@@ -941,8 +961,8 @@ async function ensureRev(cache, rev, context) {
|
|
|
941
961
|
cwd: cache
|
|
942
962
|
}
|
|
943
963
|
);
|
|
944
|
-
const
|
|
945
|
-
if (
|
|
964
|
+
const cached2 = have.ok && have.stdout.trim().length > 0;
|
|
965
|
+
if (cached2 && (context.offline || IMMUTABLE_REV.test(rev))) return void 0;
|
|
946
966
|
if (context.offline) return { ok: false, reason: "remote-unreachable" };
|
|
947
967
|
const fetched = await git(
|
|
948
968
|
[
|
|
@@ -958,7 +978,7 @@ async function ensureRev(cache, rev, context) {
|
|
|
958
978
|
);
|
|
959
979
|
if (!fetched.ok) {
|
|
960
980
|
const reason = transportReason(fetched.stderr);
|
|
961
|
-
if (
|
|
981
|
+
if (cached2 && reason !== "ref-not-found") return void 0;
|
|
962
982
|
return { ok: false, reason };
|
|
963
983
|
}
|
|
964
984
|
const head = await git(["rev-parse", "FETCH_HEAD"], { cwd: cache });
|
|
@@ -1058,8 +1078,8 @@ function resolveHeads(from, byId) {
|
|
|
1058
1078
|
while (queue.length) {
|
|
1059
1079
|
const current = queue.shift();
|
|
1060
1080
|
const next = successors(current, byId);
|
|
1061
|
-
for (const
|
|
1062
|
-
warnings.push({ kind: "broken-chain", missing });
|
|
1081
|
+
for (const missing2 of next.missing) {
|
|
1082
|
+
warnings.push({ kind: "broken-chain", missing: missing2 });
|
|
1063
1083
|
}
|
|
1064
1084
|
if (!next.records.length) {
|
|
1065
1085
|
if (current.conceptId !== from.conceptId)
|
|
@@ -1090,13 +1110,13 @@ function successors(record, byId) {
|
|
|
1090
1110
|
}
|
|
1091
1111
|
}
|
|
1092
1112
|
const records = [];
|
|
1093
|
-
const
|
|
1113
|
+
const missing2 = [];
|
|
1094
1114
|
for (const id of ids) {
|
|
1095
1115
|
const found = byId.get(id);
|
|
1096
1116
|
if (found) records.push(found);
|
|
1097
|
-
else
|
|
1117
|
+
else missing2.push(id);
|
|
1098
1118
|
}
|
|
1099
|
-
return { records, missing };
|
|
1119
|
+
return { records, missing: missing2 };
|
|
1100
1120
|
}
|
|
1101
1121
|
|
|
1102
1122
|
// src/kb-stamp.ts
|
|
@@ -1222,63 +1242,580 @@ async function readAnchorFiles(files, read, concurrency = DEFAULT_IO_CONCURRENCY
|
|
|
1222
1242
|
}
|
|
1223
1243
|
|
|
1224
1244
|
// src/anchor-resolver/resolver.ts
|
|
1245
|
+
var import_node_crypto4 = require("crypto");
|
|
1246
|
+
|
|
1247
|
+
// src/tree-sitter-resolver/languages.ts
|
|
1248
|
+
var import_node_path5 = require("path");
|
|
1249
|
+
|
|
1250
|
+
// src/grammars/index.ts
|
|
1251
|
+
var import_promises4 = require("fs/promises");
|
|
1252
|
+
|
|
1253
|
+
// src/grammars/store.ts
|
|
1225
1254
|
var import_node_crypto2 = require("crypto");
|
|
1255
|
+
var import_promises3 = require("fs/promises");
|
|
1256
|
+
var import_node_os2 = require("os");
|
|
1257
|
+
var import_node_path3 = require("path");
|
|
1258
|
+
function grammarsCacheRoot(override) {
|
|
1259
|
+
return override ?? process.env["STRAUSS_KB_GRAMMARS_DIR"] ?? (0, import_node_path3.join)((0, import_node_os2.homedir)(), ".strauss", "grammars");
|
|
1260
|
+
}
|
|
1261
|
+
function grammarCachePath(root, language, sha2564, extension = "wasm") {
|
|
1262
|
+
return (0, import_node_path3.join)(root, language, `${sha2564.slice(0, 12)}.${extension}`);
|
|
1263
|
+
}
|
|
1264
|
+
function sha2562(bytes) {
|
|
1265
|
+
return (0, import_node_crypto2.createHash)("sha256").update(bytes).digest("hex");
|
|
1266
|
+
}
|
|
1267
|
+
function matches(bytes, entry) {
|
|
1268
|
+
if (entry.bytes !== void 0 && bytes.byteLength !== entry.bytes)
|
|
1269
|
+
return false;
|
|
1270
|
+
return sha2562(bytes) === entry.sha256;
|
|
1271
|
+
}
|
|
1272
|
+
async function verifyCached(path, entry) {
|
|
1273
|
+
let bytes;
|
|
1274
|
+
try {
|
|
1275
|
+
bytes = await (0, import_promises3.readFile)(path);
|
|
1276
|
+
} catch {
|
|
1277
|
+
return false;
|
|
1278
|
+
}
|
|
1279
|
+
if (matches(bytes, entry)) return true;
|
|
1280
|
+
await (0, import_promises3.rm)(path, { force: true });
|
|
1281
|
+
return false;
|
|
1282
|
+
}
|
|
1283
|
+
async function writeCached(path, bytes) {
|
|
1284
|
+
await (0, import_promises3.mkdir)((0, import_node_path3.dirname)(path), { recursive: true });
|
|
1285
|
+
const temporary = `${path}.${process.pid}.${(0, import_node_crypto2.randomBytes)(4).toString("hex")}.tmp`;
|
|
1286
|
+
try {
|
|
1287
|
+
await (0, import_promises3.writeFile)(temporary, bytes);
|
|
1288
|
+
await (0, import_promises3.rename)(temporary, path);
|
|
1289
|
+
} catch (error) {
|
|
1290
|
+
await (0, import_promises3.rm)(temporary, { force: true });
|
|
1291
|
+
throw error;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// src/grammars/fetch.ts
|
|
1296
|
+
var ATTEMPTS = 3;
|
|
1297
|
+
var BACKOFF_MS = 250;
|
|
1298
|
+
function grammarUrl(url, override) {
|
|
1299
|
+
const base2 = grammarsBaseUrl(override);
|
|
1300
|
+
if (!base2) return url;
|
|
1301
|
+
let root = base2;
|
|
1302
|
+
while (root.endsWith("/")) root = root.slice(0, -1);
|
|
1303
|
+
const pinned = new URL(url);
|
|
1304
|
+
return `${root}${pinned.pathname}${pinned.search}`;
|
|
1305
|
+
}
|
|
1306
|
+
function grammarsBaseUrl(override) {
|
|
1307
|
+
return override ?? process.env["STRAUSS_KB_GRAMMARS_URL"];
|
|
1308
|
+
}
|
|
1309
|
+
async function downloadPart(url, name, entry, options = {}) {
|
|
1310
|
+
const log = options.log ?? ((line) => void process.stderr.write(line));
|
|
1311
|
+
const weight = entry.bytes === void 0 ? "" : ` (${size(entry.bytes)} from manifest)`;
|
|
1312
|
+
log(`strauss-kb: downloading ${name}${weight} from ${url}
|
|
1313
|
+
`);
|
|
1314
|
+
let cause = "";
|
|
1315
|
+
for (let attempt = 1; attempt <= ATTEMPTS; attempt++) {
|
|
1316
|
+
const outcome = await attemptDownload(url, entry, options.fetchTimeoutMs);
|
|
1317
|
+
if ("bytes" in outcome) return outcome;
|
|
1318
|
+
cause = outcome.cause;
|
|
1319
|
+
log(
|
|
1320
|
+
`strauss-kb: ${name} attempt ${attempt}/${ATTEMPTS} failed: ${cause}
|
|
1321
|
+
`
|
|
1322
|
+
);
|
|
1323
|
+
if (!outcome.retry) break;
|
|
1324
|
+
if (attempt < ATTEMPTS) await pause(BACKOFF_MS * attempt);
|
|
1325
|
+
}
|
|
1326
|
+
log(`strauss-kb: ${name} not downloaded: ${cause}
|
|
1327
|
+
`);
|
|
1328
|
+
return { cause };
|
|
1329
|
+
}
|
|
1330
|
+
async function attemptDownload(url, entry, timeoutMs) {
|
|
1331
|
+
try {
|
|
1332
|
+
const response = await fetch(url, {
|
|
1333
|
+
signal: AbortSignal.timeout(fetchTimeoutMs(timeoutMs))
|
|
1334
|
+
});
|
|
1335
|
+
if (!response.ok) {
|
|
1336
|
+
return {
|
|
1337
|
+
cause: `HTTP ${response.status}`,
|
|
1338
|
+
retry: response.status >= 500 || response.status === 429
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
1342
|
+
if (!matches(bytes, entry))
|
|
1343
|
+
return { cause: "sha256 mismatch", retry: false };
|
|
1344
|
+
return { bytes };
|
|
1345
|
+
} catch (error) {
|
|
1346
|
+
const timedOut = error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
|
|
1347
|
+
return { cause: timedOut ? "timeout" : "network error", retry: true };
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
function size(bytes) {
|
|
1351
|
+
return bytes >= 1024 * 1024 ? `${(bytes / (1024 * 1024)).toFixed(1)} MB` : `${Math.round(bytes / 1024)} KB`;
|
|
1352
|
+
}
|
|
1353
|
+
function pause(ms) {
|
|
1354
|
+
return new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// src/grammars/manifest.ts
|
|
1358
|
+
var import_node_fs = require("fs");
|
|
1359
|
+
var import_node_path4 = require("path");
|
|
1360
|
+
var import_node_url = require("url");
|
|
1361
|
+
|
|
1362
|
+
// src/grammars/model.ts
|
|
1363
|
+
var import_zod2 = require("zod");
|
|
1364
|
+
var sha2563 = import_zod2.z.string().regex(/^[0-9a-f]{64}$/);
|
|
1365
|
+
var grammarWasmSchema = import_zod2.z.object({
|
|
1366
|
+
url: import_zod2.z.string().min(1),
|
|
1367
|
+
sha256: sha2563,
|
|
1368
|
+
bytes: import_zod2.z.number().int().positive()
|
|
1369
|
+
});
|
|
1370
|
+
var grammarTagsSchema = import_zod2.z.object({ url: import_zod2.z.string().min(1), sha256: sha2563 });
|
|
1371
|
+
var grammarPackSchema = import_zod2.z.object({
|
|
1372
|
+
package: import_zod2.z.string().min(1),
|
|
1373
|
+
wasm: grammarWasmSchema,
|
|
1374
|
+
tags: import_zod2.z.array(grammarTagsSchema),
|
|
1375
|
+
license: import_zod2.z.string().min(1),
|
|
1376
|
+
extensions: import_zod2.z.array(import_zod2.z.string().min(1))
|
|
1377
|
+
});
|
|
1378
|
+
var grammarManifestSchema = import_zod2.z.object({
|
|
1379
|
+
/** The runtime the packs were proved against. */
|
|
1380
|
+
webTreeSitter: import_zod2.z.string().min(1),
|
|
1381
|
+
linguist: import_zod2.z.object({ tag: import_zod2.z.string().min(1), commit: import_zod2.z.string().min(1) }),
|
|
1382
|
+
packs: import_zod2.z.record(import_zod2.z.string().min(1), grammarPackSchema)
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
// src/grammars/manifest.ts
|
|
1386
|
+
var cached;
|
|
1387
|
+
function grammarManifest() {
|
|
1388
|
+
cached ??= grammarManifestSchema.parse(
|
|
1389
|
+
JSON.parse((0, import_node_fs.readFileSync)(grammarsDataPath("manifest.json"), "utf8"))
|
|
1390
|
+
);
|
|
1391
|
+
return cached;
|
|
1392
|
+
}
|
|
1393
|
+
function grammarsDataPath(...segments) {
|
|
1394
|
+
let dir = (0, import_node_path4.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
1395
|
+
for (let up = 0; up < 5; up++) {
|
|
1396
|
+
const candidate = (0, import_node_path4.join)(dir, "grammars");
|
|
1397
|
+
if ((0, import_node_fs.existsSync)((0, import_node_path4.join)(candidate, "manifest.json")))
|
|
1398
|
+
return (0, import_node_path4.join)(candidate, ...segments);
|
|
1399
|
+
dir = (0, import_node_path4.dirname)(dir);
|
|
1400
|
+
}
|
|
1401
|
+
throw new Error("grammars/manifest.json is missing from the package");
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
// src/grammars/index.ts
|
|
1405
|
+
var inFlight = /* @__PURE__ */ new Map();
|
|
1406
|
+
var missing = /* @__PURE__ */ new Map();
|
|
1407
|
+
var uncompilable = /* @__PURE__ */ new Map();
|
|
1408
|
+
var rejected = /* @__PURE__ */ new Map();
|
|
1409
|
+
function grammarsDownloadDisabled() {
|
|
1410
|
+
return process.env["STRAUSS_KB_GRAMMARS"] === "off";
|
|
1411
|
+
}
|
|
1412
|
+
async function ensureGrammar(language, options = {}) {
|
|
1413
|
+
const pack2 = grammarManifest().packs[language];
|
|
1414
|
+
if (!pack2) return null;
|
|
1415
|
+
const root = grammarsCacheRoot(options.cacheRoot);
|
|
1416
|
+
const wasm = grammarCachePath(root, language, pack2.wasm.sha256);
|
|
1417
|
+
const key2 = `${wasm} ${grammarsBaseUrl(options.baseUrl) ?? ""}`;
|
|
1418
|
+
const existing = inFlight.get(key2);
|
|
1419
|
+
if (existing) return existing;
|
|
1420
|
+
const pending = (async () => {
|
|
1421
|
+
const grammar = await ensurePart(
|
|
1422
|
+
wasm,
|
|
1423
|
+
`tree-sitter-${language}`,
|
|
1424
|
+
pack2.wasm,
|
|
1425
|
+
options
|
|
1426
|
+
);
|
|
1427
|
+
if (grammar !== true)
|
|
1428
|
+
return miss(language, `grammar tree-sitter-${language}`, grammar);
|
|
1429
|
+
const parts = [];
|
|
1430
|
+
const total = pack2.tags.length;
|
|
1431
|
+
for (const [at, part] of pack2.tags.entries()) {
|
|
1432
|
+
const name = `${language} tags${total > 1 ? ` part ${at + 1}/${total}` : ""}`;
|
|
1433
|
+
const path = grammarCachePath(root, language, part.sha256, "scm");
|
|
1434
|
+
const held = await ensurePart(path, name, part, options);
|
|
1435
|
+
if (held !== true) return miss(language, name, held);
|
|
1436
|
+
parts.push(`; ${part.url}
|
|
1437
|
+
${lf(await (0, import_promises4.readFile)(path, "utf8"))}`);
|
|
1438
|
+
}
|
|
1439
|
+
missing.delete(language);
|
|
1440
|
+
return { wasm, query: total ? parts.join("\n") : void 0 };
|
|
1441
|
+
})();
|
|
1442
|
+
inFlight.set(key2, pending);
|
|
1443
|
+
const result = await pending;
|
|
1444
|
+
if (result === null) inFlight.delete(key2);
|
|
1445
|
+
return result;
|
|
1446
|
+
}
|
|
1447
|
+
async function ensurePart(path, name, entry, options) {
|
|
1448
|
+
if (await verifyCached(path, entry)) return true;
|
|
1449
|
+
if (options.offline === true || grammarsDownloadDisabled()) return {};
|
|
1450
|
+
const download = await downloadPart(
|
|
1451
|
+
grammarUrl(entry.url, options.baseUrl),
|
|
1452
|
+
name,
|
|
1453
|
+
entry,
|
|
1454
|
+
options
|
|
1455
|
+
);
|
|
1456
|
+
if ("cause" in download) return { cause: download.cause };
|
|
1457
|
+
await writeCached(path, download.bytes).catch(() => null);
|
|
1458
|
+
return true;
|
|
1459
|
+
}
|
|
1460
|
+
function miss(language, subject, failure) {
|
|
1461
|
+
missing.set(language, { subject, ...failure });
|
|
1462
|
+
return null;
|
|
1463
|
+
}
|
|
1464
|
+
function lf(body) {
|
|
1465
|
+
return body.replace(/\r\n/g, "\n");
|
|
1466
|
+
}
|
|
1467
|
+
function noteUncompilableQuery(language, cause) {
|
|
1468
|
+
uncompilable.set(language, cause);
|
|
1469
|
+
}
|
|
1470
|
+
function noteRejectedGrammar(language, cause) {
|
|
1471
|
+
rejected.set(language, cause);
|
|
1472
|
+
}
|
|
1473
|
+
function grammarHints() {
|
|
1474
|
+
const manifest = grammarManifest();
|
|
1475
|
+
const packs = manifest.packs;
|
|
1476
|
+
const lines = /* @__PURE__ */ new Map();
|
|
1477
|
+
for (const [language, { subject, cause }] of missing)
|
|
1478
|
+
lines.set(
|
|
1479
|
+
language,
|
|
1480
|
+
`${subject} not cached${cause ? ` (${cause})` : ""}; run online once, or set STRAUSS_KB_GRAMMARS_DIR`
|
|
1481
|
+
);
|
|
1482
|
+
for (const [language, cause] of rejected)
|
|
1483
|
+
lines.set(
|
|
1484
|
+
language,
|
|
1485
|
+
`${packs[language]?.package ?? `tree-sitter-${language}`} rejected by web-tree-sitter ${manifest.webTreeSitter}${cause ? `: ${cause}` : ""}; re-pin with pnpm grammars pin ${language}`
|
|
1486
|
+
);
|
|
1487
|
+
for (const [language, cause] of uncompilable)
|
|
1488
|
+
lines.set(
|
|
1489
|
+
language,
|
|
1490
|
+
`tags query for ${language} does not compile against ${packs[language]?.package ?? `tree-sitter-${language}`}: ${cause}; re-pin with pnpm grammars pin ${language}`
|
|
1491
|
+
);
|
|
1492
|
+
return [...lines].sort(([a], [b]) => a.localeCompare(b)).map(([, line]) => line);
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
// src/tree-sitter-resolver/languages.ts
|
|
1496
|
+
var table;
|
|
1497
|
+
function extensionTable() {
|
|
1498
|
+
const manifest = grammarManifest();
|
|
1499
|
+
if (table?.of !== manifest)
|
|
1500
|
+
table = {
|
|
1501
|
+
of: manifest,
|
|
1502
|
+
extensions: Object.fromEntries(
|
|
1503
|
+
Object.entries(manifest.packs).flatMap(
|
|
1504
|
+
([language, pack2]) => pack2.extensions.map((extension) => [extension, language])
|
|
1505
|
+
)
|
|
1506
|
+
)
|
|
1507
|
+
};
|
|
1508
|
+
return table.extensions;
|
|
1509
|
+
}
|
|
1510
|
+
function hasQuery(language) {
|
|
1511
|
+
return (grammarManifest().packs[language]?.tags.length ?? 0) > 0;
|
|
1512
|
+
}
|
|
1513
|
+
function languageForFile(file) {
|
|
1514
|
+
const language = extensionTable()[(0, import_node_path5.extname)(file).toLowerCase()];
|
|
1515
|
+
return language && hasQuery(language) ? language : void 0;
|
|
1516
|
+
}
|
|
1517
|
+
function treeSitterLanguages() {
|
|
1518
|
+
return [...new Set(Object.values(extensionTable()))].filter(hasQuery).sort();
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
// src/tree-sitter-resolver/resolver.ts
|
|
1522
|
+
var import_node_crypto3 = require("crypto");
|
|
1523
|
+
var import_web_tree_sitter = require("web-tree-sitter");
|
|
1524
|
+
|
|
1525
|
+
// src/tree-sitter-resolver/definitions.ts
|
|
1526
|
+
var SCOPE_ONLY = "reference.implementation";
|
|
1527
|
+
function index(tree, query) {
|
|
1528
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1529
|
+
for (const match of query.matches(tree.rootNode)) {
|
|
1530
|
+
const nameNode = match.captures.find((capture) => capture.name === "name");
|
|
1531
|
+
const defNode = match.captures.find(
|
|
1532
|
+
(capture) => capture.name.startsWith("definition.") || capture.name === SCOPE_ONLY
|
|
1533
|
+
);
|
|
1534
|
+
if (!nameNode || !defNode) continue;
|
|
1535
|
+
const candidate = {
|
|
1536
|
+
node: defNode.node,
|
|
1537
|
+
name: nameNode.node.text,
|
|
1538
|
+
target: defNode.name !== SCOPE_ONLY
|
|
1539
|
+
};
|
|
1540
|
+
const existing = byName.get(nameNode.node.id);
|
|
1541
|
+
if (existing && width(existing.node) <= width(candidate.node)) continue;
|
|
1542
|
+
byName.set(nameNode.node.id, candidate);
|
|
1543
|
+
}
|
|
1544
|
+
const definitions = [...byName.values()];
|
|
1545
|
+
return {
|
|
1546
|
+
tree,
|
|
1547
|
+
byNodeId: new Map(
|
|
1548
|
+
definitions.map((definition) => [definition.node.id, definition])
|
|
1549
|
+
),
|
|
1550
|
+
definitions
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
function select(parsed, wanted) {
|
|
1554
|
+
const matches3 = parsed.definitions.filter(
|
|
1555
|
+
(definition) => definition.target && endsWith(chainOf(definition, parsed.byNodeId), wanted)
|
|
1556
|
+
);
|
|
1557
|
+
if (matches3.length < 2) return matches3;
|
|
1558
|
+
const bodied = matches3.filter(
|
|
1559
|
+
(definition) => definition.node.childForFieldName("body") !== null
|
|
1560
|
+
);
|
|
1561
|
+
return bodied.length === 1 ? bodied : matches3;
|
|
1562
|
+
}
|
|
1563
|
+
function chainOf(definition, byNodeId) {
|
|
1564
|
+
const chain = [definition.name];
|
|
1565
|
+
const receiver = definition.node.childForFieldName("receiver");
|
|
1566
|
+
const type = receiver && typeNameIn(receiver);
|
|
1567
|
+
if (type) chain.unshift(type);
|
|
1568
|
+
for (let node = definition.node.parent; node; node = node.parent) {
|
|
1569
|
+
const enclosing = byNodeId.get(node.id);
|
|
1570
|
+
if (enclosing && enclosing.node !== definition.node)
|
|
1571
|
+
chain.unshift(enclosing.name);
|
|
1572
|
+
}
|
|
1573
|
+
return chain;
|
|
1574
|
+
}
|
|
1575
|
+
function typeNameIn(receiver) {
|
|
1576
|
+
const stack = [receiver];
|
|
1577
|
+
while (stack.length) {
|
|
1578
|
+
const node = stack.pop();
|
|
1579
|
+
if (node.type === "type_identifier") return node.text;
|
|
1580
|
+
for (let at = 0; at < node.childCount; at++) {
|
|
1581
|
+
const child = node.child(at);
|
|
1582
|
+
if (child) stack.push(child);
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
return void 0;
|
|
1586
|
+
}
|
|
1587
|
+
function endsWith(chain, wanted) {
|
|
1588
|
+
if (wanted.length > chain.length) return false;
|
|
1589
|
+
const offset = chain.length - wanted.length;
|
|
1590
|
+
return wanted.every((segment, at) => chain[offset + at] === segment);
|
|
1591
|
+
}
|
|
1592
|
+
function width(node) {
|
|
1593
|
+
return node.endIndex - node.startIndex;
|
|
1594
|
+
}
|
|
1595
|
+
function spanOf(definition, source) {
|
|
1596
|
+
let start = definition.node;
|
|
1597
|
+
let end = definition.node;
|
|
1598
|
+
for (let sibling = start.previousSibling; sibling?.type === "decorator"; sibling = sibling.previousSibling) {
|
|
1599
|
+
start = sibling;
|
|
1600
|
+
}
|
|
1601
|
+
const parent = end.parent;
|
|
1602
|
+
if (parent?.type === "export_statement" && parent.childForFieldName("declaration")?.id === end.id) {
|
|
1603
|
+
start = parent;
|
|
1604
|
+
end = parent;
|
|
1605
|
+
}
|
|
1606
|
+
const lines = source.split("\n");
|
|
1607
|
+
const startLine = start.startPosition.row;
|
|
1608
|
+
const endLine = end.endPosition.column === 0 && end.endPosition.row > startLine ? end.endPosition.row - 1 : end.endPosition.row;
|
|
1609
|
+
return {
|
|
1610
|
+
text: lines.slice(startLine, endLine + 1).join("\n"),
|
|
1611
|
+
startLine: startLine + 1,
|
|
1612
|
+
endLine: endLine + 1
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
// src/tree-sitter-resolver/resolver.ts
|
|
1617
|
+
var TREE_CACHE_LIMIT = 32;
|
|
1618
|
+
var TreeSitterResolver = class {
|
|
1619
|
+
name = "tree-sitter";
|
|
1620
|
+
grammars;
|
|
1621
|
+
loaded = /* @__PURE__ */ new Map();
|
|
1622
|
+
trees = /* @__PURE__ */ new Map();
|
|
1623
|
+
parser;
|
|
1624
|
+
initialized = false;
|
|
1625
|
+
/** Cache effectiveness, for tests and for the latency numbers. */
|
|
1626
|
+
stats = { parses: 0, cacheHits: 0 };
|
|
1627
|
+
constructor(options = {}) {
|
|
1628
|
+
this.grammars = options;
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* Loads the grammars these files need, once per language per process,
|
|
1632
|
+
* downloading each one on first use.
|
|
1633
|
+
*
|
|
1634
|
+
* A grammar that will not load is remembered as unavailable rather than
|
|
1635
|
+
* retried per anchor, and never throws: an unobtainable WASM is a finding.
|
|
1636
|
+
*/
|
|
1637
|
+
async prepare(files) {
|
|
1638
|
+
const wanted = /* @__PURE__ */ new Set();
|
|
1639
|
+
for (const file of files) {
|
|
1640
|
+
const language = languageForFile(file);
|
|
1641
|
+
if (language && !this.loaded.has(language)) wanted.add(language);
|
|
1642
|
+
}
|
|
1643
|
+
if (!wanted.size) return;
|
|
1644
|
+
if (!this.initialized) {
|
|
1645
|
+
try {
|
|
1646
|
+
await import_web_tree_sitter.Parser.init();
|
|
1647
|
+
this.parser = new import_web_tree_sitter.Parser();
|
|
1648
|
+
this.initialized = true;
|
|
1649
|
+
} catch {
|
|
1650
|
+
for (const language of wanted) this.loaded.set(language, null);
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
const languages = [...wanted];
|
|
1655
|
+
const loaded = await mapLimit(
|
|
1656
|
+
languages,
|
|
1657
|
+
Math.min(DEFAULT_IO_CONCURRENCY, languages.length),
|
|
1658
|
+
(language) => this.load(language)
|
|
1659
|
+
);
|
|
1660
|
+
languages.forEach(
|
|
1661
|
+
(language, at) => this.loaded.set(language, loaded[at] ?? null)
|
|
1662
|
+
);
|
|
1663
|
+
}
|
|
1664
|
+
/**
|
|
1665
|
+
* An unobtainable grammar, one this runtime refuses, and a query that will
|
|
1666
|
+
* not compile are three faults with three repairs; all are reported through
|
|
1667
|
+
* the grammars module so every hint has one home.
|
|
1668
|
+
*/
|
|
1669
|
+
async load(language) {
|
|
1670
|
+
let pack2;
|
|
1671
|
+
try {
|
|
1672
|
+
pack2 = await ensureGrammar(language, this.grammars);
|
|
1673
|
+
} catch {
|
|
1674
|
+
return null;
|
|
1675
|
+
}
|
|
1676
|
+
if (!pack2?.query) return null;
|
|
1677
|
+
let grammar;
|
|
1678
|
+
try {
|
|
1679
|
+
grammar = await import_web_tree_sitter.Language.load(pack2.wasm);
|
|
1680
|
+
} catch (error) {
|
|
1681
|
+
noteRejectedGrammar(language, why(error));
|
|
1682
|
+
return null;
|
|
1683
|
+
}
|
|
1684
|
+
try {
|
|
1685
|
+
return { language: grammar, query: new import_web_tree_sitter.Query(grammar, pack2.query) };
|
|
1686
|
+
} catch (error) {
|
|
1687
|
+
noteUncompilableQuery(language, why(error));
|
|
1688
|
+
return null;
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
/**
|
|
1692
|
+
* Abstains on an extension with no grammar so the regex resolver gets a
|
|
1693
|
+
* turn; reports `resolver-unavailable` when the grammar exists in principle
|
|
1694
|
+
* but could not be loaded, because falling back there would silently trade a
|
|
1695
|
+
* precise span for a guessed one.
|
|
1696
|
+
*/
|
|
1697
|
+
attempt(source, symbol, file) {
|
|
1698
|
+
const language = file ? languageForFile(file) : void 0;
|
|
1699
|
+
if (!language) return { kind: "abstain" };
|
|
1700
|
+
if (!this.loaded.has(language)) return { kind: "abstain" };
|
|
1701
|
+
const loaded = this.loaded.get(language);
|
|
1702
|
+
if (!loaded) return { kind: "unresolved", reason: "resolver-unavailable" };
|
|
1703
|
+
const parsed = this.parse(language, loaded, source);
|
|
1704
|
+
if (!parsed) return { kind: "unresolved", reason: "resolver-unavailable" };
|
|
1705
|
+
const wanted = symbol.split(".").filter(Boolean);
|
|
1706
|
+
if (!wanted.length)
|
|
1707
|
+
return { kind: "unresolved", reason: "symbol-not-found" };
|
|
1708
|
+
const matches3 = select(parsed, wanted);
|
|
1709
|
+
if (!matches3.length)
|
|
1710
|
+
return { kind: "unresolved", reason: "symbol-not-found" };
|
|
1711
|
+
if (matches3.length > 1)
|
|
1712
|
+
return { kind: "unresolved", reason: "symbol-ambiguous" };
|
|
1713
|
+
return { kind: "resolved", span: spanOf(matches3[0], source) };
|
|
1714
|
+
}
|
|
1715
|
+
resolve(source, symbol, file) {
|
|
1716
|
+
const attempt = this.attempt(source, symbol, file);
|
|
1717
|
+
return attempt.kind === "resolved" ? attempt.span : null;
|
|
1718
|
+
}
|
|
1719
|
+
/** Parsed trees are keyed by content hash, so an unchanged file parses once. */
|
|
1720
|
+
parse(language, loaded, source) {
|
|
1721
|
+
const key2 = `${language}:${(0, import_node_crypto3.createHash)("sha256").update(source).digest("hex")}`;
|
|
1722
|
+
const cached2 = this.trees.get(key2);
|
|
1723
|
+
if (cached2) {
|
|
1724
|
+
this.stats.cacheHits += 1;
|
|
1725
|
+
return cached2;
|
|
1726
|
+
}
|
|
1727
|
+
const parser = this.parser;
|
|
1728
|
+
if (!parser) return null;
|
|
1729
|
+
let parsed;
|
|
1730
|
+
try {
|
|
1731
|
+
parser.setLanguage(loaded.language);
|
|
1732
|
+
const tree = parser.parse(source);
|
|
1733
|
+
if (!tree) return null;
|
|
1734
|
+
parsed = index(tree, loaded.query);
|
|
1735
|
+
} catch {
|
|
1736
|
+
return null;
|
|
1737
|
+
}
|
|
1738
|
+
this.stats.parses += 1;
|
|
1739
|
+
if (this.trees.size >= TREE_CACHE_LIMIT) {
|
|
1740
|
+
const oldest = this.trees.keys().next();
|
|
1741
|
+
if (!oldest.done) {
|
|
1742
|
+
this.trees.get(oldest.value)?.tree.delete();
|
|
1743
|
+
this.trees.delete(oldest.value);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
this.trees.set(key2, parsed);
|
|
1747
|
+
return parsed;
|
|
1748
|
+
}
|
|
1749
|
+
/** Drops cached trees. Grammars stay loaded — they are immutable. */
|
|
1750
|
+
reset() {
|
|
1751
|
+
for (const parsed of this.trees.values()) parsed.tree.delete();
|
|
1752
|
+
this.trees.clear();
|
|
1753
|
+
this.stats.parses = 0;
|
|
1754
|
+
this.stats.cacheHits = 0;
|
|
1755
|
+
}
|
|
1756
|
+
};
|
|
1757
|
+
function why(error) {
|
|
1758
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
1759
|
+
return text || "no reason given";
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
// src/anchor-resolver/resolver.ts
|
|
1226
1763
|
var PARENT_SCOPE_LINES = 50;
|
|
1227
1764
|
var CLEAN_STATE = { blockComment: false, template: false };
|
|
1228
1765
|
function stripLine(line, state) {
|
|
1229
1766
|
let out = "";
|
|
1230
|
-
let
|
|
1767
|
+
let index2 = 0;
|
|
1231
1768
|
let { blockComment, template } = state;
|
|
1232
|
-
while (
|
|
1233
|
-
const char = line[
|
|
1234
|
-
const next = line[
|
|
1769
|
+
while (index2 < line.length) {
|
|
1770
|
+
const char = line[index2];
|
|
1771
|
+
const next = line[index2 + 1];
|
|
1235
1772
|
if (blockComment) {
|
|
1236
1773
|
if (char === "*" && next === "/") {
|
|
1237
1774
|
blockComment = false;
|
|
1238
|
-
|
|
1775
|
+
index2 += 2;
|
|
1239
1776
|
continue;
|
|
1240
1777
|
}
|
|
1241
|
-
|
|
1778
|
+
index2 += 1;
|
|
1242
1779
|
continue;
|
|
1243
1780
|
}
|
|
1244
1781
|
if (template) {
|
|
1245
1782
|
if (char === "\\") {
|
|
1246
|
-
|
|
1783
|
+
index2 += 2;
|
|
1247
1784
|
continue;
|
|
1248
1785
|
}
|
|
1249
1786
|
if (char === "`") template = false;
|
|
1250
|
-
|
|
1787
|
+
index2 += 1;
|
|
1251
1788
|
continue;
|
|
1252
1789
|
}
|
|
1253
1790
|
if (char === "/" && next === "*") {
|
|
1254
1791
|
blockComment = true;
|
|
1255
|
-
|
|
1792
|
+
index2 += 2;
|
|
1256
1793
|
continue;
|
|
1257
1794
|
}
|
|
1258
1795
|
if (char === "/" && next === "/") break;
|
|
1259
1796
|
if (char === "`") {
|
|
1260
1797
|
template = true;
|
|
1261
|
-
|
|
1798
|
+
index2 += 1;
|
|
1262
1799
|
continue;
|
|
1263
1800
|
}
|
|
1264
1801
|
if (char === "'" || char === '"') {
|
|
1265
1802
|
const quote = char;
|
|
1266
|
-
|
|
1267
|
-
while (
|
|
1268
|
-
if (line[
|
|
1269
|
-
|
|
1803
|
+
index2 += 1;
|
|
1804
|
+
while (index2 < line.length) {
|
|
1805
|
+
if (line[index2] === "\\") {
|
|
1806
|
+
index2 += 2;
|
|
1270
1807
|
continue;
|
|
1271
1808
|
}
|
|
1272
|
-
if (line[
|
|
1273
|
-
|
|
1809
|
+
if (line[index2] === quote) {
|
|
1810
|
+
index2 += 1;
|
|
1274
1811
|
break;
|
|
1275
1812
|
}
|
|
1276
|
-
|
|
1813
|
+
index2 += 1;
|
|
1277
1814
|
}
|
|
1278
1815
|
continue;
|
|
1279
1816
|
}
|
|
1280
1817
|
out += char;
|
|
1281
|
-
|
|
1818
|
+
index2 += 1;
|
|
1282
1819
|
}
|
|
1283
1820
|
return { code: out, state: { blockComment, template } };
|
|
1284
1821
|
}
|
|
@@ -1293,8 +1830,8 @@ function captureBraceBlock(lines, matchLine) {
|
|
|
1293
1830
|
let depth = 0;
|
|
1294
1831
|
let opened = false;
|
|
1295
1832
|
let state = CLEAN_STATE;
|
|
1296
|
-
for (let
|
|
1297
|
-
const stripped = stripLine(lines[
|
|
1833
|
+
for (let index2 = matchLine; index2 < lines.length; index2++) {
|
|
1834
|
+
const stripped = stripLine(lines[index2] ?? "", state);
|
|
1298
1835
|
state = stripped.state;
|
|
1299
1836
|
for (const char of stripped.code) {
|
|
1300
1837
|
if (char === "{") {
|
|
@@ -1303,10 +1840,10 @@ function captureBraceBlock(lines, matchLine) {
|
|
|
1303
1840
|
} else if (char === "}") {
|
|
1304
1841
|
depth = Math.max(0, depth - 1);
|
|
1305
1842
|
} else if (char === ";" && !opened) {
|
|
1306
|
-
return span(lines, matchLine,
|
|
1843
|
+
return span(lines, matchLine, index2);
|
|
1307
1844
|
}
|
|
1308
1845
|
}
|
|
1309
|
-
if (opened && depth === 0) return span(lines, matchLine,
|
|
1846
|
+
if (opened && depth === 0) return span(lines, matchLine, index2);
|
|
1310
1847
|
}
|
|
1311
1848
|
return null;
|
|
1312
1849
|
}
|
|
@@ -1315,22 +1852,22 @@ function captureIndentedBlock(lines, matchLine) {
|
|
|
1315
1852
|
const header = lines[matchLine] ?? "";
|
|
1316
1853
|
const indent = header.length - header.trimStart().length;
|
|
1317
1854
|
let headerEnd = -1;
|
|
1318
|
-
for (let
|
|
1319
|
-
const code = stripLine(lines[
|
|
1855
|
+
for (let index2 = matchLine; index2 < lines.length && index2 <= matchLine + 20; index2++) {
|
|
1856
|
+
const code = stripLine(lines[index2] ?? "", CLEAN_STATE).code.trimEnd();
|
|
1320
1857
|
if (code.endsWith(":")) {
|
|
1321
|
-
headerEnd =
|
|
1858
|
+
headerEnd = index2;
|
|
1322
1859
|
break;
|
|
1323
1860
|
}
|
|
1324
|
-
if (code.includes(":")) return span(lines, matchLine,
|
|
1861
|
+
if (code.includes(":")) return span(lines, matchLine, index2);
|
|
1325
1862
|
}
|
|
1326
1863
|
if (headerEnd === -1) return null;
|
|
1327
1864
|
let end = headerEnd;
|
|
1328
|
-
for (let
|
|
1329
|
-
const line = lines[
|
|
1865
|
+
for (let index2 = headerEnd + 1; index2 < lines.length; index2++) {
|
|
1866
|
+
const line = lines[index2] ?? "";
|
|
1330
1867
|
if (line.trim() === "") continue;
|
|
1331
1868
|
const lineIndent = line.length - line.trimStart().length;
|
|
1332
1869
|
if (lineIndent <= indent) break;
|
|
1333
|
-
end =
|
|
1870
|
+
end = index2;
|
|
1334
1871
|
}
|
|
1335
1872
|
return end === headerEnd ? null : span(lines, matchLine, end);
|
|
1336
1873
|
}
|
|
@@ -1354,11 +1891,11 @@ var regexResolver = {
|
|
|
1354
1891
|
const lines = source.split("\n");
|
|
1355
1892
|
for (const tier of TIERS) {
|
|
1356
1893
|
const pattern = tier(escaped);
|
|
1357
|
-
let candidates = lines.map((line,
|
|
1894
|
+
let candidates = lines.map((line, index2) => ({ line, index: index2 })).filter((entry) => pattern.test(entry.line)).map((entry) => entry.index);
|
|
1358
1895
|
if (!candidates.length) continue;
|
|
1359
1896
|
if (parentPattern && candidates.length > 1) {
|
|
1360
1897
|
const distances = candidates.map(
|
|
1361
|
-
(
|
|
1898
|
+
(index2) => distanceToParent(lines, index2, parentPattern)
|
|
1362
1899
|
);
|
|
1363
1900
|
const nearest = Math.min(...distances);
|
|
1364
1901
|
if (Number.isFinite(nearest)) {
|
|
@@ -1375,34 +1912,79 @@ var regexResolver = {
|
|
|
1375
1912
|
function escapeRegExp(value) {
|
|
1376
1913
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1377
1914
|
}
|
|
1378
|
-
function distanceToParent(lines,
|
|
1379
|
-
const floor = Math.max(0,
|
|
1380
|
-
for (let at =
|
|
1381
|
-
if (parent.test(lines[at] ?? "")) return
|
|
1915
|
+
function distanceToParent(lines, index2, parent) {
|
|
1916
|
+
const floor = Math.max(0, index2 - PARENT_SCOPE_LINES);
|
|
1917
|
+
for (let at = index2; at >= floor; at--) {
|
|
1918
|
+
if (parent.test(lines[at] ?? "")) return index2 - at;
|
|
1382
1919
|
}
|
|
1383
1920
|
return Number.POSITIVE_INFINITY;
|
|
1384
1921
|
}
|
|
1385
1922
|
function hashAnchorText(text) {
|
|
1386
|
-
return `sha256:${(0,
|
|
1923
|
+
return `sha256:${(0, import_node_crypto4.createHash)("sha256").update(text.replace(/\r\n/g, "\n")).digest("hex")}`;
|
|
1387
1924
|
}
|
|
1388
1925
|
function resolveAnchor(source, anchor, resolver = regexResolver) {
|
|
1926
|
+
const outcome = resolveAnchorSpan(source, anchor, [resolver]);
|
|
1927
|
+
return outcome.ok ? outcome.span : null;
|
|
1928
|
+
}
|
|
1929
|
+
function resolveAnchorSpan(source, anchor, resolvers = [regexResolver]) {
|
|
1389
1930
|
const normalized = source.replace(/\r\n/g, "\n");
|
|
1390
1931
|
if (!anchor.symbol) {
|
|
1391
1932
|
const lines = normalized.split("\n");
|
|
1392
1933
|
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
1393
1934
|
return {
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1935
|
+
ok: true,
|
|
1936
|
+
span: {
|
|
1937
|
+
text: normalized,
|
|
1938
|
+
startLine: 1,
|
|
1939
|
+
endLine: Math.max(1, lines.length)
|
|
1940
|
+
}
|
|
1941
|
+
};
|
|
1942
|
+
}
|
|
1943
|
+
for (const resolver of resolvers) {
|
|
1944
|
+
const attempt = resolver.attempt ? resolver.attempt(normalized, anchor.symbol, anchor.file) : fromResolve(resolver, normalized, anchor.symbol, anchor.file);
|
|
1945
|
+
if (attempt.kind === "abstain") continue;
|
|
1946
|
+
if (attempt.kind === "unresolved") {
|
|
1947
|
+
if (attempt.reason === "symbol-not-found") continue;
|
|
1948
|
+
return { ok: false, reason: attempt.reason };
|
|
1949
|
+
}
|
|
1950
|
+
return {
|
|
1951
|
+
ok: true,
|
|
1952
|
+
span: attempt.span,
|
|
1953
|
+
...isResolverName(resolver.name) ? { resolver: resolver.name } : {}
|
|
1397
1954
|
};
|
|
1398
1955
|
}
|
|
1399
|
-
return
|
|
1956
|
+
return { ok: false, reason: "symbol-not-found" };
|
|
1957
|
+
}
|
|
1958
|
+
function fromResolve(resolver, source, symbol, file) {
|
|
1959
|
+
const span2 = resolver.resolve(source, symbol, file);
|
|
1960
|
+
return span2 ? { kind: "resolved", span: span2 } : { kind: "unresolved", reason: "symbol-not-found" };
|
|
1961
|
+
}
|
|
1962
|
+
function isResolverName(name) {
|
|
1963
|
+
return name === "tree-sitter" || name === "regex";
|
|
1964
|
+
}
|
|
1965
|
+
async function prepareResolvers(resolvers, files) {
|
|
1966
|
+
for (const resolver of resolvers) await resolver.prepare?.(files);
|
|
1967
|
+
}
|
|
1968
|
+
function defaultAnchorResolvers(grammars = {}) {
|
|
1969
|
+
return [new TreeSitterResolver(grammars), regexResolver];
|
|
1970
|
+
}
|
|
1971
|
+
function resolverChanged(source, anchor, produced) {
|
|
1972
|
+
const previous = anchor.resolver ?? "regex";
|
|
1973
|
+
if (!produced || !anchor.symbol || previous === produced) return false;
|
|
1974
|
+
if (previous !== "regex") return false;
|
|
1975
|
+
const before = regexResolver.resolve(
|
|
1976
|
+
source.replace(/\r\n/g, "\n"),
|
|
1977
|
+
anchor.symbol
|
|
1978
|
+
);
|
|
1979
|
+
return before !== null && hashAnchorText(before.text) === anchor.hash;
|
|
1400
1980
|
}
|
|
1401
1981
|
|
|
1402
1982
|
// src/anchor-resolver/drift.ts
|
|
1403
1983
|
async function detectAnchorDrift(records, options = {}) {
|
|
1404
1984
|
const repoRoot = options.repoRoot ?? process.cwd();
|
|
1405
|
-
const
|
|
1985
|
+
const resolvers = options.resolvers ?? (options.resolver ? [options.resolver] : defaultAnchorResolvers({
|
|
1986
|
+
offline: options.remote?.offline === true
|
|
1987
|
+
}));
|
|
1406
1988
|
const origin = new LazyOrigin(repoRoot);
|
|
1407
1989
|
const planned = /* @__PURE__ */ new Map();
|
|
1408
1990
|
let declaresRepo = false;
|
|
@@ -1440,12 +2022,16 @@ async function detectAnchorDrift(records, options = {}) {
|
|
|
1440
2022
|
),
|
|
1441
2023
|
(options.readRemote ?? readRemoteAnchors)(wants, options.remote ?? {})
|
|
1442
2024
|
]);
|
|
2025
|
+
await prepareResolvers(resolvers, [
|
|
2026
|
+
...files,
|
|
2027
|
+
...wants.map((want) => want.file)
|
|
2028
|
+
]);
|
|
1443
2029
|
const drift = /* @__PURE__ */ new Map();
|
|
1444
2030
|
for (const record of records) {
|
|
1445
2031
|
const entries = [];
|
|
1446
2032
|
for (const { anchor, foreign } of planned.get(record.conceptId) ?? []) {
|
|
1447
2033
|
entries.push(
|
|
1448
|
-
foreign ? remoteEntry(anchor, remote,
|
|
2034
|
+
foreign ? remoteEntry(anchor, remote, resolvers) : localEntry(anchor, reads.get(anchor.file), resolvers)
|
|
1449
2035
|
);
|
|
1450
2036
|
}
|
|
1451
2037
|
if (entries.length) drift.set(record.conceptId, entries);
|
|
@@ -1474,12 +2060,22 @@ function unresolved(anchor, reason, repo) {
|
|
|
1474
2060
|
...repo ? { repo } : {}
|
|
1475
2061
|
};
|
|
1476
2062
|
}
|
|
1477
|
-
function hashIn(source, anchor,
|
|
1478
|
-
const
|
|
1479
|
-
if (!
|
|
2063
|
+
function hashIn(source, anchor, resolvers) {
|
|
2064
|
+
const outcome = resolveAnchorSpan(source, anchor, resolvers);
|
|
2065
|
+
if (!outcome.ok) return { ok: false, reason: outcome.reason };
|
|
2066
|
+
return {
|
|
2067
|
+
ok: true,
|
|
2068
|
+
current: {
|
|
2069
|
+
hash: hashAnchorText(outcome.span.text),
|
|
2070
|
+
lines: outcome.span.endLine - outcome.span.startLine + 1,
|
|
2071
|
+
...outcome.resolver ? { resolver: outcome.resolver } : {}
|
|
2072
|
+
}
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2075
|
+
function resolverExtras(source, anchor, current) {
|
|
1480
2076
|
return {
|
|
1481
|
-
|
|
1482
|
-
|
|
2077
|
+
...current.resolver ? { resolver: current.resolver } : {},
|
|
2078
|
+
...current.hash !== anchor.hash && resolverChanged(source, anchor, current.resolver) ? { reason: "resolver-changed" } : {}
|
|
1483
2079
|
};
|
|
1484
2080
|
}
|
|
1485
2081
|
function compared(anchor, current, extra = {}) {
|
|
@@ -1491,40 +2087,58 @@ function compared(anchor, current, extra = {}) {
|
|
|
1491
2087
|
...extra
|
|
1492
2088
|
};
|
|
1493
2089
|
}
|
|
1494
|
-
function localEntry(anchor, read,
|
|
2090
|
+
function localEntry(anchor, read, resolvers) {
|
|
1495
2091
|
if (!read.ok) return unresolved(anchor, read.reason);
|
|
1496
|
-
const
|
|
1497
|
-
|
|
2092
|
+
const found = hashIn(read.source, anchor, resolvers);
|
|
2093
|
+
if (!found.ok) return unresolved(anchor, found.reason);
|
|
2094
|
+
return compared(
|
|
2095
|
+
anchor,
|
|
2096
|
+
found.current,
|
|
2097
|
+
resolverExtras(read.source, anchor, found.current)
|
|
2098
|
+
);
|
|
1498
2099
|
}
|
|
1499
|
-
function remoteEntry(anchor, remote,
|
|
2100
|
+
function remoteEntry(anchor, remote, resolvers) {
|
|
1500
2101
|
const repo = anchor.repo;
|
|
1501
2102
|
const key2 = normalizeRepoUrl(repo);
|
|
1502
2103
|
const atDefault = remote.get(wantKey(key2, void 0, anchor.file));
|
|
1503
2104
|
const primary = anchor.ref ? remote.get(wantKey(key2, anchor.ref, anchor.file)) : atDefault;
|
|
1504
2105
|
if (!primary) return unresolved(anchor, "remote-unreachable", repo);
|
|
1505
2106
|
if (!primary.ok) return unresolved(anchor, primary.reason, repo);
|
|
1506
|
-
const
|
|
1507
|
-
if (!
|
|
1508
|
-
|
|
2107
|
+
const found = hashIn(primary.source, anchor, resolvers);
|
|
2108
|
+
if (!found.ok) return unresolved(anchor, found.reason, repo);
|
|
2109
|
+
const current = found.current;
|
|
2110
|
+
const extras = resolverExtras(primary.source, anchor, current);
|
|
2111
|
+
if (!anchor.ref) return compared(anchor, current, { repo, ...extras });
|
|
1509
2112
|
if (current.hash !== anchor.hash) {
|
|
1510
|
-
return compared(anchor, current, {
|
|
2113
|
+
return compared(anchor, current, {
|
|
2114
|
+
repo,
|
|
2115
|
+
...extras,
|
|
2116
|
+
remoteState: "drifted-from-ref"
|
|
2117
|
+
});
|
|
1511
2118
|
}
|
|
1512
|
-
const head = atDefault?.ok ? hashIn(atDefault.source, anchor,
|
|
1513
|
-
return head && head.hash !== anchor.hash ? {
|
|
1514
|
-
...compared(anchor, head, {
|
|
2119
|
+
const head = atDefault?.ok ? hashIn(atDefault.source, anchor, resolvers) : null;
|
|
2120
|
+
return head?.ok && head.current.hash !== anchor.hash ? {
|
|
2121
|
+
...compared(anchor, head.current, {
|
|
2122
|
+
repo,
|
|
2123
|
+
...head.current.resolver ? { resolver: head.current.resolver } : {}
|
|
2124
|
+
}),
|
|
1515
2125
|
state: "drifted",
|
|
1516
2126
|
remoteState: "drifted-on-default"
|
|
1517
|
-
} : compared(anchor, current, {
|
|
2127
|
+
} : compared(anchor, current, {
|
|
2128
|
+
repo,
|
|
2129
|
+
...extras,
|
|
2130
|
+
remoteState: "matches-ref"
|
|
2131
|
+
});
|
|
1518
2132
|
}
|
|
1519
2133
|
|
|
1520
2134
|
// src/search-index.ts
|
|
1521
|
-
var
|
|
1522
|
-
var
|
|
2135
|
+
var import_promises5 = require("fs/promises");
|
|
2136
|
+
var import_node_path6 = require("path");
|
|
1523
2137
|
|
|
1524
2138
|
// src/kb-log.ts
|
|
1525
|
-
var
|
|
2139
|
+
var import_zod3 = require("zod");
|
|
1526
2140
|
var LOG_FILE = "log.jsonl";
|
|
1527
|
-
var kbLogEntrySchema =
|
|
2141
|
+
var kbLogEntrySchema = import_zod3.z.object({
|
|
1528
2142
|
// Validated, not just `min(1)`: `at` is a sort key (see `parseLog`
|
|
1529
2143
|
// below), and a value that isn't actually chronological — a Unix
|
|
1530
2144
|
// timestamp, a human-typed date, garbage — would sort wrong without
|
|
@@ -1533,12 +2147,12 @@ var kbLogEntrySchema = import_zod2.z.object({
|
|
|
1533
2147
|
// and rejects everything else, including a non-`Z` offset — so a
|
|
1534
2148
|
// malformed `at` is reported the same way a malformed line already is,
|
|
1535
2149
|
// rather than silently sorting into the wrong place.
|
|
1536
|
-
at:
|
|
1537
|
-
by:
|
|
1538
|
-
operation:
|
|
1539
|
-
conceptId:
|
|
2150
|
+
at: import_zod3.z.iso.datetime(),
|
|
2151
|
+
by: import_zod3.z.string().min(1),
|
|
2152
|
+
operation: import_zod3.z.string().min(1),
|
|
2153
|
+
conceptId: import_zod3.z.string().min(1),
|
|
1540
2154
|
/** Second concept id, where the operation relates two — supersession. */
|
|
1541
|
-
target:
|
|
2155
|
+
target: import_zod3.z.string().min(1).optional()
|
|
1542
2156
|
}).strict();
|
|
1543
2157
|
function renderLogEntry(entry) {
|
|
1544
2158
|
return `${JSON.stringify(kbLogEntrySchema.parse(entry))}
|
|
@@ -1548,18 +2162,18 @@ function parseLog(raw) {
|
|
|
1548
2162
|
const entries = [];
|
|
1549
2163
|
const malformed = [];
|
|
1550
2164
|
const seen = /* @__PURE__ */ new Set();
|
|
1551
|
-
raw.split("\n").forEach((text,
|
|
2165
|
+
raw.split("\n").forEach((text, index2) => {
|
|
1552
2166
|
if (!text.trim()) return;
|
|
1553
2167
|
let value;
|
|
1554
2168
|
try {
|
|
1555
2169
|
value = JSON.parse(text);
|
|
1556
2170
|
} catch {
|
|
1557
|
-
malformed.push({ line:
|
|
2171
|
+
malformed.push({ line: index2 + 1, text });
|
|
1558
2172
|
return;
|
|
1559
2173
|
}
|
|
1560
2174
|
const parsed = kbLogEntrySchema.safeParse(value);
|
|
1561
2175
|
if (!parsed.success) {
|
|
1562
|
-
malformed.push({ line:
|
|
2176
|
+
malformed.push({ line: index2 + 1, text });
|
|
1563
2177
|
return;
|
|
1564
2178
|
}
|
|
1565
2179
|
const key2 = JSON.stringify(parsed.data);
|
|
@@ -1582,7 +2196,7 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
1582
2196
|
let store = null;
|
|
1583
2197
|
try {
|
|
1584
2198
|
store = await qmd.createStore({
|
|
1585
|
-
dbPath: (0,
|
|
2199
|
+
dbPath: (0, import_node_path6.join)(bundlePath2, SEARCH_INDEX_FILE),
|
|
1586
2200
|
config: {
|
|
1587
2201
|
collections: {
|
|
1588
2202
|
[COLLECTION]: {
|
|
@@ -1617,7 +2231,7 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
1617
2231
|
}
|
|
1618
2232
|
}
|
|
1619
2233
|
async function isStale(bundlePath2) {
|
|
1620
|
-
const indexAt = await (0,
|
|
2234
|
+
const indexAt = await (0, import_promises5.stat)((0, import_node_path6.join)(bundlePath2, SEARCH_INDEX_FILE)).then((s) => s.mtimeMs).catch(() => 0);
|
|
1621
2235
|
if (!indexAt) return true;
|
|
1622
2236
|
const { readdir: readdir2 } = await import("fs/promises");
|
|
1623
2237
|
const names = (await readdir2(bundlePath2).catch(() => [])).filter(
|
|
@@ -1626,7 +2240,7 @@ async function isStale(bundlePath2) {
|
|
|
1626
2240
|
let stale = false;
|
|
1627
2241
|
await mapLimit(names, DEFAULT_IO_CONCURRENCY, async (name) => {
|
|
1628
2242
|
if (stale) return;
|
|
1629
|
-
const at = await (0,
|
|
2243
|
+
const at = await (0, import_promises5.stat)((0, import_node_path6.join)(bundlePath2, name)).then((s) => s.mtimeMs).catch(() => 0);
|
|
1630
2244
|
if (at > indexAt) stale = true;
|
|
1631
2245
|
});
|
|
1632
2246
|
return stale;
|
|
@@ -2006,8 +2620,8 @@ function byRank(left, right) {
|
|
|
2006
2620
|
) || left.record.conceptId.localeCompare(right.record.conceptId);
|
|
2007
2621
|
}
|
|
2008
2622
|
function typeRank(record) {
|
|
2009
|
-
const
|
|
2010
|
-
return
|
|
2623
|
+
const index2 = TYPE_PRIORITY.indexOf(record.frontmatter.type);
|
|
2624
|
+
return index2 === -1 ? TYPE_PRIORITY.length : index2;
|
|
2011
2625
|
}
|
|
2012
2626
|
|
|
2013
2627
|
// src/catalog.ts
|
|
@@ -2216,7 +2830,7 @@ function appendUnionMergeLine(contents) {
|
|
|
2216
2830
|
}
|
|
2217
2831
|
|
|
2218
2832
|
// src/kb-store.ts
|
|
2219
|
-
var KB_DIR = (0,
|
|
2833
|
+
var KB_DIR = (0, import_node_path7.join)(".strauss", "kb");
|
|
2220
2834
|
var STORE_OWNED = /* @__PURE__ */ new Set([INDEX_FILE, LOG_FILE, SEARCH_INDEX_FILE]);
|
|
2221
2835
|
var DEFAULT_LOAD_BUDGET = 25e3;
|
|
2222
2836
|
var KbStore = class {
|
|
@@ -2247,7 +2861,7 @@ var KbStore = class {
|
|
|
2247
2861
|
const conceptId2 = `${input.type}.${input.slug}`;
|
|
2248
2862
|
const root = this.root(bundlePath2);
|
|
2249
2863
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
2250
|
-
await (0,
|
|
2864
|
+
await (0, import_promises6.mkdir)(root, { recursive: true });
|
|
2251
2865
|
await this.publish(
|
|
2252
2866
|
target,
|
|
2253
2867
|
stringifyMarkdownWithFrontmatter(input.body, frontmatter),
|
|
@@ -2286,7 +2900,7 @@ var KbStore = class {
|
|
|
2286
2900
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
2287
2901
|
let raw;
|
|
2288
2902
|
try {
|
|
2289
|
-
raw = await (0,
|
|
2903
|
+
raw = await (0, import_promises6.readFile)(target, "utf8");
|
|
2290
2904
|
} catch {
|
|
2291
2905
|
return null;
|
|
2292
2906
|
}
|
|
@@ -2303,7 +2917,7 @@ var KbStore = class {
|
|
|
2303
2917
|
const root = this.root(bundlePath2);
|
|
2304
2918
|
let names;
|
|
2305
2919
|
try {
|
|
2306
|
-
names = await (0,
|
|
2920
|
+
names = await (0, import_promises6.readdir)(root);
|
|
2307
2921
|
} catch {
|
|
2308
2922
|
return [];
|
|
2309
2923
|
}
|
|
@@ -2311,7 +2925,7 @@ var KbStore = class {
|
|
|
2311
2925
|
const records = await mapLimit(
|
|
2312
2926
|
wanted,
|
|
2313
2927
|
DEFAULT_IO_CONCURRENCY,
|
|
2314
|
-
async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await (0,
|
|
2928
|
+
async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await (0, import_promises6.readFile)((0, import_node_path7.join)(root, name), "utf8"))
|
|
2315
2929
|
);
|
|
2316
2930
|
return records.filter((record) => record !== null);
|
|
2317
2931
|
}
|
|
@@ -2468,7 +3082,7 @@ ${answer}
|
|
|
2468
3082
|
if (found.length) return found;
|
|
2469
3083
|
}
|
|
2470
3084
|
const lowered = needle.toLowerCase();
|
|
2471
|
-
return bundle.filter((record) =>
|
|
3085
|
+
return bundle.filter((record) => matches2(record, lowered));
|
|
2472
3086
|
}
|
|
2473
3087
|
/**
|
|
2474
3088
|
* Anchor drift over the records about to be handed back. Like the search
|
|
@@ -2638,11 +3252,11 @@ ${answer}
|
|
|
2638
3252
|
async readIndex(bundlePath2) {
|
|
2639
3253
|
const root = this.root(bundlePath2);
|
|
2640
3254
|
const expected = renderIndex(await this.list(bundlePath2));
|
|
2641
|
-
const stored = await (0,
|
|
3255
|
+
const stored = await (0, import_promises6.readFile)((0, import_node_path7.join)(root, INDEX_FILE), "utf8").catch(
|
|
2642
3256
|
() => null
|
|
2643
3257
|
);
|
|
2644
3258
|
if (indexIsStale(stored, expected)) {
|
|
2645
|
-
await this.publish((0,
|
|
3259
|
+
await this.publish((0, import_node_path7.join)(root, INDEX_FILE), expected, true, INDEX_FILE);
|
|
2646
3260
|
this.logger.info?.({
|
|
2647
3261
|
operation: "kb.index.repair",
|
|
2648
3262
|
bundlePath: root,
|
|
@@ -2659,8 +3273,8 @@ ${answer}
|
|
|
2659
3273
|
* knows which agent touched what. So a bad line is surfaced and left alone.
|
|
2660
3274
|
*/
|
|
2661
3275
|
async readLog(bundlePath2) {
|
|
2662
|
-
const raw = await (0,
|
|
2663
|
-
(0,
|
|
3276
|
+
const raw = await (0, import_promises6.readFile)(
|
|
3277
|
+
(0, import_node_path7.join)(this.root(bundlePath2), LOG_FILE),
|
|
2664
3278
|
"utf8"
|
|
2665
3279
|
).catch(() => "");
|
|
2666
3280
|
const result = parseLog(raw);
|
|
@@ -2711,14 +3325,14 @@ ${answer}
|
|
|
2711
3325
|
}
|
|
2712
3326
|
async mutate(bundlePath2, conceptId2, change, entry, changeBody = (body) => body) {
|
|
2713
3327
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
2714
|
-
const before = await (0,
|
|
3328
|
+
const before = await (0, import_promises6.readFile)(target, "utf8").catch(() => null);
|
|
2715
3329
|
if (before === null) throw new KbRecordNotFoundError(conceptId2);
|
|
2716
3330
|
const parsed = this.parse(conceptId2, before);
|
|
2717
3331
|
if (!parsed) throw new KbRecordNotFoundError(conceptId2);
|
|
2718
3332
|
const frontmatter = change(parsed.frontmatter);
|
|
2719
3333
|
const body = changeBody(parsed.body);
|
|
2720
3334
|
const contents = stringifyMarkdownWithFrontmatter(body, frontmatter);
|
|
2721
|
-
const witness = await (0,
|
|
3335
|
+
const witness = await (0, import_promises6.readFile)(target, "utf8").catch(() => null);
|
|
2722
3336
|
if (witness === null || sha256(witness) !== sha256(before)) {
|
|
2723
3337
|
throw new KbWriteConflictError(conceptId2);
|
|
2724
3338
|
}
|
|
@@ -2744,20 +3358,20 @@ ${answer}
|
|
|
2744
3358
|
*/
|
|
2745
3359
|
async publish(target, contents, overwrite, conceptId2) {
|
|
2746
3360
|
const staging = `${target}.${process.pid}.tmp`;
|
|
2747
|
-
await (0,
|
|
3361
|
+
await (0, import_promises6.writeFile)(staging, contents, "utf8");
|
|
2748
3362
|
try {
|
|
2749
3363
|
if (overwrite) {
|
|
2750
|
-
await (0,
|
|
3364
|
+
await (0, import_promises6.rename)(staging, target);
|
|
2751
3365
|
return;
|
|
2752
3366
|
}
|
|
2753
|
-
await (0,
|
|
3367
|
+
await (0, import_promises6.link)(staging, target);
|
|
2754
3368
|
} catch (error) {
|
|
2755
3369
|
if (error.code === "EEXIST") {
|
|
2756
3370
|
throw new KbRecordAlreadyExistsError(conceptId2);
|
|
2757
3371
|
}
|
|
2758
3372
|
throw error;
|
|
2759
3373
|
} finally {
|
|
2760
|
-
await (0,
|
|
3374
|
+
await (0, import_promises6.unlink)(staging).catch(() => void 0);
|
|
2761
3375
|
}
|
|
2762
3376
|
}
|
|
2763
3377
|
/**
|
|
@@ -2801,18 +3415,18 @@ ${answer}
|
|
|
2801
3415
|
* file must not fail the mutation it guards.
|
|
2802
3416
|
*/
|
|
2803
3417
|
async ensureGitattributes(root) {
|
|
2804
|
-
const target = (0,
|
|
3418
|
+
const target = (0, import_node_path7.join)(root, GITATTRIBUTES_FILE);
|
|
2805
3419
|
try {
|
|
2806
3420
|
let existing;
|
|
2807
3421
|
try {
|
|
2808
|
-
existing = await (0,
|
|
3422
|
+
existing = await (0, import_promises6.readFile)(target, "utf8");
|
|
2809
3423
|
} catch (error) {
|
|
2810
3424
|
if (error.code !== "ENOENT") throw error;
|
|
2811
3425
|
existing = null;
|
|
2812
3426
|
}
|
|
2813
3427
|
if (existing === null) {
|
|
2814
3428
|
try {
|
|
2815
|
-
await (0,
|
|
3429
|
+
await (0, import_promises6.writeFile)(target, appendUnionMergeLine(""), {
|
|
2816
3430
|
encoding: "utf8",
|
|
2817
3431
|
flag: "wx"
|
|
2818
3432
|
});
|
|
@@ -2833,7 +3447,7 @@ ${answer}
|
|
|
2833
3447
|
return;
|
|
2834
3448
|
}
|
|
2835
3449
|
if (!hasMergeDeclaration(existing)) {
|
|
2836
|
-
await (0,
|
|
3450
|
+
await (0, import_promises6.appendFile)(target, appendUnionMergeLine(existing), "utf8");
|
|
2837
3451
|
this.logger.info?.({
|
|
2838
3452
|
operation: "kb.gitattributes.ensure",
|
|
2839
3453
|
bundlePath: root,
|
|
@@ -2852,7 +3466,7 @@ ${answer}
|
|
|
2852
3466
|
async record(root, entry) {
|
|
2853
3467
|
await this.ensureGitattributes(root);
|
|
2854
3468
|
const line = renderLogEntry({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
|
|
2855
|
-
await (0,
|
|
3469
|
+
await (0, import_promises6.appendFile)((0, import_node_path7.join)(root, LOG_FILE), line, "utf8").catch((error) => {
|
|
2856
3470
|
this.logger.warn?.({
|
|
2857
3471
|
operation: "kb.log.append",
|
|
2858
3472
|
outcome: "failed",
|
|
@@ -2878,18 +3492,18 @@ ${answer}
|
|
|
2878
3492
|
};
|
|
2879
3493
|
}
|
|
2880
3494
|
root(bundlePath2) {
|
|
2881
|
-
return (0,
|
|
3495
|
+
return (0, import_node_path7.resolve)(bundlePath2);
|
|
2882
3496
|
}
|
|
2883
3497
|
// Concept ids are `<type>.<slug>` and map to a single file directly under the
|
|
2884
3498
|
// bundle root; anything carrying a separator would escape it.
|
|
2885
3499
|
recordPath(bundlePath2, conceptId2) {
|
|
2886
|
-
if (conceptId2.includes(
|
|
3500
|
+
if (conceptId2.includes(import_node_path7.sep) || conceptId2.includes("/")) {
|
|
2887
3501
|
throw new KbInvalidConceptIdError(
|
|
2888
3502
|
"concept id must not contain a path separator",
|
|
2889
3503
|
{ conceptId: conceptId2 }
|
|
2890
3504
|
);
|
|
2891
3505
|
}
|
|
2892
|
-
return (0,
|
|
3506
|
+
return (0, import_node_path7.join)(this.root(bundlePath2), `${conceptId2}.md`);
|
|
2893
3507
|
}
|
|
2894
3508
|
};
|
|
2895
3509
|
function estimateTokens(record) {
|
|
@@ -2916,7 +3530,7 @@ function stub(hit) {
|
|
|
2916
3530
|
at: hit.record.frontmatter.generated?.at ?? null
|
|
2917
3531
|
};
|
|
2918
3532
|
}
|
|
2919
|
-
function
|
|
3533
|
+
function matches2(record, needle) {
|
|
2920
3534
|
const { title, description } = record.frontmatter;
|
|
2921
3535
|
return [record.conceptId, title, description, record.body].some(
|
|
2922
3536
|
(field) => field?.toLowerCase().includes(needle)
|
|
@@ -2929,37 +3543,37 @@ function normalizeActor(id) {
|
|
|
2929
3543
|
}
|
|
2930
3544
|
|
|
2931
3545
|
// src/compose.ts
|
|
2932
|
-
var
|
|
2933
|
-
var composeLinkSchema =
|
|
3546
|
+
var import_zod4 = require("zod");
|
|
3547
|
+
var composeLinkSchema = import_zod4.z.object({
|
|
2934
3548
|
target: kbConceptIdSchema,
|
|
2935
|
-
rel:
|
|
3549
|
+
rel: import_zod4.z.enum(KB_LINK_RELS)
|
|
2936
3550
|
}).strict();
|
|
2937
|
-
var composeInputSchema =
|
|
2938
|
-
slug:
|
|
3551
|
+
var composeInputSchema = import_zod4.z.object({
|
|
3552
|
+
slug: import_zod4.z.string().min(1),
|
|
2939
3553
|
/** One line, in the reader's terms. Becomes OKF `title`. */
|
|
2940
|
-
title:
|
|
3554
|
+
title: import_zod4.z.string().min(1),
|
|
2941
3555
|
/** The consequence — what breaks if this is wrong. Becomes `description`. */
|
|
2942
|
-
why:
|
|
3556
|
+
why: import_zod4.z.string().min(1),
|
|
2943
3557
|
/** Keyed by section heading from the type's spec. Unknown keys rejected. */
|
|
2944
|
-
sections:
|
|
2945
|
-
anchors:
|
|
2946
|
-
sources:
|
|
3558
|
+
sections: import_zod4.z.record(import_zod4.z.string(), import_zod4.z.string().min(1)).optional(),
|
|
3559
|
+
anchors: import_zod4.z.array(kbAnchorSchema).optional(),
|
|
3560
|
+
sources: import_zod4.z.array(kbSourceSchema).optional(),
|
|
2947
3561
|
/** No source exists, as a claim rather than a sentinel in `sources`. */
|
|
2948
|
-
assumption:
|
|
3562
|
+
assumption: import_zod4.z.boolean().optional(),
|
|
2949
3563
|
/**
|
|
2950
3564
|
* OKF `stale_after`: the absolute date this record stops being trusted.
|
|
2951
3565
|
* Anything the outside world can change — pricing, quotas, versions,
|
|
2952
3566
|
* reception counts — should carry one.
|
|
2953
3567
|
*/
|
|
2954
|
-
stale_after:
|
|
3568
|
+
stale_after: import_zod4.z.string().regex(/^\d{4}-\d{2}-\d{2}$/, {
|
|
2955
3569
|
message: "stale_after must be YYYY-MM-DD"
|
|
2956
3570
|
}).refine((date) => !Number.isNaN(Date.parse(date)), {
|
|
2957
3571
|
message: "stale_after must be a real date"
|
|
2958
3572
|
}).optional(),
|
|
2959
|
-
verify:
|
|
2960
|
-
tags:
|
|
3573
|
+
verify: import_zod4.z.array(import_zod4.z.string().min(1)).optional(),
|
|
3574
|
+
tags: import_zod4.z.array(import_zod4.z.string().min(1)).optional(),
|
|
2961
3575
|
/** Concept ids this record relates to; rendered as body links. */
|
|
2962
|
-
relatedConceptIds:
|
|
3576
|
+
relatedConceptIds: import_zod4.z.array(kbConceptIdSchema).optional(),
|
|
2963
3577
|
/**
|
|
2964
3578
|
* Typed causal edges, source → target: `{ target: "fact.b", rel:
|
|
2965
3579
|
* "depends_on" }` on record A says A needs B. Stored in frontmatter and
|
|
@@ -2967,16 +3581,16 @@ var composeInputSchema = import_zod3.z.object({
|
|
|
2967
3581
|
* reader that knows only OKF. The vocabulary goes into the description from
|
|
2968
3582
|
* the same table the walk uses, so `kb_schema` emits it.
|
|
2969
3583
|
*/
|
|
2970
|
-
links:
|
|
3584
|
+
links: import_zod4.z.array(composeLinkSchema).max(64).optional().describe(
|
|
2971
3585
|
`Typed causal edges, source \u2192 target \u2014 a link on this record says this record <rel> the target. ${KB_LINK_RELS.map(
|
|
2972
3586
|
(rel) => `${rel}: ${LINK_RELS[rel].purpose}`
|
|
2973
3587
|
).join("; ")}.`
|
|
2974
3588
|
),
|
|
2975
3589
|
/** Concept ids this record replaces. The store settles the backlinks. */
|
|
2976
|
-
supersedes:
|
|
2977
|
-
materiality:
|
|
2978
|
-
confidence:
|
|
2979
|
-
owner:
|
|
3590
|
+
supersedes: import_zod4.z.array(kbConceptIdSchema).max(32).optional(),
|
|
3591
|
+
materiality: import_zod4.z.enum(KB_MATERIALITIES).optional(),
|
|
3592
|
+
confidence: import_zod4.z.enum(KB_CONFIDENCES).optional(),
|
|
3593
|
+
owner: import_zod4.z.string().min(1).optional()
|
|
2980
3594
|
}).strict();
|
|
2981
3595
|
function composeRecord(type, input, writtenBy, writtenAt) {
|
|
2982
3596
|
const parsed = composeInputSchema.parse(input);
|
|
@@ -3053,9 +3667,9 @@ ${text}`);
|
|
|
3053
3667
|
// src/kb-pins/budgets.ts
|
|
3054
3668
|
function asBudgets(value) {
|
|
3055
3669
|
if (value === null || typeof value !== "object") return {};
|
|
3056
|
-
const
|
|
3670
|
+
const table2 = value;
|
|
3057
3671
|
const pick = (key2, min) => {
|
|
3058
|
-
const raw =
|
|
3672
|
+
const raw = table2[key2];
|
|
3059
3673
|
return typeof raw === "number" && Number.isInteger(raw) && raw >= min ? raw : void 0;
|
|
3060
3674
|
};
|
|
3061
3675
|
const budgetTokens = pick("budgetTokens", 1);
|
|
@@ -3066,9 +3680,9 @@ function asBudgets(value) {
|
|
|
3066
3680
|
};
|
|
3067
3681
|
}
|
|
3068
3682
|
function contextProfileBudgets(manifest, profile) {
|
|
3069
|
-
const
|
|
3070
|
-
if (
|
|
3071
|
-
const entries =
|
|
3683
|
+
const table2 = manifest.context;
|
|
3684
|
+
if (table2 === null || typeof table2 !== "object") return {};
|
|
3685
|
+
const entries = table2;
|
|
3072
3686
|
return {
|
|
3073
3687
|
...asBudgets(entries["default"]),
|
|
3074
3688
|
...profile ? asBudgets(entries[profile]) : {}
|
|
@@ -3099,23 +3713,23 @@ var KbBaseFrozenError = class extends Error {
|
|
|
3099
3713
|
};
|
|
3100
3714
|
|
|
3101
3715
|
// src/kb-pins/frozen.ts
|
|
3102
|
-
var
|
|
3716
|
+
var import_node_path10 = require("path");
|
|
3103
3717
|
|
|
3104
3718
|
// src/kb-pins/layers.ts
|
|
3105
|
-
var
|
|
3106
|
-
var
|
|
3107
|
-
var
|
|
3719
|
+
var import_promises7 = require("fs/promises");
|
|
3720
|
+
var import_node_os3 = require("os");
|
|
3721
|
+
var import_node_path9 = require("path");
|
|
3108
3722
|
|
|
3109
3723
|
// src/kb-pins/model.ts
|
|
3110
|
-
var
|
|
3111
|
-
var
|
|
3112
|
-
var PINS_FILE = (0,
|
|
3113
|
-
var PINS_LOCAL_FILE = (0,
|
|
3724
|
+
var import_node_path8 = require("path");
|
|
3725
|
+
var import_zod5 = require("zod");
|
|
3726
|
+
var PINS_FILE = (0, import_node_path8.join)(".strauss", "kb-pins.json");
|
|
3727
|
+
var PINS_LOCAL_FILE = (0, import_node_path8.join)(".strauss", "kb-pins.local.json");
|
|
3114
3728
|
var PIN_LAYERS = ["project", "local", "user"];
|
|
3115
|
-
var pinSchema =
|
|
3729
|
+
var pinSchema = import_zod5.z.object({
|
|
3116
3730
|
/** Relative to the manifest's root, so the file is committable. */
|
|
3117
|
-
path:
|
|
3118
|
-
pinnedAt:
|
|
3731
|
+
path: import_zod5.z.string().min(1),
|
|
3732
|
+
pinnedAt: import_zod5.z.string().min(1).optional(),
|
|
3119
3733
|
/**
|
|
3120
3734
|
* How `context` renders this base. `full` preloads the whole base into
|
|
3121
3735
|
* the block regardless of the full-under threshold — for a base whose
|
|
@@ -3125,7 +3739,7 @@ var pinSchema = import_zod4.z.object({
|
|
|
3125
3739
|
* Absent: the profile's full-under threshold decides. Invalid values
|
|
3126
3740
|
* degrade to absent rather than failing the manifest.
|
|
3127
3741
|
*/
|
|
3128
|
-
mode:
|
|
3742
|
+
mode: import_zod5.z.enum(["full", "index"]).optional().catch(void 0),
|
|
3129
3743
|
/**
|
|
3130
3744
|
* Context profiles this pin surfaces in (e.g. only at session-start,
|
|
3131
3745
|
* not per turn). Absent: every profile. A run without a profile sees
|
|
@@ -3133,17 +3747,17 @@ var pinSchema = import_zod4.z.object({
|
|
|
3133
3747
|
* that skill at point of use than pinned at all — pins are what every
|
|
3134
3748
|
* session should see.
|
|
3135
3749
|
*/
|
|
3136
|
-
profiles:
|
|
3750
|
+
profiles: import_zod5.z.array(import_zod5.z.string()).optional().catch(void 0),
|
|
3137
3751
|
/**
|
|
3138
3752
|
* The base is concluded — a finished piece of research, a frozen ADR
|
|
3139
3753
|
* set. Write commands against it refuse while this workspace holds the
|
|
3140
3754
|
* pin, and `context` labels it read-only. Workspace policy, not base
|
|
3141
3755
|
* state: the base itself stays copyable and writable elsewhere.
|
|
3142
3756
|
*/
|
|
3143
|
-
frozen:
|
|
3757
|
+
frozen: import_zod5.z.boolean().optional().catch(void 0)
|
|
3144
3758
|
}).passthrough();
|
|
3145
|
-
var pinsManifestSchema =
|
|
3146
|
-
pins:
|
|
3759
|
+
var pinsManifestSchema = import_zod5.z.object({
|
|
3760
|
+
pins: import_zod5.z.array(pinSchema).default([]),
|
|
3147
3761
|
/**
|
|
3148
3762
|
* Per-repo budgets for the `context` command, keyed by profile —
|
|
3149
3763
|
* `"session-start"`, `"compact"`, `"turn"`, or `"default"` for all of
|
|
@@ -3152,18 +3766,18 @@ var pinsManifestSchema = import_zod4.z.object({
|
|
|
3152
3766
|
* the index at every session start. `contextProfileBudgets` does the
|
|
3153
3767
|
* tolerant read.
|
|
3154
3768
|
*/
|
|
3155
|
-
context:
|
|
3769
|
+
context: import_zod5.z.unknown().optional()
|
|
3156
3770
|
}).passthrough();
|
|
3157
3771
|
|
|
3158
3772
|
// src/kb-pins/layers.ts
|
|
3159
3773
|
function userRoot() {
|
|
3160
|
-
return process.env.STRAUSS_KB_USER_ROOT || (0,
|
|
3774
|
+
return process.env.STRAUSS_KB_USER_ROOT || (0, import_node_os3.homedir)();
|
|
3161
3775
|
}
|
|
3162
3776
|
function layerRoot(workspaceDir, layer) {
|
|
3163
|
-
return layer === "user" ? userRoot() : (0,
|
|
3777
|
+
return layer === "user" ? userRoot() : (0, import_node_path9.resolve)(workspaceDir);
|
|
3164
3778
|
}
|
|
3165
3779
|
function layerFile(workspaceDir, layer) {
|
|
3166
|
-
return (0,
|
|
3780
|
+
return (0, import_node_path9.join)(
|
|
3167
3781
|
layerRoot(workspaceDir, layer),
|
|
3168
3782
|
layer === "local" ? PINS_LOCAL_FILE : PINS_FILE
|
|
3169
3783
|
);
|
|
@@ -3172,7 +3786,7 @@ async function readPinsLayer(workspaceDir, layer) {
|
|
|
3172
3786
|
const file = layerFile(workspaceDir, layer);
|
|
3173
3787
|
let raw;
|
|
3174
3788
|
try {
|
|
3175
|
-
raw = await (0,
|
|
3789
|
+
raw = await (0, import_promises7.readFile)(file, "utf8");
|
|
3176
3790
|
} catch {
|
|
3177
3791
|
return { pins: [] };
|
|
3178
3792
|
}
|
|
@@ -3196,16 +3810,16 @@ async function readPinsLayer(workspaceDir, layer) {
|
|
|
3196
3810
|
}
|
|
3197
3811
|
async function writePinsLayer(workspaceDir, layer, manifest) {
|
|
3198
3812
|
const file = layerFile(workspaceDir, layer);
|
|
3199
|
-
await (0,
|
|
3200
|
-
await (0,
|
|
3813
|
+
await (0, import_promises7.mkdir)((0, import_node_path9.dirname)(file), { recursive: true });
|
|
3814
|
+
await (0, import_promises7.writeFile)(file, `${JSON.stringify(manifest, null, 2)}
|
|
3201
3815
|
`, "utf8");
|
|
3202
3816
|
}
|
|
3203
3817
|
function resolvePinPath(rootDir, path) {
|
|
3204
|
-
return (0,
|
|
3818
|
+
return (0, import_node_path9.isAbsolute)(path) ? (0, import_node_path9.resolve)(path) : (0, import_node_path9.resolve)(rootDir, path.split("/").join(import_node_path9.sep));
|
|
3205
3819
|
}
|
|
3206
3820
|
function storablePath(rootDir, bundlePath2) {
|
|
3207
|
-
const rel = (0,
|
|
3208
|
-
return (rel === "" ? "." : rel).split(
|
|
3821
|
+
const rel = (0, import_node_path9.relative)((0, import_node_path9.resolve)(rootDir), (0, import_node_path9.resolve)(bundlePath2));
|
|
3822
|
+
return (rel === "" ? "." : rel).split(import_node_path9.sep).join("/");
|
|
3209
3823
|
}
|
|
3210
3824
|
async function readMergedPins(workspaceDir) {
|
|
3211
3825
|
const manifests = {};
|
|
@@ -3233,7 +3847,7 @@ async function readMergedPins(workspaceDir) {
|
|
|
3233
3847
|
// src/kb-pins/frozen.ts
|
|
3234
3848
|
async function assertBaseNotFrozen(workspaceDir, bundlePath2) {
|
|
3235
3849
|
const merged = await readMergedPins(workspaceDir);
|
|
3236
|
-
const absolute = (0,
|
|
3850
|
+
const absolute = (0, import_node_path10.resolve)(bundlePath2);
|
|
3237
3851
|
const pin = merged.pins.find((entry) => entry.absolutePath === absolute);
|
|
3238
3852
|
if (pin?.frozen === true) {
|
|
3239
3853
|
throw new KbBaseFrozenError(pin.path, pin.layer);
|
|
@@ -3318,7 +3932,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
|
|
|
3318
3932
|
}
|
|
3319
3933
|
|
|
3320
3934
|
// src/kb-pins/unpin.ts
|
|
3321
|
-
var
|
|
3935
|
+
var import_node_path11 = require("path");
|
|
3322
3936
|
async function unpinBase(workspaceDir, bundlePath2) {
|
|
3323
3937
|
const layers = [];
|
|
3324
3938
|
for (const layer of PIN_LAYERS) {
|
|
@@ -3339,14 +3953,14 @@ async function unpinBase(workspaceDir, bundlePath2) {
|
|
|
3339
3953
|
}
|
|
3340
3954
|
}
|
|
3341
3955
|
return {
|
|
3342
|
-
path: storablePath((0,
|
|
3956
|
+
path: storablePath((0, import_node_path11.resolve)(workspaceDir), bundlePath2),
|
|
3343
3957
|
removed: layers.length > 0,
|
|
3344
3958
|
layers
|
|
3345
3959
|
};
|
|
3346
3960
|
}
|
|
3347
3961
|
|
|
3348
3962
|
// src/kb-context.ts
|
|
3349
|
-
var
|
|
3963
|
+
var import_promises8 = require("fs/promises");
|
|
3350
3964
|
var HEADING2 = "## Knowledge bases (pinned)";
|
|
3351
3965
|
var DEFAULT_CONTEXT_BUDGET = 4e3;
|
|
3352
3966
|
var CONTEXT_PROFILES = {
|
|
@@ -3550,13 +4164,13 @@ function toHookJson(block, event) {
|
|
|
3550
4164
|
var CONTEXT_BEGIN = "<!-- strauss-kb:begin -->";
|
|
3551
4165
|
var CONTEXT_END = "<!-- strauss-kb:end -->";
|
|
3552
4166
|
async function syncInstructions(file, block) {
|
|
3553
|
-
const existing = await (0,
|
|
4167
|
+
const existing = await (0, import_promises8.readFile)(file, "utf8").catch(() => null);
|
|
3554
4168
|
const region = block ? `${CONTEXT_BEGIN}
|
|
3555
4169
|
${block.trim()}
|
|
3556
4170
|
${CONTEXT_END}` : null;
|
|
3557
4171
|
if (existing === null) {
|
|
3558
4172
|
if (!region) return { file, action: "unchanged" };
|
|
3559
|
-
await (0,
|
|
4173
|
+
await (0, import_promises8.writeFile)(file, `${region}
|
|
3560
4174
|
`, "utf8");
|
|
3561
4175
|
return { file, action: "created" };
|
|
3562
4176
|
}
|
|
@@ -3567,11 +4181,11 @@ ${CONTEXT_END}` : null;
|
|
|
3567
4181
|
const after = existing.slice(end + CONTEXT_END.length);
|
|
3568
4182
|
const next = region ? `${before}${region}${after}` : `${before.replace(/\n+$/, "\n")}${after.replace(/^\n+/, "\n")}`;
|
|
3569
4183
|
if (next === existing) return { file, action: "unchanged" };
|
|
3570
|
-
await (0,
|
|
4184
|
+
await (0, import_promises8.writeFile)(file, next, "utf8");
|
|
3571
4185
|
return { file, action: region ? "replaced" : "removed" };
|
|
3572
4186
|
}
|
|
3573
4187
|
if (!region) return { file, action: "unchanged" };
|
|
3574
|
-
await (0,
|
|
4188
|
+
await (0, import_promises8.writeFile)(
|
|
3575
4189
|
file,
|
|
3576
4190
|
`${existing.replace(/\n*$/, "\n\n")}${region}
|
|
3577
4191
|
`,
|
|
@@ -3581,14 +4195,14 @@ ${CONTEXT_END}` : null;
|
|
|
3581
4195
|
}
|
|
3582
4196
|
|
|
3583
4197
|
// src/json-schema.ts
|
|
3584
|
-
var
|
|
4198
|
+
var import_zod6 = require("zod");
|
|
3585
4199
|
function kbJsonSchemas() {
|
|
3586
4200
|
return {
|
|
3587
|
-
recordFrontmatter:
|
|
4201
|
+
recordFrontmatter: import_zod6.z.toJSONSchema(kbRecordFrontmatterSchema, {
|
|
3588
4202
|
io: "input"
|
|
3589
4203
|
}),
|
|
3590
|
-
composeInput:
|
|
3591
|
-
logEntry:
|
|
4204
|
+
composeInput: import_zod6.z.toJSONSchema(composeInputSchema, { io: "input" }),
|
|
4205
|
+
logEntry: import_zod6.z.toJSONSchema(kbLogEntrySchema, { io: "input" })
|
|
3592
4206
|
};
|
|
3593
4207
|
}
|
|
3594
4208
|
|
|
@@ -3598,7 +4212,7 @@ function matchToDiff(files, records, options = {}) {
|
|
|
3598
4212
|
const anchored = records.filter(
|
|
3599
4213
|
(record) => (record.frontmatter.strauss_anchors ?? []).length > 0
|
|
3600
4214
|
);
|
|
3601
|
-
const
|
|
4215
|
+
const matches3 = [];
|
|
3602
4216
|
for (const file of files) {
|
|
3603
4217
|
const candidates = anchored.map((record) => ({
|
|
3604
4218
|
record,
|
|
@@ -3617,7 +4231,7 @@ function matchToDiff(files, records, options = {}) {
|
|
|
3617
4231
|
hits.push(record);
|
|
3618
4232
|
}
|
|
3619
4233
|
if (!hits.length) continue;
|
|
3620
|
-
|
|
4234
|
+
matches3.push({
|
|
3621
4235
|
filePath: file.filePath,
|
|
3622
4236
|
hunk,
|
|
3623
4237
|
records: order(adjudicate(hits, records, options.now)),
|
|
@@ -3625,7 +4239,7 @@ function matchToDiff(files, records, options = {}) {
|
|
|
3625
4239
|
});
|
|
3626
4240
|
}
|
|
3627
4241
|
}
|
|
3628
|
-
return
|
|
4242
|
+
return matches3;
|
|
3629
4243
|
}
|
|
3630
4244
|
function place(anchors, filePath, hunk, ranges) {
|
|
3631
4245
|
let fallback = "miss";
|
|
@@ -3774,6 +4388,18 @@ var CHECK_HEADLINES = {
|
|
|
3774
4388
|
unchecked: "an anchor in another repository nothing could reach"
|
|
3775
4389
|
};
|
|
3776
4390
|
var DAY_MS = 864e5;
|
|
4391
|
+
function anchorResolverCounts(bundle) {
|
|
4392
|
+
let treeSitter = 0;
|
|
4393
|
+
let regex = 0;
|
|
4394
|
+
for (const record of bundle) {
|
|
4395
|
+
for (const anchor of record.frontmatter.strauss_anchors ?? []) {
|
|
4396
|
+
if (!anchor.hash || !anchor.symbol) continue;
|
|
4397
|
+
if (anchor.resolver === "tree-sitter") treeSitter += 1;
|
|
4398
|
+
else regex += 1;
|
|
4399
|
+
}
|
|
4400
|
+
}
|
|
4401
|
+
return { total: treeSitter + regex, treeSitter, regex };
|
|
4402
|
+
}
|
|
3777
4403
|
function doctor(bundle, options = {}) {
|
|
3778
4404
|
const thresholds = {
|
|
3779
4405
|
expiringDays: options.expiringDays ?? DEFAULT_EXPIRING_DAYS,
|
|
@@ -3809,6 +4435,7 @@ function doctor(bundle, options = {}) {
|
|
|
3809
4435
|
counts,
|
|
3810
4436
|
groups,
|
|
3811
4437
|
findingCount,
|
|
4438
|
+
anchorResolvers: anchorResolverCounts(bundle),
|
|
3812
4439
|
healthy: findingCount === 0
|
|
3813
4440
|
};
|
|
3814
4441
|
}
|
|
@@ -4055,12 +4682,12 @@ function ageInDays(record, now) {
|
|
|
4055
4682
|
}
|
|
4056
4683
|
|
|
4057
4684
|
// src/decision-record.ts
|
|
4058
|
-
var
|
|
4685
|
+
var import_zod7 = require("zod");
|
|
4059
4686
|
var DECISION_TYPE = "decision";
|
|
4060
4687
|
var NO_DECISION_SLUG = "none";
|
|
4061
4688
|
var decisionInputSchema = composeInputSchema.omit({ sections: true }).extend({
|
|
4062
|
-
alternative:
|
|
4063
|
-
impact:
|
|
4689
|
+
alternative: import_zod7.z.string().min(1).optional(),
|
|
4690
|
+
impact: import_zod7.z.string().min(1).optional()
|
|
4064
4691
|
}).strict();
|
|
4065
4692
|
function composeDecisionRecord(input, writtenBy, writtenAt) {
|
|
4066
4693
|
const { alternative, impact: impact2, ...rest } = input;
|
|
@@ -4102,13 +4729,13 @@ function selectDecisions(records) {
|
|
|
4102
4729
|
}
|
|
4103
4730
|
|
|
4104
4731
|
// src/commands/anchor-resolve.ts
|
|
4105
|
-
var
|
|
4732
|
+
var import_zod9 = require("zod");
|
|
4106
4733
|
|
|
4107
4734
|
// src/commands/model.ts
|
|
4108
|
-
var
|
|
4109
|
-
var bundlePath =
|
|
4110
|
-
var conceptId =
|
|
4111
|
-
var REPO_ROOT =
|
|
4735
|
+
var import_zod8 = require("zod");
|
|
4736
|
+
var bundlePath = import_zod8.z.string().min(1).describe("Absolute path to the knowledge base directory.");
|
|
4737
|
+
var conceptId = import_zod8.z.string().min(1).describe("e.g. decision.cursor-v2");
|
|
4738
|
+
var REPO_ROOT = import_zod8.z.string().min(1).optional().describe(
|
|
4112
4739
|
"Where the anchored source lives, for the drift check. Defaults to the working directory."
|
|
4113
4740
|
);
|
|
4114
4741
|
function define(command) {
|
|
@@ -4131,22 +4758,30 @@ function argvFlag(argv, name) {
|
|
|
4131
4758
|
}
|
|
4132
4759
|
|
|
4133
4760
|
// src/commands/anchor-resolve.ts
|
|
4761
|
+
function resolverSummary(results) {
|
|
4762
|
+
const names = [
|
|
4763
|
+
...new Set(
|
|
4764
|
+
results.flatMap((entry) => entry.resolver ? [entry.resolver] : [])
|
|
4765
|
+
)
|
|
4766
|
+
].sort();
|
|
4767
|
+
return names.length ? `${names.join(" + ")} resolver` : "whole-file";
|
|
4768
|
+
}
|
|
4134
4769
|
var anchorResolveCommand = define({
|
|
4135
4770
|
name: "anchor-resolve",
|
|
4136
4771
|
tool: "kb_anchor_resolve",
|
|
4137
4772
|
usage: "anchor-resolve <concept-id> [--repo-root <path>] [--offline] [--rebaseline] [--restamp]",
|
|
4138
4773
|
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.",
|
|
4139
|
-
input:
|
|
4774
|
+
input: import_zod9.z.object({
|
|
4140
4775
|
bundlePath,
|
|
4141
4776
|
conceptId,
|
|
4142
|
-
repoRoot:
|
|
4143
|
-
offline:
|
|
4777
|
+
repoRoot: import_zod9.z.string().min(1).optional(),
|
|
4778
|
+
offline: import_zod9.z.boolean().optional().describe(
|
|
4144
4779
|
"Resolve foreign anchors from the local repo cache only, never fetching."
|
|
4145
4780
|
),
|
|
4146
|
-
rebaseline:
|
|
4781
|
+
rebaseline: import_zod9.z.boolean().optional().describe(
|
|
4147
4782
|
"Accept the current code as the new baseline for anchors that drifted."
|
|
4148
4783
|
),
|
|
4149
|
-
restamp:
|
|
4784
|
+
restamp: import_zod9.z.boolean().optional().describe(
|
|
4150
4785
|
"Refresh `resolved_at` on anchors that already match. Off by default, so a green run writes nothing."
|
|
4151
4786
|
)
|
|
4152
4787
|
}),
|
|
@@ -4175,6 +4810,11 @@ var anchorResolveCommand = define({
|
|
|
4175
4810
|
const updated = [];
|
|
4176
4811
|
let dirty = false;
|
|
4177
4812
|
const sources = await readSources(anchors, root, offline === true);
|
|
4813
|
+
const resolvers = defaultAnchorResolvers({ offline: offline === true });
|
|
4814
|
+
await prepareResolvers(
|
|
4815
|
+
resolvers,
|
|
4816
|
+
anchors.map((anchor) => anchor.file)
|
|
4817
|
+
);
|
|
4178
4818
|
for (const anchor of anchors) {
|
|
4179
4819
|
const base2 = {
|
|
4180
4820
|
file: anchor.file,
|
|
@@ -4191,27 +4831,35 @@ var anchorResolveCommand = define({
|
|
|
4191
4831
|
updated.push(anchor);
|
|
4192
4832
|
continue;
|
|
4193
4833
|
}
|
|
4194
|
-
const
|
|
4195
|
-
if (!
|
|
4834
|
+
const outcome = resolveAnchorSpan(source.source, anchor, resolvers);
|
|
4835
|
+
if (!outcome.ok) {
|
|
4196
4836
|
results.push({
|
|
4197
4837
|
...base2,
|
|
4198
4838
|
state: "unresolved",
|
|
4199
|
-
reason:
|
|
4839
|
+
reason: outcome.reason
|
|
4200
4840
|
});
|
|
4201
4841
|
updated.push(anchor);
|
|
4202
4842
|
continue;
|
|
4203
4843
|
}
|
|
4844
|
+
const resolved = outcome.span;
|
|
4845
|
+
const producedBy = outcome.resolver;
|
|
4204
4846
|
const currentHash = hashAnchorText(resolved.text);
|
|
4205
4847
|
const currentLines = resolved.endLine - resolved.startLine + 1;
|
|
4206
4848
|
const stamped = {
|
|
4207
4849
|
...anchor,
|
|
4208
4850
|
hash: currentHash,
|
|
4209
4851
|
lines: currentLines,
|
|
4210
|
-
resolved_at: now()
|
|
4852
|
+
resolved_at: now(),
|
|
4853
|
+
...producedBy ? { resolver: producedBy } : {}
|
|
4211
4854
|
};
|
|
4212
4855
|
const pinned = anchor.ref !== void 0 && source.repo !== void 0;
|
|
4213
4856
|
if (!anchor.hash) {
|
|
4214
|
-
results.push({
|
|
4857
|
+
results.push({
|
|
4858
|
+
...base2,
|
|
4859
|
+
state: "stamped",
|
|
4860
|
+
currentHash,
|
|
4861
|
+
...producedBy ? { resolver: producedBy } : {}
|
|
4862
|
+
});
|
|
4215
4863
|
updated.push(stamped);
|
|
4216
4864
|
dirty = true;
|
|
4217
4865
|
continue;
|
|
@@ -4222,6 +4870,10 @@ var anchorResolveCommand = define({
|
|
|
4222
4870
|
state: "drifted",
|
|
4223
4871
|
currentHash,
|
|
4224
4872
|
diffSize: lineDelta(anchor, currentLines),
|
|
4873
|
+
...producedBy ? { resolver: producedBy } : {},
|
|
4874
|
+
// A regex-stamped anchor re-read by tree-sitter drifts because the
|
|
4875
|
+
// resolver changed, not because the code did.
|
|
4876
|
+
...resolverChanged(source.source, anchor, producedBy) ? { reason: "resolver-changed" } : {},
|
|
4225
4877
|
...pinned ? { remoteState: "drifted-from-ref" } : {},
|
|
4226
4878
|
...rebaseline ? { rebaselined: true } : {}
|
|
4227
4879
|
});
|
|
@@ -4229,7 +4881,7 @@ var anchorResolveCommand = define({
|
|
|
4229
4881
|
if (rebaseline) dirty = true;
|
|
4230
4882
|
continue;
|
|
4231
4883
|
}
|
|
4232
|
-
const onDefault = pinned ? headHash(source, anchor) : void 0;
|
|
4884
|
+
const onDefault = pinned ? headHash(source, anchor, resolvers) : void 0;
|
|
4233
4885
|
if (onDefault && onDefault.hash !== anchor.hash) {
|
|
4234
4886
|
results.push({
|
|
4235
4887
|
...base2,
|
|
@@ -4245,6 +4897,7 @@ var anchorResolveCommand = define({
|
|
|
4245
4897
|
...base2,
|
|
4246
4898
|
state: "match",
|
|
4247
4899
|
currentHash,
|
|
4900
|
+
...producedBy ? { resolver: producedBy } : {},
|
|
4248
4901
|
...pinned ? { remoteState: "matches-ref" } : {}
|
|
4249
4902
|
});
|
|
4250
4903
|
const refresh = restamp || anchor.resolved_at === void 0;
|
|
@@ -4262,19 +4915,21 @@ var anchorResolveCommand = define({
|
|
|
4262
4915
|
if (!frozen) await store.updateAnchors(path, id, updated, actor);
|
|
4263
4916
|
}
|
|
4264
4917
|
const frozenNote = frozen ? { frozen: true, note: "base is frozen: nothing was stamped" } : {};
|
|
4918
|
+
const hints = grammarHints();
|
|
4919
|
+
const hintNote = hints.length ? { hints } : {};
|
|
4265
4920
|
const unreachable = results.filter(
|
|
4266
4921
|
(entry) => isUncheckedReason(entry.reason)
|
|
4267
4922
|
).length;
|
|
4268
4923
|
const checked = results.length - unreachable;
|
|
4269
|
-
const
|
|
4270
|
-
const note = `${
|
|
4271
|
-
const clean = checked > 0 &&
|
|
4924
|
+
const matches3 = results.filter((entry) => entry.state === "match").length;
|
|
4925
|
+
const note = `${matches3}/${checked} anchors match${unreachable ? `, ${unreachable} unreachable` : ""}`;
|
|
4926
|
+
const clean = checked > 0 && matches3 === checked && unreachable === 0;
|
|
4272
4927
|
if (clean) {
|
|
4273
4928
|
try {
|
|
4274
4929
|
await store.verify(
|
|
4275
4930
|
path,
|
|
4276
4931
|
id,
|
|
4277
|
-
`anchor-resolve: ${note} (
|
|
4932
|
+
`anchor-resolve: ${note} (${resolverSummary(results)})`,
|
|
4278
4933
|
actor,
|
|
4279
4934
|
now()
|
|
4280
4935
|
);
|
|
@@ -4285,17 +4940,25 @@ var anchorResolveCommand = define({
|
|
|
4285
4940
|
results,
|
|
4286
4941
|
verified: false,
|
|
4287
4942
|
verifyRefused: "self-verification",
|
|
4288
|
-
...frozenNote
|
|
4943
|
+
...frozenNote,
|
|
4944
|
+
...hintNote
|
|
4289
4945
|
};
|
|
4290
4946
|
}
|
|
4291
|
-
return {
|
|
4947
|
+
return {
|
|
4948
|
+
conceptId: id,
|
|
4949
|
+
results,
|
|
4950
|
+
verified: true,
|
|
4951
|
+
...frozenNote,
|
|
4952
|
+
...hintNote
|
|
4953
|
+
};
|
|
4292
4954
|
}
|
|
4293
4955
|
return {
|
|
4294
4956
|
conceptId: id,
|
|
4295
4957
|
results,
|
|
4296
4958
|
verified: false,
|
|
4297
4959
|
...unreachable ? { note } : {},
|
|
4298
|
-
...frozenNote
|
|
4960
|
+
...frozenNote,
|
|
4961
|
+
...hintNote
|
|
4299
4962
|
};
|
|
4300
4963
|
},
|
|
4301
4964
|
// A stored hash that no longer resolves is a broken anchor, not an absence:
|
|
@@ -4311,13 +4974,13 @@ var anchorResolveCommand = define({
|
|
|
4311
4974
|
function lineDelta(anchor, current) {
|
|
4312
4975
|
return anchor.lines === void 0 ? null : Math.abs(current - anchor.lines);
|
|
4313
4976
|
}
|
|
4314
|
-
function headHash(source, anchor) {
|
|
4977
|
+
function headHash(source, anchor, resolvers) {
|
|
4315
4978
|
if (source.head === void 0) return void 0;
|
|
4316
|
-
const
|
|
4317
|
-
if (!
|
|
4979
|
+
const outcome = resolveAnchorSpan(source.head, anchor, resolvers);
|
|
4980
|
+
if (!outcome.ok) return void 0;
|
|
4318
4981
|
return {
|
|
4319
|
-
hash: hashAnchorText(
|
|
4320
|
-
lines:
|
|
4982
|
+
hash: hashAnchorText(outcome.span.text),
|
|
4983
|
+
lines: outcome.span.endLine - outcome.span.startLine + 1
|
|
4321
4984
|
};
|
|
4322
4985
|
}
|
|
4323
4986
|
async function readSources(anchors, root, offline) {
|
|
@@ -4367,13 +5030,13 @@ async function readSources(anchors, root, offline) {
|
|
|
4367
5030
|
}
|
|
4368
5031
|
|
|
4369
5032
|
// src/commands/answer.ts
|
|
4370
|
-
var
|
|
5033
|
+
var import_zod10 = require("zod");
|
|
4371
5034
|
var answerCommand = define({
|
|
4372
5035
|
name: "answer",
|
|
4373
5036
|
tool: "kb_answer",
|
|
4374
5037
|
usage: "answer <concept-id> <answer...>",
|
|
4375
5038
|
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.",
|
|
4376
|
-
input:
|
|
5039
|
+
input: import_zod10.z.object({ bundlePath, conceptId, answer: import_zod10.z.string().min(1) }),
|
|
4377
5040
|
fromArgv: (argv, path) => ({
|
|
4378
5041
|
bundlePath: path,
|
|
4379
5042
|
conceptId: argv[1],
|
|
@@ -4387,27 +5050,27 @@ var answerCommand = define({
|
|
|
4387
5050
|
});
|
|
4388
5051
|
|
|
4389
5052
|
// src/commands/backlinks.ts
|
|
4390
|
-
var
|
|
5053
|
+
var import_zod11 = require("zod");
|
|
4391
5054
|
var backlinksCommand = define({
|
|
4392
5055
|
name: "backlinks",
|
|
4393
5056
|
tool: "kb_backlinks",
|
|
4394
5057
|
usage: "backlinks <concept-id>",
|
|
4395
5058
|
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.",
|
|
4396
|
-
input:
|
|
5059
|
+
input: import_zod11.z.object({ bundlePath, conceptId }),
|
|
4397
5060
|
fromArgv: (argv, path) => ({ bundlePath: path, conceptId: argv[1] }),
|
|
4398
5061
|
run: async ({ store }, { bundlePath: path, conceptId: id }) => store.backlinks(path, id)
|
|
4399
5062
|
});
|
|
4400
5063
|
|
|
4401
5064
|
// src/commands/catalog.ts
|
|
4402
|
-
var
|
|
5065
|
+
var import_zod12 = require("zod");
|
|
4403
5066
|
var catalogCommand = define({
|
|
4404
5067
|
name: "catalog",
|
|
4405
5068
|
tool: "kb_catalog",
|
|
4406
5069
|
usage: "catalog [type]",
|
|
4407
5070
|
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.",
|
|
4408
|
-
input:
|
|
5071
|
+
input: import_zod12.z.object({
|
|
4409
5072
|
bundlePath,
|
|
4410
|
-
type:
|
|
5073
|
+
type: import_zod12.z.enum(KB_RECORD_TYPES).optional()
|
|
4411
5074
|
}),
|
|
4412
5075
|
fromArgv: (argv, path) => ({
|
|
4413
5076
|
bundlePath: path,
|
|
@@ -4462,26 +5125,26 @@ function count(value, noun) {
|
|
|
4462
5125
|
}
|
|
4463
5126
|
|
|
4464
5127
|
// src/commands/context.ts
|
|
4465
|
-
var
|
|
5128
|
+
var import_zod13 = require("zod");
|
|
4466
5129
|
var contextCommand = define({
|
|
4467
5130
|
name: "context",
|
|
4468
5131
|
tool: "kb_context",
|
|
4469
5132
|
usage: "context [--profile NAME] [--budget N] [--full-under N] [--format json] [--event NAME]",
|
|
4470
5133
|
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.",
|
|
4471
|
-
input:
|
|
4472
|
-
budgetTokens:
|
|
5134
|
+
input: import_zod13.z.object({
|
|
5135
|
+
budgetTokens: import_zod13.z.number().int().positive().optional().describe(
|
|
4473
5136
|
"Ceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000."
|
|
4474
5137
|
),
|
|
4475
|
-
fullUnderTokens:
|
|
5138
|
+
fullUnderTokens: import_zod13.z.number().int().positive().optional().describe(
|
|
4476
5139
|
"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."
|
|
4477
5140
|
),
|
|
4478
|
-
profile:
|
|
5141
|
+
profile: import_zod13.z.string().optional().describe(
|
|
4479
5142
|
"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."
|
|
4480
5143
|
),
|
|
4481
|
-
format:
|
|
5144
|
+
format: import_zod13.z.enum(["markdown", "json"]).optional().describe(
|
|
4482
5145
|
"CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
|
|
4483
5146
|
),
|
|
4484
|
-
event:
|
|
5147
|
+
event: import_zod13.z.string().optional().describe(
|
|
4485
5148
|
"hookEventName stamped into the JSON envelope. Only meaningful with format=json."
|
|
4486
5149
|
)
|
|
4487
5150
|
}),
|
|
@@ -4517,14 +5180,14 @@ var contextCommand = define({
|
|
|
4517
5180
|
});
|
|
4518
5181
|
|
|
4519
5182
|
// src/commands/doctor.ts
|
|
4520
|
-
var
|
|
4521
|
-
var days = (what, fallback) =>
|
|
5183
|
+
var import_zod14 = require("zod");
|
|
5184
|
+
var days = (what, fallback) => import_zod14.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
|
|
4522
5185
|
var doctorCommand = define({
|
|
4523
5186
|
name: "doctor",
|
|
4524
5187
|
tool: "kb_doctor",
|
|
4525
5188
|
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--offline] [--strict]",
|
|
4526
5189
|
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.",
|
|
4527
|
-
input:
|
|
5190
|
+
input: import_zod14.z.object({
|
|
4528
5191
|
bundlePath,
|
|
4529
5192
|
repoRoot: REPO_ROOT,
|
|
4530
5193
|
expiringDays: days(
|
|
@@ -4539,10 +5202,10 @@ var doctorCommand = define({
|
|
|
4539
5202
|
"How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
|
|
4540
5203
|
DEFAULT_AGING_DAYS
|
|
4541
5204
|
),
|
|
4542
|
-
offline:
|
|
5205
|
+
offline: import_zod14.z.boolean().optional().describe(
|
|
4543
5206
|
"Read foreign anchors from the local repo cache only, never fetching."
|
|
4544
5207
|
),
|
|
4545
|
-
strict:
|
|
5208
|
+
strict: import_zod14.z.boolean().optional().describe(
|
|
4546
5209
|
"Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
|
|
4547
5210
|
)
|
|
4548
5211
|
}),
|
|
@@ -4585,7 +5248,13 @@ var doctorCommand = define({
|
|
|
4585
5248
|
...anchorDrift !== void 0 ? { anchorDrift } : {},
|
|
4586
5249
|
now: new Date(checkedAt)
|
|
4587
5250
|
});
|
|
4588
|
-
|
|
5251
|
+
const hints = grammarHints();
|
|
5252
|
+
return {
|
|
5253
|
+
bundlePath: path,
|
|
5254
|
+
checkedAt,
|
|
5255
|
+
...report,
|
|
5256
|
+
...hints.length ? { hints } : {}
|
|
5257
|
+
};
|
|
4589
5258
|
},
|
|
4590
5259
|
render: (result) => render2(result),
|
|
4591
5260
|
// Only expiry, and only under --strict. The other seven checks report debt a
|
|
@@ -4603,12 +5272,15 @@ function render2(result) {
|
|
|
4603
5272
|
`records: ${result.recordCount}`,
|
|
4604
5273
|
`thresholds: expiring within ${thresholds.expiringDays}d, unverified over ${thresholds.unverifiedDays}d, aging over ${thresholds.agingDays}d`,
|
|
4605
5274
|
`checked: ${result.checkedAt}`,
|
|
5275
|
+
...result.anchorResolvers.total ? [
|
|
5276
|
+
`anchors: ${result.anchorResolvers.total} hashed \u2014 ${result.anchorResolvers.treeSitter} tree-sitter, ${result.anchorResolvers.regex} regex`
|
|
5277
|
+
] : [],
|
|
4606
5278
|
""
|
|
4607
5279
|
];
|
|
4608
|
-
const
|
|
5280
|
+
const width2 = Math.max(...result.groups.map((group2) => group2.check.length));
|
|
4609
5281
|
for (const group2 of result.groups) {
|
|
4610
5282
|
lines.push(
|
|
4611
|
-
` ${group2.check.padEnd(
|
|
5283
|
+
` ${group2.check.padEnd(width2)} ${String(group2.count).padStart(3)} ${group2.headline}`
|
|
4612
5284
|
);
|
|
4613
5285
|
}
|
|
4614
5286
|
for (const group2 of result.groups) {
|
|
@@ -4620,6 +5292,7 @@ function render2(result) {
|
|
|
4620
5292
|
);
|
|
4621
5293
|
}
|
|
4622
5294
|
}
|
|
5295
|
+
for (const hint of result.hints ?? []) lines.push("", hint);
|
|
4623
5296
|
lines.push(
|
|
4624
5297
|
"",
|
|
4625
5298
|
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.`
|
|
@@ -4628,19 +5301,19 @@ function render2(result) {
|
|
|
4628
5301
|
}
|
|
4629
5302
|
|
|
4630
5303
|
// src/commands/impact.ts
|
|
4631
|
-
var
|
|
5304
|
+
var import_zod15 = require("zod");
|
|
4632
5305
|
var impactCommand = define({
|
|
4633
5306
|
name: "impact",
|
|
4634
5307
|
tool: "kb_impact",
|
|
4635
5308
|
usage: "impact <concept-id> [--depth N] [--rels a,b]",
|
|
4636
5309
|
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.",
|
|
4637
|
-
input:
|
|
5310
|
+
input: import_zod15.z.object({
|
|
4638
5311
|
bundlePath,
|
|
4639
5312
|
conceptId,
|
|
4640
|
-
depth:
|
|
5313
|
+
depth: import_zod15.z.number().int().positive().optional().describe(
|
|
4641
5314
|
"Hops out from the record. Unbounded when omitted; a walk this cuts reports truncated: true."
|
|
4642
5315
|
),
|
|
4643
|
-
rels:
|
|
5316
|
+
rels: import_zod15.z.array(import_zod15.z.enum(KB_CAUSAL_LINK_RELS)).optional().describe(
|
|
4644
5317
|
"Narrow which rels the walk follows. Defaults to every rel that carries a dependence \u2014 all but related_to."
|
|
4645
5318
|
)
|
|
4646
5319
|
}),
|
|
@@ -4661,13 +5334,13 @@ var impactCommand = define({
|
|
|
4661
5334
|
});
|
|
4662
5335
|
|
|
4663
5336
|
// src/commands/list.ts
|
|
4664
|
-
var
|
|
5337
|
+
var import_zod16 = require("zod");
|
|
4665
5338
|
var listCommand = define({
|
|
4666
5339
|
name: "list",
|
|
4667
5340
|
tool: "kb_list",
|
|
4668
5341
|
usage: "list [type]",
|
|
4669
5342
|
description: "Every record, optionally one type. For enumerating; use kb_query for a question.",
|
|
4670
|
-
input:
|
|
5343
|
+
input: import_zod16.z.object({ bundlePath, type: import_zod16.z.enum(KB_RECORD_TYPES).optional() }),
|
|
4671
5344
|
fromArgv: (argv, path) => ({ bundlePath: path, type: argv[1] }),
|
|
4672
5345
|
run: async ({ store }, { bundlePath: path, type }) => (await store.list(path, type)).map((record) => ({
|
|
4673
5346
|
conceptId: record.conceptId,
|
|
@@ -4679,17 +5352,17 @@ var listCommand = define({
|
|
|
4679
5352
|
});
|
|
4680
5353
|
|
|
4681
5354
|
// src/commands/load.ts
|
|
4682
|
-
var
|
|
5355
|
+
var import_zod17 = require("zod");
|
|
4683
5356
|
var loadCommand = define({
|
|
4684
5357
|
name: "load",
|
|
4685
5358
|
tool: "kb_load",
|
|
4686
5359
|
usage: "load [type] [--budget N | --all] [--repo-root PATH]",
|
|
4687
5360
|
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.",
|
|
4688
|
-
input:
|
|
5361
|
+
input: import_zod17.z.object({
|
|
4689
5362
|
bundlePath,
|
|
4690
|
-
type:
|
|
4691
|
-
budgetTokens:
|
|
4692
|
-
all:
|
|
5363
|
+
type: import_zod17.z.enum(KB_RECORD_TYPES).optional(),
|
|
5364
|
+
budgetTokens: import_zod17.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
|
|
5365
|
+
all: import_zod17.z.boolean().optional().describe(
|
|
4693
5366
|
"Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
|
|
4694
5367
|
),
|
|
4695
5368
|
repoRoot: REPO_ROOT
|
|
@@ -4731,25 +5404,25 @@ var loadCommand = define({
|
|
|
4731
5404
|
});
|
|
4732
5405
|
|
|
4733
5406
|
// src/commands/log.ts
|
|
4734
|
-
var
|
|
5407
|
+
var import_zod18 = require("zod");
|
|
4735
5408
|
var logCommand = define({
|
|
4736
5409
|
name: "log",
|
|
4737
5410
|
tool: "kb_log",
|
|
4738
5411
|
usage: "log",
|
|
4739
5412
|
description: "Who touched what, and when. Append-only; malformed lines are reported, never repaired.",
|
|
4740
|
-
input:
|
|
5413
|
+
input: import_zod18.z.object({ bundlePath }),
|
|
4741
5414
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
4742
5415
|
run: ({ store }, { bundlePath: path }) => store.readLog(path)
|
|
4743
5416
|
});
|
|
4744
5417
|
|
|
4745
5418
|
// src/commands/no-decision.ts
|
|
4746
|
-
var
|
|
5419
|
+
var import_zod19 = require("zod");
|
|
4747
5420
|
var noDecisionCommand = define({
|
|
4748
5421
|
name: "no-decision",
|
|
4749
5422
|
tool: "kb_no_decision",
|
|
4750
5423
|
usage: "no-decision <reason...>",
|
|
4751
5424
|
description: "Record in one sentence that a piece of work had nothing to decide. Idempotent.",
|
|
4752
|
-
input:
|
|
5425
|
+
input: import_zod19.z.object({ bundlePath, reason: import_zod19.z.string().min(1) }),
|
|
4753
5426
|
fromArgv: (argv, path) => ({
|
|
4754
5427
|
bundlePath: path,
|
|
4755
5428
|
reason: argv.slice(1).join(" ").trim()
|
|
@@ -4766,20 +5439,20 @@ var noDecisionCommand = define({
|
|
|
4766
5439
|
});
|
|
4767
5440
|
|
|
4768
5441
|
// src/commands/pack.ts
|
|
4769
|
-
var
|
|
5442
|
+
var import_zod20 = require("zod");
|
|
4770
5443
|
var packCommand = define({
|
|
4771
5444
|
name: "pack",
|
|
4772
5445
|
tool: "kb_pack",
|
|
4773
5446
|
usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
|
|
4774
5447
|
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.",
|
|
4775
|
-
input:
|
|
5448
|
+
input: import_zod20.z.object({
|
|
4776
5449
|
bundlePath,
|
|
4777
5450
|
conceptId,
|
|
4778
|
-
hops:
|
|
4779
|
-
maxNodes:
|
|
5451
|
+
hops: import_zod20.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
|
|
5452
|
+
maxNodes: import_zod20.z.number().int().positive().optional().describe(
|
|
4780
5453
|
"How many records the pack may hold, root included. Defaults to 20."
|
|
4781
5454
|
),
|
|
4782
|
-
budgetTokens:
|
|
5455
|
+
budgetTokens: import_zod20.z.number().int().positive().optional().describe(
|
|
4783
5456
|
"Approximate token ceiling over what is actually emitted. Defaults to 25000."
|
|
4784
5457
|
)
|
|
4785
5458
|
}),
|
|
@@ -4866,22 +5539,22 @@ function warningLabel(warning) {
|
|
|
4866
5539
|
}
|
|
4867
5540
|
|
|
4868
5541
|
// src/commands/pin.ts
|
|
4869
|
-
var
|
|
5542
|
+
var import_zod21 = require("zod");
|
|
4870
5543
|
var pinCommand = define({
|
|
4871
5544
|
name: "pin",
|
|
4872
5545
|
tool: "kb_pin",
|
|
4873
5546
|
usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
|
|
4874
5547
|
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.",
|
|
4875
|
-
input:
|
|
5548
|
+
input: import_zod21.z.object({
|
|
4876
5549
|
bundlePath,
|
|
4877
|
-
mode:
|
|
5550
|
+
mode: import_zod21.z.enum(["full", "index"]).optional().describe(
|
|
4878
5551
|
"full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
|
|
4879
5552
|
),
|
|
4880
|
-
profiles:
|
|
4881
|
-
layer:
|
|
5553
|
+
profiles: import_zod21.z.array(import_zod21.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
|
|
5554
|
+
layer: import_zod21.z.enum(["project", "local", "user"]).optional().describe(
|
|
4882
5555
|
"Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
|
|
4883
5556
|
),
|
|
4884
|
-
frozen:
|
|
5557
|
+
frozen: import_zod21.z.boolean().optional().describe(
|
|
4885
5558
|
"true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
|
|
4886
5559
|
)
|
|
4887
5560
|
}),
|
|
@@ -4910,29 +5583,29 @@ var pinCommand = define({
|
|
|
4910
5583
|
});
|
|
4911
5584
|
|
|
4912
5585
|
// src/commands/pins.ts
|
|
4913
|
-
var
|
|
5586
|
+
var import_zod22 = require("zod");
|
|
4914
5587
|
var pinsCommand = define({
|
|
4915
5588
|
name: "pins",
|
|
4916
5589
|
tool: "kb_pins",
|
|
4917
5590
|
usage: "pins",
|
|
4918
5591
|
description: "Every pinned base across the manifest layers, with its layer and whether it resolves to records. Takes no bundlePath.",
|
|
4919
|
-
input:
|
|
5592
|
+
input: import_zod22.z.object({}),
|
|
4920
5593
|
fromArgv: () => ({}),
|
|
4921
5594
|
run: ({ store }) => listPins(store, process.cwd())
|
|
4922
5595
|
});
|
|
4923
5596
|
|
|
4924
5597
|
// src/commands/query.ts
|
|
4925
|
-
var
|
|
5598
|
+
var import_zod23 = require("zod");
|
|
4926
5599
|
var queryCommand = define({
|
|
4927
5600
|
name: "query",
|
|
4928
5601
|
tool: "kb_query",
|
|
4929
5602
|
usage: "query <text...> [--repo-root PATH]",
|
|
4930
5603
|
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.",
|
|
4931
|
-
input:
|
|
5604
|
+
input: import_zod23.z.object({
|
|
4932
5605
|
bundlePath,
|
|
4933
|
-
text:
|
|
4934
|
-
type:
|
|
4935
|
-
includeNonCurrent:
|
|
5606
|
+
text: import_zod23.z.string().optional(),
|
|
5607
|
+
type: import_zod23.z.enum(KB_RECORD_TYPES).optional(),
|
|
5608
|
+
includeNonCurrent: import_zod23.z.boolean().optional(),
|
|
4936
5609
|
repoRoot: REPO_ROOT
|
|
4937
5610
|
}),
|
|
4938
5611
|
// `--repo-root` is a flag, so its value must not fall into the search text.
|
|
@@ -4964,43 +5637,43 @@ var queryCommand = define({
|
|
|
4964
5637
|
});
|
|
4965
5638
|
|
|
4966
5639
|
// src/commands/read-index.ts
|
|
4967
|
-
var
|
|
5640
|
+
var import_zod24 = require("zod");
|
|
4968
5641
|
var readIndexCommand = define({
|
|
4969
5642
|
name: "index",
|
|
4970
5643
|
tool: "kb_index",
|
|
4971
5644
|
usage: "index",
|
|
4972
5645
|
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.",
|
|
4973
|
-
input:
|
|
5646
|
+
input: import_zod24.z.object({ bundlePath }),
|
|
4974
5647
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
4975
5648
|
run: ({ store }, { bundlePath: path }) => store.readIndex(path)
|
|
4976
5649
|
});
|
|
4977
5650
|
|
|
4978
5651
|
// src/commands/schema.ts
|
|
4979
|
-
var
|
|
5652
|
+
var import_zod25 = require("zod");
|
|
4980
5653
|
var schemaCommand = define({
|
|
4981
5654
|
name: "schema",
|
|
4982
5655
|
tool: "kb_schema",
|
|
4983
5656
|
usage: "schema",
|
|
4984
5657
|
description: "JSON Schema for frontmatter, write input, and log entries, generated from the enforcing code.",
|
|
4985
|
-
input:
|
|
5658
|
+
input: import_zod25.z.object({}),
|
|
4986
5659
|
fromArgv: () => ({}),
|
|
4987
5660
|
run: () => Promise.resolve(kbJsonSchemas())
|
|
4988
5661
|
});
|
|
4989
5662
|
|
|
4990
5663
|
// src/commands/stamp.ts
|
|
4991
|
-
var
|
|
4992
|
-
var
|
|
5664
|
+
var import_promises9 = require("fs/promises");
|
|
5665
|
+
var import_zod26 = require("zod");
|
|
4993
5666
|
var DIGEST = /^[0-9a-f]{64}$/;
|
|
4994
5667
|
var stampCommand = define({
|
|
4995
5668
|
name: "stamp",
|
|
4996
5669
|
tool: "kb_stamp",
|
|
4997
5670
|
usage: "stamp [--bundle PATH] [--since DIGEST|FILE]",
|
|
4998
5671
|
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.",
|
|
4999
|
-
input:
|
|
5000
|
-
bundlePath:
|
|
5672
|
+
input: import_zod26.z.object({
|
|
5673
|
+
bundlePath: import_zod26.z.string().min(1).optional().describe(
|
|
5001
5674
|
"Absolute path to one knowledge base. Omit to stamp every pinned base."
|
|
5002
5675
|
),
|
|
5003
|
-
since:
|
|
5676
|
+
since: import_zod26.z.string().min(1).optional().describe(
|
|
5004
5677
|
"Prior digest, or path to a prior `stamp --json`; only moved bases return, with changed ids when the baseline is a file."
|
|
5005
5678
|
)
|
|
5006
5679
|
}),
|
|
@@ -5062,7 +5735,7 @@ async function readBaseline(since) {
|
|
|
5062
5735
|
if (DIGEST.test(since)) return { digest: since, byPath: /* @__PURE__ */ new Map() };
|
|
5063
5736
|
let parsed;
|
|
5064
5737
|
try {
|
|
5065
|
-
parsed = JSON.parse(await (0,
|
|
5738
|
+
parsed = JSON.parse(await (0, import_promises9.readFile)(since, "utf8"));
|
|
5066
5739
|
} catch {
|
|
5067
5740
|
throw new KbStampBaselineError(since);
|
|
5068
5741
|
}
|
|
@@ -5086,16 +5759,16 @@ async function readBaseline(since) {
|
|
|
5086
5759
|
}
|
|
5087
5760
|
|
|
5088
5761
|
// src/commands/status.ts
|
|
5089
|
-
var
|
|
5762
|
+
var import_zod27 = require("zod");
|
|
5090
5763
|
var statusCommand = define({
|
|
5091
5764
|
name: "status",
|
|
5092
5765
|
tool: "kb_status",
|
|
5093
5766
|
usage: "status <concept-id> <status>",
|
|
5094
5767
|
description: "Move a record's status. Compare-and-swap: a concurrent change fails instead of being overwritten.",
|
|
5095
|
-
input:
|
|
5768
|
+
input: import_zod27.z.object({
|
|
5096
5769
|
bundlePath,
|
|
5097
5770
|
conceptId,
|
|
5098
|
-
status:
|
|
5771
|
+
status: import_zod27.z.enum(KB_RECORD_STATUSES)
|
|
5099
5772
|
}),
|
|
5100
5773
|
fromArgv: (argv, path) => ({
|
|
5101
5774
|
bundlePath: path,
|
|
@@ -5110,13 +5783,13 @@ var statusCommand = define({
|
|
|
5110
5783
|
});
|
|
5111
5784
|
|
|
5112
5785
|
// src/commands/supersede.ts
|
|
5113
|
-
var
|
|
5786
|
+
var import_zod28 = require("zod");
|
|
5114
5787
|
var supersedeCommand = define({
|
|
5115
5788
|
name: "supersede",
|
|
5116
5789
|
tool: "kb_supersede",
|
|
5117
5790
|
usage: "supersede <concept-id> <replacement-id>",
|
|
5118
5791
|
description: "Mark a record superseded by another, linked in both directions. Use instead of editing a record whose meaning changed.",
|
|
5119
|
-
input:
|
|
5792
|
+
input: import_zod28.z.object({ bundlePath, conceptId, replacementId: conceptId }),
|
|
5120
5793
|
fromArgv: (argv, path) => ({
|
|
5121
5794
|
bundlePath: path,
|
|
5122
5795
|
conceptId: argv[1],
|
|
@@ -5130,16 +5803,16 @@ var supersedeCommand = define({
|
|
|
5130
5803
|
});
|
|
5131
5804
|
|
|
5132
5805
|
// src/commands/sync-instructions.ts
|
|
5133
|
-
var
|
|
5806
|
+
var import_zod29 = require("zod");
|
|
5134
5807
|
var syncInstructionsCommand = define({
|
|
5135
5808
|
name: "sync-instructions",
|
|
5136
5809
|
usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
|
|
5137
5810
|
description: "CLI-only: plant the kb_context block between sentinel comments in AGENTS.md or CLAUDE.md, idempotently.",
|
|
5138
|
-
input:
|
|
5139
|
-
file:
|
|
5140
|
-
budgetTokens:
|
|
5141
|
-
fullUnderTokens:
|
|
5142
|
-
profile:
|
|
5811
|
+
input: import_zod29.z.object({
|
|
5812
|
+
file: import_zod29.z.string().min(1).describe("The instruction file to edit in place."),
|
|
5813
|
+
budgetTokens: import_zod29.z.number().int().positive().optional(),
|
|
5814
|
+
fullUnderTokens: import_zod29.z.number().int().positive().optional(),
|
|
5815
|
+
profile: import_zod29.z.string().optional()
|
|
5143
5816
|
}),
|
|
5144
5817
|
fromArgv: (argv) => {
|
|
5145
5818
|
const budget = argvFlag(argv, "--budget");
|
|
@@ -5165,17 +5838,17 @@ var syncInstructionsCommand = define({
|
|
|
5165
5838
|
});
|
|
5166
5839
|
|
|
5167
5840
|
// src/commands/trace.ts
|
|
5168
|
-
var
|
|
5841
|
+
var import_zod30 = require("zod");
|
|
5169
5842
|
var traceCommand = define({
|
|
5170
5843
|
name: "trace",
|
|
5171
5844
|
tool: "kb_trace",
|
|
5172
5845
|
usage: "trace <concept-id> [edges...]",
|
|
5173
5846
|
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".',
|
|
5174
|
-
input:
|
|
5847
|
+
input: import_zod30.z.object({
|
|
5175
5848
|
bundlePath,
|
|
5176
5849
|
conceptId,
|
|
5177
|
-
edges:
|
|
5178
|
-
depth:
|
|
5850
|
+
edges: import_zod30.z.array(import_zod30.z.enum(TRACE_EDGES)).optional(),
|
|
5851
|
+
depth: import_zod30.z.number().int().positive().optional()
|
|
5179
5852
|
}),
|
|
5180
5853
|
fromArgv: (argv, path) => ({
|
|
5181
5854
|
bundlePath: path,
|
|
@@ -5197,37 +5870,37 @@ var traceCommand = define({
|
|
|
5197
5870
|
});
|
|
5198
5871
|
|
|
5199
5872
|
// src/commands/types.ts
|
|
5200
|
-
var
|
|
5873
|
+
var import_zod31 = require("zod");
|
|
5201
5874
|
var typesCommand = define({
|
|
5202
5875
|
name: "types",
|
|
5203
5876
|
tool: "kb_types",
|
|
5204
5877
|
usage: "types",
|
|
5205
5878
|
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.",
|
|
5206
|
-
input:
|
|
5879
|
+
input: import_zod31.z.object({}),
|
|
5207
5880
|
fromArgv: () => ({}),
|
|
5208
5881
|
run: () => Promise.resolve(RECORD_TYPES)
|
|
5209
5882
|
});
|
|
5210
5883
|
|
|
5211
5884
|
// src/commands/unpin.ts
|
|
5212
|
-
var
|
|
5885
|
+
var import_zod32 = require("zod");
|
|
5213
5886
|
var unpinCommand = define({
|
|
5214
5887
|
name: "unpin",
|
|
5215
5888
|
tool: "kb_unpin",
|
|
5216
5889
|
usage: "unpin [bundle-path]",
|
|
5217
5890
|
description: "Remove a base from every manifest layer that holds it. Reports the layers touched.",
|
|
5218
|
-
input:
|
|
5891
|
+
input: import_zod32.z.object({ bundlePath }),
|
|
5219
5892
|
fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
|
|
5220
5893
|
run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
|
|
5221
5894
|
});
|
|
5222
5895
|
|
|
5223
5896
|
// src/commands/validate.ts
|
|
5224
|
-
var
|
|
5897
|
+
var import_zod33 = require("zod");
|
|
5225
5898
|
var validateCommand = define({
|
|
5226
5899
|
name: "validate",
|
|
5227
5900
|
tool: "kb_validate",
|
|
5228
5901
|
usage: "validate",
|
|
5229
5902
|
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.",
|
|
5230
|
-
input:
|
|
5903
|
+
input: import_zod33.z.object({ bundlePath }),
|
|
5231
5904
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
5232
5905
|
run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
|
|
5233
5906
|
// Warnings never fail the exit code; every other severity does.
|
|
@@ -5237,16 +5910,16 @@ var validateCommand = define({
|
|
|
5237
5910
|
});
|
|
5238
5911
|
|
|
5239
5912
|
// src/commands/verify.ts
|
|
5240
|
-
var
|
|
5913
|
+
var import_zod34 = require("zod");
|
|
5241
5914
|
var verifyCommand = define({
|
|
5242
5915
|
name: "verify",
|
|
5243
5916
|
tool: "kb_verify",
|
|
5244
5917
|
usage: "verify <concept-id> --note <text>",
|
|
5245
5918
|
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.",
|
|
5246
|
-
input:
|
|
5919
|
+
input: import_zod34.z.object({
|
|
5247
5920
|
bundlePath,
|
|
5248
5921
|
conceptId,
|
|
5249
|
-
note:
|
|
5922
|
+
note: import_zod34.z.string().refine((s) => s.trim().length > 0, {
|
|
5250
5923
|
message: "note must say what the check found"
|
|
5251
5924
|
})
|
|
5252
5925
|
}),
|
|
@@ -5266,15 +5939,15 @@ var verifyCommand = define({
|
|
|
5266
5939
|
});
|
|
5267
5940
|
|
|
5268
5941
|
// src/commands/write.ts
|
|
5269
|
-
var
|
|
5942
|
+
var import_zod35 = require("zod");
|
|
5270
5943
|
var writeCommand = define({
|
|
5271
5944
|
name: "write",
|
|
5272
5945
|
tool: "kb_write",
|
|
5273
5946
|
usage: "write <type> < record.json",
|
|
5274
5947
|
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.",
|
|
5275
|
-
input:
|
|
5948
|
+
input: import_zod35.z.object({
|
|
5276
5949
|
bundlePath,
|
|
5277
|
-
type:
|
|
5950
|
+
type: import_zod35.z.enum(KB_RECORD_TYPES),
|
|
5278
5951
|
input: composeInputSchema
|
|
5279
5952
|
}),
|
|
5280
5953
|
fromArgv: async (argv, path, stdin) => ({
|
|
@@ -5298,13 +5971,13 @@ var writeCommand = define({
|
|
|
5298
5971
|
});
|
|
5299
5972
|
|
|
5300
5973
|
// src/commands/write-decision.ts
|
|
5301
|
-
var
|
|
5974
|
+
var import_zod36 = require("zod");
|
|
5302
5975
|
var writeDecisionCommand = define({
|
|
5303
5976
|
name: "write-decision",
|
|
5304
5977
|
tool: "kb_write_decision",
|
|
5305
5978
|
usage: "write-decision < decision.json",
|
|
5306
5979
|
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.",
|
|
5307
|
-
input:
|
|
5980
|
+
input: import_zod36.z.object({ bundlePath, input: decisionInputSchema }),
|
|
5308
5981
|
fromArgv: async (_argv, path, stdin) => ({
|
|
5309
5982
|
bundlePath: path,
|
|
5310
5983
|
input: JSON.parse(await stdin())
|
|
@@ -5364,7 +6037,7 @@ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
|
5364
6037
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
5365
6038
|
|
|
5366
6039
|
// src/version.ts
|
|
5367
|
-
var VERSION = true ? "0.1.
|
|
6040
|
+
var VERSION = true ? "0.1.17" : "0.0.0-dev";
|
|
5368
6041
|
|
|
5369
6042
|
// src/mcp.ts
|
|
5370
6043
|
function createKbMcpServer() {
|
|
@@ -5403,7 +6076,7 @@ async function runKbMcpServer() {
|
|
|
5403
6076
|
}
|
|
5404
6077
|
|
|
5405
6078
|
// src/cli.ts
|
|
5406
|
-
var
|
|
6079
|
+
var import_node_path12 = require("path");
|
|
5407
6080
|
async function runKbCli(argv) {
|
|
5408
6081
|
const { flags, literal } = takeLiteral(argv);
|
|
5409
6082
|
const {
|
|
@@ -5465,7 +6138,7 @@ function takeLiteral(argv) {
|
|
|
5465
6138
|
function takeBundle(argv) {
|
|
5466
6139
|
const at = argv.indexOf("--bundle");
|
|
5467
6140
|
if (at === -1) {
|
|
5468
|
-
return { bundle: (0,
|
|
6141
|
+
return { bundle: (0, import_node_path12.join)(process.cwd(), KB_DIR), explicit: false, rest: argv };
|
|
5469
6142
|
}
|
|
5470
6143
|
const bundle = argv[at + 1];
|
|
5471
6144
|
if (!bundle) die("--bundle requires a path");
|
|
@@ -5495,14 +6168,14 @@ function summarise(description) {
|
|
|
5495
6168
|
return sentence.length > 78 ? `${sentence.slice(0, 75)}\u2026` : sentence;
|
|
5496
6169
|
}
|
|
5497
6170
|
function usage() {
|
|
5498
|
-
const
|
|
6171
|
+
const width2 = Math.max(...KB_COMMANDS.map((command) => command.usage.length));
|
|
5499
6172
|
return [
|
|
5500
6173
|
"strauss-kb \u2014 knowledge base commands",
|
|
5501
6174
|
"",
|
|
5502
6175
|
"Usage: strauss-kb [--bundle PATH] <command> [args]",
|
|
5503
6176
|
"",
|
|
5504
6177
|
...KB_COMMANDS.map(
|
|
5505
|
-
(command) => ` ${command.usage.padEnd(
|
|
6178
|
+
(command) => ` ${command.usage.padEnd(width2)} ${summarise(command.description)}`
|
|
5506
6179
|
),
|
|
5507
6180
|
"",
|
|
5508
6181
|
` --bundle PATH defaults to ./${KB_DIR}`,
|
|
@@ -5563,6 +6236,7 @@ function usage() {
|
|
|
5563
6236
|
RECORD_TYPES,
|
|
5564
6237
|
SEARCH_INDEX_FILE,
|
|
5565
6238
|
TRACE_EDGES,
|
|
6239
|
+
TreeSitterResolver,
|
|
5566
6240
|
adjudicate,
|
|
5567
6241
|
anchorFilePath,
|
|
5568
6242
|
assertBaseNotFrozen,
|
|
@@ -5577,9 +6251,14 @@ function usage() {
|
|
|
5577
6251
|
contextProfileBudgets,
|
|
5578
6252
|
createKbMcpServer,
|
|
5579
6253
|
decisionInputSchema,
|
|
6254
|
+
defaultAnchorResolvers,
|
|
5580
6255
|
detectAnchorDrift,
|
|
5581
6256
|
doctor,
|
|
5582
6257
|
edgeNeighbours,
|
|
6258
|
+
ensureGrammar,
|
|
6259
|
+
grammarHints,
|
|
6260
|
+
grammarManifest,
|
|
6261
|
+
grammarsCacheRoot,
|
|
5583
6262
|
hashAnchorText,
|
|
5584
6263
|
impact,
|
|
5585
6264
|
inboundIndex,
|
|
@@ -5597,6 +6276,7 @@ function usage() {
|
|
|
5597
6276
|
kbRecordFrontmatterSchema,
|
|
5598
6277
|
kbSourceSchema,
|
|
5599
6278
|
kbVerifiedEventSchema,
|
|
6279
|
+
languageForFile,
|
|
5600
6280
|
listPins,
|
|
5601
6281
|
loadQmd,
|
|
5602
6282
|
matchToDiff,
|
|
@@ -5606,6 +6286,7 @@ function usage() {
|
|
|
5606
6286
|
parseLog,
|
|
5607
6287
|
parseMarkdownWithFrontmatter,
|
|
5608
6288
|
pinBase,
|
|
6289
|
+
prepareResolvers,
|
|
5609
6290
|
readMergedPins,
|
|
5610
6291
|
readPinsLayer,
|
|
5611
6292
|
readRemoteAnchors,
|
|
@@ -5616,6 +6297,7 @@ function usage() {
|
|
|
5616
6297
|
renderLogEntry,
|
|
5617
6298
|
repoCacheDir,
|
|
5618
6299
|
resolveAnchor,
|
|
6300
|
+
resolveAnchorSpan,
|
|
5619
6301
|
resolveHeads,
|
|
5620
6302
|
resolveHits,
|
|
5621
6303
|
resolvePinPath,
|
|
@@ -5628,6 +6310,7 @@ function usage() {
|
|
|
5628
6310
|
syncInstructions,
|
|
5629
6311
|
toHookJson,
|
|
5630
6312
|
trace,
|
|
6313
|
+
treeSitterLanguages,
|
|
5631
6314
|
unpinBase,
|
|
5632
6315
|
validateBundle
|
|
5633
6316
|
});
|