@saasontools/strauss-kb 0.1.15 → 0.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +22 -5
- package/README.md +31 -15
- package/dist/{chunk-MK7GU4DX.js → chunk-RMJUGTAQ.js} +15 -6
- package/dist/chunk-RMJUGTAQ.js.map +1 -0
- package/dist/{chunk-LACCRB2Y.js → chunk-SA3A2SPY.js} +2 -2
- package/dist/{chunk-KNIUBCZY.js → chunk-ZKIQOBHT.js} +1122 -292
- package/dist/chunk-ZKIQOBHT.js.map +1 -0
- package/dist/cli-main.cjs +1138 -310
- package/dist/cli-main.cjs.map +1 -1
- package/dist/cli-main.js +2 -2
- package/dist/index.cjs +1193 -336
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +244 -7
- package/dist/index.d.ts +244 -7
- package/dist/index.js +23 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp-main.cjs +1122 -303
- package/dist/mcp-main.cjs.map +1 -1
- package/dist/mcp-main.js +2 -2
- package/grammars/README.md +78 -0
- package/grammars/manifest.json +595 -0
- package/grammars/packs.json +127 -0
- package/package.json +7 -2
- package/dist/chunk-KNIUBCZY.js.map +0 -1
- package/dist/chunk-MK7GU4DX.js.map +0 -1
- /package/dist/{chunk-LACCRB2Y.js.map → chunk-SA3A2SPY.js.map} +0 -0
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,15 +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
|
|
154
|
-
var import_node_path4 = require("path");
|
|
166
|
+
var import_promises6 = require("fs/promises");
|
|
167
|
+
var import_node_path7 = require("path");
|
|
155
168
|
|
|
156
169
|
// src/concurrency.ts
|
|
157
170
|
var DEFAULT_IO_CONCURRENCY = 16;
|
|
@@ -252,7 +265,13 @@ var kbAnchorSchema = import_zod.z.object({
|
|
|
252
265
|
/** ISO 8601 timestamp of the last successful resolution. */
|
|
253
266
|
resolved_at: import_zod.z.string().min(1).optional(),
|
|
254
267
|
/** Line count of the text the hash was taken over. */
|
|
255
|
-
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()
|
|
256
275
|
}).strict();
|
|
257
276
|
var kbLinkSchema = import_zod.z.object({
|
|
258
277
|
target: import_zod.z.string().min(1),
|
|
@@ -345,6 +364,8 @@ var ErrorTypes = /* @__PURE__ */ ((ErrorTypes2) => {
|
|
|
345
364
|
ErrorTypes2["KbPackBudgetExceeded"] = "KbPackBudgetExceeded";
|
|
346
365
|
ErrorTypes2["KbRecordNotFound"] = "KbRecordNotFound";
|
|
347
366
|
ErrorTypes2["KbSelfVerification"] = "KbSelfVerification";
|
|
367
|
+
ErrorTypes2["KbStampBaselineUnreadable"] = "KbStampBaselineUnreadable";
|
|
368
|
+
ErrorTypes2["KbStampDigestBaselineAmbiguous"] = "KbStampDigestBaselineAmbiguous";
|
|
348
369
|
ErrorTypes2["KbUnknownLinkRel"] = "KbUnknownLinkRel";
|
|
349
370
|
ErrorTypes2["KbWriteConflict"] = "KbWriteConflict";
|
|
350
371
|
return ErrorTypes2;
|
|
@@ -499,6 +520,36 @@ var KbInvalidConceptIdError = class extends BaseError {
|
|
|
499
520
|
});
|
|
500
521
|
}
|
|
501
522
|
};
|
|
523
|
+
var KbStampBaselineError = class extends BaseError {
|
|
524
|
+
constructor(since) {
|
|
525
|
+
super({
|
|
526
|
+
message: `kb: --since ${since} is neither a 64-character digest nor a readable stamp file`,
|
|
527
|
+
errorType: "KbStampBaselineUnreadable" /* KbStampBaselineUnreadable */,
|
|
528
|
+
code: 400,
|
|
529
|
+
fault: "User" /* User */,
|
|
530
|
+
retriable: false,
|
|
531
|
+
reportToUser: true,
|
|
532
|
+
details: { since }
|
|
533
|
+
});
|
|
534
|
+
this.since = since;
|
|
535
|
+
}
|
|
536
|
+
since;
|
|
537
|
+
};
|
|
538
|
+
var KbStampDigestBaselineError = class extends BaseError {
|
|
539
|
+
constructor(since) {
|
|
540
|
+
super({
|
|
541
|
+
message: `kb: --since ${since} is a digest, which needs --bundle (one base) \u2014 a file baseline works for many`,
|
|
542
|
+
errorType: "KbStampDigestBaselineAmbiguous" /* KbStampDigestBaselineAmbiguous */,
|
|
543
|
+
code: 400,
|
|
544
|
+
fault: "User" /* User */,
|
|
545
|
+
retriable: false,
|
|
546
|
+
reportToUser: true,
|
|
547
|
+
details: { since }
|
|
548
|
+
});
|
|
549
|
+
this.since = since;
|
|
550
|
+
}
|
|
551
|
+
since;
|
|
552
|
+
};
|
|
502
553
|
|
|
503
554
|
// src/kb-index.ts
|
|
504
555
|
var INDEX_FILE = "INDEX.md";
|
|
@@ -787,18 +838,18 @@ async function readOneRepo(repo, url, declared, context) {
|
|
|
787
838
|
if (!repoUrlIsSafe(url)) return all({ ok: false, reason: "repo-invalid" });
|
|
788
839
|
const cache = cachePathFor(repo, context.cacheDir);
|
|
789
840
|
if (!cache) return all({ ok: false, reason: "remote-unreachable" });
|
|
790
|
-
const
|
|
841
|
+
const rejected2 = /* @__PURE__ */ new Map();
|
|
791
842
|
const usable = [];
|
|
792
843
|
for (const want of wants) {
|
|
793
844
|
const reason = wantReason(want);
|
|
794
845
|
if (reason)
|
|
795
|
-
|
|
846
|
+
rejected2.set(wantKey(repo, want.ref, want.file), { ok: false, reason });
|
|
796
847
|
else usable.push(want);
|
|
797
848
|
}
|
|
798
|
-
if (!usable.length) return
|
|
849
|
+
if (!usable.length) return rejected2;
|
|
799
850
|
wants = usable;
|
|
800
851
|
const opened = await openCache(cache, url, context);
|
|
801
|
-
if (opened) return new Map([...
|
|
852
|
+
if (opened) return new Map([...rejected2, ...all(opened)]);
|
|
802
853
|
const wantsDefault = wants.some((want) => want.ref === void 0);
|
|
803
854
|
const branch = wantsDefault ? await defaultBranch(cache, context) : {};
|
|
804
855
|
const revs = /* @__PURE__ */ new Map();
|
|
@@ -825,7 +876,7 @@ async function readOneRepo(repo, url, declared, context) {
|
|
|
825
876
|
}
|
|
826
877
|
);
|
|
827
878
|
return new Map([
|
|
828
|
-
...
|
|
879
|
+
...rejected2,
|
|
829
880
|
...wants.map(
|
|
830
881
|
(want, at) => [wantKey(repo, want.ref, want.file), reads[at]]
|
|
831
882
|
)
|
|
@@ -877,13 +928,13 @@ async function defaultBranch(cache, context) {
|
|
|
877
928
|
if (!listed.ok) {
|
|
878
929
|
const reason = transportReason(listed.stderr);
|
|
879
930
|
if (reason !== "ref-not-found") {
|
|
880
|
-
const
|
|
881
|
-
return
|
|
931
|
+
const cached3 = await cachedBranch(cache);
|
|
932
|
+
return cached3 ? { name: cached3 } : { reason };
|
|
882
933
|
}
|
|
883
934
|
}
|
|
884
935
|
}
|
|
885
|
-
const
|
|
886
|
-
if (
|
|
936
|
+
const cached2 = await cachedBranch(cache);
|
|
937
|
+
if (cached2) return { name: cached2 };
|
|
887
938
|
return {
|
|
888
939
|
reason: context.offline ? "remote-unreachable" : "default-branch-unknown"
|
|
889
940
|
};
|
|
@@ -910,8 +961,8 @@ async function ensureRev(cache, rev, context) {
|
|
|
910
961
|
cwd: cache
|
|
911
962
|
}
|
|
912
963
|
);
|
|
913
|
-
const
|
|
914
|
-
if (
|
|
964
|
+
const cached2 = have.ok && have.stdout.trim().length > 0;
|
|
965
|
+
if (cached2 && (context.offline || IMMUTABLE_REV.test(rev))) return void 0;
|
|
915
966
|
if (context.offline) return { ok: false, reason: "remote-unreachable" };
|
|
916
967
|
const fetched = await git(
|
|
917
968
|
[
|
|
@@ -927,7 +978,7 @@ async function ensureRev(cache, rev, context) {
|
|
|
927
978
|
);
|
|
928
979
|
if (!fetched.ok) {
|
|
929
980
|
const reason = transportReason(fetched.stderr);
|
|
930
|
-
if (
|
|
981
|
+
if (cached2 && reason !== "ref-not-found") return void 0;
|
|
931
982
|
return { ok: false, reason };
|
|
932
983
|
}
|
|
933
984
|
const head = await git(["rev-parse", "FETCH_HEAD"], { cwd: cache });
|
|
@@ -1027,8 +1078,8 @@ function resolveHeads(from, byId) {
|
|
|
1027
1078
|
while (queue.length) {
|
|
1028
1079
|
const current = queue.shift();
|
|
1029
1080
|
const next = successors(current, byId);
|
|
1030
|
-
for (const
|
|
1031
|
-
warnings.push({ kind: "broken-chain", missing });
|
|
1081
|
+
for (const missing2 of next.missing) {
|
|
1082
|
+
warnings.push({ kind: "broken-chain", missing: missing2 });
|
|
1032
1083
|
}
|
|
1033
1084
|
if (!next.records.length) {
|
|
1034
1085
|
if (current.conceptId !== from.conceptId)
|
|
@@ -1059,13 +1110,45 @@ function successors(record, byId) {
|
|
|
1059
1110
|
}
|
|
1060
1111
|
}
|
|
1061
1112
|
const records = [];
|
|
1062
|
-
const
|
|
1113
|
+
const missing2 = [];
|
|
1063
1114
|
for (const id of ids) {
|
|
1064
1115
|
const found = byId.get(id);
|
|
1065
1116
|
if (found) records.push(found);
|
|
1066
|
-
else
|
|
1117
|
+
else missing2.push(id);
|
|
1067
1118
|
}
|
|
1068
|
-
return { records, missing };
|
|
1119
|
+
return { records, missing: missing2 };
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// src/kb-stamp.ts
|
|
1123
|
+
var import_node_crypto = require("crypto");
|
|
1124
|
+
function sha256(contents) {
|
|
1125
|
+
return (0, import_node_crypto.createHash)("sha256").update(contents).digest("hex");
|
|
1126
|
+
}
|
|
1127
|
+
function bundleStamp(records, superseded) {
|
|
1128
|
+
const entries = [
|
|
1129
|
+
...records.map((hit) => ({
|
|
1130
|
+
conceptId: hit.record.conceptId,
|
|
1131
|
+
digest: `current:${sha256(
|
|
1132
|
+
stringifyMarkdownWithFrontmatter(
|
|
1133
|
+
hit.record.body,
|
|
1134
|
+
hit.record.frontmatter
|
|
1135
|
+
)
|
|
1136
|
+
)}`
|
|
1137
|
+
})),
|
|
1138
|
+
...superseded.map((entry) => ({
|
|
1139
|
+
conceptId: entry.conceptId,
|
|
1140
|
+
digest: `superseded:${sha256(JSON.stringify(entry))}`
|
|
1141
|
+
}))
|
|
1142
|
+
].sort((a, b) => a.conceptId < b.conceptId ? -1 : 1);
|
|
1143
|
+
return {
|
|
1144
|
+
digest: sha256(
|
|
1145
|
+
entries.map((entry) => `${entry.conceptId}:${entry.digest}`).join("\n")
|
|
1146
|
+
),
|
|
1147
|
+
records: entries
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
function bundleDigest(records, superseded) {
|
|
1151
|
+
return bundleStamp(records, superseded).digest;
|
|
1069
1152
|
}
|
|
1070
1153
|
|
|
1071
1154
|
// src/anchor-resolver/read.ts
|
|
@@ -1159,63 +1242,580 @@ async function readAnchorFiles(files, read, concurrency = DEFAULT_IO_CONCURRENCY
|
|
|
1159
1242
|
}
|
|
1160
1243
|
|
|
1161
1244
|
// src/anchor-resolver/resolver.ts
|
|
1162
|
-
var
|
|
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
|
|
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
|
|
1163
1763
|
var PARENT_SCOPE_LINES = 50;
|
|
1164
1764
|
var CLEAN_STATE = { blockComment: false, template: false };
|
|
1165
1765
|
function stripLine(line, state) {
|
|
1166
1766
|
let out = "";
|
|
1167
|
-
let
|
|
1767
|
+
let index2 = 0;
|
|
1168
1768
|
let { blockComment, template } = state;
|
|
1169
|
-
while (
|
|
1170
|
-
const char = line[
|
|
1171
|
-
const next = line[
|
|
1769
|
+
while (index2 < line.length) {
|
|
1770
|
+
const char = line[index2];
|
|
1771
|
+
const next = line[index2 + 1];
|
|
1172
1772
|
if (blockComment) {
|
|
1173
1773
|
if (char === "*" && next === "/") {
|
|
1174
1774
|
blockComment = false;
|
|
1175
|
-
|
|
1775
|
+
index2 += 2;
|
|
1176
1776
|
continue;
|
|
1177
1777
|
}
|
|
1178
|
-
|
|
1778
|
+
index2 += 1;
|
|
1179
1779
|
continue;
|
|
1180
1780
|
}
|
|
1181
1781
|
if (template) {
|
|
1182
1782
|
if (char === "\\") {
|
|
1183
|
-
|
|
1783
|
+
index2 += 2;
|
|
1184
1784
|
continue;
|
|
1185
1785
|
}
|
|
1186
1786
|
if (char === "`") template = false;
|
|
1187
|
-
|
|
1787
|
+
index2 += 1;
|
|
1188
1788
|
continue;
|
|
1189
1789
|
}
|
|
1190
1790
|
if (char === "/" && next === "*") {
|
|
1191
1791
|
blockComment = true;
|
|
1192
|
-
|
|
1792
|
+
index2 += 2;
|
|
1193
1793
|
continue;
|
|
1194
1794
|
}
|
|
1195
1795
|
if (char === "/" && next === "/") break;
|
|
1196
1796
|
if (char === "`") {
|
|
1197
1797
|
template = true;
|
|
1198
|
-
|
|
1798
|
+
index2 += 1;
|
|
1199
1799
|
continue;
|
|
1200
1800
|
}
|
|
1201
1801
|
if (char === "'" || char === '"') {
|
|
1202
1802
|
const quote = char;
|
|
1203
|
-
|
|
1204
|
-
while (
|
|
1205
|
-
if (line[
|
|
1206
|
-
|
|
1803
|
+
index2 += 1;
|
|
1804
|
+
while (index2 < line.length) {
|
|
1805
|
+
if (line[index2] === "\\") {
|
|
1806
|
+
index2 += 2;
|
|
1207
1807
|
continue;
|
|
1208
1808
|
}
|
|
1209
|
-
if (line[
|
|
1210
|
-
|
|
1809
|
+
if (line[index2] === quote) {
|
|
1810
|
+
index2 += 1;
|
|
1211
1811
|
break;
|
|
1212
1812
|
}
|
|
1213
|
-
|
|
1813
|
+
index2 += 1;
|
|
1214
1814
|
}
|
|
1215
1815
|
continue;
|
|
1216
1816
|
}
|
|
1217
1817
|
out += char;
|
|
1218
|
-
|
|
1818
|
+
index2 += 1;
|
|
1219
1819
|
}
|
|
1220
1820
|
return { code: out, state: { blockComment, template } };
|
|
1221
1821
|
}
|
|
@@ -1230,8 +1830,8 @@ function captureBraceBlock(lines, matchLine) {
|
|
|
1230
1830
|
let depth = 0;
|
|
1231
1831
|
let opened = false;
|
|
1232
1832
|
let state = CLEAN_STATE;
|
|
1233
|
-
for (let
|
|
1234
|
-
const stripped = stripLine(lines[
|
|
1833
|
+
for (let index2 = matchLine; index2 < lines.length; index2++) {
|
|
1834
|
+
const stripped = stripLine(lines[index2] ?? "", state);
|
|
1235
1835
|
state = stripped.state;
|
|
1236
1836
|
for (const char of stripped.code) {
|
|
1237
1837
|
if (char === "{") {
|
|
@@ -1240,10 +1840,10 @@ function captureBraceBlock(lines, matchLine) {
|
|
|
1240
1840
|
} else if (char === "}") {
|
|
1241
1841
|
depth = Math.max(0, depth - 1);
|
|
1242
1842
|
} else if (char === ";" && !opened) {
|
|
1243
|
-
return span(lines, matchLine,
|
|
1843
|
+
return span(lines, matchLine, index2);
|
|
1244
1844
|
}
|
|
1245
1845
|
}
|
|
1246
|
-
if (opened && depth === 0) return span(lines, matchLine,
|
|
1846
|
+
if (opened && depth === 0) return span(lines, matchLine, index2);
|
|
1247
1847
|
}
|
|
1248
1848
|
return null;
|
|
1249
1849
|
}
|
|
@@ -1252,22 +1852,22 @@ function captureIndentedBlock(lines, matchLine) {
|
|
|
1252
1852
|
const header = lines[matchLine] ?? "";
|
|
1253
1853
|
const indent = header.length - header.trimStart().length;
|
|
1254
1854
|
let headerEnd = -1;
|
|
1255
|
-
for (let
|
|
1256
|
-
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();
|
|
1257
1857
|
if (code.endsWith(":")) {
|
|
1258
|
-
headerEnd =
|
|
1858
|
+
headerEnd = index2;
|
|
1259
1859
|
break;
|
|
1260
1860
|
}
|
|
1261
|
-
if (code.includes(":")) return span(lines, matchLine,
|
|
1861
|
+
if (code.includes(":")) return span(lines, matchLine, index2);
|
|
1262
1862
|
}
|
|
1263
1863
|
if (headerEnd === -1) return null;
|
|
1264
1864
|
let end = headerEnd;
|
|
1265
|
-
for (let
|
|
1266
|
-
const line = lines[
|
|
1865
|
+
for (let index2 = headerEnd + 1; index2 < lines.length; index2++) {
|
|
1866
|
+
const line = lines[index2] ?? "";
|
|
1267
1867
|
if (line.trim() === "") continue;
|
|
1268
1868
|
const lineIndent = line.length - line.trimStart().length;
|
|
1269
1869
|
if (lineIndent <= indent) break;
|
|
1270
|
-
end =
|
|
1870
|
+
end = index2;
|
|
1271
1871
|
}
|
|
1272
1872
|
return end === headerEnd ? null : span(lines, matchLine, end);
|
|
1273
1873
|
}
|
|
@@ -1291,11 +1891,11 @@ var regexResolver = {
|
|
|
1291
1891
|
const lines = source.split("\n");
|
|
1292
1892
|
for (const tier of TIERS) {
|
|
1293
1893
|
const pattern = tier(escaped);
|
|
1294
|
-
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);
|
|
1295
1895
|
if (!candidates.length) continue;
|
|
1296
1896
|
if (parentPattern && candidates.length > 1) {
|
|
1297
1897
|
const distances = candidates.map(
|
|
1298
|
-
(
|
|
1898
|
+
(index2) => distanceToParent(lines, index2, parentPattern)
|
|
1299
1899
|
);
|
|
1300
1900
|
const nearest = Math.min(...distances);
|
|
1301
1901
|
if (Number.isFinite(nearest)) {
|
|
@@ -1312,34 +1912,79 @@ var regexResolver = {
|
|
|
1312
1912
|
function escapeRegExp(value) {
|
|
1313
1913
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1314
1914
|
}
|
|
1315
|
-
function distanceToParent(lines,
|
|
1316
|
-
const floor = Math.max(0,
|
|
1317
|
-
for (let at =
|
|
1318
|
-
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;
|
|
1319
1919
|
}
|
|
1320
1920
|
return Number.POSITIVE_INFINITY;
|
|
1321
1921
|
}
|
|
1322
1922
|
function hashAnchorText(text) {
|
|
1323
|
-
return `sha256:${(0,
|
|
1923
|
+
return `sha256:${(0, import_node_crypto4.createHash)("sha256").update(text.replace(/\r\n/g, "\n")).digest("hex")}`;
|
|
1324
1924
|
}
|
|
1325
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]) {
|
|
1326
1930
|
const normalized = source.replace(/\r\n/g, "\n");
|
|
1327
1931
|
if (!anchor.symbol) {
|
|
1328
1932
|
const lines = normalized.split("\n");
|
|
1329
1933
|
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
1330
1934
|
return {
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
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 } : {}
|
|
1334
1954
|
};
|
|
1335
1955
|
}
|
|
1336
|
-
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;
|
|
1337
1980
|
}
|
|
1338
1981
|
|
|
1339
1982
|
// src/anchor-resolver/drift.ts
|
|
1340
1983
|
async function detectAnchorDrift(records, options = {}) {
|
|
1341
1984
|
const repoRoot = options.repoRoot ?? process.cwd();
|
|
1342
|
-
const
|
|
1985
|
+
const resolvers = options.resolvers ?? (options.resolver ? [options.resolver] : defaultAnchorResolvers({
|
|
1986
|
+
offline: options.remote?.offline === true
|
|
1987
|
+
}));
|
|
1343
1988
|
const origin = new LazyOrigin(repoRoot);
|
|
1344
1989
|
const planned = /* @__PURE__ */ new Map();
|
|
1345
1990
|
let declaresRepo = false;
|
|
@@ -1377,12 +2022,16 @@ async function detectAnchorDrift(records, options = {}) {
|
|
|
1377
2022
|
),
|
|
1378
2023
|
(options.readRemote ?? readRemoteAnchors)(wants, options.remote ?? {})
|
|
1379
2024
|
]);
|
|
2025
|
+
await prepareResolvers(resolvers, [
|
|
2026
|
+
...files,
|
|
2027
|
+
...wants.map((want) => want.file)
|
|
2028
|
+
]);
|
|
1380
2029
|
const drift = /* @__PURE__ */ new Map();
|
|
1381
2030
|
for (const record of records) {
|
|
1382
2031
|
const entries = [];
|
|
1383
2032
|
for (const { anchor, foreign } of planned.get(record.conceptId) ?? []) {
|
|
1384
2033
|
entries.push(
|
|
1385
|
-
foreign ? remoteEntry(anchor, remote,
|
|
2034
|
+
foreign ? remoteEntry(anchor, remote, resolvers) : localEntry(anchor, reads.get(anchor.file), resolvers)
|
|
1386
2035
|
);
|
|
1387
2036
|
}
|
|
1388
2037
|
if (entries.length) drift.set(record.conceptId, entries);
|
|
@@ -1411,12 +2060,22 @@ function unresolved(anchor, reason, repo) {
|
|
|
1411
2060
|
...repo ? { repo } : {}
|
|
1412
2061
|
};
|
|
1413
2062
|
}
|
|
1414
|
-
function hashIn(source, anchor,
|
|
1415
|
-
const
|
|
1416
|
-
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) {
|
|
1417
2076
|
return {
|
|
1418
|
-
|
|
1419
|
-
|
|
2077
|
+
...current.resolver ? { resolver: current.resolver } : {},
|
|
2078
|
+
...current.hash !== anchor.hash && resolverChanged(source, anchor, current.resolver) ? { reason: "resolver-changed" } : {}
|
|
1420
2079
|
};
|
|
1421
2080
|
}
|
|
1422
2081
|
function compared(anchor, current, extra = {}) {
|
|
@@ -1428,40 +2087,58 @@ function compared(anchor, current, extra = {}) {
|
|
|
1428
2087
|
...extra
|
|
1429
2088
|
};
|
|
1430
2089
|
}
|
|
1431
|
-
function localEntry(anchor, read,
|
|
2090
|
+
function localEntry(anchor, read, resolvers) {
|
|
1432
2091
|
if (!read.ok) return unresolved(anchor, read.reason);
|
|
1433
|
-
const
|
|
1434
|
-
|
|
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
|
+
);
|
|
1435
2099
|
}
|
|
1436
|
-
function remoteEntry(anchor, remote,
|
|
2100
|
+
function remoteEntry(anchor, remote, resolvers) {
|
|
1437
2101
|
const repo = anchor.repo;
|
|
1438
2102
|
const key2 = normalizeRepoUrl(repo);
|
|
1439
2103
|
const atDefault = remote.get(wantKey(key2, void 0, anchor.file));
|
|
1440
2104
|
const primary = anchor.ref ? remote.get(wantKey(key2, anchor.ref, anchor.file)) : atDefault;
|
|
1441
2105
|
if (!primary) return unresolved(anchor, "remote-unreachable", repo);
|
|
1442
2106
|
if (!primary.ok) return unresolved(anchor, primary.reason, repo);
|
|
1443
|
-
const
|
|
1444
|
-
if (!
|
|
1445
|
-
|
|
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 });
|
|
1446
2112
|
if (current.hash !== anchor.hash) {
|
|
1447
|
-
return compared(anchor, current, {
|
|
2113
|
+
return compared(anchor, current, {
|
|
2114
|
+
repo,
|
|
2115
|
+
...extras,
|
|
2116
|
+
remoteState: "drifted-from-ref"
|
|
2117
|
+
});
|
|
1448
2118
|
}
|
|
1449
|
-
const head = atDefault?.ok ? hashIn(atDefault.source, anchor,
|
|
1450
|
-
return head && head.hash !== anchor.hash ? {
|
|
1451
|
-
...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
|
+
}),
|
|
1452
2125
|
state: "drifted",
|
|
1453
2126
|
remoteState: "drifted-on-default"
|
|
1454
|
-
} : compared(anchor, current, {
|
|
2127
|
+
} : compared(anchor, current, {
|
|
2128
|
+
repo,
|
|
2129
|
+
...extras,
|
|
2130
|
+
remoteState: "matches-ref"
|
|
2131
|
+
});
|
|
1455
2132
|
}
|
|
1456
2133
|
|
|
1457
2134
|
// src/search-index.ts
|
|
1458
|
-
var
|
|
1459
|
-
var
|
|
2135
|
+
var import_promises5 = require("fs/promises");
|
|
2136
|
+
var import_node_path6 = require("path");
|
|
1460
2137
|
|
|
1461
2138
|
// src/kb-log.ts
|
|
1462
|
-
var
|
|
2139
|
+
var import_zod3 = require("zod");
|
|
1463
2140
|
var LOG_FILE = "log.jsonl";
|
|
1464
|
-
var kbLogEntrySchema =
|
|
2141
|
+
var kbLogEntrySchema = import_zod3.z.object({
|
|
1465
2142
|
// Validated, not just `min(1)`: `at` is a sort key (see `parseLog`
|
|
1466
2143
|
// below), and a value that isn't actually chronological — a Unix
|
|
1467
2144
|
// timestamp, a human-typed date, garbage — would sort wrong without
|
|
@@ -1470,12 +2147,12 @@ var kbLogEntrySchema = import_zod2.z.object({
|
|
|
1470
2147
|
// and rejects everything else, including a non-`Z` offset — so a
|
|
1471
2148
|
// malformed `at` is reported the same way a malformed line already is,
|
|
1472
2149
|
// rather than silently sorting into the wrong place.
|
|
1473
|
-
at:
|
|
1474
|
-
by:
|
|
1475
|
-
operation:
|
|
1476
|
-
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),
|
|
1477
2154
|
/** Second concept id, where the operation relates two — supersession. */
|
|
1478
|
-
target:
|
|
2155
|
+
target: import_zod3.z.string().min(1).optional()
|
|
1479
2156
|
}).strict();
|
|
1480
2157
|
function renderLogEntry(entry) {
|
|
1481
2158
|
return `${JSON.stringify(kbLogEntrySchema.parse(entry))}
|
|
@@ -1485,18 +2162,18 @@ function parseLog(raw) {
|
|
|
1485
2162
|
const entries = [];
|
|
1486
2163
|
const malformed = [];
|
|
1487
2164
|
const seen = /* @__PURE__ */ new Set();
|
|
1488
|
-
raw.split("\n").forEach((text,
|
|
2165
|
+
raw.split("\n").forEach((text, index2) => {
|
|
1489
2166
|
if (!text.trim()) return;
|
|
1490
2167
|
let value;
|
|
1491
2168
|
try {
|
|
1492
2169
|
value = JSON.parse(text);
|
|
1493
2170
|
} catch {
|
|
1494
|
-
malformed.push({ line:
|
|
2171
|
+
malformed.push({ line: index2 + 1, text });
|
|
1495
2172
|
return;
|
|
1496
2173
|
}
|
|
1497
2174
|
const parsed = kbLogEntrySchema.safeParse(value);
|
|
1498
2175
|
if (!parsed.success) {
|
|
1499
|
-
malformed.push({ line:
|
|
2176
|
+
malformed.push({ line: index2 + 1, text });
|
|
1500
2177
|
return;
|
|
1501
2178
|
}
|
|
1502
2179
|
const key2 = JSON.stringify(parsed.data);
|
|
@@ -1519,7 +2196,7 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
1519
2196
|
let store = null;
|
|
1520
2197
|
try {
|
|
1521
2198
|
store = await qmd.createStore({
|
|
1522
|
-
dbPath: (0,
|
|
2199
|
+
dbPath: (0, import_node_path6.join)(bundlePath2, SEARCH_INDEX_FILE),
|
|
1523
2200
|
config: {
|
|
1524
2201
|
collections: {
|
|
1525
2202
|
[COLLECTION]: {
|
|
@@ -1554,7 +2231,7 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
1554
2231
|
}
|
|
1555
2232
|
}
|
|
1556
2233
|
async function isStale(bundlePath2) {
|
|
1557
|
-
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);
|
|
1558
2235
|
if (!indexAt) return true;
|
|
1559
2236
|
const { readdir: readdir2 } = await import("fs/promises");
|
|
1560
2237
|
const names = (await readdir2(bundlePath2).catch(() => [])).filter(
|
|
@@ -1563,7 +2240,7 @@ async function isStale(bundlePath2) {
|
|
|
1563
2240
|
let stale = false;
|
|
1564
2241
|
await mapLimit(names, DEFAULT_IO_CONCURRENCY, async (name) => {
|
|
1565
2242
|
if (stale) return;
|
|
1566
|
-
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);
|
|
1567
2244
|
if (at > indexAt) stale = true;
|
|
1568
2245
|
});
|
|
1569
2246
|
return stale;
|
|
@@ -1943,8 +2620,8 @@ function byRank(left, right) {
|
|
|
1943
2620
|
) || left.record.conceptId.localeCompare(right.record.conceptId);
|
|
1944
2621
|
}
|
|
1945
2622
|
function typeRank(record) {
|
|
1946
|
-
const
|
|
1947
|
-
return
|
|
2623
|
+
const index2 = TYPE_PRIORITY.indexOf(record.frontmatter.type);
|
|
2624
|
+
return index2 === -1 ? TYPE_PRIORITY.length : index2;
|
|
1948
2625
|
}
|
|
1949
2626
|
|
|
1950
2627
|
// src/catalog.ts
|
|
@@ -2153,7 +2830,7 @@ function appendUnionMergeLine(contents) {
|
|
|
2153
2830
|
}
|
|
2154
2831
|
|
|
2155
2832
|
// src/kb-store.ts
|
|
2156
|
-
var KB_DIR = (0,
|
|
2833
|
+
var KB_DIR = (0, import_node_path7.join)(".strauss", "kb");
|
|
2157
2834
|
var STORE_OWNED = /* @__PURE__ */ new Set([INDEX_FILE, LOG_FILE, SEARCH_INDEX_FILE]);
|
|
2158
2835
|
var DEFAULT_LOAD_BUDGET = 25e3;
|
|
2159
2836
|
var KbStore = class {
|
|
@@ -2184,7 +2861,7 @@ var KbStore = class {
|
|
|
2184
2861
|
const conceptId2 = `${input.type}.${input.slug}`;
|
|
2185
2862
|
const root = this.root(bundlePath2);
|
|
2186
2863
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
2187
|
-
await (0,
|
|
2864
|
+
await (0, import_promises6.mkdir)(root, { recursive: true });
|
|
2188
2865
|
await this.publish(
|
|
2189
2866
|
target,
|
|
2190
2867
|
stringifyMarkdownWithFrontmatter(input.body, frontmatter),
|
|
@@ -2223,7 +2900,7 @@ var KbStore = class {
|
|
|
2223
2900
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
2224
2901
|
let raw;
|
|
2225
2902
|
try {
|
|
2226
|
-
raw = await (0,
|
|
2903
|
+
raw = await (0, import_promises6.readFile)(target, "utf8");
|
|
2227
2904
|
} catch {
|
|
2228
2905
|
return null;
|
|
2229
2906
|
}
|
|
@@ -2240,7 +2917,7 @@ var KbStore = class {
|
|
|
2240
2917
|
const root = this.root(bundlePath2);
|
|
2241
2918
|
let names;
|
|
2242
2919
|
try {
|
|
2243
|
-
names = await (0,
|
|
2920
|
+
names = await (0, import_promises6.readdir)(root);
|
|
2244
2921
|
} catch {
|
|
2245
2922
|
return [];
|
|
2246
2923
|
}
|
|
@@ -2248,7 +2925,7 @@ var KbStore = class {
|
|
|
2248
2925
|
const records = await mapLimit(
|
|
2249
2926
|
wanted,
|
|
2250
2927
|
DEFAULT_IO_CONCURRENCY,
|
|
2251
|
-
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"))
|
|
2252
2929
|
);
|
|
2253
2930
|
return records.filter((record) => record !== null);
|
|
2254
2931
|
}
|
|
@@ -2405,7 +3082,7 @@ ${answer}
|
|
|
2405
3082
|
if (found.length) return found;
|
|
2406
3083
|
}
|
|
2407
3084
|
const lowered = needle.toLowerCase();
|
|
2408
|
-
return bundle.filter((record) =>
|
|
3085
|
+
return bundle.filter((record) => matches2(record, lowered));
|
|
2409
3086
|
}
|
|
2410
3087
|
/**
|
|
2411
3088
|
* Anchor drift over the records about to be handed back. Like the search
|
|
@@ -2523,6 +3200,28 @@ ${answer}
|
|
|
2523
3200
|
digest: bundleDigestValue
|
|
2524
3201
|
};
|
|
2525
3202
|
}
|
|
3203
|
+
/**
|
|
3204
|
+
* `load`'s digest without `load`'s bodies — the same records, adjudicated
|
|
3205
|
+
* the same way, handed back as a stamp. Skips the anchor drift pass, which
|
|
3206
|
+
* reads source files and only ever adds warnings: no warning reaches the
|
|
3207
|
+
* digest, so the value is identical to the one `load` returns.
|
|
3208
|
+
*/
|
|
3209
|
+
async stamp(bundlePath2) {
|
|
3210
|
+
const bundle = await this.list(bundlePath2);
|
|
3211
|
+
const adjudicated = adjudicate(bundle, bundle, /* @__PURE__ */ new Date());
|
|
3212
|
+
const current = adjudicated.filter((hit) => hit.standing !== "superseded");
|
|
3213
|
+
const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(stub);
|
|
3214
|
+
const stamped = bundleStamp(current, superseded);
|
|
3215
|
+
const dates = bundle.map((record) => record.frontmatter.generated?.at ?? null).filter((at) => typeof at === "string").sort();
|
|
3216
|
+
return {
|
|
3217
|
+
path: bundlePath2,
|
|
3218
|
+
digest: stamped.digest,
|
|
3219
|
+
recordCount: bundle.length,
|
|
3220
|
+
superseded: superseded.length,
|
|
3221
|
+
newestAt: dates.at(-1) ?? null,
|
|
3222
|
+
records: stamped.records
|
|
3223
|
+
};
|
|
3224
|
+
}
|
|
2526
3225
|
/** How a position was arrived at, as a timeline. See `trace.ts`. */
|
|
2527
3226
|
async trace(bundlePath2, seedId, options = {}) {
|
|
2528
3227
|
return trace(seedId, await this.list(bundlePath2), options);
|
|
@@ -2553,11 +3252,11 @@ ${answer}
|
|
|
2553
3252
|
async readIndex(bundlePath2) {
|
|
2554
3253
|
const root = this.root(bundlePath2);
|
|
2555
3254
|
const expected = renderIndex(await this.list(bundlePath2));
|
|
2556
|
-
const stored = await (0,
|
|
3255
|
+
const stored = await (0, import_promises6.readFile)((0, import_node_path7.join)(root, INDEX_FILE), "utf8").catch(
|
|
2557
3256
|
() => null
|
|
2558
3257
|
);
|
|
2559
3258
|
if (indexIsStale(stored, expected)) {
|
|
2560
|
-
await this.publish((0,
|
|
3259
|
+
await this.publish((0, import_node_path7.join)(root, INDEX_FILE), expected, true, INDEX_FILE);
|
|
2561
3260
|
this.logger.info?.({
|
|
2562
3261
|
operation: "kb.index.repair",
|
|
2563
3262
|
bundlePath: root,
|
|
@@ -2574,8 +3273,8 @@ ${answer}
|
|
|
2574
3273
|
* knows which agent touched what. So a bad line is surfaced and left alone.
|
|
2575
3274
|
*/
|
|
2576
3275
|
async readLog(bundlePath2) {
|
|
2577
|
-
const raw = await (0,
|
|
2578
|
-
(0,
|
|
3276
|
+
const raw = await (0, import_promises6.readFile)(
|
|
3277
|
+
(0, import_node_path7.join)(this.root(bundlePath2), LOG_FILE),
|
|
2579
3278
|
"utf8"
|
|
2580
3279
|
).catch(() => "");
|
|
2581
3280
|
const result = parseLog(raw);
|
|
@@ -2626,15 +3325,15 @@ ${answer}
|
|
|
2626
3325
|
}
|
|
2627
3326
|
async mutate(bundlePath2, conceptId2, change, entry, changeBody = (body) => body) {
|
|
2628
3327
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
2629
|
-
const before = await (0,
|
|
3328
|
+
const before = await (0, import_promises6.readFile)(target, "utf8").catch(() => null);
|
|
2630
3329
|
if (before === null) throw new KbRecordNotFoundError(conceptId2);
|
|
2631
3330
|
const parsed = this.parse(conceptId2, before);
|
|
2632
3331
|
if (!parsed) throw new KbRecordNotFoundError(conceptId2);
|
|
2633
3332
|
const frontmatter = change(parsed.frontmatter);
|
|
2634
3333
|
const body = changeBody(parsed.body);
|
|
2635
3334
|
const contents = stringifyMarkdownWithFrontmatter(body, frontmatter);
|
|
2636
|
-
const witness = await (0,
|
|
2637
|
-
if (witness === null ||
|
|
3335
|
+
const witness = await (0, import_promises6.readFile)(target, "utf8").catch(() => null);
|
|
3336
|
+
if (witness === null || sha256(witness) !== sha256(before)) {
|
|
2638
3337
|
throw new KbWriteConflictError(conceptId2);
|
|
2639
3338
|
}
|
|
2640
3339
|
await this.publish(target, contents, true, conceptId2);
|
|
@@ -2659,20 +3358,20 @@ ${answer}
|
|
|
2659
3358
|
*/
|
|
2660
3359
|
async publish(target, contents, overwrite, conceptId2) {
|
|
2661
3360
|
const staging = `${target}.${process.pid}.tmp`;
|
|
2662
|
-
await (0,
|
|
3361
|
+
await (0, import_promises6.writeFile)(staging, contents, "utf8");
|
|
2663
3362
|
try {
|
|
2664
3363
|
if (overwrite) {
|
|
2665
|
-
await (0,
|
|
3364
|
+
await (0, import_promises6.rename)(staging, target);
|
|
2666
3365
|
return;
|
|
2667
3366
|
}
|
|
2668
|
-
await (0,
|
|
3367
|
+
await (0, import_promises6.link)(staging, target);
|
|
2669
3368
|
} catch (error) {
|
|
2670
3369
|
if (error.code === "EEXIST") {
|
|
2671
3370
|
throw new KbRecordAlreadyExistsError(conceptId2);
|
|
2672
3371
|
}
|
|
2673
3372
|
throw error;
|
|
2674
3373
|
} finally {
|
|
2675
|
-
await (0,
|
|
3374
|
+
await (0, import_promises6.unlink)(staging).catch(() => void 0);
|
|
2676
3375
|
}
|
|
2677
3376
|
}
|
|
2678
3377
|
/**
|
|
@@ -2716,18 +3415,18 @@ ${answer}
|
|
|
2716
3415
|
* file must not fail the mutation it guards.
|
|
2717
3416
|
*/
|
|
2718
3417
|
async ensureGitattributes(root) {
|
|
2719
|
-
const target = (0,
|
|
3418
|
+
const target = (0, import_node_path7.join)(root, GITATTRIBUTES_FILE);
|
|
2720
3419
|
try {
|
|
2721
3420
|
let existing;
|
|
2722
3421
|
try {
|
|
2723
|
-
existing = await (0,
|
|
3422
|
+
existing = await (0, import_promises6.readFile)(target, "utf8");
|
|
2724
3423
|
} catch (error) {
|
|
2725
3424
|
if (error.code !== "ENOENT") throw error;
|
|
2726
3425
|
existing = null;
|
|
2727
3426
|
}
|
|
2728
3427
|
if (existing === null) {
|
|
2729
3428
|
try {
|
|
2730
|
-
await (0,
|
|
3429
|
+
await (0, import_promises6.writeFile)(target, appendUnionMergeLine(""), {
|
|
2731
3430
|
encoding: "utf8",
|
|
2732
3431
|
flag: "wx"
|
|
2733
3432
|
});
|
|
@@ -2748,7 +3447,7 @@ ${answer}
|
|
|
2748
3447
|
return;
|
|
2749
3448
|
}
|
|
2750
3449
|
if (!hasMergeDeclaration(existing)) {
|
|
2751
|
-
await (0,
|
|
3450
|
+
await (0, import_promises6.appendFile)(target, appendUnionMergeLine(existing), "utf8");
|
|
2752
3451
|
this.logger.info?.({
|
|
2753
3452
|
operation: "kb.gitattributes.ensure",
|
|
2754
3453
|
bundlePath: root,
|
|
@@ -2767,7 +3466,7 @@ ${answer}
|
|
|
2767
3466
|
async record(root, entry) {
|
|
2768
3467
|
await this.ensureGitattributes(root);
|
|
2769
3468
|
const line = renderLogEntry({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
|
|
2770
|
-
await (0,
|
|
3469
|
+
await (0, import_promises6.appendFile)((0, import_node_path7.join)(root, LOG_FILE), line, "utf8").catch((error) => {
|
|
2771
3470
|
this.logger.warn?.({
|
|
2772
3471
|
operation: "kb.log.append",
|
|
2773
3472
|
outcome: "failed",
|
|
@@ -2793,18 +3492,18 @@ ${answer}
|
|
|
2793
3492
|
};
|
|
2794
3493
|
}
|
|
2795
3494
|
root(bundlePath2) {
|
|
2796
|
-
return (0,
|
|
3495
|
+
return (0, import_node_path7.resolve)(bundlePath2);
|
|
2797
3496
|
}
|
|
2798
3497
|
// Concept ids are `<type>.<slug>` and map to a single file directly under the
|
|
2799
3498
|
// bundle root; anything carrying a separator would escape it.
|
|
2800
3499
|
recordPath(bundlePath2, conceptId2) {
|
|
2801
|
-
if (conceptId2.includes(
|
|
3500
|
+
if (conceptId2.includes(import_node_path7.sep) || conceptId2.includes("/")) {
|
|
2802
3501
|
throw new KbInvalidConceptIdError(
|
|
2803
3502
|
"concept id must not contain a path separator",
|
|
2804
3503
|
{ conceptId: conceptId2 }
|
|
2805
3504
|
);
|
|
2806
3505
|
}
|
|
2807
|
-
return (0,
|
|
3506
|
+
return (0, import_node_path7.join)(this.root(bundlePath2), `${conceptId2}.md`);
|
|
2808
3507
|
}
|
|
2809
3508
|
};
|
|
2810
3509
|
function estimateTokens(record) {
|
|
@@ -2831,7 +3530,7 @@ function stub(hit) {
|
|
|
2831
3530
|
at: hit.record.frontmatter.generated?.at ?? null
|
|
2832
3531
|
};
|
|
2833
3532
|
}
|
|
2834
|
-
function
|
|
3533
|
+
function matches2(record, needle) {
|
|
2835
3534
|
const { title, description } = record.frontmatter;
|
|
2836
3535
|
return [record.conceptId, title, description, record.body].some(
|
|
2837
3536
|
(field) => field?.toLowerCase().includes(needle)
|
|
@@ -2842,58 +3541,39 @@ function normalizeActor(id) {
|
|
|
2842
3541
|
if (colon === -1) return id.toLowerCase();
|
|
2843
3542
|
return id.slice(0, colon + 1).toLowerCase() + id.slice(colon + 1);
|
|
2844
3543
|
}
|
|
2845
|
-
function digest(contents) {
|
|
2846
|
-
return (0, import_node_crypto2.createHash)("sha256").update(contents).digest("hex");
|
|
2847
|
-
}
|
|
2848
|
-
function bundleDigest(records, superseded) {
|
|
2849
|
-
const entries = [
|
|
2850
|
-
...records.map(
|
|
2851
|
-
(hit) => `${hit.record.conceptId}:current:${digest(
|
|
2852
|
-
stringifyMarkdownWithFrontmatter(
|
|
2853
|
-
hit.record.body,
|
|
2854
|
-
hit.record.frontmatter
|
|
2855
|
-
)
|
|
2856
|
-
)}`
|
|
2857
|
-
),
|
|
2858
|
-
...superseded.map(
|
|
2859
|
-
(entry) => `${entry.conceptId}:superseded:${digest(JSON.stringify(entry))}`
|
|
2860
|
-
)
|
|
2861
|
-
].sort();
|
|
2862
|
-
return digest(entries.join("\n"));
|
|
2863
|
-
}
|
|
2864
3544
|
|
|
2865
3545
|
// src/compose.ts
|
|
2866
|
-
var
|
|
2867
|
-
var composeLinkSchema =
|
|
3546
|
+
var import_zod4 = require("zod");
|
|
3547
|
+
var composeLinkSchema = import_zod4.z.object({
|
|
2868
3548
|
target: kbConceptIdSchema,
|
|
2869
|
-
rel:
|
|
3549
|
+
rel: import_zod4.z.enum(KB_LINK_RELS)
|
|
2870
3550
|
}).strict();
|
|
2871
|
-
var composeInputSchema =
|
|
2872
|
-
slug:
|
|
3551
|
+
var composeInputSchema = import_zod4.z.object({
|
|
3552
|
+
slug: import_zod4.z.string().min(1),
|
|
2873
3553
|
/** One line, in the reader's terms. Becomes OKF `title`. */
|
|
2874
|
-
title:
|
|
3554
|
+
title: import_zod4.z.string().min(1),
|
|
2875
3555
|
/** The consequence — what breaks if this is wrong. Becomes `description`. */
|
|
2876
|
-
why:
|
|
3556
|
+
why: import_zod4.z.string().min(1),
|
|
2877
3557
|
/** Keyed by section heading from the type's spec. Unknown keys rejected. */
|
|
2878
|
-
sections:
|
|
2879
|
-
anchors:
|
|
2880
|
-
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(),
|
|
2881
3561
|
/** No source exists, as a claim rather than a sentinel in `sources`. */
|
|
2882
|
-
assumption:
|
|
3562
|
+
assumption: import_zod4.z.boolean().optional(),
|
|
2883
3563
|
/**
|
|
2884
3564
|
* OKF `stale_after`: the absolute date this record stops being trusted.
|
|
2885
3565
|
* Anything the outside world can change — pricing, quotas, versions,
|
|
2886
3566
|
* reception counts — should carry one.
|
|
2887
3567
|
*/
|
|
2888
|
-
stale_after:
|
|
3568
|
+
stale_after: import_zod4.z.string().regex(/^\d{4}-\d{2}-\d{2}$/, {
|
|
2889
3569
|
message: "stale_after must be YYYY-MM-DD"
|
|
2890
3570
|
}).refine((date) => !Number.isNaN(Date.parse(date)), {
|
|
2891
3571
|
message: "stale_after must be a real date"
|
|
2892
3572
|
}).optional(),
|
|
2893
|
-
verify:
|
|
2894
|
-
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(),
|
|
2895
3575
|
/** Concept ids this record relates to; rendered as body links. */
|
|
2896
|
-
relatedConceptIds:
|
|
3576
|
+
relatedConceptIds: import_zod4.z.array(kbConceptIdSchema).optional(),
|
|
2897
3577
|
/**
|
|
2898
3578
|
* Typed causal edges, source → target: `{ target: "fact.b", rel:
|
|
2899
3579
|
* "depends_on" }` on record A says A needs B. Stored in frontmatter and
|
|
@@ -2901,16 +3581,16 @@ var composeInputSchema = import_zod3.z.object({
|
|
|
2901
3581
|
* reader that knows only OKF. The vocabulary goes into the description from
|
|
2902
3582
|
* the same table the walk uses, so `kb_schema` emits it.
|
|
2903
3583
|
*/
|
|
2904
|
-
links:
|
|
3584
|
+
links: import_zod4.z.array(composeLinkSchema).max(64).optional().describe(
|
|
2905
3585
|
`Typed causal edges, source \u2192 target \u2014 a link on this record says this record <rel> the target. ${KB_LINK_RELS.map(
|
|
2906
3586
|
(rel) => `${rel}: ${LINK_RELS[rel].purpose}`
|
|
2907
3587
|
).join("; ")}.`
|
|
2908
3588
|
),
|
|
2909
3589
|
/** Concept ids this record replaces. The store settles the backlinks. */
|
|
2910
|
-
supersedes:
|
|
2911
|
-
materiality:
|
|
2912
|
-
confidence:
|
|
2913
|
-
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()
|
|
2914
3594
|
}).strict();
|
|
2915
3595
|
function composeRecord(type, input, writtenBy, writtenAt) {
|
|
2916
3596
|
const parsed = composeInputSchema.parse(input);
|
|
@@ -2987,9 +3667,9 @@ ${text}`);
|
|
|
2987
3667
|
// src/kb-pins/budgets.ts
|
|
2988
3668
|
function asBudgets(value) {
|
|
2989
3669
|
if (value === null || typeof value !== "object") return {};
|
|
2990
|
-
const
|
|
3670
|
+
const table2 = value;
|
|
2991
3671
|
const pick = (key2, min) => {
|
|
2992
|
-
const raw =
|
|
3672
|
+
const raw = table2[key2];
|
|
2993
3673
|
return typeof raw === "number" && Number.isInteger(raw) && raw >= min ? raw : void 0;
|
|
2994
3674
|
};
|
|
2995
3675
|
const budgetTokens = pick("budgetTokens", 1);
|
|
@@ -3000,9 +3680,9 @@ function asBudgets(value) {
|
|
|
3000
3680
|
};
|
|
3001
3681
|
}
|
|
3002
3682
|
function contextProfileBudgets(manifest, profile) {
|
|
3003
|
-
const
|
|
3004
|
-
if (
|
|
3005
|
-
const entries =
|
|
3683
|
+
const table2 = manifest.context;
|
|
3684
|
+
if (table2 === null || typeof table2 !== "object") return {};
|
|
3685
|
+
const entries = table2;
|
|
3006
3686
|
return {
|
|
3007
3687
|
...asBudgets(entries["default"]),
|
|
3008
3688
|
...profile ? asBudgets(entries[profile]) : {}
|
|
@@ -3033,23 +3713,23 @@ var KbBaseFrozenError = class extends Error {
|
|
|
3033
3713
|
};
|
|
3034
3714
|
|
|
3035
3715
|
// src/kb-pins/frozen.ts
|
|
3036
|
-
var
|
|
3716
|
+
var import_node_path10 = require("path");
|
|
3037
3717
|
|
|
3038
3718
|
// src/kb-pins/layers.ts
|
|
3039
|
-
var
|
|
3040
|
-
var
|
|
3041
|
-
var
|
|
3719
|
+
var import_promises7 = require("fs/promises");
|
|
3720
|
+
var import_node_os3 = require("os");
|
|
3721
|
+
var import_node_path9 = require("path");
|
|
3042
3722
|
|
|
3043
3723
|
// src/kb-pins/model.ts
|
|
3044
|
-
var
|
|
3045
|
-
var
|
|
3046
|
-
var PINS_FILE = (0,
|
|
3047
|
-
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");
|
|
3048
3728
|
var PIN_LAYERS = ["project", "local", "user"];
|
|
3049
|
-
var pinSchema =
|
|
3729
|
+
var pinSchema = import_zod5.z.object({
|
|
3050
3730
|
/** Relative to the manifest's root, so the file is committable. */
|
|
3051
|
-
path:
|
|
3052
|
-
pinnedAt:
|
|
3731
|
+
path: import_zod5.z.string().min(1),
|
|
3732
|
+
pinnedAt: import_zod5.z.string().min(1).optional(),
|
|
3053
3733
|
/**
|
|
3054
3734
|
* How `context` renders this base. `full` preloads the whole base into
|
|
3055
3735
|
* the block regardless of the full-under threshold — for a base whose
|
|
@@ -3059,7 +3739,7 @@ var pinSchema = import_zod4.z.object({
|
|
|
3059
3739
|
* Absent: the profile's full-under threshold decides. Invalid values
|
|
3060
3740
|
* degrade to absent rather than failing the manifest.
|
|
3061
3741
|
*/
|
|
3062
|
-
mode:
|
|
3742
|
+
mode: import_zod5.z.enum(["full", "index"]).optional().catch(void 0),
|
|
3063
3743
|
/**
|
|
3064
3744
|
* Context profiles this pin surfaces in (e.g. only at session-start,
|
|
3065
3745
|
* not per turn). Absent: every profile. A run without a profile sees
|
|
@@ -3067,17 +3747,17 @@ var pinSchema = import_zod4.z.object({
|
|
|
3067
3747
|
* that skill at point of use than pinned at all — pins are what every
|
|
3068
3748
|
* session should see.
|
|
3069
3749
|
*/
|
|
3070
|
-
profiles:
|
|
3750
|
+
profiles: import_zod5.z.array(import_zod5.z.string()).optional().catch(void 0),
|
|
3071
3751
|
/**
|
|
3072
3752
|
* The base is concluded — a finished piece of research, a frozen ADR
|
|
3073
3753
|
* set. Write commands against it refuse while this workspace holds the
|
|
3074
3754
|
* pin, and `context` labels it read-only. Workspace policy, not base
|
|
3075
3755
|
* state: the base itself stays copyable and writable elsewhere.
|
|
3076
3756
|
*/
|
|
3077
|
-
frozen:
|
|
3757
|
+
frozen: import_zod5.z.boolean().optional().catch(void 0)
|
|
3078
3758
|
}).passthrough();
|
|
3079
|
-
var pinsManifestSchema =
|
|
3080
|
-
pins:
|
|
3759
|
+
var pinsManifestSchema = import_zod5.z.object({
|
|
3760
|
+
pins: import_zod5.z.array(pinSchema).default([]),
|
|
3081
3761
|
/**
|
|
3082
3762
|
* Per-repo budgets for the `context` command, keyed by profile —
|
|
3083
3763
|
* `"session-start"`, `"compact"`, `"turn"`, or `"default"` for all of
|
|
@@ -3086,18 +3766,18 @@ var pinsManifestSchema = import_zod4.z.object({
|
|
|
3086
3766
|
* the index at every session start. `contextProfileBudgets` does the
|
|
3087
3767
|
* tolerant read.
|
|
3088
3768
|
*/
|
|
3089
|
-
context:
|
|
3769
|
+
context: import_zod5.z.unknown().optional()
|
|
3090
3770
|
}).passthrough();
|
|
3091
3771
|
|
|
3092
3772
|
// src/kb-pins/layers.ts
|
|
3093
3773
|
function userRoot() {
|
|
3094
|
-
return process.env.STRAUSS_KB_USER_ROOT || (0,
|
|
3774
|
+
return process.env.STRAUSS_KB_USER_ROOT || (0, import_node_os3.homedir)();
|
|
3095
3775
|
}
|
|
3096
3776
|
function layerRoot(workspaceDir, layer) {
|
|
3097
|
-
return layer === "user" ? userRoot() : (0,
|
|
3777
|
+
return layer === "user" ? userRoot() : (0, import_node_path9.resolve)(workspaceDir);
|
|
3098
3778
|
}
|
|
3099
3779
|
function layerFile(workspaceDir, layer) {
|
|
3100
|
-
return (0,
|
|
3780
|
+
return (0, import_node_path9.join)(
|
|
3101
3781
|
layerRoot(workspaceDir, layer),
|
|
3102
3782
|
layer === "local" ? PINS_LOCAL_FILE : PINS_FILE
|
|
3103
3783
|
);
|
|
@@ -3106,7 +3786,7 @@ async function readPinsLayer(workspaceDir, layer) {
|
|
|
3106
3786
|
const file = layerFile(workspaceDir, layer);
|
|
3107
3787
|
let raw;
|
|
3108
3788
|
try {
|
|
3109
|
-
raw = await (0,
|
|
3789
|
+
raw = await (0, import_promises7.readFile)(file, "utf8");
|
|
3110
3790
|
} catch {
|
|
3111
3791
|
return { pins: [] };
|
|
3112
3792
|
}
|
|
@@ -3130,16 +3810,16 @@ async function readPinsLayer(workspaceDir, layer) {
|
|
|
3130
3810
|
}
|
|
3131
3811
|
async function writePinsLayer(workspaceDir, layer, manifest) {
|
|
3132
3812
|
const file = layerFile(workspaceDir, layer);
|
|
3133
|
-
await (0,
|
|
3134
|
-
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)}
|
|
3135
3815
|
`, "utf8");
|
|
3136
3816
|
}
|
|
3137
3817
|
function resolvePinPath(rootDir, path) {
|
|
3138
|
-
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));
|
|
3139
3819
|
}
|
|
3140
3820
|
function storablePath(rootDir, bundlePath2) {
|
|
3141
|
-
const rel = (0,
|
|
3142
|
-
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("/");
|
|
3143
3823
|
}
|
|
3144
3824
|
async function readMergedPins(workspaceDir) {
|
|
3145
3825
|
const manifests = {};
|
|
@@ -3167,7 +3847,7 @@ async function readMergedPins(workspaceDir) {
|
|
|
3167
3847
|
// src/kb-pins/frozen.ts
|
|
3168
3848
|
async function assertBaseNotFrozen(workspaceDir, bundlePath2) {
|
|
3169
3849
|
const merged = await readMergedPins(workspaceDir);
|
|
3170
|
-
const absolute = (0,
|
|
3850
|
+
const absolute = (0, import_node_path10.resolve)(bundlePath2);
|
|
3171
3851
|
const pin = merged.pins.find((entry) => entry.absolutePath === absolute);
|
|
3172
3852
|
if (pin?.frozen === true) {
|
|
3173
3853
|
throw new KbBaseFrozenError(pin.path, pin.layer);
|
|
@@ -3252,7 +3932,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
|
|
|
3252
3932
|
}
|
|
3253
3933
|
|
|
3254
3934
|
// src/kb-pins/unpin.ts
|
|
3255
|
-
var
|
|
3935
|
+
var import_node_path11 = require("path");
|
|
3256
3936
|
async function unpinBase(workspaceDir, bundlePath2) {
|
|
3257
3937
|
const layers = [];
|
|
3258
3938
|
for (const layer of PIN_LAYERS) {
|
|
@@ -3273,14 +3953,14 @@ async function unpinBase(workspaceDir, bundlePath2) {
|
|
|
3273
3953
|
}
|
|
3274
3954
|
}
|
|
3275
3955
|
return {
|
|
3276
|
-
path: storablePath((0,
|
|
3956
|
+
path: storablePath((0, import_node_path11.resolve)(workspaceDir), bundlePath2),
|
|
3277
3957
|
removed: layers.length > 0,
|
|
3278
3958
|
layers
|
|
3279
3959
|
};
|
|
3280
3960
|
}
|
|
3281
3961
|
|
|
3282
3962
|
// src/kb-context.ts
|
|
3283
|
-
var
|
|
3963
|
+
var import_promises8 = require("fs/promises");
|
|
3284
3964
|
var HEADING2 = "## Knowledge bases (pinned)";
|
|
3285
3965
|
var DEFAULT_CONTEXT_BUDGET = 4e3;
|
|
3286
3966
|
var CONTEXT_PROFILES = {
|
|
@@ -3484,13 +4164,13 @@ function toHookJson(block, event) {
|
|
|
3484
4164
|
var CONTEXT_BEGIN = "<!-- strauss-kb:begin -->";
|
|
3485
4165
|
var CONTEXT_END = "<!-- strauss-kb:end -->";
|
|
3486
4166
|
async function syncInstructions(file, block) {
|
|
3487
|
-
const existing = await (0,
|
|
4167
|
+
const existing = await (0, import_promises8.readFile)(file, "utf8").catch(() => null);
|
|
3488
4168
|
const region = block ? `${CONTEXT_BEGIN}
|
|
3489
4169
|
${block.trim()}
|
|
3490
4170
|
${CONTEXT_END}` : null;
|
|
3491
4171
|
if (existing === null) {
|
|
3492
4172
|
if (!region) return { file, action: "unchanged" };
|
|
3493
|
-
await (0,
|
|
4173
|
+
await (0, import_promises8.writeFile)(file, `${region}
|
|
3494
4174
|
`, "utf8");
|
|
3495
4175
|
return { file, action: "created" };
|
|
3496
4176
|
}
|
|
@@ -3501,11 +4181,11 @@ ${CONTEXT_END}` : null;
|
|
|
3501
4181
|
const after = existing.slice(end + CONTEXT_END.length);
|
|
3502
4182
|
const next = region ? `${before}${region}${after}` : `${before.replace(/\n+$/, "\n")}${after.replace(/^\n+/, "\n")}`;
|
|
3503
4183
|
if (next === existing) return { file, action: "unchanged" };
|
|
3504
|
-
await (0,
|
|
4184
|
+
await (0, import_promises8.writeFile)(file, next, "utf8");
|
|
3505
4185
|
return { file, action: region ? "replaced" : "removed" };
|
|
3506
4186
|
}
|
|
3507
4187
|
if (!region) return { file, action: "unchanged" };
|
|
3508
|
-
await (0,
|
|
4188
|
+
await (0, import_promises8.writeFile)(
|
|
3509
4189
|
file,
|
|
3510
4190
|
`${existing.replace(/\n*$/, "\n\n")}${region}
|
|
3511
4191
|
`,
|
|
@@ -3515,14 +4195,14 @@ ${CONTEXT_END}` : null;
|
|
|
3515
4195
|
}
|
|
3516
4196
|
|
|
3517
4197
|
// src/json-schema.ts
|
|
3518
|
-
var
|
|
4198
|
+
var import_zod6 = require("zod");
|
|
3519
4199
|
function kbJsonSchemas() {
|
|
3520
4200
|
return {
|
|
3521
|
-
recordFrontmatter:
|
|
4201
|
+
recordFrontmatter: import_zod6.z.toJSONSchema(kbRecordFrontmatterSchema, {
|
|
3522
4202
|
io: "input"
|
|
3523
4203
|
}),
|
|
3524
|
-
composeInput:
|
|
3525
|
-
logEntry:
|
|
4204
|
+
composeInput: import_zod6.z.toJSONSchema(composeInputSchema, { io: "input" }),
|
|
4205
|
+
logEntry: import_zod6.z.toJSONSchema(kbLogEntrySchema, { io: "input" })
|
|
3526
4206
|
};
|
|
3527
4207
|
}
|
|
3528
4208
|
|
|
@@ -3532,7 +4212,7 @@ function matchToDiff(files, records, options = {}) {
|
|
|
3532
4212
|
const anchored = records.filter(
|
|
3533
4213
|
(record) => (record.frontmatter.strauss_anchors ?? []).length > 0
|
|
3534
4214
|
);
|
|
3535
|
-
const
|
|
4215
|
+
const matches3 = [];
|
|
3536
4216
|
for (const file of files) {
|
|
3537
4217
|
const candidates = anchored.map((record) => ({
|
|
3538
4218
|
record,
|
|
@@ -3551,7 +4231,7 @@ function matchToDiff(files, records, options = {}) {
|
|
|
3551
4231
|
hits.push(record);
|
|
3552
4232
|
}
|
|
3553
4233
|
if (!hits.length) continue;
|
|
3554
|
-
|
|
4234
|
+
matches3.push({
|
|
3555
4235
|
filePath: file.filePath,
|
|
3556
4236
|
hunk,
|
|
3557
4237
|
records: order(adjudicate(hits, records, options.now)),
|
|
@@ -3559,7 +4239,7 @@ function matchToDiff(files, records, options = {}) {
|
|
|
3559
4239
|
});
|
|
3560
4240
|
}
|
|
3561
4241
|
}
|
|
3562
|
-
return
|
|
4242
|
+
return matches3;
|
|
3563
4243
|
}
|
|
3564
4244
|
function place(anchors, filePath, hunk, ranges) {
|
|
3565
4245
|
let fallback = "miss";
|
|
@@ -3708,6 +4388,18 @@ var CHECK_HEADLINES = {
|
|
|
3708
4388
|
unchecked: "an anchor in another repository nothing could reach"
|
|
3709
4389
|
};
|
|
3710
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
|
+
}
|
|
3711
4403
|
function doctor(bundle, options = {}) {
|
|
3712
4404
|
const thresholds = {
|
|
3713
4405
|
expiringDays: options.expiringDays ?? DEFAULT_EXPIRING_DAYS,
|
|
@@ -3743,6 +4435,7 @@ function doctor(bundle, options = {}) {
|
|
|
3743
4435
|
counts,
|
|
3744
4436
|
groups,
|
|
3745
4437
|
findingCount,
|
|
4438
|
+
anchorResolvers: anchorResolverCounts(bundle),
|
|
3746
4439
|
healthy: findingCount === 0
|
|
3747
4440
|
};
|
|
3748
4441
|
}
|
|
@@ -3989,12 +4682,12 @@ function ageInDays(record, now) {
|
|
|
3989
4682
|
}
|
|
3990
4683
|
|
|
3991
4684
|
// src/decision-record.ts
|
|
3992
|
-
var
|
|
4685
|
+
var import_zod7 = require("zod");
|
|
3993
4686
|
var DECISION_TYPE = "decision";
|
|
3994
4687
|
var NO_DECISION_SLUG = "none";
|
|
3995
4688
|
var decisionInputSchema = composeInputSchema.omit({ sections: true }).extend({
|
|
3996
|
-
alternative:
|
|
3997
|
-
impact:
|
|
4689
|
+
alternative: import_zod7.z.string().min(1).optional(),
|
|
4690
|
+
impact: import_zod7.z.string().min(1).optional()
|
|
3998
4691
|
}).strict();
|
|
3999
4692
|
function composeDecisionRecord(input, writtenBy, writtenAt) {
|
|
4000
4693
|
const { alternative, impact: impact2, ...rest } = input;
|
|
@@ -4036,13 +4729,13 @@ function selectDecisions(records) {
|
|
|
4036
4729
|
}
|
|
4037
4730
|
|
|
4038
4731
|
// src/commands/anchor-resolve.ts
|
|
4039
|
-
var
|
|
4732
|
+
var import_zod9 = require("zod");
|
|
4040
4733
|
|
|
4041
4734
|
// src/commands/model.ts
|
|
4042
|
-
var
|
|
4043
|
-
var bundlePath =
|
|
4044
|
-
var conceptId =
|
|
4045
|
-
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(
|
|
4046
4739
|
"Where the anchored source lives, for the drift check. Defaults to the working directory."
|
|
4047
4740
|
);
|
|
4048
4741
|
function define(command) {
|
|
@@ -4065,22 +4758,30 @@ function argvFlag(argv, name) {
|
|
|
4065
4758
|
}
|
|
4066
4759
|
|
|
4067
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
|
+
}
|
|
4068
4769
|
var anchorResolveCommand = define({
|
|
4069
4770
|
name: "anchor-resolve",
|
|
4070
4771
|
tool: "kb_anchor_resolve",
|
|
4071
4772
|
usage: "anchor-resolve <concept-id> [--repo-root <path>] [--offline] [--rebaseline] [--restamp]",
|
|
4072
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.",
|
|
4073
|
-
input:
|
|
4774
|
+
input: import_zod9.z.object({
|
|
4074
4775
|
bundlePath,
|
|
4075
4776
|
conceptId,
|
|
4076
|
-
repoRoot:
|
|
4077
|
-
offline:
|
|
4777
|
+
repoRoot: import_zod9.z.string().min(1).optional(),
|
|
4778
|
+
offline: import_zod9.z.boolean().optional().describe(
|
|
4078
4779
|
"Resolve foreign anchors from the local repo cache only, never fetching."
|
|
4079
4780
|
),
|
|
4080
|
-
rebaseline:
|
|
4781
|
+
rebaseline: import_zod9.z.boolean().optional().describe(
|
|
4081
4782
|
"Accept the current code as the new baseline for anchors that drifted."
|
|
4082
4783
|
),
|
|
4083
|
-
restamp:
|
|
4784
|
+
restamp: import_zod9.z.boolean().optional().describe(
|
|
4084
4785
|
"Refresh `resolved_at` on anchors that already match. Off by default, so a green run writes nothing."
|
|
4085
4786
|
)
|
|
4086
4787
|
}),
|
|
@@ -4109,6 +4810,11 @@ var anchorResolveCommand = define({
|
|
|
4109
4810
|
const updated = [];
|
|
4110
4811
|
let dirty = false;
|
|
4111
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
|
+
);
|
|
4112
4818
|
for (const anchor of anchors) {
|
|
4113
4819
|
const base2 = {
|
|
4114
4820
|
file: anchor.file,
|
|
@@ -4125,27 +4831,35 @@ var anchorResolveCommand = define({
|
|
|
4125
4831
|
updated.push(anchor);
|
|
4126
4832
|
continue;
|
|
4127
4833
|
}
|
|
4128
|
-
const
|
|
4129
|
-
if (!
|
|
4834
|
+
const outcome = resolveAnchorSpan(source.source, anchor, resolvers);
|
|
4835
|
+
if (!outcome.ok) {
|
|
4130
4836
|
results.push({
|
|
4131
4837
|
...base2,
|
|
4132
4838
|
state: "unresolved",
|
|
4133
|
-
reason:
|
|
4839
|
+
reason: outcome.reason
|
|
4134
4840
|
});
|
|
4135
4841
|
updated.push(anchor);
|
|
4136
4842
|
continue;
|
|
4137
4843
|
}
|
|
4844
|
+
const resolved = outcome.span;
|
|
4845
|
+
const producedBy = outcome.resolver;
|
|
4138
4846
|
const currentHash = hashAnchorText(resolved.text);
|
|
4139
4847
|
const currentLines = resolved.endLine - resolved.startLine + 1;
|
|
4140
4848
|
const stamped = {
|
|
4141
4849
|
...anchor,
|
|
4142
4850
|
hash: currentHash,
|
|
4143
4851
|
lines: currentLines,
|
|
4144
|
-
resolved_at: now()
|
|
4852
|
+
resolved_at: now(),
|
|
4853
|
+
...producedBy ? { resolver: producedBy } : {}
|
|
4145
4854
|
};
|
|
4146
4855
|
const pinned = anchor.ref !== void 0 && source.repo !== void 0;
|
|
4147
4856
|
if (!anchor.hash) {
|
|
4148
|
-
results.push({
|
|
4857
|
+
results.push({
|
|
4858
|
+
...base2,
|
|
4859
|
+
state: "stamped",
|
|
4860
|
+
currentHash,
|
|
4861
|
+
...producedBy ? { resolver: producedBy } : {}
|
|
4862
|
+
});
|
|
4149
4863
|
updated.push(stamped);
|
|
4150
4864
|
dirty = true;
|
|
4151
4865
|
continue;
|
|
@@ -4156,6 +4870,10 @@ var anchorResolveCommand = define({
|
|
|
4156
4870
|
state: "drifted",
|
|
4157
4871
|
currentHash,
|
|
4158
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" } : {},
|
|
4159
4877
|
...pinned ? { remoteState: "drifted-from-ref" } : {},
|
|
4160
4878
|
...rebaseline ? { rebaselined: true } : {}
|
|
4161
4879
|
});
|
|
@@ -4163,7 +4881,7 @@ var anchorResolveCommand = define({
|
|
|
4163
4881
|
if (rebaseline) dirty = true;
|
|
4164
4882
|
continue;
|
|
4165
4883
|
}
|
|
4166
|
-
const onDefault = pinned ? headHash(source, anchor) : void 0;
|
|
4884
|
+
const onDefault = pinned ? headHash(source, anchor, resolvers) : void 0;
|
|
4167
4885
|
if (onDefault && onDefault.hash !== anchor.hash) {
|
|
4168
4886
|
results.push({
|
|
4169
4887
|
...base2,
|
|
@@ -4179,6 +4897,7 @@ var anchorResolveCommand = define({
|
|
|
4179
4897
|
...base2,
|
|
4180
4898
|
state: "match",
|
|
4181
4899
|
currentHash,
|
|
4900
|
+
...producedBy ? { resolver: producedBy } : {},
|
|
4182
4901
|
...pinned ? { remoteState: "matches-ref" } : {}
|
|
4183
4902
|
});
|
|
4184
4903
|
const refresh = restamp || anchor.resolved_at === void 0;
|
|
@@ -4196,19 +4915,21 @@ var anchorResolveCommand = define({
|
|
|
4196
4915
|
if (!frozen) await store.updateAnchors(path, id, updated, actor);
|
|
4197
4916
|
}
|
|
4198
4917
|
const frozenNote = frozen ? { frozen: true, note: "base is frozen: nothing was stamped" } : {};
|
|
4918
|
+
const hints = grammarHints();
|
|
4919
|
+
const hintNote = hints.length ? { hints } : {};
|
|
4199
4920
|
const unreachable = results.filter(
|
|
4200
4921
|
(entry) => isUncheckedReason(entry.reason)
|
|
4201
4922
|
).length;
|
|
4202
4923
|
const checked = results.length - unreachable;
|
|
4203
|
-
const
|
|
4204
|
-
const note = `${
|
|
4205
|
-
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;
|
|
4206
4927
|
if (clean) {
|
|
4207
4928
|
try {
|
|
4208
4929
|
await store.verify(
|
|
4209
4930
|
path,
|
|
4210
4931
|
id,
|
|
4211
|
-
`anchor-resolve: ${note} (
|
|
4932
|
+
`anchor-resolve: ${note} (${resolverSummary(results)})`,
|
|
4212
4933
|
actor,
|
|
4213
4934
|
now()
|
|
4214
4935
|
);
|
|
@@ -4219,17 +4940,25 @@ var anchorResolveCommand = define({
|
|
|
4219
4940
|
results,
|
|
4220
4941
|
verified: false,
|
|
4221
4942
|
verifyRefused: "self-verification",
|
|
4222
|
-
...frozenNote
|
|
4943
|
+
...frozenNote,
|
|
4944
|
+
...hintNote
|
|
4223
4945
|
};
|
|
4224
4946
|
}
|
|
4225
|
-
return {
|
|
4947
|
+
return {
|
|
4948
|
+
conceptId: id,
|
|
4949
|
+
results,
|
|
4950
|
+
verified: true,
|
|
4951
|
+
...frozenNote,
|
|
4952
|
+
...hintNote
|
|
4953
|
+
};
|
|
4226
4954
|
}
|
|
4227
4955
|
return {
|
|
4228
4956
|
conceptId: id,
|
|
4229
4957
|
results,
|
|
4230
4958
|
verified: false,
|
|
4231
4959
|
...unreachable ? { note } : {},
|
|
4232
|
-
...frozenNote
|
|
4960
|
+
...frozenNote,
|
|
4961
|
+
...hintNote
|
|
4233
4962
|
};
|
|
4234
4963
|
},
|
|
4235
4964
|
// A stored hash that no longer resolves is a broken anchor, not an absence:
|
|
@@ -4245,13 +4974,13 @@ var anchorResolveCommand = define({
|
|
|
4245
4974
|
function lineDelta(anchor, current) {
|
|
4246
4975
|
return anchor.lines === void 0 ? null : Math.abs(current - anchor.lines);
|
|
4247
4976
|
}
|
|
4248
|
-
function headHash(source, anchor) {
|
|
4977
|
+
function headHash(source, anchor, resolvers) {
|
|
4249
4978
|
if (source.head === void 0) return void 0;
|
|
4250
|
-
const
|
|
4251
|
-
if (!
|
|
4979
|
+
const outcome = resolveAnchorSpan(source.head, anchor, resolvers);
|
|
4980
|
+
if (!outcome.ok) return void 0;
|
|
4252
4981
|
return {
|
|
4253
|
-
hash: hashAnchorText(
|
|
4254
|
-
lines:
|
|
4982
|
+
hash: hashAnchorText(outcome.span.text),
|
|
4983
|
+
lines: outcome.span.endLine - outcome.span.startLine + 1
|
|
4255
4984
|
};
|
|
4256
4985
|
}
|
|
4257
4986
|
async function readSources(anchors, root, offline) {
|
|
@@ -4301,13 +5030,13 @@ async function readSources(anchors, root, offline) {
|
|
|
4301
5030
|
}
|
|
4302
5031
|
|
|
4303
5032
|
// src/commands/answer.ts
|
|
4304
|
-
var
|
|
5033
|
+
var import_zod10 = require("zod");
|
|
4305
5034
|
var answerCommand = define({
|
|
4306
5035
|
name: "answer",
|
|
4307
5036
|
tool: "kb_answer",
|
|
4308
5037
|
usage: "answer <concept-id> <answer...>",
|
|
4309
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.",
|
|
4310
|
-
input:
|
|
5039
|
+
input: import_zod10.z.object({ bundlePath, conceptId, answer: import_zod10.z.string().min(1) }),
|
|
4311
5040
|
fromArgv: (argv, path) => ({
|
|
4312
5041
|
bundlePath: path,
|
|
4313
5042
|
conceptId: argv[1],
|
|
@@ -4321,27 +5050,27 @@ var answerCommand = define({
|
|
|
4321
5050
|
});
|
|
4322
5051
|
|
|
4323
5052
|
// src/commands/backlinks.ts
|
|
4324
|
-
var
|
|
5053
|
+
var import_zod11 = require("zod");
|
|
4325
5054
|
var backlinksCommand = define({
|
|
4326
5055
|
name: "backlinks",
|
|
4327
5056
|
tool: "kb_backlinks",
|
|
4328
5057
|
usage: "backlinks <concept-id>",
|
|
4329
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.",
|
|
4330
|
-
input:
|
|
5059
|
+
input: import_zod11.z.object({ bundlePath, conceptId }),
|
|
4331
5060
|
fromArgv: (argv, path) => ({ bundlePath: path, conceptId: argv[1] }),
|
|
4332
5061
|
run: async ({ store }, { bundlePath: path, conceptId: id }) => store.backlinks(path, id)
|
|
4333
5062
|
});
|
|
4334
5063
|
|
|
4335
5064
|
// src/commands/catalog.ts
|
|
4336
|
-
var
|
|
5065
|
+
var import_zod12 = require("zod");
|
|
4337
5066
|
var catalogCommand = define({
|
|
4338
5067
|
name: "catalog",
|
|
4339
5068
|
tool: "kb_catalog",
|
|
4340
5069
|
usage: "catalog [type]",
|
|
4341
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.",
|
|
4342
|
-
input:
|
|
5071
|
+
input: import_zod12.z.object({
|
|
4343
5072
|
bundlePath,
|
|
4344
|
-
type:
|
|
5073
|
+
type: import_zod12.z.enum(KB_RECORD_TYPES).optional()
|
|
4345
5074
|
}),
|
|
4346
5075
|
fromArgv: (argv, path) => ({
|
|
4347
5076
|
bundlePath: path,
|
|
@@ -4396,26 +5125,26 @@ function count(value, noun) {
|
|
|
4396
5125
|
}
|
|
4397
5126
|
|
|
4398
5127
|
// src/commands/context.ts
|
|
4399
|
-
var
|
|
5128
|
+
var import_zod13 = require("zod");
|
|
4400
5129
|
var contextCommand = define({
|
|
4401
5130
|
name: "context",
|
|
4402
5131
|
tool: "kb_context",
|
|
4403
5132
|
usage: "context [--profile NAME] [--budget N] [--full-under N] [--format json] [--event NAME]",
|
|
4404
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.",
|
|
4405
|
-
input:
|
|
4406
|
-
budgetTokens:
|
|
5134
|
+
input: import_zod13.z.object({
|
|
5135
|
+
budgetTokens: import_zod13.z.number().int().positive().optional().describe(
|
|
4407
5136
|
"Ceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000."
|
|
4408
5137
|
),
|
|
4409
|
-
fullUnderTokens:
|
|
5138
|
+
fullUnderTokens: import_zod13.z.number().int().positive().optional().describe(
|
|
4410
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."
|
|
4411
5140
|
),
|
|
4412
|
-
profile:
|
|
5141
|
+
profile: import_zod13.z.string().optional().describe(
|
|
4413
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."
|
|
4414
5143
|
),
|
|
4415
|
-
format:
|
|
5144
|
+
format: import_zod13.z.enum(["markdown", "json"]).optional().describe(
|
|
4416
5145
|
"CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
|
|
4417
5146
|
),
|
|
4418
|
-
event:
|
|
5147
|
+
event: import_zod13.z.string().optional().describe(
|
|
4419
5148
|
"hookEventName stamped into the JSON envelope. Only meaningful with format=json."
|
|
4420
5149
|
)
|
|
4421
5150
|
}),
|
|
@@ -4451,14 +5180,14 @@ var contextCommand = define({
|
|
|
4451
5180
|
});
|
|
4452
5181
|
|
|
4453
5182
|
// src/commands/doctor.ts
|
|
4454
|
-
var
|
|
4455
|
-
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}.`);
|
|
4456
5185
|
var doctorCommand = define({
|
|
4457
5186
|
name: "doctor",
|
|
4458
5187
|
tool: "kb_doctor",
|
|
4459
5188
|
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--offline] [--strict]",
|
|
4460
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.",
|
|
4461
|
-
input:
|
|
5190
|
+
input: import_zod14.z.object({
|
|
4462
5191
|
bundlePath,
|
|
4463
5192
|
repoRoot: REPO_ROOT,
|
|
4464
5193
|
expiringDays: days(
|
|
@@ -4473,10 +5202,10 @@ var doctorCommand = define({
|
|
|
4473
5202
|
"How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
|
|
4474
5203
|
DEFAULT_AGING_DAYS
|
|
4475
5204
|
),
|
|
4476
|
-
offline:
|
|
5205
|
+
offline: import_zod14.z.boolean().optional().describe(
|
|
4477
5206
|
"Read foreign anchors from the local repo cache only, never fetching."
|
|
4478
5207
|
),
|
|
4479
|
-
strict:
|
|
5208
|
+
strict: import_zod14.z.boolean().optional().describe(
|
|
4480
5209
|
"Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
|
|
4481
5210
|
)
|
|
4482
5211
|
}),
|
|
@@ -4519,7 +5248,13 @@ var doctorCommand = define({
|
|
|
4519
5248
|
...anchorDrift !== void 0 ? { anchorDrift } : {},
|
|
4520
5249
|
now: new Date(checkedAt)
|
|
4521
5250
|
});
|
|
4522
|
-
|
|
5251
|
+
const hints = grammarHints();
|
|
5252
|
+
return {
|
|
5253
|
+
bundlePath: path,
|
|
5254
|
+
checkedAt,
|
|
5255
|
+
...report,
|
|
5256
|
+
...hints.length ? { hints } : {}
|
|
5257
|
+
};
|
|
4523
5258
|
},
|
|
4524
5259
|
render: (result) => render2(result),
|
|
4525
5260
|
// Only expiry, and only under --strict. The other seven checks report debt a
|
|
@@ -4537,12 +5272,15 @@ function render2(result) {
|
|
|
4537
5272
|
`records: ${result.recordCount}`,
|
|
4538
5273
|
`thresholds: expiring within ${thresholds.expiringDays}d, unverified over ${thresholds.unverifiedDays}d, aging over ${thresholds.agingDays}d`,
|
|
4539
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
|
+
] : [],
|
|
4540
5278
|
""
|
|
4541
5279
|
];
|
|
4542
|
-
const
|
|
5280
|
+
const width2 = Math.max(...result.groups.map((group2) => group2.check.length));
|
|
4543
5281
|
for (const group2 of result.groups) {
|
|
4544
5282
|
lines.push(
|
|
4545
|
-
` ${group2.check.padEnd(
|
|
5283
|
+
` ${group2.check.padEnd(width2)} ${String(group2.count).padStart(3)} ${group2.headline}`
|
|
4546
5284
|
);
|
|
4547
5285
|
}
|
|
4548
5286
|
for (const group2 of result.groups) {
|
|
@@ -4554,6 +5292,7 @@ function render2(result) {
|
|
|
4554
5292
|
);
|
|
4555
5293
|
}
|
|
4556
5294
|
}
|
|
5295
|
+
for (const hint of result.hints ?? []) lines.push("", hint);
|
|
4557
5296
|
lines.push(
|
|
4558
5297
|
"",
|
|
4559
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.`
|
|
@@ -4562,19 +5301,19 @@ function render2(result) {
|
|
|
4562
5301
|
}
|
|
4563
5302
|
|
|
4564
5303
|
// src/commands/impact.ts
|
|
4565
|
-
var
|
|
5304
|
+
var import_zod15 = require("zod");
|
|
4566
5305
|
var impactCommand = define({
|
|
4567
5306
|
name: "impact",
|
|
4568
5307
|
tool: "kb_impact",
|
|
4569
5308
|
usage: "impact <concept-id> [--depth N] [--rels a,b]",
|
|
4570
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.",
|
|
4571
|
-
input:
|
|
5310
|
+
input: import_zod15.z.object({
|
|
4572
5311
|
bundlePath,
|
|
4573
5312
|
conceptId,
|
|
4574
|
-
depth:
|
|
5313
|
+
depth: import_zod15.z.number().int().positive().optional().describe(
|
|
4575
5314
|
"Hops out from the record. Unbounded when omitted; a walk this cuts reports truncated: true."
|
|
4576
5315
|
),
|
|
4577
|
-
rels:
|
|
5316
|
+
rels: import_zod15.z.array(import_zod15.z.enum(KB_CAUSAL_LINK_RELS)).optional().describe(
|
|
4578
5317
|
"Narrow which rels the walk follows. Defaults to every rel that carries a dependence \u2014 all but related_to."
|
|
4579
5318
|
)
|
|
4580
5319
|
}),
|
|
@@ -4595,13 +5334,13 @@ var impactCommand = define({
|
|
|
4595
5334
|
});
|
|
4596
5335
|
|
|
4597
5336
|
// src/commands/list.ts
|
|
4598
|
-
var
|
|
5337
|
+
var import_zod16 = require("zod");
|
|
4599
5338
|
var listCommand = define({
|
|
4600
5339
|
name: "list",
|
|
4601
5340
|
tool: "kb_list",
|
|
4602
5341
|
usage: "list [type]",
|
|
4603
5342
|
description: "Every record, optionally one type. For enumerating; use kb_query for a question.",
|
|
4604
|
-
input:
|
|
5343
|
+
input: import_zod16.z.object({ bundlePath, type: import_zod16.z.enum(KB_RECORD_TYPES).optional() }),
|
|
4605
5344
|
fromArgv: (argv, path) => ({ bundlePath: path, type: argv[1] }),
|
|
4606
5345
|
run: async ({ store }, { bundlePath: path, type }) => (await store.list(path, type)).map((record) => ({
|
|
4607
5346
|
conceptId: record.conceptId,
|
|
@@ -4613,17 +5352,17 @@ var listCommand = define({
|
|
|
4613
5352
|
});
|
|
4614
5353
|
|
|
4615
5354
|
// src/commands/load.ts
|
|
4616
|
-
var
|
|
5355
|
+
var import_zod17 = require("zod");
|
|
4617
5356
|
var loadCommand = define({
|
|
4618
5357
|
name: "load",
|
|
4619
5358
|
tool: "kb_load",
|
|
4620
5359
|
usage: "load [type] [--budget N | --all] [--repo-root PATH]",
|
|
4621
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.",
|
|
4622
|
-
input:
|
|
5361
|
+
input: import_zod17.z.object({
|
|
4623
5362
|
bundlePath,
|
|
4624
|
-
type:
|
|
4625
|
-
budgetTokens:
|
|
4626
|
-
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(
|
|
4627
5366
|
"Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
|
|
4628
5367
|
),
|
|
4629
5368
|
repoRoot: REPO_ROOT
|
|
@@ -4665,25 +5404,25 @@ var loadCommand = define({
|
|
|
4665
5404
|
});
|
|
4666
5405
|
|
|
4667
5406
|
// src/commands/log.ts
|
|
4668
|
-
var
|
|
5407
|
+
var import_zod18 = require("zod");
|
|
4669
5408
|
var logCommand = define({
|
|
4670
5409
|
name: "log",
|
|
4671
5410
|
tool: "kb_log",
|
|
4672
5411
|
usage: "log",
|
|
4673
5412
|
description: "Who touched what, and when. Append-only; malformed lines are reported, never repaired.",
|
|
4674
|
-
input:
|
|
5413
|
+
input: import_zod18.z.object({ bundlePath }),
|
|
4675
5414
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
4676
5415
|
run: ({ store }, { bundlePath: path }) => store.readLog(path)
|
|
4677
5416
|
});
|
|
4678
5417
|
|
|
4679
5418
|
// src/commands/no-decision.ts
|
|
4680
|
-
var
|
|
5419
|
+
var import_zod19 = require("zod");
|
|
4681
5420
|
var noDecisionCommand = define({
|
|
4682
5421
|
name: "no-decision",
|
|
4683
5422
|
tool: "kb_no_decision",
|
|
4684
5423
|
usage: "no-decision <reason...>",
|
|
4685
5424
|
description: "Record in one sentence that a piece of work had nothing to decide. Idempotent.",
|
|
4686
|
-
input:
|
|
5425
|
+
input: import_zod19.z.object({ bundlePath, reason: import_zod19.z.string().min(1) }),
|
|
4687
5426
|
fromArgv: (argv, path) => ({
|
|
4688
5427
|
bundlePath: path,
|
|
4689
5428
|
reason: argv.slice(1).join(" ").trim()
|
|
@@ -4700,20 +5439,20 @@ var noDecisionCommand = define({
|
|
|
4700
5439
|
});
|
|
4701
5440
|
|
|
4702
5441
|
// src/commands/pack.ts
|
|
4703
|
-
var
|
|
5442
|
+
var import_zod20 = require("zod");
|
|
4704
5443
|
var packCommand = define({
|
|
4705
5444
|
name: "pack",
|
|
4706
5445
|
tool: "kb_pack",
|
|
4707
5446
|
usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
|
|
4708
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.",
|
|
4709
|
-
input:
|
|
5448
|
+
input: import_zod20.z.object({
|
|
4710
5449
|
bundlePath,
|
|
4711
5450
|
conceptId,
|
|
4712
|
-
hops:
|
|
4713
|
-
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(
|
|
4714
5453
|
"How many records the pack may hold, root included. Defaults to 20."
|
|
4715
5454
|
),
|
|
4716
|
-
budgetTokens:
|
|
5455
|
+
budgetTokens: import_zod20.z.number().int().positive().optional().describe(
|
|
4717
5456
|
"Approximate token ceiling over what is actually emitted. Defaults to 25000."
|
|
4718
5457
|
)
|
|
4719
5458
|
}),
|
|
@@ -4800,22 +5539,22 @@ function warningLabel(warning) {
|
|
|
4800
5539
|
}
|
|
4801
5540
|
|
|
4802
5541
|
// src/commands/pin.ts
|
|
4803
|
-
var
|
|
5542
|
+
var import_zod21 = require("zod");
|
|
4804
5543
|
var pinCommand = define({
|
|
4805
5544
|
name: "pin",
|
|
4806
5545
|
tool: "kb_pin",
|
|
4807
5546
|
usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
|
|
4808
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.",
|
|
4809
|
-
input:
|
|
5548
|
+
input: import_zod21.z.object({
|
|
4810
5549
|
bundlePath,
|
|
4811
|
-
mode:
|
|
5550
|
+
mode: import_zod21.z.enum(["full", "index"]).optional().describe(
|
|
4812
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."
|
|
4813
5552
|
),
|
|
4814
|
-
profiles:
|
|
4815
|
-
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(
|
|
4816
5555
|
"Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
|
|
4817
5556
|
),
|
|
4818
|
-
frozen:
|
|
5557
|
+
frozen: import_zod21.z.boolean().optional().describe(
|
|
4819
5558
|
"true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
|
|
4820
5559
|
)
|
|
4821
5560
|
}),
|
|
@@ -4844,29 +5583,29 @@ var pinCommand = define({
|
|
|
4844
5583
|
});
|
|
4845
5584
|
|
|
4846
5585
|
// src/commands/pins.ts
|
|
4847
|
-
var
|
|
5586
|
+
var import_zod22 = require("zod");
|
|
4848
5587
|
var pinsCommand = define({
|
|
4849
5588
|
name: "pins",
|
|
4850
5589
|
tool: "kb_pins",
|
|
4851
5590
|
usage: "pins",
|
|
4852
5591
|
description: "Every pinned base across the manifest layers, with its layer and whether it resolves to records. Takes no bundlePath.",
|
|
4853
|
-
input:
|
|
5592
|
+
input: import_zod22.z.object({}),
|
|
4854
5593
|
fromArgv: () => ({}),
|
|
4855
5594
|
run: ({ store }) => listPins(store, process.cwd())
|
|
4856
5595
|
});
|
|
4857
5596
|
|
|
4858
5597
|
// src/commands/query.ts
|
|
4859
|
-
var
|
|
5598
|
+
var import_zod23 = require("zod");
|
|
4860
5599
|
var queryCommand = define({
|
|
4861
5600
|
name: "query",
|
|
4862
5601
|
tool: "kb_query",
|
|
4863
5602
|
usage: "query <text...> [--repo-root PATH]",
|
|
4864
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.",
|
|
4865
|
-
input:
|
|
5604
|
+
input: import_zod23.z.object({
|
|
4866
5605
|
bundlePath,
|
|
4867
|
-
text:
|
|
4868
|
-
type:
|
|
4869
|
-
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(),
|
|
4870
5609
|
repoRoot: REPO_ROOT
|
|
4871
5610
|
}),
|
|
4872
5611
|
// `--repo-root` is a flag, so its value must not fall into the search text.
|
|
@@ -4898,40 +5637,138 @@ var queryCommand = define({
|
|
|
4898
5637
|
});
|
|
4899
5638
|
|
|
4900
5639
|
// src/commands/read-index.ts
|
|
4901
|
-
var
|
|
5640
|
+
var import_zod24 = require("zod");
|
|
4902
5641
|
var readIndexCommand = define({
|
|
4903
5642
|
name: "index",
|
|
4904
5643
|
tool: "kb_index",
|
|
4905
5644
|
usage: "index",
|
|
4906
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.",
|
|
4907
|
-
input:
|
|
5646
|
+
input: import_zod24.z.object({ bundlePath }),
|
|
4908
5647
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
4909
5648
|
run: ({ store }, { bundlePath: path }) => store.readIndex(path)
|
|
4910
5649
|
});
|
|
4911
5650
|
|
|
4912
5651
|
// src/commands/schema.ts
|
|
4913
|
-
var
|
|
5652
|
+
var import_zod25 = require("zod");
|
|
4914
5653
|
var schemaCommand = define({
|
|
4915
5654
|
name: "schema",
|
|
4916
5655
|
tool: "kb_schema",
|
|
4917
5656
|
usage: "schema",
|
|
4918
5657
|
description: "JSON Schema for frontmatter, write input, and log entries, generated from the enforcing code.",
|
|
4919
|
-
input:
|
|
5658
|
+
input: import_zod25.z.object({}),
|
|
4920
5659
|
fromArgv: () => ({}),
|
|
4921
5660
|
run: () => Promise.resolve(kbJsonSchemas())
|
|
4922
5661
|
});
|
|
4923
5662
|
|
|
5663
|
+
// src/commands/stamp.ts
|
|
5664
|
+
var import_promises9 = require("fs/promises");
|
|
5665
|
+
var import_zod26 = require("zod");
|
|
5666
|
+
var DIGEST = /^[0-9a-f]{64}$/;
|
|
5667
|
+
var stampCommand = define({
|
|
5668
|
+
name: "stamp",
|
|
5669
|
+
tool: "kb_stamp",
|
|
5670
|
+
usage: "stamp [--bundle PATH] [--since DIGEST|FILE]",
|
|
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.",
|
|
5672
|
+
input: import_zod26.z.object({
|
|
5673
|
+
bundlePath: import_zod26.z.string().min(1).optional().describe(
|
|
5674
|
+
"Absolute path to one knowledge base. Omit to stamp every pinned base."
|
|
5675
|
+
),
|
|
5676
|
+
since: import_zod26.z.string().min(1).optional().describe(
|
|
5677
|
+
"Prior digest, or path to a prior `stamp --json`; only moved bases return, with changed ids when the baseline is a file."
|
|
5678
|
+
)
|
|
5679
|
+
}),
|
|
5680
|
+
fromArgv: (argv, path, _stdin, bundleExplicit) => {
|
|
5681
|
+
const since = argvFlag(argv, "--since");
|
|
5682
|
+
return {
|
|
5683
|
+
...bundleExplicit ? { bundlePath: path } : {},
|
|
5684
|
+
...since !== void 0 ? { since } : {}
|
|
5685
|
+
};
|
|
5686
|
+
},
|
|
5687
|
+
run: async ({ store }, { bundlePath: bundlePath2, since }) => {
|
|
5688
|
+
const targets = bundlePath2 ? [bundlePath2] : (await readMergedPins(process.cwd())).pins.map(
|
|
5689
|
+
(pin) => pin.absolutePath
|
|
5690
|
+
);
|
|
5691
|
+
if (since !== void 0 && DIGEST.test(since) && targets.length > 1) {
|
|
5692
|
+
throw new KbStampDigestBaselineError(since);
|
|
5693
|
+
}
|
|
5694
|
+
const stamps = await Promise.all(
|
|
5695
|
+
targets.map((target) => store.stamp(target))
|
|
5696
|
+
);
|
|
5697
|
+
if (since === void 0) {
|
|
5698
|
+
return stamps.map((stamp) => ({ ...stamp, changed: null }));
|
|
5699
|
+
}
|
|
5700
|
+
const baseline = await readBaseline(since);
|
|
5701
|
+
const reports = [];
|
|
5702
|
+
for (const stamp of stamps) {
|
|
5703
|
+
const before = baseline.byPath.get(stamp.path);
|
|
5704
|
+
if (baseline.digest !== null) {
|
|
5705
|
+
if (baseline.digest === stamp.digest) continue;
|
|
5706
|
+
reports.push({ ...stamp, changed: null });
|
|
5707
|
+
continue;
|
|
5708
|
+
}
|
|
5709
|
+
if (before && before.digest === stamp.digest) continue;
|
|
5710
|
+
reports.push({ ...stamp, changed: changedIds(before?.records, stamp) });
|
|
5711
|
+
}
|
|
5712
|
+
return reports;
|
|
5713
|
+
},
|
|
5714
|
+
render: (result) => result.map((report) => {
|
|
5715
|
+
const counts = `${report.recordCount} record(s), ${report.superseded} superseded`;
|
|
5716
|
+
const head = `${report.path} ${report.digest} ${counts}${report.newestAt ? ` newest ${report.newestAt}` : ""}`;
|
|
5717
|
+
return report.changed?.length ? `${head}
|
|
5718
|
+
changed: ${report.changed.join(", ")}` : head;
|
|
5719
|
+
}).join("\n")
|
|
5720
|
+
});
|
|
5721
|
+
function changedIds(before, stamp) {
|
|
5722
|
+
const now = new Map(
|
|
5723
|
+
stamp.records.map((record) => [record.conceptId, record.digest])
|
|
5724
|
+
);
|
|
5725
|
+
const ids = /* @__PURE__ */ new Set();
|
|
5726
|
+
for (const [conceptId2, digest] of now) {
|
|
5727
|
+
if (before?.get(conceptId2) !== digest) ids.add(conceptId2);
|
|
5728
|
+
}
|
|
5729
|
+
for (const conceptId2 of before?.keys() ?? []) {
|
|
5730
|
+
if (!now.has(conceptId2)) ids.add(conceptId2);
|
|
5731
|
+
}
|
|
5732
|
+
return [...ids].sort();
|
|
5733
|
+
}
|
|
5734
|
+
async function readBaseline(since) {
|
|
5735
|
+
if (DIGEST.test(since)) return { digest: since, byPath: /* @__PURE__ */ new Map() };
|
|
5736
|
+
let parsed;
|
|
5737
|
+
try {
|
|
5738
|
+
parsed = JSON.parse(await (0, import_promises9.readFile)(since, "utf8"));
|
|
5739
|
+
} catch {
|
|
5740
|
+
throw new KbStampBaselineError(since);
|
|
5741
|
+
}
|
|
5742
|
+
const entries = Array.isArray(parsed) ? parsed : parsed?.stamps ?? [];
|
|
5743
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
5744
|
+
for (const entry of entries) {
|
|
5745
|
+
if (typeof entry?.path !== "string" || typeof entry?.digest !== "string") {
|
|
5746
|
+
continue;
|
|
5747
|
+
}
|
|
5748
|
+
byPath.set(entry.path, {
|
|
5749
|
+
digest: entry.digest,
|
|
5750
|
+
records: new Map(
|
|
5751
|
+
(entry.records ?? []).map((record) => [
|
|
5752
|
+
record.conceptId,
|
|
5753
|
+
record.digest
|
|
5754
|
+
])
|
|
5755
|
+
)
|
|
5756
|
+
});
|
|
5757
|
+
}
|
|
5758
|
+
return { digest: null, byPath };
|
|
5759
|
+
}
|
|
5760
|
+
|
|
4924
5761
|
// src/commands/status.ts
|
|
4925
|
-
var
|
|
5762
|
+
var import_zod27 = require("zod");
|
|
4926
5763
|
var statusCommand = define({
|
|
4927
5764
|
name: "status",
|
|
4928
5765
|
tool: "kb_status",
|
|
4929
5766
|
usage: "status <concept-id> <status>",
|
|
4930
5767
|
description: "Move a record's status. Compare-and-swap: a concurrent change fails instead of being overwritten.",
|
|
4931
|
-
input:
|
|
5768
|
+
input: import_zod27.z.object({
|
|
4932
5769
|
bundlePath,
|
|
4933
5770
|
conceptId,
|
|
4934
|
-
status:
|
|
5771
|
+
status: import_zod27.z.enum(KB_RECORD_STATUSES)
|
|
4935
5772
|
}),
|
|
4936
5773
|
fromArgv: (argv, path) => ({
|
|
4937
5774
|
bundlePath: path,
|
|
@@ -4946,13 +5783,13 @@ var statusCommand = define({
|
|
|
4946
5783
|
});
|
|
4947
5784
|
|
|
4948
5785
|
// src/commands/supersede.ts
|
|
4949
|
-
var
|
|
5786
|
+
var import_zod28 = require("zod");
|
|
4950
5787
|
var supersedeCommand = define({
|
|
4951
5788
|
name: "supersede",
|
|
4952
5789
|
tool: "kb_supersede",
|
|
4953
5790
|
usage: "supersede <concept-id> <replacement-id>",
|
|
4954
5791
|
description: "Mark a record superseded by another, linked in both directions. Use instead of editing a record whose meaning changed.",
|
|
4955
|
-
input:
|
|
5792
|
+
input: import_zod28.z.object({ bundlePath, conceptId, replacementId: conceptId }),
|
|
4956
5793
|
fromArgv: (argv, path) => ({
|
|
4957
5794
|
bundlePath: path,
|
|
4958
5795
|
conceptId: argv[1],
|
|
@@ -4966,16 +5803,16 @@ var supersedeCommand = define({
|
|
|
4966
5803
|
});
|
|
4967
5804
|
|
|
4968
5805
|
// src/commands/sync-instructions.ts
|
|
4969
|
-
var
|
|
5806
|
+
var import_zod29 = require("zod");
|
|
4970
5807
|
var syncInstructionsCommand = define({
|
|
4971
5808
|
name: "sync-instructions",
|
|
4972
5809
|
usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
|
|
4973
5810
|
description: "CLI-only: plant the kb_context block between sentinel comments in AGENTS.md or CLAUDE.md, idempotently.",
|
|
4974
|
-
input:
|
|
4975
|
-
file:
|
|
4976
|
-
budgetTokens:
|
|
4977
|
-
fullUnderTokens:
|
|
4978
|
-
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()
|
|
4979
5816
|
}),
|
|
4980
5817
|
fromArgv: (argv) => {
|
|
4981
5818
|
const budget = argvFlag(argv, "--budget");
|
|
@@ -5001,17 +5838,17 @@ var syncInstructionsCommand = define({
|
|
|
5001
5838
|
});
|
|
5002
5839
|
|
|
5003
5840
|
// src/commands/trace.ts
|
|
5004
|
-
var
|
|
5841
|
+
var import_zod30 = require("zod");
|
|
5005
5842
|
var traceCommand = define({
|
|
5006
5843
|
name: "trace",
|
|
5007
5844
|
tool: "kb_trace",
|
|
5008
5845
|
usage: "trace <concept-id> [edges...]",
|
|
5009
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".',
|
|
5010
|
-
input:
|
|
5847
|
+
input: import_zod30.z.object({
|
|
5011
5848
|
bundlePath,
|
|
5012
5849
|
conceptId,
|
|
5013
|
-
edges:
|
|
5014
|
-
depth:
|
|
5850
|
+
edges: import_zod30.z.array(import_zod30.z.enum(TRACE_EDGES)).optional(),
|
|
5851
|
+
depth: import_zod30.z.number().int().positive().optional()
|
|
5015
5852
|
}),
|
|
5016
5853
|
fromArgv: (argv, path) => ({
|
|
5017
5854
|
bundlePath: path,
|
|
@@ -5033,37 +5870,37 @@ var traceCommand = define({
|
|
|
5033
5870
|
});
|
|
5034
5871
|
|
|
5035
5872
|
// src/commands/types.ts
|
|
5036
|
-
var
|
|
5873
|
+
var import_zod31 = require("zod");
|
|
5037
5874
|
var typesCommand = define({
|
|
5038
5875
|
name: "types",
|
|
5039
5876
|
tool: "kb_types",
|
|
5040
5877
|
usage: "types",
|
|
5041
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.",
|
|
5042
|
-
input:
|
|
5879
|
+
input: import_zod31.z.object({}),
|
|
5043
5880
|
fromArgv: () => ({}),
|
|
5044
5881
|
run: () => Promise.resolve(RECORD_TYPES)
|
|
5045
5882
|
});
|
|
5046
5883
|
|
|
5047
5884
|
// src/commands/unpin.ts
|
|
5048
|
-
var
|
|
5885
|
+
var import_zod32 = require("zod");
|
|
5049
5886
|
var unpinCommand = define({
|
|
5050
5887
|
name: "unpin",
|
|
5051
5888
|
tool: "kb_unpin",
|
|
5052
5889
|
usage: "unpin [bundle-path]",
|
|
5053
5890
|
description: "Remove a base from every manifest layer that holds it. Reports the layers touched.",
|
|
5054
|
-
input:
|
|
5891
|
+
input: import_zod32.z.object({ bundlePath }),
|
|
5055
5892
|
fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
|
|
5056
5893
|
run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
|
|
5057
5894
|
});
|
|
5058
5895
|
|
|
5059
5896
|
// src/commands/validate.ts
|
|
5060
|
-
var
|
|
5897
|
+
var import_zod33 = require("zod");
|
|
5061
5898
|
var validateCommand = define({
|
|
5062
5899
|
name: "validate",
|
|
5063
5900
|
tool: "kb_validate",
|
|
5064
5901
|
usage: "validate",
|
|
5065
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.",
|
|
5066
|
-
input:
|
|
5903
|
+
input: import_zod33.z.object({ bundlePath }),
|
|
5067
5904
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
5068
5905
|
run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
|
|
5069
5906
|
// Warnings never fail the exit code; every other severity does.
|
|
@@ -5073,16 +5910,16 @@ var validateCommand = define({
|
|
|
5073
5910
|
});
|
|
5074
5911
|
|
|
5075
5912
|
// src/commands/verify.ts
|
|
5076
|
-
var
|
|
5913
|
+
var import_zod34 = require("zod");
|
|
5077
5914
|
var verifyCommand = define({
|
|
5078
5915
|
name: "verify",
|
|
5079
5916
|
tool: "kb_verify",
|
|
5080
5917
|
usage: "verify <concept-id> --note <text>",
|
|
5081
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.",
|
|
5082
|
-
input:
|
|
5919
|
+
input: import_zod34.z.object({
|
|
5083
5920
|
bundlePath,
|
|
5084
5921
|
conceptId,
|
|
5085
|
-
note:
|
|
5922
|
+
note: import_zod34.z.string().refine((s) => s.trim().length > 0, {
|
|
5086
5923
|
message: "note must say what the check found"
|
|
5087
5924
|
})
|
|
5088
5925
|
}),
|
|
@@ -5102,15 +5939,15 @@ var verifyCommand = define({
|
|
|
5102
5939
|
});
|
|
5103
5940
|
|
|
5104
5941
|
// src/commands/write.ts
|
|
5105
|
-
var
|
|
5942
|
+
var import_zod35 = require("zod");
|
|
5106
5943
|
var writeCommand = define({
|
|
5107
5944
|
name: "write",
|
|
5108
5945
|
tool: "kb_write",
|
|
5109
5946
|
usage: "write <type> < record.json",
|
|
5110
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.",
|
|
5111
|
-
input:
|
|
5948
|
+
input: import_zod35.z.object({
|
|
5112
5949
|
bundlePath,
|
|
5113
|
-
type:
|
|
5950
|
+
type: import_zod35.z.enum(KB_RECORD_TYPES),
|
|
5114
5951
|
input: composeInputSchema
|
|
5115
5952
|
}),
|
|
5116
5953
|
fromArgv: async (argv, path, stdin) => ({
|
|
@@ -5134,13 +5971,13 @@ var writeCommand = define({
|
|
|
5134
5971
|
});
|
|
5135
5972
|
|
|
5136
5973
|
// src/commands/write-decision.ts
|
|
5137
|
-
var
|
|
5974
|
+
var import_zod36 = require("zod");
|
|
5138
5975
|
var writeDecisionCommand = define({
|
|
5139
5976
|
name: "write-decision",
|
|
5140
5977
|
tool: "kb_write_decision",
|
|
5141
5978
|
usage: "write-decision < decision.json",
|
|
5142
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.",
|
|
5143
|
-
input:
|
|
5980
|
+
input: import_zod36.z.object({ bundlePath, input: decisionInputSchema }),
|
|
5144
5981
|
fromArgv: async (_argv, path, stdin) => ({
|
|
5145
5982
|
bundlePath: path,
|
|
5146
5983
|
input: JSON.parse(await stdin())
|
|
@@ -5180,6 +6017,7 @@ var KB_COMMANDS = [
|
|
|
5180
6017
|
listCommand,
|
|
5181
6018
|
readIndexCommand,
|
|
5182
6019
|
logCommand,
|
|
6020
|
+
stampCommand,
|
|
5183
6021
|
validateCommand,
|
|
5184
6022
|
doctorCommand,
|
|
5185
6023
|
schemaCommand,
|
|
@@ -5199,7 +6037,7 @@ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
|
5199
6037
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
5200
6038
|
|
|
5201
6039
|
// src/version.ts
|
|
5202
|
-
var VERSION = true ? "0.1.
|
|
6040
|
+
var VERSION = true ? "0.1.17" : "0.0.0-dev";
|
|
5203
6041
|
|
|
5204
6042
|
// src/mcp.ts
|
|
5205
6043
|
function createKbMcpServer() {
|
|
@@ -5238,10 +6076,14 @@ async function runKbMcpServer() {
|
|
|
5238
6076
|
}
|
|
5239
6077
|
|
|
5240
6078
|
// src/cli.ts
|
|
5241
|
-
var
|
|
6079
|
+
var import_node_path12 = require("path");
|
|
5242
6080
|
async function runKbCli(argv) {
|
|
5243
6081
|
const { flags, literal } = takeLiteral(argv);
|
|
5244
|
-
const {
|
|
6082
|
+
const {
|
|
6083
|
+
bundle,
|
|
6084
|
+
explicit: bundleExplicit,
|
|
6085
|
+
rest: withFlags
|
|
6086
|
+
} = takeBundle(flags);
|
|
5245
6087
|
const name = withFlags[0] ?? "";
|
|
5246
6088
|
if (!name || name === "-h" || name === "--help") {
|
|
5247
6089
|
process.stdout.write(usage());
|
|
@@ -5262,7 +6104,7 @@ async function runKbCli(argv) {
|
|
|
5262
6104
|
...json ? withFlags.filter((argument) => argument !== "--json") : withFlags,
|
|
5263
6105
|
...literal
|
|
5264
6106
|
];
|
|
5265
|
-
const raw = await command.fromArgv(rest, bundle, readStdin);
|
|
6107
|
+
const raw = await command.fromArgv(rest, bundle, readStdin, bundleExplicit);
|
|
5266
6108
|
const parsed = command.input.safeParse(raw);
|
|
5267
6109
|
if (!parsed.success) {
|
|
5268
6110
|
die(
|
|
@@ -5284,6 +6126,7 @@ async function runKbCli(argv) {
|
|
|
5284
6126
|
if (command.failsWhen?.(result, parsed.data)) process.exitCode = 1;
|
|
5285
6127
|
if (result === "") return;
|
|
5286
6128
|
const text = command.render && !json ? command.render(result) : typeof result === "string" ? result : JSON.stringify(result, null, 2);
|
|
6129
|
+
if (text === "") return;
|
|
5287
6130
|
process.stdout.write(text.endsWith("\n") ? text : `${text}
|
|
5288
6131
|
`);
|
|
5289
6132
|
}
|
|
@@ -5295,11 +6138,15 @@ function takeLiteral(argv) {
|
|
|
5295
6138
|
function takeBundle(argv) {
|
|
5296
6139
|
const at = argv.indexOf("--bundle");
|
|
5297
6140
|
if (at === -1) {
|
|
5298
|
-
return { bundle: (0,
|
|
6141
|
+
return { bundle: (0, import_node_path12.join)(process.cwd(), KB_DIR), explicit: false, rest: argv };
|
|
5299
6142
|
}
|
|
5300
6143
|
const bundle = argv[at + 1];
|
|
5301
6144
|
if (!bundle) die("--bundle requires a path");
|
|
5302
|
-
return {
|
|
6145
|
+
return {
|
|
6146
|
+
bundle,
|
|
6147
|
+
explicit: true,
|
|
6148
|
+
rest: [...argv.slice(0, at), ...argv.slice(at + 2)]
|
|
6149
|
+
};
|
|
5303
6150
|
}
|
|
5304
6151
|
function readStdin() {
|
|
5305
6152
|
return new Promise((resolve6, reject) => {
|
|
@@ -5321,14 +6168,14 @@ function summarise(description) {
|
|
|
5321
6168
|
return sentence.length > 78 ? `${sentence.slice(0, 75)}\u2026` : sentence;
|
|
5322
6169
|
}
|
|
5323
6170
|
function usage() {
|
|
5324
|
-
const
|
|
6171
|
+
const width2 = Math.max(...KB_COMMANDS.map((command) => command.usage.length));
|
|
5325
6172
|
return [
|
|
5326
6173
|
"strauss-kb \u2014 knowledge base commands",
|
|
5327
6174
|
"",
|
|
5328
6175
|
"Usage: strauss-kb [--bundle PATH] <command> [args]",
|
|
5329
6176
|
"",
|
|
5330
6177
|
...KB_COMMANDS.map(
|
|
5331
|
-
(command) => ` ${command.usage.padEnd(
|
|
6178
|
+
(command) => ` ${command.usage.padEnd(width2)} ${summarise(command.description)}`
|
|
5332
6179
|
),
|
|
5333
6180
|
"",
|
|
5334
6181
|
` --bundle PATH defaults to ./${KB_DIR}`,
|
|
@@ -5389,6 +6236,7 @@ function usage() {
|
|
|
5389
6236
|
RECORD_TYPES,
|
|
5390
6237
|
SEARCH_INDEX_FILE,
|
|
5391
6238
|
TRACE_EDGES,
|
|
6239
|
+
TreeSitterResolver,
|
|
5392
6240
|
adjudicate,
|
|
5393
6241
|
anchorFilePath,
|
|
5394
6242
|
assertBaseNotFrozen,
|
|
@@ -5403,9 +6251,14 @@ function usage() {
|
|
|
5403
6251
|
contextProfileBudgets,
|
|
5404
6252
|
createKbMcpServer,
|
|
5405
6253
|
decisionInputSchema,
|
|
6254
|
+
defaultAnchorResolvers,
|
|
5406
6255
|
detectAnchorDrift,
|
|
5407
6256
|
doctor,
|
|
5408
6257
|
edgeNeighbours,
|
|
6258
|
+
ensureGrammar,
|
|
6259
|
+
grammarHints,
|
|
6260
|
+
grammarManifest,
|
|
6261
|
+
grammarsCacheRoot,
|
|
5409
6262
|
hashAnchorText,
|
|
5410
6263
|
impact,
|
|
5411
6264
|
inboundIndex,
|
|
@@ -5423,6 +6276,7 @@ function usage() {
|
|
|
5423
6276
|
kbRecordFrontmatterSchema,
|
|
5424
6277
|
kbSourceSchema,
|
|
5425
6278
|
kbVerifiedEventSchema,
|
|
6279
|
+
languageForFile,
|
|
5426
6280
|
listPins,
|
|
5427
6281
|
loadQmd,
|
|
5428
6282
|
matchToDiff,
|
|
@@ -5432,6 +6286,7 @@ function usage() {
|
|
|
5432
6286
|
parseLog,
|
|
5433
6287
|
parseMarkdownWithFrontmatter,
|
|
5434
6288
|
pinBase,
|
|
6289
|
+
prepareResolvers,
|
|
5435
6290
|
readMergedPins,
|
|
5436
6291
|
readPinsLayer,
|
|
5437
6292
|
readRemoteAnchors,
|
|
@@ -5442,6 +6297,7 @@ function usage() {
|
|
|
5442
6297
|
renderLogEntry,
|
|
5443
6298
|
repoCacheDir,
|
|
5444
6299
|
resolveAnchor,
|
|
6300
|
+
resolveAnchorSpan,
|
|
5445
6301
|
resolveHeads,
|
|
5446
6302
|
resolveHits,
|
|
5447
6303
|
resolvePinPath,
|
|
@@ -5454,6 +6310,7 @@ function usage() {
|
|
|
5454
6310
|
syncInstructions,
|
|
5455
6311
|
toHookJson,
|
|
5456
6312
|
trace,
|
|
6313
|
+
treeSitterLanguages,
|
|
5457
6314
|
unpinBase,
|
|
5458
6315
|
validateBundle
|
|
5459
6316
|
});
|