@saasontools/strauss-kb 0.1.16 → 0.1.18
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 +48 -5
- package/README.md +44 -6
- package/dist/{chunk-H5W53NVU.js → chunk-GSOTMWZZ.js} +1765 -342
- package/dist/chunk-GSOTMWZZ.js.map +1 -0
- package/dist/{chunk-RINBOAQZ.js → chunk-IOIUS26S.js} +2 -2
- package/dist/{chunk-MEZCF646.js → chunk-ROGVYSMV.js} +2 -2
- package/dist/cli-main.cjs +1772 -363
- package/dist/cli-main.cjs.map +1 -1
- package/dist/cli-main.js +2 -2
- package/dist/index.cjs +1825 -381
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +504 -9
- package/dist/index.d.ts +504 -9
- package/dist/index.js +29 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp-main.cjs +1761 -352
- package/dist/mcp-main.cjs.map +1 -1
- package/dist/mcp-main.js +2 -2
- package/grammars/README.md +78 -0
- package/grammars/manifest.json +595 -0
- package/grammars/packs.json +127 -0
- package/package.json +7 -2
- package/dist/chunk-H5W53NVU.js.map +0 -1
- /package/dist/{chunk-RINBOAQZ.js.map → chunk-IOIUS26S.js.map} +0 -0
- /package/dist/{chunk-MEZCF646.js.map → chunk-ROGVYSMV.js.map} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -78,12 +78,14 @@ __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,
|
|
84
85
|
backlinks: () => backlinks,
|
|
85
86
|
buildContext: () => buildContext,
|
|
86
87
|
catalog: () => catalog,
|
|
88
|
+
classifyDrift: () => classifyDrift,
|
|
87
89
|
composeDecisionRecord: () => composeDecisionRecord,
|
|
88
90
|
composeInputSchema: () => composeInputSchema,
|
|
89
91
|
composeLinkSchema: () => composeLinkSchema,
|
|
@@ -92,9 +94,14 @@ __export(index_exports, {
|
|
|
92
94
|
contextProfileBudgets: () => contextProfileBudgets,
|
|
93
95
|
createKbMcpServer: () => createKbMcpServer,
|
|
94
96
|
decisionInputSchema: () => decisionInputSchema,
|
|
97
|
+
defaultAnchorResolvers: () => defaultAnchorResolvers,
|
|
95
98
|
detectAnchorDrift: () => detectAnchorDrift,
|
|
96
99
|
doctor: () => doctor,
|
|
97
100
|
edgeNeighbours: () => edgeNeighbours,
|
|
101
|
+
ensureGrammar: () => ensureGrammar,
|
|
102
|
+
grammarHints: () => grammarHints,
|
|
103
|
+
grammarManifest: () => grammarManifest,
|
|
104
|
+
grammarsCacheRoot: () => grammarsCacheRoot,
|
|
98
105
|
hashAnchorText: () => hashAnchorText,
|
|
99
106
|
impact: () => impact,
|
|
100
107
|
inboundIndex: () => inboundIndex,
|
|
@@ -112,6 +119,7 @@ __export(index_exports, {
|
|
|
112
119
|
kbRecordFrontmatterSchema: () => kbRecordFrontmatterSchema,
|
|
113
120
|
kbSourceSchema: () => kbSourceSchema,
|
|
114
121
|
kbVerifiedEventSchema: () => kbVerifiedEventSchema,
|
|
122
|
+
languageForFile: () => languageForFile,
|
|
115
123
|
listPins: () => listPins,
|
|
116
124
|
loadQmd: () => loadQmd,
|
|
117
125
|
matchToDiff: () => matchToDiff,
|
|
@@ -121,9 +129,11 @@ __export(index_exports, {
|
|
|
121
129
|
parseLog: () => parseLog,
|
|
122
130
|
parseMarkdownWithFrontmatter: () => parseMarkdownWithFrontmatter,
|
|
123
131
|
pinBase: () => pinBase,
|
|
132
|
+
prepareResolvers: () => prepareResolvers,
|
|
124
133
|
readMergedPins: () => readMergedPins,
|
|
125
134
|
readPinsLayer: () => readPinsLayer,
|
|
126
135
|
readRemoteAnchors: () => readRemoteAnchors,
|
|
136
|
+
reassessPacket: () => reassessPacket,
|
|
127
137
|
regexResolver: () => regexResolver,
|
|
128
138
|
renderCatalogLine: () => renderCatalogLine,
|
|
129
139
|
renderIndex: () => renderIndex,
|
|
@@ -131,6 +141,7 @@ __export(index_exports, {
|
|
|
131
141
|
renderLogEntry: () => renderLogEntry,
|
|
132
142
|
repoCacheDir: () => repoCacheDir,
|
|
133
143
|
resolveAnchor: () => resolveAnchor,
|
|
144
|
+
resolveAnchorSpan: () => resolveAnchorSpan,
|
|
134
145
|
resolveHeads: () => resolveHeads,
|
|
135
146
|
resolveHits: () => resolveHits,
|
|
136
147
|
resolvePinPath: () => resolvePinPath,
|
|
@@ -143,14 +154,20 @@ __export(index_exports, {
|
|
|
143
154
|
syncInstructions: () => syncInstructions,
|
|
144
155
|
toHookJson: () => toHookJson,
|
|
145
156
|
trace: () => trace,
|
|
157
|
+
treeSitterLanguages: () => treeSitterLanguages,
|
|
158
|
+
unifiedDiff: () => unifiedDiff,
|
|
146
159
|
unpinBase: () => unpinBase,
|
|
147
160
|
validateBundle: () => validateBundle
|
|
148
161
|
});
|
|
149
162
|
module.exports = __toCommonJS(index_exports);
|
|
150
163
|
|
|
164
|
+
// ../../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
|
|
165
|
+
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;
|
|
166
|
+
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
167
|
+
|
|
151
168
|
// src/kb-store.ts
|
|
152
|
-
var
|
|
153
|
-
var
|
|
169
|
+
var import_promises6 = require("fs/promises");
|
|
170
|
+
var import_node_path7 = require("path");
|
|
154
171
|
|
|
155
172
|
// src/concurrency.ts
|
|
156
173
|
var DEFAULT_IO_CONCURRENCY = 16;
|
|
@@ -167,9 +184,9 @@ async function mapLimit(items, limit, fn) {
|
|
|
167
184
|
{ length: Math.min(limit, items.length) },
|
|
168
185
|
async () => {
|
|
169
186
|
while (!failed && next < items.length) {
|
|
170
|
-
const
|
|
187
|
+
const at2 = next++;
|
|
171
188
|
try {
|
|
172
|
-
out[
|
|
189
|
+
out[at2] = await fn(items[at2], at2);
|
|
173
190
|
} catch (error) {
|
|
174
191
|
failed = true;
|
|
175
192
|
throw error;
|
|
@@ -248,10 +265,24 @@ var kbAnchorSchema = import_zod.z.object({
|
|
|
248
265
|
hash: import_zod.z.string().regex(/^sha256:[0-9a-f]{64}$/, {
|
|
249
266
|
message: "hash must be sha256:<64 hex chars>"
|
|
250
267
|
}).optional(),
|
|
268
|
+
/**
|
|
269
|
+
* What `hash` was taken over: the span's raw text, or the normalised token
|
|
270
|
+
* stream a parser sees (`ast`). Absent means `raw`, which is what every
|
|
271
|
+
* anchor stamped before this field carries, so old hashes keep comparing
|
|
272
|
+
* the way they were written. An `ast` hash is blind to whitespace and
|
|
273
|
+
* comments, so reformatting the anchored code is not drift.
|
|
274
|
+
*/
|
|
275
|
+
hash_kind: import_zod.z.enum(["raw", "ast"]).optional(),
|
|
251
276
|
/** ISO 8601 timestamp of the last successful resolution. */
|
|
252
277
|
resolved_at: import_zod.z.string().min(1).optional(),
|
|
253
278
|
/** Line count of the text the hash was taken over. */
|
|
254
|
-
lines: import_zod.z.number().int().positive().optional()
|
|
279
|
+
lines: import_zod.z.number().int().positive().optional(),
|
|
280
|
+
/**
|
|
281
|
+
* Which resolver produced the hashed span. Absent means an anchor stamped
|
|
282
|
+
* before resolvers were named, which is read as `regex` — the only one
|
|
283
|
+
* there was. A hash from a different resolver is drift, not a match.
|
|
284
|
+
*/
|
|
285
|
+
resolver: import_zod.z.enum(["tree-sitter", "regex"]).optional()
|
|
255
286
|
}).strict();
|
|
256
287
|
var kbLinkSchema = import_zod.z.object({
|
|
257
288
|
target: import_zod.z.string().min(1),
|
|
@@ -656,8 +687,8 @@ function safeSegment(value) {
|
|
|
656
687
|
function revRef(rev) {
|
|
657
688
|
const safe = rev.replace(/[^A-Za-z0-9_-]/g, "-").slice(0, 64);
|
|
658
689
|
let hash = 5381;
|
|
659
|
-
for (let
|
|
660
|
-
hash = (hash * 33 ^ rev.charCodeAt(
|
|
690
|
+
for (let at2 = 0; at2 < rev.length; at2++) {
|
|
691
|
+
hash = (hash * 33 ^ rev.charCodeAt(at2)) >>> 0;
|
|
661
692
|
}
|
|
662
693
|
return `refs/strauss/${safe}-${hash.toString(16)}`;
|
|
663
694
|
}
|
|
@@ -766,8 +797,8 @@ function repoUrlIsSafe(repo) {
|
|
|
766
797
|
if (!scheme?.[1]) return false;
|
|
767
798
|
if (!allowed.includes(scheme[1].toLowerCase())) return false;
|
|
768
799
|
const authority = url.slice(scheme[0].length).split("/")[0] ?? "";
|
|
769
|
-
const
|
|
770
|
-
return
|
|
800
|
+
const at2 = authority.lastIndexOf("@");
|
|
801
|
+
return at2 < 0 || !authority.slice(0, at2).includes(":");
|
|
771
802
|
}
|
|
772
803
|
function protocolArgs() {
|
|
773
804
|
const allowed = allowedProtocols();
|
|
@@ -818,18 +849,18 @@ async function readOneRepo(repo, url, declared, context) {
|
|
|
818
849
|
if (!repoUrlIsSafe(url)) return all({ ok: false, reason: "repo-invalid" });
|
|
819
850
|
const cache = cachePathFor(repo, context.cacheDir);
|
|
820
851
|
if (!cache) return all({ ok: false, reason: "remote-unreachable" });
|
|
821
|
-
const
|
|
852
|
+
const rejected2 = /* @__PURE__ */ new Map();
|
|
822
853
|
const usable = [];
|
|
823
854
|
for (const want of wants) {
|
|
824
855
|
const reason = wantReason(want);
|
|
825
856
|
if (reason)
|
|
826
|
-
|
|
857
|
+
rejected2.set(wantKey(repo, want.ref, want.file), { ok: false, reason });
|
|
827
858
|
else usable.push(want);
|
|
828
859
|
}
|
|
829
|
-
if (!usable.length) return
|
|
860
|
+
if (!usable.length) return rejected2;
|
|
830
861
|
wants = usable;
|
|
831
862
|
const opened = await openCache(cache, url, context);
|
|
832
|
-
if (opened) return new Map([...
|
|
863
|
+
if (opened) return new Map([...rejected2, ...all(opened)]);
|
|
833
864
|
const wantsDefault = wants.some((want) => want.ref === void 0);
|
|
834
865
|
const branch = wantsDefault ? await defaultBranch(cache, context) : {};
|
|
835
866
|
const revs = /* @__PURE__ */ new Map();
|
|
@@ -856,9 +887,9 @@ async function readOneRepo(repo, url, declared, context) {
|
|
|
856
887
|
}
|
|
857
888
|
);
|
|
858
889
|
return new Map([
|
|
859
|
-
...
|
|
890
|
+
...rejected2,
|
|
860
891
|
...wants.map(
|
|
861
|
-
(want,
|
|
892
|
+
(want, at2) => [wantKey(repo, want.ref, want.file), reads[at2]]
|
|
862
893
|
)
|
|
863
894
|
]);
|
|
864
895
|
}
|
|
@@ -908,13 +939,13 @@ async function defaultBranch(cache, context) {
|
|
|
908
939
|
if (!listed.ok) {
|
|
909
940
|
const reason = transportReason(listed.stderr);
|
|
910
941
|
if (reason !== "ref-not-found") {
|
|
911
|
-
const
|
|
912
|
-
return
|
|
942
|
+
const cached3 = await cachedBranch(cache);
|
|
943
|
+
return cached3 ? { name: cached3 } : { reason };
|
|
913
944
|
}
|
|
914
945
|
}
|
|
915
946
|
}
|
|
916
|
-
const
|
|
917
|
-
if (
|
|
947
|
+
const cached2 = await cachedBranch(cache);
|
|
948
|
+
if (cached2) return { name: cached2 };
|
|
918
949
|
return {
|
|
919
950
|
reason: context.offline ? "remote-unreachable" : "default-branch-unknown"
|
|
920
951
|
};
|
|
@@ -941,8 +972,8 @@ async function ensureRev(cache, rev, context) {
|
|
|
941
972
|
cwd: cache
|
|
942
973
|
}
|
|
943
974
|
);
|
|
944
|
-
const
|
|
945
|
-
if (
|
|
975
|
+
const cached2 = have.ok && have.stdout.trim().length > 0;
|
|
976
|
+
if (cached2 && (context.offline || IMMUTABLE_REV.test(rev))) return void 0;
|
|
946
977
|
if (context.offline) return { ok: false, reason: "remote-unreachable" };
|
|
947
978
|
const fetched = await git(
|
|
948
979
|
[
|
|
@@ -958,7 +989,7 @@ async function ensureRev(cache, rev, context) {
|
|
|
958
989
|
);
|
|
959
990
|
if (!fetched.ok) {
|
|
960
991
|
const reason = transportReason(fetched.stderr);
|
|
961
|
-
if (
|
|
992
|
+
if (cached2 && reason !== "ref-not-found") return void 0;
|
|
962
993
|
return { ok: false, reason };
|
|
963
994
|
}
|
|
964
995
|
const head = await git(["rev-parse", "FETCH_HEAD"], { cwd: cache });
|
|
@@ -1047,7 +1078,8 @@ function warningAnchor(entry) {
|
|
|
1047
1078
|
diffSize,
|
|
1048
1079
|
...reason !== void 0 ? { reason } : {},
|
|
1049
1080
|
...repo !== void 0 ? { repo } : {},
|
|
1050
|
-
...remoteState !== void 0 ? { remoteState } : {}
|
|
1081
|
+
...remoteState !== void 0 ? { remoteState } : {},
|
|
1082
|
+
...entry.class !== void 0 ? { class: entry.class } : {}
|
|
1051
1083
|
};
|
|
1052
1084
|
}
|
|
1053
1085
|
function resolveHeads(from, byId) {
|
|
@@ -1058,8 +1090,8 @@ function resolveHeads(from, byId) {
|
|
|
1058
1090
|
while (queue.length) {
|
|
1059
1091
|
const current = queue.shift();
|
|
1060
1092
|
const next = successors(current, byId);
|
|
1061
|
-
for (const
|
|
1062
|
-
warnings.push({ kind: "broken-chain", missing });
|
|
1093
|
+
for (const missing2 of next.missing) {
|
|
1094
|
+
warnings.push({ kind: "broken-chain", missing: missing2 });
|
|
1063
1095
|
}
|
|
1064
1096
|
if (!next.records.length) {
|
|
1065
1097
|
if (current.conceptId !== from.conceptId)
|
|
@@ -1090,13 +1122,13 @@ function successors(record, byId) {
|
|
|
1090
1122
|
}
|
|
1091
1123
|
}
|
|
1092
1124
|
const records = [];
|
|
1093
|
-
const
|
|
1125
|
+
const missing2 = [];
|
|
1094
1126
|
for (const id of ids) {
|
|
1095
1127
|
const found = byId.get(id);
|
|
1096
1128
|
if (found) records.push(found);
|
|
1097
|
-
else
|
|
1129
|
+
else missing2.push(id);
|
|
1098
1130
|
}
|
|
1099
|
-
return { records, missing };
|
|
1131
|
+
return { records, missing: missing2 };
|
|
1100
1132
|
}
|
|
1101
1133
|
|
|
1102
1134
|
// src/kb-stamp.ts
|
|
@@ -1218,67 +1250,658 @@ async function readAnchorFiles(files, read, concurrency = DEFAULT_IO_CONCURRENCY
|
|
|
1218
1250
|
return { ok: false, reason: "file-unreadable" };
|
|
1219
1251
|
}
|
|
1220
1252
|
});
|
|
1221
|
-
return new Map(wanted.map((file,
|
|
1253
|
+
return new Map(wanted.map((file, at2) => [file, results[at2]]));
|
|
1222
1254
|
}
|
|
1223
1255
|
|
|
1224
1256
|
// src/anchor-resolver/resolver.ts
|
|
1257
|
+
var import_node_crypto4 = require("crypto");
|
|
1258
|
+
|
|
1259
|
+
// src/tree-sitter-resolver/languages.ts
|
|
1260
|
+
var import_node_path5 = require("path");
|
|
1261
|
+
|
|
1262
|
+
// src/grammars/index.ts
|
|
1263
|
+
var import_promises4 = require("fs/promises");
|
|
1264
|
+
|
|
1265
|
+
// src/grammars/store.ts
|
|
1225
1266
|
var import_node_crypto2 = require("crypto");
|
|
1267
|
+
var import_promises3 = require("fs/promises");
|
|
1268
|
+
var import_node_os2 = require("os");
|
|
1269
|
+
var import_node_path3 = require("path");
|
|
1270
|
+
function grammarsCacheRoot(override) {
|
|
1271
|
+
return override ?? process.env["STRAUSS_KB_GRAMMARS_DIR"] ?? (0, import_node_path3.join)((0, import_node_os2.homedir)(), ".strauss", "grammars");
|
|
1272
|
+
}
|
|
1273
|
+
function grammarCachePath(root, language, sha2564, extension = "wasm") {
|
|
1274
|
+
return (0, import_node_path3.join)(root, language, `${sha2564.slice(0, 12)}.${extension}`);
|
|
1275
|
+
}
|
|
1276
|
+
function sha2562(bytes) {
|
|
1277
|
+
return (0, import_node_crypto2.createHash)("sha256").update(bytes).digest("hex");
|
|
1278
|
+
}
|
|
1279
|
+
function matches(bytes, entry) {
|
|
1280
|
+
if (entry.bytes !== void 0 && bytes.byteLength !== entry.bytes)
|
|
1281
|
+
return false;
|
|
1282
|
+
return sha2562(bytes) === entry.sha256;
|
|
1283
|
+
}
|
|
1284
|
+
async function verifyCached(path, entry) {
|
|
1285
|
+
let bytes;
|
|
1286
|
+
try {
|
|
1287
|
+
bytes = await (0, import_promises3.readFile)(path);
|
|
1288
|
+
} catch {
|
|
1289
|
+
return false;
|
|
1290
|
+
}
|
|
1291
|
+
if (matches(bytes, entry)) return true;
|
|
1292
|
+
await (0, import_promises3.rm)(path, { force: true });
|
|
1293
|
+
return false;
|
|
1294
|
+
}
|
|
1295
|
+
async function writeCached(path, bytes) {
|
|
1296
|
+
await (0, import_promises3.mkdir)((0, import_node_path3.dirname)(path), { recursive: true });
|
|
1297
|
+
const temporary = `${path}.${process.pid}.${(0, import_node_crypto2.randomBytes)(4).toString("hex")}.tmp`;
|
|
1298
|
+
try {
|
|
1299
|
+
await (0, import_promises3.writeFile)(temporary, bytes);
|
|
1300
|
+
await (0, import_promises3.rename)(temporary, path);
|
|
1301
|
+
} catch (error) {
|
|
1302
|
+
await (0, import_promises3.rm)(temporary, { force: true });
|
|
1303
|
+
throw error;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// src/grammars/fetch.ts
|
|
1308
|
+
var ATTEMPTS = 3;
|
|
1309
|
+
var BACKOFF_MS = 250;
|
|
1310
|
+
function grammarUrl(url, override) {
|
|
1311
|
+
const base2 = grammarsBaseUrl(override);
|
|
1312
|
+
if (!base2) return url;
|
|
1313
|
+
let root = base2;
|
|
1314
|
+
while (root.endsWith("/")) root = root.slice(0, -1);
|
|
1315
|
+
const pinned = new URL(url);
|
|
1316
|
+
return `${root}${pinned.pathname}${pinned.search}`;
|
|
1317
|
+
}
|
|
1318
|
+
function grammarsBaseUrl(override) {
|
|
1319
|
+
return override ?? process.env["STRAUSS_KB_GRAMMARS_URL"];
|
|
1320
|
+
}
|
|
1321
|
+
async function downloadPart(url, name, entry, options = {}) {
|
|
1322
|
+
const log = options.log ?? ((line) => void process.stderr.write(line));
|
|
1323
|
+
const weight = entry.bytes === void 0 ? "" : ` (${size(entry.bytes)} from manifest)`;
|
|
1324
|
+
log(`strauss-kb: downloading ${name}${weight} from ${url}
|
|
1325
|
+
`);
|
|
1326
|
+
let cause = "";
|
|
1327
|
+
for (let attempt = 1; attempt <= ATTEMPTS; attempt++) {
|
|
1328
|
+
const outcome = await attemptDownload(url, entry, options.fetchTimeoutMs);
|
|
1329
|
+
if ("bytes" in outcome) return outcome;
|
|
1330
|
+
cause = outcome.cause;
|
|
1331
|
+
log(
|
|
1332
|
+
`strauss-kb: ${name} attempt ${attempt}/${ATTEMPTS} failed: ${cause}
|
|
1333
|
+
`
|
|
1334
|
+
);
|
|
1335
|
+
if (!outcome.retry) break;
|
|
1336
|
+
if (attempt < ATTEMPTS) await pause(BACKOFF_MS * attempt);
|
|
1337
|
+
}
|
|
1338
|
+
log(`strauss-kb: ${name} not downloaded: ${cause}
|
|
1339
|
+
`);
|
|
1340
|
+
return { cause };
|
|
1341
|
+
}
|
|
1342
|
+
async function attemptDownload(url, entry, timeoutMs) {
|
|
1343
|
+
try {
|
|
1344
|
+
const response = await fetch(url, {
|
|
1345
|
+
signal: AbortSignal.timeout(fetchTimeoutMs(timeoutMs))
|
|
1346
|
+
});
|
|
1347
|
+
if (!response.ok) {
|
|
1348
|
+
return {
|
|
1349
|
+
cause: `HTTP ${response.status}`,
|
|
1350
|
+
retry: response.status >= 500 || response.status === 429
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
1354
|
+
if (!matches(bytes, entry))
|
|
1355
|
+
return { cause: "sha256 mismatch", retry: false };
|
|
1356
|
+
return { bytes };
|
|
1357
|
+
} catch (error) {
|
|
1358
|
+
const timedOut = error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
|
|
1359
|
+
return { cause: timedOut ? "timeout" : "network error", retry: true };
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
function size(bytes) {
|
|
1363
|
+
return bytes >= 1024 * 1024 ? `${(bytes / (1024 * 1024)).toFixed(1)} MB` : `${Math.round(bytes / 1024)} KB`;
|
|
1364
|
+
}
|
|
1365
|
+
function pause(ms) {
|
|
1366
|
+
return new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// src/grammars/manifest.ts
|
|
1370
|
+
var import_node_fs = require("fs");
|
|
1371
|
+
var import_node_path4 = require("path");
|
|
1372
|
+
var import_node_url = require("url");
|
|
1373
|
+
|
|
1374
|
+
// src/grammars/model.ts
|
|
1375
|
+
var import_zod2 = require("zod");
|
|
1376
|
+
var sha2563 = import_zod2.z.string().regex(/^[0-9a-f]{64}$/);
|
|
1377
|
+
var grammarWasmSchema = import_zod2.z.object({
|
|
1378
|
+
url: import_zod2.z.string().min(1),
|
|
1379
|
+
sha256: sha2563,
|
|
1380
|
+
bytes: import_zod2.z.number().int().positive()
|
|
1381
|
+
});
|
|
1382
|
+
var grammarTagsSchema = import_zod2.z.object({ url: import_zod2.z.string().min(1), sha256: sha2563 });
|
|
1383
|
+
var grammarPackSchema = import_zod2.z.object({
|
|
1384
|
+
package: import_zod2.z.string().min(1),
|
|
1385
|
+
wasm: grammarWasmSchema,
|
|
1386
|
+
tags: import_zod2.z.array(grammarTagsSchema),
|
|
1387
|
+
license: import_zod2.z.string().min(1),
|
|
1388
|
+
extensions: import_zod2.z.array(import_zod2.z.string().min(1))
|
|
1389
|
+
});
|
|
1390
|
+
var grammarManifestSchema = import_zod2.z.object({
|
|
1391
|
+
/** The runtime the packs were proved against. */
|
|
1392
|
+
webTreeSitter: import_zod2.z.string().min(1),
|
|
1393
|
+
linguist: import_zod2.z.object({ tag: import_zod2.z.string().min(1), commit: import_zod2.z.string().min(1) }),
|
|
1394
|
+
packs: import_zod2.z.record(import_zod2.z.string().min(1), grammarPackSchema)
|
|
1395
|
+
});
|
|
1396
|
+
|
|
1397
|
+
// src/grammars/manifest.ts
|
|
1398
|
+
var cached;
|
|
1399
|
+
function grammarManifest() {
|
|
1400
|
+
cached ??= grammarManifestSchema.parse(
|
|
1401
|
+
JSON.parse((0, import_node_fs.readFileSync)(grammarsDataPath("manifest.json"), "utf8"))
|
|
1402
|
+
);
|
|
1403
|
+
return cached;
|
|
1404
|
+
}
|
|
1405
|
+
function grammarsDataPath(...segments) {
|
|
1406
|
+
let dir = (0, import_node_path4.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
1407
|
+
for (let up = 0; up < 5; up++) {
|
|
1408
|
+
const candidate = (0, import_node_path4.join)(dir, "grammars");
|
|
1409
|
+
if ((0, import_node_fs.existsSync)((0, import_node_path4.join)(candidate, "manifest.json")))
|
|
1410
|
+
return (0, import_node_path4.join)(candidate, ...segments);
|
|
1411
|
+
dir = (0, import_node_path4.dirname)(dir);
|
|
1412
|
+
}
|
|
1413
|
+
throw new Error("grammars/manifest.json is missing from the package");
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// src/grammars/index.ts
|
|
1417
|
+
var inFlight = /* @__PURE__ */ new Map();
|
|
1418
|
+
var missing = /* @__PURE__ */ new Map();
|
|
1419
|
+
var uncompilable = /* @__PURE__ */ new Map();
|
|
1420
|
+
var rejected = /* @__PURE__ */ new Map();
|
|
1421
|
+
function grammarsDownloadDisabled() {
|
|
1422
|
+
return process.env["STRAUSS_KB_GRAMMARS"] === "off";
|
|
1423
|
+
}
|
|
1424
|
+
async function ensureGrammar(language, options = {}) {
|
|
1425
|
+
const pack2 = grammarManifest().packs[language];
|
|
1426
|
+
if (!pack2) return null;
|
|
1427
|
+
const root = grammarsCacheRoot(options.cacheRoot);
|
|
1428
|
+
const wasm = grammarCachePath(root, language, pack2.wasm.sha256);
|
|
1429
|
+
const key2 = `${wasm} ${grammarsBaseUrl(options.baseUrl) ?? ""}`;
|
|
1430
|
+
const existing = inFlight.get(key2);
|
|
1431
|
+
if (existing) return existing;
|
|
1432
|
+
const pending = (async () => {
|
|
1433
|
+
const grammar = await ensurePart(
|
|
1434
|
+
wasm,
|
|
1435
|
+
`tree-sitter-${language}`,
|
|
1436
|
+
pack2.wasm,
|
|
1437
|
+
options
|
|
1438
|
+
);
|
|
1439
|
+
if (grammar !== true)
|
|
1440
|
+
return miss(language, `grammar tree-sitter-${language}`, grammar);
|
|
1441
|
+
const parts = [];
|
|
1442
|
+
const total = pack2.tags.length;
|
|
1443
|
+
for (const [at2, part] of pack2.tags.entries()) {
|
|
1444
|
+
const name = `${language} tags${total > 1 ? ` part ${at2 + 1}/${total}` : ""}`;
|
|
1445
|
+
const path = grammarCachePath(root, language, part.sha256, "scm");
|
|
1446
|
+
const held = await ensurePart(path, name, part, options);
|
|
1447
|
+
if (held !== true) return miss(language, name, held);
|
|
1448
|
+
parts.push(`; ${part.url}
|
|
1449
|
+
${lf(await (0, import_promises4.readFile)(path, "utf8"))}`);
|
|
1450
|
+
}
|
|
1451
|
+
missing.delete(language);
|
|
1452
|
+
return { wasm, query: total ? parts.join("\n") : void 0 };
|
|
1453
|
+
})();
|
|
1454
|
+
inFlight.set(key2, pending);
|
|
1455
|
+
const result = await pending;
|
|
1456
|
+
if (result === null) inFlight.delete(key2);
|
|
1457
|
+
return result;
|
|
1458
|
+
}
|
|
1459
|
+
async function ensurePart(path, name, entry, options) {
|
|
1460
|
+
if (await verifyCached(path, entry)) return true;
|
|
1461
|
+
if (options.offline === true || grammarsDownloadDisabled()) return {};
|
|
1462
|
+
const download = await downloadPart(
|
|
1463
|
+
grammarUrl(entry.url, options.baseUrl),
|
|
1464
|
+
name,
|
|
1465
|
+
entry,
|
|
1466
|
+
options
|
|
1467
|
+
);
|
|
1468
|
+
if ("cause" in download) return { cause: download.cause };
|
|
1469
|
+
await writeCached(path, download.bytes).catch(() => null);
|
|
1470
|
+
return true;
|
|
1471
|
+
}
|
|
1472
|
+
function miss(language, subject, failure) {
|
|
1473
|
+
missing.set(language, { subject, ...failure });
|
|
1474
|
+
return null;
|
|
1475
|
+
}
|
|
1476
|
+
function lf(body) {
|
|
1477
|
+
return body.replace(/\r\n/g, "\n");
|
|
1478
|
+
}
|
|
1479
|
+
function noteUncompilableQuery(language, cause) {
|
|
1480
|
+
uncompilable.set(language, cause);
|
|
1481
|
+
}
|
|
1482
|
+
function noteRejectedGrammar(language, cause) {
|
|
1483
|
+
rejected.set(language, cause);
|
|
1484
|
+
}
|
|
1485
|
+
function grammarHints() {
|
|
1486
|
+
const manifest = grammarManifest();
|
|
1487
|
+
const packs = manifest.packs;
|
|
1488
|
+
const lines = /* @__PURE__ */ new Map();
|
|
1489
|
+
for (const [language, { subject, cause }] of missing)
|
|
1490
|
+
lines.set(
|
|
1491
|
+
language,
|
|
1492
|
+
`${subject} not cached${cause ? ` (${cause})` : ""}; run online once, or set STRAUSS_KB_GRAMMARS_DIR`
|
|
1493
|
+
);
|
|
1494
|
+
for (const [language, cause] of rejected)
|
|
1495
|
+
lines.set(
|
|
1496
|
+
language,
|
|
1497
|
+
`${packs[language]?.package ?? `tree-sitter-${language}`} rejected by web-tree-sitter ${manifest.webTreeSitter}${cause ? `: ${cause}` : ""}; re-pin with pnpm grammars pin ${language}`
|
|
1498
|
+
);
|
|
1499
|
+
for (const [language, cause] of uncompilable)
|
|
1500
|
+
lines.set(
|
|
1501
|
+
language,
|
|
1502
|
+
`tags query for ${language} does not compile against ${packs[language]?.package ?? `tree-sitter-${language}`}: ${cause}; re-pin with pnpm grammars pin ${language}`
|
|
1503
|
+
);
|
|
1504
|
+
return [...lines].sort(([a], [b]) => a.localeCompare(b)).map(([, line]) => line);
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
// src/tree-sitter-resolver/languages.ts
|
|
1508
|
+
var table;
|
|
1509
|
+
function extensionTable() {
|
|
1510
|
+
const manifest = grammarManifest();
|
|
1511
|
+
if (table?.of !== manifest)
|
|
1512
|
+
table = {
|
|
1513
|
+
of: manifest,
|
|
1514
|
+
extensions: Object.fromEntries(
|
|
1515
|
+
Object.entries(manifest.packs).flatMap(
|
|
1516
|
+
([language, pack2]) => pack2.extensions.map((extension) => [extension, language])
|
|
1517
|
+
)
|
|
1518
|
+
)
|
|
1519
|
+
};
|
|
1520
|
+
return table.extensions;
|
|
1521
|
+
}
|
|
1522
|
+
function hasQuery(language) {
|
|
1523
|
+
return (grammarManifest().packs[language]?.tags.length ?? 0) > 0;
|
|
1524
|
+
}
|
|
1525
|
+
function languageForFile(file) {
|
|
1526
|
+
const language = extensionTable()[(0, import_node_path5.extname)(file).toLowerCase()];
|
|
1527
|
+
return language && hasQuery(language) ? language : void 0;
|
|
1528
|
+
}
|
|
1529
|
+
function treeSitterLanguages() {
|
|
1530
|
+
return [...new Set(Object.values(extensionTable()))].filter(hasQuery).sort();
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
// src/tree-sitter-resolver/resolver.ts
|
|
1534
|
+
var import_node_crypto3 = require("crypto");
|
|
1535
|
+
var import_web_tree_sitter = require("web-tree-sitter");
|
|
1536
|
+
|
|
1537
|
+
// src/tree-sitter-resolver/definitions.ts
|
|
1538
|
+
var SCOPE_ONLY = "reference.implementation";
|
|
1539
|
+
function index(tree, query) {
|
|
1540
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1541
|
+
for (const match of query.matches(tree.rootNode)) {
|
|
1542
|
+
const nameNode = match.captures.find((capture) => capture.name === "name");
|
|
1543
|
+
const defNode = match.captures.find(
|
|
1544
|
+
(capture) => capture.name.startsWith("definition.") || capture.name === SCOPE_ONLY
|
|
1545
|
+
);
|
|
1546
|
+
if (!nameNode || !defNode) continue;
|
|
1547
|
+
const candidate = {
|
|
1548
|
+
node: defNode.node,
|
|
1549
|
+
name: nameNode.node.text,
|
|
1550
|
+
target: defNode.name !== SCOPE_ONLY
|
|
1551
|
+
};
|
|
1552
|
+
const existing = byName.get(nameNode.node.id);
|
|
1553
|
+
if (existing && width(existing.node) <= width(candidate.node)) continue;
|
|
1554
|
+
byName.set(nameNode.node.id, candidate);
|
|
1555
|
+
}
|
|
1556
|
+
const definitions = [...byName.values()];
|
|
1557
|
+
return {
|
|
1558
|
+
tree,
|
|
1559
|
+
byNodeId: new Map(
|
|
1560
|
+
definitions.map((definition) => [definition.node.id, definition])
|
|
1561
|
+
),
|
|
1562
|
+
definitions
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
function select(parsed, wanted) {
|
|
1566
|
+
const matches3 = parsed.definitions.filter(
|
|
1567
|
+
(definition) => definition.target && endsWith(chainOf(definition, parsed.byNodeId), wanted)
|
|
1568
|
+
);
|
|
1569
|
+
if (matches3.length < 2) return matches3;
|
|
1570
|
+
const bodied = matches3.filter(
|
|
1571
|
+
(definition) => definition.node.childForFieldName("body") !== null
|
|
1572
|
+
);
|
|
1573
|
+
return bodied.length === 1 ? bodied : matches3;
|
|
1574
|
+
}
|
|
1575
|
+
function chainOf(definition, byNodeId) {
|
|
1576
|
+
const chain = [definition.name];
|
|
1577
|
+
const receiver = definition.node.childForFieldName("receiver");
|
|
1578
|
+
const type = receiver && typeNameIn(receiver);
|
|
1579
|
+
if (type) chain.unshift(type);
|
|
1580
|
+
for (let node = definition.node.parent; node; node = node.parent) {
|
|
1581
|
+
const enclosing = byNodeId.get(node.id);
|
|
1582
|
+
if (enclosing && enclosing.node !== definition.node)
|
|
1583
|
+
chain.unshift(enclosing.name);
|
|
1584
|
+
}
|
|
1585
|
+
return chain;
|
|
1586
|
+
}
|
|
1587
|
+
function typeNameIn(receiver) {
|
|
1588
|
+
const stack = [receiver];
|
|
1589
|
+
while (stack.length) {
|
|
1590
|
+
const node = stack.pop();
|
|
1591
|
+
if (node.type === "type_identifier") return node.text;
|
|
1592
|
+
for (let at2 = 0; at2 < node.childCount; at2++) {
|
|
1593
|
+
const child = node.child(at2);
|
|
1594
|
+
if (child) stack.push(child);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
return void 0;
|
|
1598
|
+
}
|
|
1599
|
+
function endsWith(chain, wanted) {
|
|
1600
|
+
if (wanted.length > chain.length) return false;
|
|
1601
|
+
const offset = chain.length - wanted.length;
|
|
1602
|
+
return wanted.every((segment, at2) => chain[offset + at2] === segment);
|
|
1603
|
+
}
|
|
1604
|
+
function width(node) {
|
|
1605
|
+
return node.endIndex - node.startIndex;
|
|
1606
|
+
}
|
|
1607
|
+
function spanOf(definition, source) {
|
|
1608
|
+
let start = definition.node;
|
|
1609
|
+
let end = definition.node;
|
|
1610
|
+
for (let sibling = start.previousSibling; sibling?.type === "decorator"; sibling = sibling.previousSibling) {
|
|
1611
|
+
start = sibling;
|
|
1612
|
+
}
|
|
1613
|
+
const parent = end.parent;
|
|
1614
|
+
if (parent?.type === "export_statement" && parent.childForFieldName("declaration")?.id === end.id) {
|
|
1615
|
+
start = parent;
|
|
1616
|
+
end = parent;
|
|
1617
|
+
}
|
|
1618
|
+
const lines = source.split("\n");
|
|
1619
|
+
const startLine = start.startPosition.row;
|
|
1620
|
+
const endLine = end.endPosition.column === 0 && end.endPosition.row > startLine ? end.endPosition.row - 1 : end.endPosition.row;
|
|
1621
|
+
return {
|
|
1622
|
+
text: lines.slice(startLine, endLine + 1).join("\n"),
|
|
1623
|
+
startLine: startLine + 1,
|
|
1624
|
+
endLine: endLine + 1
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
// src/tree-sitter-resolver/tokens.ts
|
|
1629
|
+
function tokens(root) {
|
|
1630
|
+
const out = [];
|
|
1631
|
+
const stack = [root];
|
|
1632
|
+
while (stack.length) {
|
|
1633
|
+
const node = stack.pop();
|
|
1634
|
+
if (node.type.includes("comment")) continue;
|
|
1635
|
+
if (node.childCount === 0) {
|
|
1636
|
+
const text = node.text.trim();
|
|
1637
|
+
if (text) out.push(text);
|
|
1638
|
+
continue;
|
|
1639
|
+
}
|
|
1640
|
+
for (let at2 = node.childCount - 1; at2 >= 0; at2--) {
|
|
1641
|
+
const child = node.child(at2);
|
|
1642
|
+
if (child) stack.push(child);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
return out;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
// src/tree-sitter-resolver/resolver.ts
|
|
1649
|
+
var TREE_CACHE_LIMIT = 32;
|
|
1650
|
+
var TreeSitterResolver = class {
|
|
1651
|
+
name = "tree-sitter";
|
|
1652
|
+
grammars;
|
|
1653
|
+
loaded = /* @__PURE__ */ new Map();
|
|
1654
|
+
trees = /* @__PURE__ */ new Map();
|
|
1655
|
+
parser;
|
|
1656
|
+
initialized = false;
|
|
1657
|
+
/** Cache effectiveness, for tests and for the latency numbers. */
|
|
1658
|
+
stats = { parses: 0, cacheHits: 0 };
|
|
1659
|
+
constructor(options = {}) {
|
|
1660
|
+
this.grammars = options;
|
|
1661
|
+
}
|
|
1662
|
+
/**
|
|
1663
|
+
* Loads the grammars these files need, once per language per process,
|
|
1664
|
+
* downloading each one on first use.
|
|
1665
|
+
*
|
|
1666
|
+
* A grammar that will not load is remembered as unavailable rather than
|
|
1667
|
+
* retried per anchor, and never throws: an unobtainable WASM is a finding.
|
|
1668
|
+
*/
|
|
1669
|
+
async prepare(files) {
|
|
1670
|
+
const wanted = /* @__PURE__ */ new Set();
|
|
1671
|
+
for (const file of files) {
|
|
1672
|
+
const language = languageForFile(file);
|
|
1673
|
+
if (language && !this.loaded.has(language)) wanted.add(language);
|
|
1674
|
+
}
|
|
1675
|
+
if (!wanted.size) return;
|
|
1676
|
+
if (!this.initialized) {
|
|
1677
|
+
try {
|
|
1678
|
+
await import_web_tree_sitter.Parser.init();
|
|
1679
|
+
this.parser = new import_web_tree_sitter.Parser();
|
|
1680
|
+
this.initialized = true;
|
|
1681
|
+
} catch {
|
|
1682
|
+
for (const language of wanted) this.loaded.set(language, null);
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
const languages = [...wanted];
|
|
1687
|
+
const loaded = await mapLimit(
|
|
1688
|
+
languages,
|
|
1689
|
+
Math.min(DEFAULT_IO_CONCURRENCY, languages.length),
|
|
1690
|
+
(language) => this.load(language)
|
|
1691
|
+
);
|
|
1692
|
+
languages.forEach(
|
|
1693
|
+
(language, at2) => this.loaded.set(language, loaded[at2] ?? null)
|
|
1694
|
+
);
|
|
1695
|
+
}
|
|
1696
|
+
/**
|
|
1697
|
+
* An unobtainable grammar, one this runtime refuses, and a query that will
|
|
1698
|
+
* not compile are three faults with three repairs; all are reported through
|
|
1699
|
+
* the grammars module so every hint has one home.
|
|
1700
|
+
*/
|
|
1701
|
+
async load(language) {
|
|
1702
|
+
let pack2;
|
|
1703
|
+
try {
|
|
1704
|
+
pack2 = await ensureGrammar(language, this.grammars);
|
|
1705
|
+
} catch {
|
|
1706
|
+
return null;
|
|
1707
|
+
}
|
|
1708
|
+
if (!pack2?.query) return null;
|
|
1709
|
+
let grammar;
|
|
1710
|
+
try {
|
|
1711
|
+
grammar = await import_web_tree_sitter.Language.load(pack2.wasm);
|
|
1712
|
+
} catch (error) {
|
|
1713
|
+
noteRejectedGrammar(language, why(error));
|
|
1714
|
+
return null;
|
|
1715
|
+
}
|
|
1716
|
+
try {
|
|
1717
|
+
return { language: grammar, query: new import_web_tree_sitter.Query(grammar, pack2.query) };
|
|
1718
|
+
} catch (error) {
|
|
1719
|
+
noteUncompilableQuery(language, why(error));
|
|
1720
|
+
return null;
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
/**
|
|
1724
|
+
* Abstains on an extension with no grammar so the regex resolver gets a
|
|
1725
|
+
* turn; reports `resolver-unavailable` when the grammar exists in principle
|
|
1726
|
+
* but could not be loaded, because falling back there would silently trade a
|
|
1727
|
+
* precise span for a guessed one.
|
|
1728
|
+
*/
|
|
1729
|
+
attempt(source, symbol, file) {
|
|
1730
|
+
const language = file ? languageForFile(file) : void 0;
|
|
1731
|
+
if (!language) return { kind: "abstain" };
|
|
1732
|
+
if (!this.loaded.has(language)) return { kind: "abstain" };
|
|
1733
|
+
const loaded = this.loaded.get(language);
|
|
1734
|
+
if (!loaded) return { kind: "unresolved", reason: "resolver-unavailable" };
|
|
1735
|
+
const parsed = this.parse(language, loaded, source);
|
|
1736
|
+
if (!parsed) return { kind: "unresolved", reason: "resolver-unavailable" };
|
|
1737
|
+
const wanted = symbol.split(".").filter(Boolean);
|
|
1738
|
+
if (!wanted.length)
|
|
1739
|
+
return { kind: "unresolved", reason: "symbol-not-found" };
|
|
1740
|
+
const matches3 = select(parsed, wanted);
|
|
1741
|
+
if (!matches3.length)
|
|
1742
|
+
return { kind: "unresolved", reason: "symbol-not-found" };
|
|
1743
|
+
if (matches3.length > 1)
|
|
1744
|
+
return { kind: "unresolved", reason: "symbol-ambiguous" };
|
|
1745
|
+
return { kind: "resolved", span: spanOf(matches3[0], source) };
|
|
1746
|
+
}
|
|
1747
|
+
resolve(source, symbol, file) {
|
|
1748
|
+
const attempt = this.attempt(source, symbol, file);
|
|
1749
|
+
return attempt.kind === "resolved" ? attempt.span : null;
|
|
1750
|
+
}
|
|
1751
|
+
/** Parsed trees are keyed by content hash, so an unchanged file parses once. */
|
|
1752
|
+
parse(language, loaded, source) {
|
|
1753
|
+
const key2 = `${language}:${(0, import_node_crypto3.createHash)("sha256").update(source).digest("hex")}`;
|
|
1754
|
+
const cached2 = this.trees.get(key2);
|
|
1755
|
+
if (cached2) {
|
|
1756
|
+
this.stats.cacheHits += 1;
|
|
1757
|
+
return cached2;
|
|
1758
|
+
}
|
|
1759
|
+
const parser = this.parser;
|
|
1760
|
+
if (!parser) return null;
|
|
1761
|
+
let parsed;
|
|
1762
|
+
try {
|
|
1763
|
+
parser.setLanguage(loaded.language);
|
|
1764
|
+
const tree = parser.parse(source);
|
|
1765
|
+
if (!tree) return null;
|
|
1766
|
+
parsed = index(tree, loaded.query);
|
|
1767
|
+
} catch {
|
|
1768
|
+
return null;
|
|
1769
|
+
}
|
|
1770
|
+
this.stats.parses += 1;
|
|
1771
|
+
if (this.trees.size >= TREE_CACHE_LIMIT) {
|
|
1772
|
+
const oldest = this.trees.keys().next();
|
|
1773
|
+
if (!oldest.done) {
|
|
1774
|
+
this.trees.get(oldest.value)?.tree.delete();
|
|
1775
|
+
this.trees.delete(oldest.value);
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
this.trees.set(key2, parsed);
|
|
1779
|
+
return parsed;
|
|
1780
|
+
}
|
|
1781
|
+
/**
|
|
1782
|
+
* Every definition this file declares, as dotted symbol and span.
|
|
1783
|
+
*
|
|
1784
|
+
* The inverse of `attempt`: that asks "where is this name", this asks "what
|
|
1785
|
+
* names are here". `moved` needs the second — the stored hash has to be
|
|
1786
|
+
* looked for at every definition in the repository, and there is no name to
|
|
1787
|
+
* ask about, since the whole question is which name now carries that code.
|
|
1788
|
+
*/
|
|
1789
|
+
spans(source, file) {
|
|
1790
|
+
const language = languageForFile(file);
|
|
1791
|
+
if (!language) return [];
|
|
1792
|
+
const loaded = this.loaded.get(language);
|
|
1793
|
+
if (!loaded) return [];
|
|
1794
|
+
const parsed = this.parse(language, loaded, source);
|
|
1795
|
+
if (!parsed) return [];
|
|
1796
|
+
return parsed.definitions.filter((definition) => definition.target).map((definition) => ({
|
|
1797
|
+
symbol: chainOf(definition, parsed.byNodeId).join("."),
|
|
1798
|
+
span: spanOf(definition, source)
|
|
1799
|
+
}));
|
|
1800
|
+
}
|
|
1801
|
+
/**
|
|
1802
|
+
* The token stream of a span: every leaf the parser sees, comments dropped,
|
|
1803
|
+
* joined by single spaces.
|
|
1804
|
+
*
|
|
1805
|
+
* This is what makes a reformat not be drift. Hashing it rather than the raw
|
|
1806
|
+
* text means indentation, line breaks, trailing commas the formatter moved,
|
|
1807
|
+
* and every comment above or inside the definition are outside the hash —
|
|
1808
|
+
* and a renamed identifier or a changed literal is still inside it, because
|
|
1809
|
+
* those are leaves.
|
|
1810
|
+
*
|
|
1811
|
+
* `null` when the file has no grammar, the grammar would not load, or the
|
|
1812
|
+
* text will not parse: no normalisation is better than a guessed one.
|
|
1813
|
+
*/
|
|
1814
|
+
normalize(text, file) {
|
|
1815
|
+
const language = file ? languageForFile(file) : void 0;
|
|
1816
|
+
if (!language) return null;
|
|
1817
|
+
const loaded = this.loaded.get(language);
|
|
1818
|
+
if (!loaded) return null;
|
|
1819
|
+
const parser = this.parser;
|
|
1820
|
+
if (!parser) return null;
|
|
1821
|
+
let tree;
|
|
1822
|
+
try {
|
|
1823
|
+
parser.setLanguage(loaded.language);
|
|
1824
|
+
tree = parser.parse(text);
|
|
1825
|
+
} catch {
|
|
1826
|
+
return null;
|
|
1827
|
+
}
|
|
1828
|
+
if (!tree) return null;
|
|
1829
|
+
try {
|
|
1830
|
+
return tokens(tree.rootNode).join(" ");
|
|
1831
|
+
} finally {
|
|
1832
|
+
tree.delete();
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
/** Drops cached trees. Grammars stay loaded — they are immutable. */
|
|
1836
|
+
reset() {
|
|
1837
|
+
for (const parsed of this.trees.values()) parsed.tree.delete();
|
|
1838
|
+
this.trees.clear();
|
|
1839
|
+
this.stats.parses = 0;
|
|
1840
|
+
this.stats.cacheHits = 0;
|
|
1841
|
+
}
|
|
1842
|
+
};
|
|
1843
|
+
function why(error) {
|
|
1844
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
1845
|
+
return text || "no reason given";
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
// src/anchor-resolver/resolver.ts
|
|
1226
1849
|
var PARENT_SCOPE_LINES = 50;
|
|
1227
1850
|
var CLEAN_STATE = { blockComment: false, template: false };
|
|
1228
1851
|
function stripLine(line, state) {
|
|
1229
1852
|
let out = "";
|
|
1230
|
-
let
|
|
1853
|
+
let index2 = 0;
|
|
1231
1854
|
let { blockComment, template } = state;
|
|
1232
|
-
while (
|
|
1233
|
-
const char = line[
|
|
1234
|
-
const next = line[
|
|
1855
|
+
while (index2 < line.length) {
|
|
1856
|
+
const char = line[index2];
|
|
1857
|
+
const next = line[index2 + 1];
|
|
1235
1858
|
if (blockComment) {
|
|
1236
1859
|
if (char === "*" && next === "/") {
|
|
1237
1860
|
blockComment = false;
|
|
1238
|
-
|
|
1861
|
+
index2 += 2;
|
|
1239
1862
|
continue;
|
|
1240
1863
|
}
|
|
1241
|
-
|
|
1864
|
+
index2 += 1;
|
|
1242
1865
|
continue;
|
|
1243
1866
|
}
|
|
1244
1867
|
if (template) {
|
|
1245
1868
|
if (char === "\\") {
|
|
1246
|
-
|
|
1869
|
+
index2 += 2;
|
|
1247
1870
|
continue;
|
|
1248
1871
|
}
|
|
1249
1872
|
if (char === "`") template = false;
|
|
1250
|
-
|
|
1873
|
+
index2 += 1;
|
|
1251
1874
|
continue;
|
|
1252
1875
|
}
|
|
1253
1876
|
if (char === "/" && next === "*") {
|
|
1254
1877
|
blockComment = true;
|
|
1255
|
-
|
|
1878
|
+
index2 += 2;
|
|
1256
1879
|
continue;
|
|
1257
1880
|
}
|
|
1258
1881
|
if (char === "/" && next === "/") break;
|
|
1259
1882
|
if (char === "`") {
|
|
1260
1883
|
template = true;
|
|
1261
|
-
|
|
1884
|
+
index2 += 1;
|
|
1262
1885
|
continue;
|
|
1263
1886
|
}
|
|
1264
1887
|
if (char === "'" || char === '"') {
|
|
1265
1888
|
const quote = char;
|
|
1266
|
-
|
|
1267
|
-
while (
|
|
1268
|
-
if (line[
|
|
1269
|
-
|
|
1889
|
+
index2 += 1;
|
|
1890
|
+
while (index2 < line.length) {
|
|
1891
|
+
if (line[index2] === "\\") {
|
|
1892
|
+
index2 += 2;
|
|
1270
1893
|
continue;
|
|
1271
1894
|
}
|
|
1272
|
-
if (line[
|
|
1273
|
-
|
|
1895
|
+
if (line[index2] === quote) {
|
|
1896
|
+
index2 += 1;
|
|
1274
1897
|
break;
|
|
1275
1898
|
}
|
|
1276
|
-
|
|
1899
|
+
index2 += 1;
|
|
1277
1900
|
}
|
|
1278
1901
|
continue;
|
|
1279
1902
|
}
|
|
1280
1903
|
out += char;
|
|
1281
|
-
|
|
1904
|
+
index2 += 1;
|
|
1282
1905
|
}
|
|
1283
1906
|
return { code: out, state: { blockComment, template } };
|
|
1284
1907
|
}
|
|
@@ -1293,8 +1916,8 @@ function captureBraceBlock(lines, matchLine) {
|
|
|
1293
1916
|
let depth = 0;
|
|
1294
1917
|
let opened = false;
|
|
1295
1918
|
let state = CLEAN_STATE;
|
|
1296
|
-
for (let
|
|
1297
|
-
const stripped = stripLine(lines[
|
|
1919
|
+
for (let index2 = matchLine; index2 < lines.length; index2++) {
|
|
1920
|
+
const stripped = stripLine(lines[index2] ?? "", state);
|
|
1298
1921
|
state = stripped.state;
|
|
1299
1922
|
for (const char of stripped.code) {
|
|
1300
1923
|
if (char === "{") {
|
|
@@ -1303,10 +1926,10 @@ function captureBraceBlock(lines, matchLine) {
|
|
|
1303
1926
|
} else if (char === "}") {
|
|
1304
1927
|
depth = Math.max(0, depth - 1);
|
|
1305
1928
|
} else if (char === ";" && !opened) {
|
|
1306
|
-
return span(lines, matchLine,
|
|
1929
|
+
return span(lines, matchLine, index2);
|
|
1307
1930
|
}
|
|
1308
1931
|
}
|
|
1309
|
-
if (opened && depth === 0) return span(lines, matchLine,
|
|
1932
|
+
if (opened && depth === 0) return span(lines, matchLine, index2);
|
|
1310
1933
|
}
|
|
1311
1934
|
return null;
|
|
1312
1935
|
}
|
|
@@ -1315,22 +1938,22 @@ function captureIndentedBlock(lines, matchLine) {
|
|
|
1315
1938
|
const header = lines[matchLine] ?? "";
|
|
1316
1939
|
const indent = header.length - header.trimStart().length;
|
|
1317
1940
|
let headerEnd = -1;
|
|
1318
|
-
for (let
|
|
1319
|
-
const code = stripLine(lines[
|
|
1941
|
+
for (let index2 = matchLine; index2 < lines.length && index2 <= matchLine + 20; index2++) {
|
|
1942
|
+
const code = stripLine(lines[index2] ?? "", CLEAN_STATE).code.trimEnd();
|
|
1320
1943
|
if (code.endsWith(":")) {
|
|
1321
|
-
headerEnd =
|
|
1944
|
+
headerEnd = index2;
|
|
1322
1945
|
break;
|
|
1323
1946
|
}
|
|
1324
|
-
if (code.includes(":")) return span(lines, matchLine,
|
|
1947
|
+
if (code.includes(":")) return span(lines, matchLine, index2);
|
|
1325
1948
|
}
|
|
1326
1949
|
if (headerEnd === -1) return null;
|
|
1327
1950
|
let end = headerEnd;
|
|
1328
|
-
for (let
|
|
1329
|
-
const line = lines[
|
|
1951
|
+
for (let index2 = headerEnd + 1; index2 < lines.length; index2++) {
|
|
1952
|
+
const line = lines[index2] ?? "";
|
|
1330
1953
|
if (line.trim() === "") continue;
|
|
1331
1954
|
const lineIndent = line.length - line.trimStart().length;
|
|
1332
1955
|
if (lineIndent <= indent) break;
|
|
1333
|
-
end =
|
|
1956
|
+
end = index2;
|
|
1334
1957
|
}
|
|
1335
1958
|
return end === headerEnd ? null : span(lines, matchLine, end);
|
|
1336
1959
|
}
|
|
@@ -1354,15 +1977,15 @@ var regexResolver = {
|
|
|
1354
1977
|
const lines = source.split("\n");
|
|
1355
1978
|
for (const tier of TIERS) {
|
|
1356
1979
|
const pattern = tier(escaped);
|
|
1357
|
-
let candidates = lines.map((line,
|
|
1980
|
+
let candidates = lines.map((line, index2) => ({ line, index: index2 })).filter((entry) => pattern.test(entry.line)).map((entry) => entry.index);
|
|
1358
1981
|
if (!candidates.length) continue;
|
|
1359
1982
|
if (parentPattern && candidates.length > 1) {
|
|
1360
1983
|
const distances = candidates.map(
|
|
1361
|
-
(
|
|
1984
|
+
(index2) => distanceToParent(lines, index2, parentPattern)
|
|
1362
1985
|
);
|
|
1363
1986
|
const nearest = Math.min(...distances);
|
|
1364
1987
|
if (Number.isFinite(nearest)) {
|
|
1365
|
-
candidates = candidates.filter((_,
|
|
1988
|
+
candidates = candidates.filter((_, at2) => distances[at2] === nearest);
|
|
1366
1989
|
}
|
|
1367
1990
|
}
|
|
1368
1991
|
if (candidates.length !== 1) return null;
|
|
@@ -1375,34 +1998,86 @@ var regexResolver = {
|
|
|
1375
1998
|
function escapeRegExp(value) {
|
|
1376
1999
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1377
2000
|
}
|
|
1378
|
-
function distanceToParent(lines,
|
|
1379
|
-
const floor = Math.max(0,
|
|
1380
|
-
for (let
|
|
1381
|
-
if (parent.test(lines[
|
|
2001
|
+
function distanceToParent(lines, index2, parent) {
|
|
2002
|
+
const floor = Math.max(0, index2 - PARENT_SCOPE_LINES);
|
|
2003
|
+
for (let at2 = index2; at2 >= floor; at2--) {
|
|
2004
|
+
if (parent.test(lines[at2] ?? "")) return index2 - at2;
|
|
1382
2005
|
}
|
|
1383
2006
|
return Number.POSITIVE_INFINITY;
|
|
1384
2007
|
}
|
|
1385
2008
|
function hashAnchorText(text) {
|
|
1386
|
-
return `sha256:${(0,
|
|
2009
|
+
return `sha256:${(0, import_node_crypto4.createHash)("sha256").update(text.replace(/\r\n/g, "\n")).digest("hex")}`;
|
|
1387
2010
|
}
|
|
1388
2011
|
function resolveAnchor(source, anchor, resolver = regexResolver) {
|
|
2012
|
+
const outcome = resolveAnchorSpan(source, anchor, [resolver]);
|
|
2013
|
+
return outcome.ok ? outcome.span : null;
|
|
2014
|
+
}
|
|
2015
|
+
function resolveAnchorSpan(source, anchor, resolvers = [regexResolver]) {
|
|
1389
2016
|
const normalized = source.replace(/\r\n/g, "\n");
|
|
1390
2017
|
if (!anchor.symbol) {
|
|
1391
2018
|
const lines = normalized.split("\n");
|
|
1392
2019
|
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
1393
2020
|
return {
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
2021
|
+
ok: true,
|
|
2022
|
+
span: {
|
|
2023
|
+
text: normalized,
|
|
2024
|
+
startLine: 1,
|
|
2025
|
+
endLine: Math.max(1, lines.length)
|
|
2026
|
+
}
|
|
1397
2027
|
};
|
|
1398
2028
|
}
|
|
1399
|
-
|
|
2029
|
+
for (const resolver of resolvers) {
|
|
2030
|
+
const attempt = resolver.attempt ? resolver.attempt(normalized, anchor.symbol, anchor.file) : fromResolve(resolver, normalized, anchor.symbol, anchor.file);
|
|
2031
|
+
if (attempt.kind === "abstain") continue;
|
|
2032
|
+
if (attempt.kind === "unresolved") {
|
|
2033
|
+
if (attempt.reason === "symbol-not-found") continue;
|
|
2034
|
+
return { ok: false, reason: attempt.reason };
|
|
2035
|
+
}
|
|
2036
|
+
const tokens2 = resolver.normalize?.(attempt.span.text, anchor.file);
|
|
2037
|
+
return {
|
|
2038
|
+
ok: true,
|
|
2039
|
+
span: attempt.span,
|
|
2040
|
+
...isResolverName(resolver.name) ? { resolver: resolver.name } : {},
|
|
2041
|
+
...tokens2 ? { normalized: tokens2 } : {}
|
|
2042
|
+
};
|
|
2043
|
+
}
|
|
2044
|
+
return { ok: false, reason: "symbol-not-found" };
|
|
2045
|
+
}
|
|
2046
|
+
function fromResolve(resolver, source, symbol, file) {
|
|
2047
|
+
const span2 = resolver.resolve(source, symbol, file);
|
|
2048
|
+
return span2 ? { kind: "resolved", span: span2 } : { kind: "unresolved", reason: "symbol-not-found" };
|
|
2049
|
+
}
|
|
2050
|
+
function isResolverName(name) {
|
|
2051
|
+
return name === "tree-sitter" || name === "regex";
|
|
2052
|
+
}
|
|
2053
|
+
async function prepareResolvers(resolvers, files) {
|
|
2054
|
+
for (const resolver of resolvers) await resolver.prepare?.(files);
|
|
2055
|
+
}
|
|
2056
|
+
function defaultAnchorResolvers(grammars = {}) {
|
|
2057
|
+
return [new TreeSitterResolver(grammars), regexResolver];
|
|
2058
|
+
}
|
|
2059
|
+
function resolverChanged(source, anchor, produced) {
|
|
2060
|
+
const previous = anchor.resolver ?? "regex";
|
|
2061
|
+
if (!produced || !anchor.symbol || previous === produced) return false;
|
|
2062
|
+
if (previous !== "regex") return false;
|
|
2063
|
+
const before = regexResolver.resolve(
|
|
2064
|
+
source.replace(/\r\n/g, "\n"),
|
|
2065
|
+
anchor.symbol
|
|
2066
|
+
);
|
|
2067
|
+
return before !== null && hashAnchorText(before.text) === anchor.hash;
|
|
2068
|
+
}
|
|
2069
|
+
function anchorHashOf(anchor, outcome) {
|
|
2070
|
+
const stored = anchor.hash ? anchor.hash_kind ?? "raw" : void 0;
|
|
2071
|
+
const wanted = stored ?? (outcome.normalized ? "ast" : "raw");
|
|
2072
|
+
return wanted === "ast" && outcome.normalized ? { hash: hashAnchorText(outcome.normalized), kind: "ast" } : { hash: hashAnchorText(outcome.span.text), kind: "raw" };
|
|
1400
2073
|
}
|
|
1401
2074
|
|
|
1402
2075
|
// src/anchor-resolver/drift.ts
|
|
1403
2076
|
async function detectAnchorDrift(records, options = {}) {
|
|
1404
2077
|
const repoRoot = options.repoRoot ?? process.cwd();
|
|
1405
|
-
const
|
|
2078
|
+
const resolvers = options.resolvers ?? (options.resolver ? [options.resolver] : defaultAnchorResolvers({
|
|
2079
|
+
offline: options.remote?.offline === true
|
|
2080
|
+
}));
|
|
1406
2081
|
const origin = new LazyOrigin(repoRoot);
|
|
1407
2082
|
const planned = /* @__PURE__ */ new Map();
|
|
1408
2083
|
let declaresRepo = false;
|
|
@@ -1440,12 +2115,16 @@ async function detectAnchorDrift(records, options = {}) {
|
|
|
1440
2115
|
),
|
|
1441
2116
|
(options.readRemote ?? readRemoteAnchors)(wants, options.remote ?? {})
|
|
1442
2117
|
]);
|
|
2118
|
+
await prepareResolvers(resolvers, [
|
|
2119
|
+
...files,
|
|
2120
|
+
...wants.map((want) => want.file)
|
|
2121
|
+
]);
|
|
1443
2122
|
const drift = /* @__PURE__ */ new Map();
|
|
1444
2123
|
for (const record of records) {
|
|
1445
2124
|
const entries = [];
|
|
1446
2125
|
for (const { anchor, foreign } of planned.get(record.conceptId) ?? []) {
|
|
1447
2126
|
entries.push(
|
|
1448
|
-
foreign ? remoteEntry(anchor, remote,
|
|
2127
|
+
foreign ? remoteEntry(anchor, remote, resolvers) : localEntry(anchor, reads.get(anchor.file), resolvers)
|
|
1449
2128
|
);
|
|
1450
2129
|
}
|
|
1451
2130
|
if (entries.length) drift.set(record.conceptId, entries);
|
|
@@ -1471,60 +2150,104 @@ function unresolved(anchor, reason, repo) {
|
|
|
1471
2150
|
state: "unresolved",
|
|
1472
2151
|
diffSize: null,
|
|
1473
2152
|
...reason ? { reason } : {},
|
|
1474
|
-
...repo ? { repo } : {}
|
|
2153
|
+
...repo ? { repo } : {},
|
|
2154
|
+
...classOf(reason)
|
|
1475
2155
|
};
|
|
1476
2156
|
}
|
|
1477
|
-
function
|
|
1478
|
-
|
|
1479
|
-
|
|
2157
|
+
function provisionalDriftClass(entry) {
|
|
2158
|
+
if (entry.state === "unresolved") {
|
|
2159
|
+
return entry.reason === "file-missing" || entry.reason === "symbol-not-found" ? "gone" : void 0;
|
|
2160
|
+
}
|
|
2161
|
+
return entry.state === "drifted" ? "changed" : void 0;
|
|
2162
|
+
}
|
|
2163
|
+
function classOf(reason) {
|
|
2164
|
+
const settled = provisionalDriftClass({ state: "unresolved", reason });
|
|
2165
|
+
return settled ? { class: settled } : {};
|
|
2166
|
+
}
|
|
2167
|
+
function hashIn(source, anchor, resolvers) {
|
|
2168
|
+
const outcome = resolveAnchorSpan(source, anchor, resolvers);
|
|
2169
|
+
if (!outcome.ok) return { ok: false, reason: outcome.reason };
|
|
2170
|
+
const { hash, kind } = anchorHashOf(anchor, outcome);
|
|
2171
|
+
return {
|
|
2172
|
+
ok: true,
|
|
2173
|
+
current: {
|
|
2174
|
+
hash,
|
|
2175
|
+
kind,
|
|
2176
|
+
lines: outcome.span.endLine - outcome.span.startLine + 1,
|
|
2177
|
+
...outcome.resolver ? { resolver: outcome.resolver } : {}
|
|
2178
|
+
}
|
|
2179
|
+
};
|
|
2180
|
+
}
|
|
2181
|
+
function resolverExtras(source, anchor, current) {
|
|
1480
2182
|
return {
|
|
1481
|
-
|
|
1482
|
-
|
|
2183
|
+
...current.resolver ? { resolver: current.resolver } : {},
|
|
2184
|
+
...current.hash !== anchor.hash && resolverChanged(source, anchor, current.resolver) ? { reason: "resolver-changed" } : {}
|
|
1483
2185
|
};
|
|
1484
2186
|
}
|
|
1485
2187
|
function compared(anchor, current, extra = {}) {
|
|
2188
|
+
const matched = current.hash === anchor.hash;
|
|
1486
2189
|
return {
|
|
1487
2190
|
...base(anchor),
|
|
1488
|
-
state:
|
|
2191
|
+
state: matched ? "match" : "drifted",
|
|
1489
2192
|
currentHash: current.hash,
|
|
2193
|
+
hashKind: current.kind,
|
|
1490
2194
|
diffSize: anchor.lines === void 0 ? null : Math.abs(current.lines - anchor.lines),
|
|
2195
|
+
...matched ? {} : { class: "changed" },
|
|
1491
2196
|
...extra
|
|
1492
2197
|
};
|
|
1493
2198
|
}
|
|
1494
|
-
function localEntry(anchor, read,
|
|
2199
|
+
function localEntry(anchor, read, resolvers) {
|
|
1495
2200
|
if (!read.ok) return unresolved(anchor, read.reason);
|
|
1496
|
-
const
|
|
1497
|
-
|
|
2201
|
+
const found = hashIn(read.source, anchor, resolvers);
|
|
2202
|
+
if (!found.ok) return unresolved(anchor, found.reason);
|
|
2203
|
+
return compared(
|
|
2204
|
+
anchor,
|
|
2205
|
+
found.current,
|
|
2206
|
+
resolverExtras(read.source, anchor, found.current)
|
|
2207
|
+
);
|
|
1498
2208
|
}
|
|
1499
|
-
function remoteEntry(anchor, remote,
|
|
2209
|
+
function remoteEntry(anchor, remote, resolvers) {
|
|
1500
2210
|
const repo = anchor.repo;
|
|
1501
2211
|
const key2 = normalizeRepoUrl(repo);
|
|
1502
2212
|
const atDefault = remote.get(wantKey(key2, void 0, anchor.file));
|
|
1503
2213
|
const primary = anchor.ref ? remote.get(wantKey(key2, anchor.ref, anchor.file)) : atDefault;
|
|
1504
2214
|
if (!primary) return unresolved(anchor, "remote-unreachable", repo);
|
|
1505
2215
|
if (!primary.ok) return unresolved(anchor, primary.reason, repo);
|
|
1506
|
-
const
|
|
1507
|
-
if (!
|
|
1508
|
-
|
|
2216
|
+
const found = hashIn(primary.source, anchor, resolvers);
|
|
2217
|
+
if (!found.ok) return unresolved(anchor, found.reason, repo);
|
|
2218
|
+
const current = found.current;
|
|
2219
|
+
const extras = resolverExtras(primary.source, anchor, current);
|
|
2220
|
+
if (!anchor.ref) return compared(anchor, current, { repo, ...extras });
|
|
1509
2221
|
if (current.hash !== anchor.hash) {
|
|
1510
|
-
return compared(anchor, current, {
|
|
2222
|
+
return compared(anchor, current, {
|
|
2223
|
+
repo,
|
|
2224
|
+
...extras,
|
|
2225
|
+
remoteState: "drifted-from-ref"
|
|
2226
|
+
});
|
|
1511
2227
|
}
|
|
1512
|
-
const head = atDefault?.ok ? hashIn(atDefault.source, anchor,
|
|
1513
|
-
return head && head.hash !== anchor.hash ? {
|
|
1514
|
-
...compared(anchor, head, {
|
|
2228
|
+
const head = atDefault?.ok ? hashIn(atDefault.source, anchor, resolvers) : null;
|
|
2229
|
+
return head?.ok && head.current.hash !== anchor.hash ? {
|
|
2230
|
+
...compared(anchor, head.current, {
|
|
2231
|
+
repo,
|
|
2232
|
+
...head.current.resolver ? { resolver: head.current.resolver } : {}
|
|
2233
|
+
}),
|
|
1515
2234
|
state: "drifted",
|
|
1516
2235
|
remoteState: "drifted-on-default"
|
|
1517
|
-
} : compared(anchor, current, {
|
|
2236
|
+
} : compared(anchor, current, {
|
|
2237
|
+
repo,
|
|
2238
|
+
...extras,
|
|
2239
|
+
remoteState: "matches-ref"
|
|
2240
|
+
});
|
|
1518
2241
|
}
|
|
1519
2242
|
|
|
1520
2243
|
// src/search-index.ts
|
|
1521
|
-
var
|
|
1522
|
-
var
|
|
2244
|
+
var import_promises5 = require("fs/promises");
|
|
2245
|
+
var import_node_path6 = require("path");
|
|
1523
2246
|
|
|
1524
2247
|
// src/kb-log.ts
|
|
1525
|
-
var
|
|
2248
|
+
var import_zod3 = require("zod");
|
|
1526
2249
|
var LOG_FILE = "log.jsonl";
|
|
1527
|
-
var kbLogEntrySchema =
|
|
2250
|
+
var kbLogEntrySchema = import_zod3.z.object({
|
|
1528
2251
|
// Validated, not just `min(1)`: `at` is a sort key (see `parseLog`
|
|
1529
2252
|
// below), and a value that isn't actually chronological — a Unix
|
|
1530
2253
|
// timestamp, a human-typed date, garbage — would sort wrong without
|
|
@@ -1533,12 +2256,12 @@ var kbLogEntrySchema = import_zod2.z.object({
|
|
|
1533
2256
|
// and rejects everything else, including a non-`Z` offset — so a
|
|
1534
2257
|
// malformed `at` is reported the same way a malformed line already is,
|
|
1535
2258
|
// rather than silently sorting into the wrong place.
|
|
1536
|
-
at:
|
|
1537
|
-
by:
|
|
1538
|
-
operation:
|
|
1539
|
-
conceptId:
|
|
2259
|
+
at: import_zod3.z.iso.datetime(),
|
|
2260
|
+
by: import_zod3.z.string().min(1),
|
|
2261
|
+
operation: import_zod3.z.string().min(1),
|
|
2262
|
+
conceptId: import_zod3.z.string().min(1),
|
|
1540
2263
|
/** Second concept id, where the operation relates two — supersession. */
|
|
1541
|
-
target:
|
|
2264
|
+
target: import_zod3.z.string().min(1).optional()
|
|
1542
2265
|
}).strict();
|
|
1543
2266
|
function renderLogEntry(entry) {
|
|
1544
2267
|
return `${JSON.stringify(kbLogEntrySchema.parse(entry))}
|
|
@@ -1548,18 +2271,18 @@ function parseLog(raw) {
|
|
|
1548
2271
|
const entries = [];
|
|
1549
2272
|
const malformed = [];
|
|
1550
2273
|
const seen = /* @__PURE__ */ new Set();
|
|
1551
|
-
raw.split("\n").forEach((text,
|
|
2274
|
+
raw.split("\n").forEach((text, index2) => {
|
|
1552
2275
|
if (!text.trim()) return;
|
|
1553
2276
|
let value;
|
|
1554
2277
|
try {
|
|
1555
2278
|
value = JSON.parse(text);
|
|
1556
2279
|
} catch {
|
|
1557
|
-
malformed.push({ line:
|
|
2280
|
+
malformed.push({ line: index2 + 1, text });
|
|
1558
2281
|
return;
|
|
1559
2282
|
}
|
|
1560
2283
|
const parsed = kbLogEntrySchema.safeParse(value);
|
|
1561
2284
|
if (!parsed.success) {
|
|
1562
|
-
malformed.push({ line:
|
|
2285
|
+
malformed.push({ line: index2 + 1, text });
|
|
1563
2286
|
return;
|
|
1564
2287
|
}
|
|
1565
2288
|
const key2 = JSON.stringify(parsed.data);
|
|
@@ -1582,7 +2305,7 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
1582
2305
|
let store = null;
|
|
1583
2306
|
try {
|
|
1584
2307
|
store = await qmd.createStore({
|
|
1585
|
-
dbPath: (0,
|
|
2308
|
+
dbPath: (0, import_node_path6.join)(bundlePath2, SEARCH_INDEX_FILE),
|
|
1586
2309
|
config: {
|
|
1587
2310
|
collections: {
|
|
1588
2311
|
[COLLECTION]: {
|
|
@@ -1617,7 +2340,7 @@ async function searchBase(bundlePath2, query, options = {}) {
|
|
|
1617
2340
|
}
|
|
1618
2341
|
}
|
|
1619
2342
|
async function isStale(bundlePath2) {
|
|
1620
|
-
const indexAt = await (0,
|
|
2343
|
+
const indexAt = await (0, import_promises5.stat)((0, import_node_path6.join)(bundlePath2, SEARCH_INDEX_FILE)).then((s) => s.mtimeMs).catch(() => 0);
|
|
1621
2344
|
if (!indexAt) return true;
|
|
1622
2345
|
const { readdir: readdir2 } = await import("fs/promises");
|
|
1623
2346
|
const names = (await readdir2(bundlePath2).catch(() => [])).filter(
|
|
@@ -1626,8 +2349,8 @@ async function isStale(bundlePath2) {
|
|
|
1626
2349
|
let stale = false;
|
|
1627
2350
|
await mapLimit(names, DEFAULT_IO_CONCURRENCY, async (name) => {
|
|
1628
2351
|
if (stale) return;
|
|
1629
|
-
const
|
|
1630
|
-
if (
|
|
2352
|
+
const at2 = await (0, import_promises5.stat)((0, import_node_path6.join)(bundlePath2, name)).then((s) => s.mtimeMs).catch(() => 0);
|
|
2353
|
+
if (at2 > indexAt) stale = true;
|
|
1631
2354
|
});
|
|
1632
2355
|
return stale;
|
|
1633
2356
|
}
|
|
@@ -1922,8 +2645,8 @@ function trace(seedId, bundle, options = {}) {
|
|
|
1922
2645
|
return [...reached.values()].sort(byGeneratedAt);
|
|
1923
2646
|
}
|
|
1924
2647
|
function byGeneratedAt(left, right) {
|
|
1925
|
-
const
|
|
1926
|
-
return
|
|
2648
|
+
const at2 = (step) => step.record.frontmatter.generated?.at ?? "";
|
|
2649
|
+
return at2(left).localeCompare(at2(right)) || left.depth - right.depth;
|
|
1927
2650
|
}
|
|
1928
2651
|
|
|
1929
2652
|
// src/pack.ts
|
|
@@ -2006,8 +2729,8 @@ function byRank(left, right) {
|
|
|
2006
2729
|
) || left.record.conceptId.localeCompare(right.record.conceptId);
|
|
2007
2730
|
}
|
|
2008
2731
|
function typeRank(record) {
|
|
2009
|
-
const
|
|
2010
|
-
return
|
|
2732
|
+
const index2 = TYPE_PRIORITY.indexOf(record.frontmatter.type);
|
|
2733
|
+
return index2 === -1 ? TYPE_PRIORITY.length : index2;
|
|
2011
2734
|
}
|
|
2012
2735
|
|
|
2013
2736
|
// src/catalog.ts
|
|
@@ -2216,7 +2939,7 @@ function appendUnionMergeLine(contents) {
|
|
|
2216
2939
|
}
|
|
2217
2940
|
|
|
2218
2941
|
// src/kb-store.ts
|
|
2219
|
-
var KB_DIR = (0,
|
|
2942
|
+
var KB_DIR = (0, import_node_path7.join)(".strauss", "kb");
|
|
2220
2943
|
var STORE_OWNED = /* @__PURE__ */ new Set([INDEX_FILE, LOG_FILE, SEARCH_INDEX_FILE]);
|
|
2221
2944
|
var DEFAULT_LOAD_BUDGET = 25e3;
|
|
2222
2945
|
var KbStore = class {
|
|
@@ -2247,7 +2970,7 @@ var KbStore = class {
|
|
|
2247
2970
|
const conceptId2 = `${input.type}.${input.slug}`;
|
|
2248
2971
|
const root = this.root(bundlePath2);
|
|
2249
2972
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
2250
|
-
await (0,
|
|
2973
|
+
await (0, import_promises6.mkdir)(root, { recursive: true });
|
|
2251
2974
|
await this.publish(
|
|
2252
2975
|
target,
|
|
2253
2976
|
stringifyMarkdownWithFrontmatter(input.body, frontmatter),
|
|
@@ -2286,7 +3009,7 @@ var KbStore = class {
|
|
|
2286
3009
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
2287
3010
|
let raw;
|
|
2288
3011
|
try {
|
|
2289
|
-
raw = await (0,
|
|
3012
|
+
raw = await (0, import_promises6.readFile)(target, "utf8");
|
|
2290
3013
|
} catch {
|
|
2291
3014
|
return null;
|
|
2292
3015
|
}
|
|
@@ -2303,7 +3026,7 @@ var KbStore = class {
|
|
|
2303
3026
|
const root = this.root(bundlePath2);
|
|
2304
3027
|
let names;
|
|
2305
3028
|
try {
|
|
2306
|
-
names = await (0,
|
|
3029
|
+
names = await (0, import_promises6.readdir)(root);
|
|
2307
3030
|
} catch {
|
|
2308
3031
|
return [];
|
|
2309
3032
|
}
|
|
@@ -2311,7 +3034,7 @@ var KbStore = class {
|
|
|
2311
3034
|
const records = await mapLimit(
|
|
2312
3035
|
wanted,
|
|
2313
3036
|
DEFAULT_IO_CONCURRENCY,
|
|
2314
|
-
async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await (0,
|
|
3037
|
+
async ({ name, conceptId: conceptId2 }) => this.parse(conceptId2, await (0, import_promises6.readFile)((0, import_node_path7.join)(root, name), "utf8"))
|
|
2315
3038
|
);
|
|
2316
3039
|
return records.filter((record) => record !== null);
|
|
2317
3040
|
}
|
|
@@ -2359,8 +3082,8 @@ var KbStore = class {
|
|
|
2359
3082
|
* and the refusal is logged under its own operation name — `mutate` only
|
|
2360
3083
|
* logs what it publishes.
|
|
2361
3084
|
*/
|
|
2362
|
-
async verify(bundlePath2, conceptId2, note, actor = "unknown",
|
|
2363
|
-
const event = kbVerifiedEventSchema.parse({ by: actor, at, note });
|
|
3085
|
+
async verify(bundlePath2, conceptId2, note, actor = "unknown", at2 = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
3086
|
+
const event = kbVerifiedEventSchema.parse({ by: actor, at: at2, note });
|
|
2364
3087
|
const existing = await this.read(bundlePath2, conceptId2);
|
|
2365
3088
|
if (!existing) throw new KbRecordNotFoundError(conceptId2);
|
|
2366
3089
|
const generatedBy = existing.frontmatter.generated?.by;
|
|
@@ -2412,14 +3135,14 @@ var KbStore = class {
|
|
|
2412
3135
|
return superseded;
|
|
2413
3136
|
}
|
|
2414
3137
|
/** Resolves an open question, stamping who answered and when. */
|
|
2415
|
-
async answer(bundlePath2, conceptId2, answer, actor = "unknown",
|
|
3138
|
+
async answer(bundlePath2, conceptId2, answer, actor = "unknown", at2 = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
2416
3139
|
return this.mutate(
|
|
2417
3140
|
bundlePath2,
|
|
2418
3141
|
conceptId2,
|
|
2419
3142
|
(frontmatter) => ({
|
|
2420
3143
|
...frontmatter,
|
|
2421
3144
|
strauss_status: "resolved",
|
|
2422
|
-
strauss_answered: { by: actor, at }
|
|
3145
|
+
strauss_answered: { by: actor, at: at2 }
|
|
2423
3146
|
}),
|
|
2424
3147
|
{ operation: "answer", by: actor },
|
|
2425
3148
|
(body) => `${body.trimEnd()}
|
|
@@ -2468,7 +3191,7 @@ ${answer}
|
|
|
2468
3191
|
if (found.length) return found;
|
|
2469
3192
|
}
|
|
2470
3193
|
const lowered = needle.toLowerCase();
|
|
2471
|
-
return bundle.filter((record) =>
|
|
3194
|
+
return bundle.filter((record) => matches2(record, lowered));
|
|
2472
3195
|
}
|
|
2473
3196
|
/**
|
|
2474
3197
|
* Anchor drift over the records about to be handed back. Like the search
|
|
@@ -2588,24 +3311,34 @@ ${answer}
|
|
|
2588
3311
|
}
|
|
2589
3312
|
/**
|
|
2590
3313
|
* `load`'s digest without `load`'s bodies — the same records, adjudicated
|
|
2591
|
-
* the same way, handed back as a stamp.
|
|
2592
|
-
*
|
|
2593
|
-
*
|
|
3314
|
+
* the same way, handed back as a stamp.
|
|
3315
|
+
*
|
|
3316
|
+
* Drift is counted but kept out of the digest, which is what lets the reload
|
|
3317
|
+
* hook ask one question and get two answers: whether the base moved, and
|
|
3318
|
+
* whether the code under it did. A `load` and a `stamp` of the same base
|
|
3319
|
+
* still agree on the digest, because no warning has ever reached it.
|
|
2594
3320
|
*/
|
|
2595
|
-
async stamp(bundlePath2) {
|
|
3321
|
+
async stamp(bundlePath2, options = {}) {
|
|
2596
3322
|
const bundle = await this.list(bundlePath2);
|
|
2597
3323
|
const adjudicated = adjudicate(bundle, bundle, /* @__PURE__ */ new Date());
|
|
2598
3324
|
const current = adjudicated.filter((hit) => hit.standing !== "superseded");
|
|
2599
3325
|
const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(stub);
|
|
2600
3326
|
const stamped = bundleStamp(current, superseded);
|
|
2601
|
-
const dates = bundle.map((record) => record.frontmatter.generated?.at ?? null).filter((
|
|
3327
|
+
const dates = bundle.map((record) => record.frontmatter.generated?.at ?? null).filter((at2) => typeof at2 === "string").sort();
|
|
3328
|
+
const drift = await this.detectDrift(bundle, options.repoRoot);
|
|
3329
|
+
const drifted2 = drift === void 0 ? null : [...drift.values()].filter(
|
|
3330
|
+
(entries) => entries.some(
|
|
3331
|
+
(entry) => entry.state !== "match" && !isUncheckedReason(entry.reason)
|
|
3332
|
+
)
|
|
3333
|
+
).length;
|
|
2602
3334
|
return {
|
|
2603
3335
|
path: bundlePath2,
|
|
2604
3336
|
digest: stamped.digest,
|
|
2605
3337
|
recordCount: bundle.length,
|
|
2606
3338
|
superseded: superseded.length,
|
|
2607
3339
|
newestAt: dates.at(-1) ?? null,
|
|
2608
|
-
records: stamped.records
|
|
3340
|
+
records: stamped.records,
|
|
3341
|
+
drifted: drifted2
|
|
2609
3342
|
};
|
|
2610
3343
|
}
|
|
2611
3344
|
/** How a position was arrived at, as a timeline. See `trace.ts`. */
|
|
@@ -2638,11 +3371,11 @@ ${answer}
|
|
|
2638
3371
|
async readIndex(bundlePath2) {
|
|
2639
3372
|
const root = this.root(bundlePath2);
|
|
2640
3373
|
const expected = renderIndex(await this.list(bundlePath2));
|
|
2641
|
-
const stored = await (0,
|
|
3374
|
+
const stored = await (0, import_promises6.readFile)((0, import_node_path7.join)(root, INDEX_FILE), "utf8").catch(
|
|
2642
3375
|
() => null
|
|
2643
3376
|
);
|
|
2644
3377
|
if (indexIsStale(stored, expected)) {
|
|
2645
|
-
await this.publish((0,
|
|
3378
|
+
await this.publish((0, import_node_path7.join)(root, INDEX_FILE), expected, true, INDEX_FILE);
|
|
2646
3379
|
this.logger.info?.({
|
|
2647
3380
|
operation: "kb.index.repair",
|
|
2648
3381
|
bundlePath: root,
|
|
@@ -2659,8 +3392,8 @@ ${answer}
|
|
|
2659
3392
|
* knows which agent touched what. So a bad line is surfaced and left alone.
|
|
2660
3393
|
*/
|
|
2661
3394
|
async readLog(bundlePath2) {
|
|
2662
|
-
const raw = await (0,
|
|
2663
|
-
(0,
|
|
3395
|
+
const raw = await (0, import_promises6.readFile)(
|
|
3396
|
+
(0, import_node_path7.join)(this.root(bundlePath2), LOG_FILE),
|
|
2664
3397
|
"utf8"
|
|
2665
3398
|
).catch(() => "");
|
|
2666
3399
|
const result = parseLog(raw);
|
|
@@ -2711,14 +3444,14 @@ ${answer}
|
|
|
2711
3444
|
}
|
|
2712
3445
|
async mutate(bundlePath2, conceptId2, change, entry, changeBody = (body) => body) {
|
|
2713
3446
|
const target = this.recordPath(bundlePath2, conceptId2);
|
|
2714
|
-
const before = await (0,
|
|
3447
|
+
const before = await (0, import_promises6.readFile)(target, "utf8").catch(() => null);
|
|
2715
3448
|
if (before === null) throw new KbRecordNotFoundError(conceptId2);
|
|
2716
3449
|
const parsed = this.parse(conceptId2, before);
|
|
2717
3450
|
if (!parsed) throw new KbRecordNotFoundError(conceptId2);
|
|
2718
3451
|
const frontmatter = change(parsed.frontmatter);
|
|
2719
3452
|
const body = changeBody(parsed.body);
|
|
2720
3453
|
const contents = stringifyMarkdownWithFrontmatter(body, frontmatter);
|
|
2721
|
-
const witness = await (0,
|
|
3454
|
+
const witness = await (0, import_promises6.readFile)(target, "utf8").catch(() => null);
|
|
2722
3455
|
if (witness === null || sha256(witness) !== sha256(before)) {
|
|
2723
3456
|
throw new KbWriteConflictError(conceptId2);
|
|
2724
3457
|
}
|
|
@@ -2744,20 +3477,20 @@ ${answer}
|
|
|
2744
3477
|
*/
|
|
2745
3478
|
async publish(target, contents, overwrite, conceptId2) {
|
|
2746
3479
|
const staging = `${target}.${process.pid}.tmp`;
|
|
2747
|
-
await (0,
|
|
3480
|
+
await (0, import_promises6.writeFile)(staging, contents, "utf8");
|
|
2748
3481
|
try {
|
|
2749
3482
|
if (overwrite) {
|
|
2750
|
-
await (0,
|
|
3483
|
+
await (0, import_promises6.rename)(staging, target);
|
|
2751
3484
|
return;
|
|
2752
3485
|
}
|
|
2753
|
-
await (0,
|
|
3486
|
+
await (0, import_promises6.link)(staging, target);
|
|
2754
3487
|
} catch (error) {
|
|
2755
3488
|
if (error.code === "EEXIST") {
|
|
2756
3489
|
throw new KbRecordAlreadyExistsError(conceptId2);
|
|
2757
3490
|
}
|
|
2758
3491
|
throw error;
|
|
2759
3492
|
} finally {
|
|
2760
|
-
await (0,
|
|
3493
|
+
await (0, import_promises6.unlink)(staging).catch(() => void 0);
|
|
2761
3494
|
}
|
|
2762
3495
|
}
|
|
2763
3496
|
/**
|
|
@@ -2801,18 +3534,18 @@ ${answer}
|
|
|
2801
3534
|
* file must not fail the mutation it guards.
|
|
2802
3535
|
*/
|
|
2803
3536
|
async ensureGitattributes(root) {
|
|
2804
|
-
const target = (0,
|
|
3537
|
+
const target = (0, import_node_path7.join)(root, GITATTRIBUTES_FILE);
|
|
2805
3538
|
try {
|
|
2806
3539
|
let existing;
|
|
2807
3540
|
try {
|
|
2808
|
-
existing = await (0,
|
|
3541
|
+
existing = await (0, import_promises6.readFile)(target, "utf8");
|
|
2809
3542
|
} catch (error) {
|
|
2810
3543
|
if (error.code !== "ENOENT") throw error;
|
|
2811
3544
|
existing = null;
|
|
2812
3545
|
}
|
|
2813
3546
|
if (existing === null) {
|
|
2814
3547
|
try {
|
|
2815
|
-
await (0,
|
|
3548
|
+
await (0, import_promises6.writeFile)(target, appendUnionMergeLine(""), {
|
|
2816
3549
|
encoding: "utf8",
|
|
2817
3550
|
flag: "wx"
|
|
2818
3551
|
});
|
|
@@ -2833,7 +3566,7 @@ ${answer}
|
|
|
2833
3566
|
return;
|
|
2834
3567
|
}
|
|
2835
3568
|
if (!hasMergeDeclaration(existing)) {
|
|
2836
|
-
await (0,
|
|
3569
|
+
await (0, import_promises6.appendFile)(target, appendUnionMergeLine(existing), "utf8");
|
|
2837
3570
|
this.logger.info?.({
|
|
2838
3571
|
operation: "kb.gitattributes.ensure",
|
|
2839
3572
|
bundlePath: root,
|
|
@@ -2852,7 +3585,7 @@ ${answer}
|
|
|
2852
3585
|
async record(root, entry) {
|
|
2853
3586
|
await this.ensureGitattributes(root);
|
|
2854
3587
|
const line = renderLogEntry({ at: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
|
|
2855
|
-
await (0,
|
|
3588
|
+
await (0, import_promises6.appendFile)((0, import_node_path7.join)(root, LOG_FILE), line, "utf8").catch((error) => {
|
|
2856
3589
|
this.logger.warn?.({
|
|
2857
3590
|
operation: "kb.log.append",
|
|
2858
3591
|
outcome: "failed",
|
|
@@ -2878,18 +3611,18 @@ ${answer}
|
|
|
2878
3611
|
};
|
|
2879
3612
|
}
|
|
2880
3613
|
root(bundlePath2) {
|
|
2881
|
-
return (0,
|
|
3614
|
+
return (0, import_node_path7.resolve)(bundlePath2);
|
|
2882
3615
|
}
|
|
2883
3616
|
// Concept ids are `<type>.<slug>` and map to a single file directly under the
|
|
2884
3617
|
// bundle root; anything carrying a separator would escape it.
|
|
2885
3618
|
recordPath(bundlePath2, conceptId2) {
|
|
2886
|
-
if (conceptId2.includes(
|
|
3619
|
+
if (conceptId2.includes(import_node_path7.sep) || conceptId2.includes("/")) {
|
|
2887
3620
|
throw new KbInvalidConceptIdError(
|
|
2888
3621
|
"concept id must not contain a path separator",
|
|
2889
3622
|
{ conceptId: conceptId2 }
|
|
2890
3623
|
);
|
|
2891
3624
|
}
|
|
2892
|
-
return (0,
|
|
3625
|
+
return (0, import_node_path7.join)(this.root(bundlePath2), `${conceptId2}.md`);
|
|
2893
3626
|
}
|
|
2894
3627
|
};
|
|
2895
3628
|
function estimateTokens(record) {
|
|
@@ -2916,7 +3649,7 @@ function stub(hit) {
|
|
|
2916
3649
|
at: hit.record.frontmatter.generated?.at ?? null
|
|
2917
3650
|
};
|
|
2918
3651
|
}
|
|
2919
|
-
function
|
|
3652
|
+
function matches2(record, needle) {
|
|
2920
3653
|
const { title, description } = record.frontmatter;
|
|
2921
3654
|
return [record.conceptId, title, description, record.body].some(
|
|
2922
3655
|
(field) => field?.toLowerCase().includes(needle)
|
|
@@ -2929,37 +3662,37 @@ function normalizeActor(id) {
|
|
|
2929
3662
|
}
|
|
2930
3663
|
|
|
2931
3664
|
// src/compose.ts
|
|
2932
|
-
var
|
|
2933
|
-
var composeLinkSchema =
|
|
3665
|
+
var import_zod4 = require("zod");
|
|
3666
|
+
var composeLinkSchema = import_zod4.z.object({
|
|
2934
3667
|
target: kbConceptIdSchema,
|
|
2935
|
-
rel:
|
|
3668
|
+
rel: import_zod4.z.enum(KB_LINK_RELS)
|
|
2936
3669
|
}).strict();
|
|
2937
|
-
var composeInputSchema =
|
|
2938
|
-
slug:
|
|
3670
|
+
var composeInputSchema = import_zod4.z.object({
|
|
3671
|
+
slug: import_zod4.z.string().min(1),
|
|
2939
3672
|
/** One line, in the reader's terms. Becomes OKF `title`. */
|
|
2940
|
-
title:
|
|
3673
|
+
title: import_zod4.z.string().min(1),
|
|
2941
3674
|
/** The consequence — what breaks if this is wrong. Becomes `description`. */
|
|
2942
|
-
why:
|
|
3675
|
+
why: import_zod4.z.string().min(1),
|
|
2943
3676
|
/** Keyed by section heading from the type's spec. Unknown keys rejected. */
|
|
2944
|
-
sections:
|
|
2945
|
-
anchors:
|
|
2946
|
-
sources:
|
|
3677
|
+
sections: import_zod4.z.record(import_zod4.z.string(), import_zod4.z.string().min(1)).optional(),
|
|
3678
|
+
anchors: import_zod4.z.array(kbAnchorSchema).optional(),
|
|
3679
|
+
sources: import_zod4.z.array(kbSourceSchema).optional(),
|
|
2947
3680
|
/** No source exists, as a claim rather than a sentinel in `sources`. */
|
|
2948
|
-
assumption:
|
|
3681
|
+
assumption: import_zod4.z.boolean().optional(),
|
|
2949
3682
|
/**
|
|
2950
3683
|
* OKF `stale_after`: the absolute date this record stops being trusted.
|
|
2951
3684
|
* Anything the outside world can change — pricing, quotas, versions,
|
|
2952
3685
|
* reception counts — should carry one.
|
|
2953
3686
|
*/
|
|
2954
|
-
stale_after:
|
|
3687
|
+
stale_after: import_zod4.z.string().regex(/^\d{4}-\d{2}-\d{2}$/, {
|
|
2955
3688
|
message: "stale_after must be YYYY-MM-DD"
|
|
2956
3689
|
}).refine((date) => !Number.isNaN(Date.parse(date)), {
|
|
2957
3690
|
message: "stale_after must be a real date"
|
|
2958
3691
|
}).optional(),
|
|
2959
|
-
verify:
|
|
2960
|
-
tags:
|
|
3692
|
+
verify: import_zod4.z.array(import_zod4.z.string().min(1)).optional(),
|
|
3693
|
+
tags: import_zod4.z.array(import_zod4.z.string().min(1)).optional(),
|
|
2961
3694
|
/** Concept ids this record relates to; rendered as body links. */
|
|
2962
|
-
relatedConceptIds:
|
|
3695
|
+
relatedConceptIds: import_zod4.z.array(kbConceptIdSchema).optional(),
|
|
2963
3696
|
/**
|
|
2964
3697
|
* Typed causal edges, source → target: `{ target: "fact.b", rel:
|
|
2965
3698
|
* "depends_on" }` on record A says A needs B. Stored in frontmatter and
|
|
@@ -2967,16 +3700,16 @@ var composeInputSchema = import_zod3.z.object({
|
|
|
2967
3700
|
* reader that knows only OKF. The vocabulary goes into the description from
|
|
2968
3701
|
* the same table the walk uses, so `kb_schema` emits it.
|
|
2969
3702
|
*/
|
|
2970
|
-
links:
|
|
3703
|
+
links: import_zod4.z.array(composeLinkSchema).max(64).optional().describe(
|
|
2971
3704
|
`Typed causal edges, source \u2192 target \u2014 a link on this record says this record <rel> the target. ${KB_LINK_RELS.map(
|
|
2972
3705
|
(rel) => `${rel}: ${LINK_RELS[rel].purpose}`
|
|
2973
3706
|
).join("; ")}.`
|
|
2974
3707
|
),
|
|
2975
3708
|
/** Concept ids this record replaces. The store settles the backlinks. */
|
|
2976
|
-
supersedes:
|
|
2977
|
-
materiality:
|
|
2978
|
-
confidence:
|
|
2979
|
-
owner:
|
|
3709
|
+
supersedes: import_zod4.z.array(kbConceptIdSchema).max(32).optional(),
|
|
3710
|
+
materiality: import_zod4.z.enum(KB_MATERIALITIES).optional(),
|
|
3711
|
+
confidence: import_zod4.z.enum(KB_CONFIDENCES).optional(),
|
|
3712
|
+
owner: import_zod4.z.string().min(1).optional()
|
|
2980
3713
|
}).strict();
|
|
2981
3714
|
function composeRecord(type, input, writtenBy, writtenAt) {
|
|
2982
3715
|
const parsed = composeInputSchema.parse(input);
|
|
@@ -3053,9 +3786,9 @@ ${text}`);
|
|
|
3053
3786
|
// src/kb-pins/budgets.ts
|
|
3054
3787
|
function asBudgets(value) {
|
|
3055
3788
|
if (value === null || typeof value !== "object") return {};
|
|
3056
|
-
const
|
|
3789
|
+
const table2 = value;
|
|
3057
3790
|
const pick = (key2, min) => {
|
|
3058
|
-
const raw =
|
|
3791
|
+
const raw = table2[key2];
|
|
3059
3792
|
return typeof raw === "number" && Number.isInteger(raw) && raw >= min ? raw : void 0;
|
|
3060
3793
|
};
|
|
3061
3794
|
const budgetTokens = pick("budgetTokens", 1);
|
|
@@ -3066,9 +3799,9 @@ function asBudgets(value) {
|
|
|
3066
3799
|
};
|
|
3067
3800
|
}
|
|
3068
3801
|
function contextProfileBudgets(manifest, profile) {
|
|
3069
|
-
const
|
|
3070
|
-
if (
|
|
3071
|
-
const entries =
|
|
3802
|
+
const table2 = manifest.context;
|
|
3803
|
+
if (table2 === null || typeof table2 !== "object") return {};
|
|
3804
|
+
const entries = table2;
|
|
3072
3805
|
return {
|
|
3073
3806
|
...asBudgets(entries["default"]),
|
|
3074
3807
|
...profile ? asBudgets(entries[profile]) : {}
|
|
@@ -3099,23 +3832,23 @@ var KbBaseFrozenError = class extends Error {
|
|
|
3099
3832
|
};
|
|
3100
3833
|
|
|
3101
3834
|
// src/kb-pins/frozen.ts
|
|
3102
|
-
var
|
|
3835
|
+
var import_node_path10 = require("path");
|
|
3103
3836
|
|
|
3104
3837
|
// src/kb-pins/layers.ts
|
|
3105
|
-
var
|
|
3106
|
-
var
|
|
3107
|
-
var
|
|
3838
|
+
var import_promises7 = require("fs/promises");
|
|
3839
|
+
var import_node_os3 = require("os");
|
|
3840
|
+
var import_node_path9 = require("path");
|
|
3108
3841
|
|
|
3109
3842
|
// src/kb-pins/model.ts
|
|
3110
|
-
var
|
|
3111
|
-
var
|
|
3112
|
-
var PINS_FILE = (0,
|
|
3113
|
-
var PINS_LOCAL_FILE = (0,
|
|
3843
|
+
var import_node_path8 = require("path");
|
|
3844
|
+
var import_zod5 = require("zod");
|
|
3845
|
+
var PINS_FILE = (0, import_node_path8.join)(".strauss", "kb-pins.json");
|
|
3846
|
+
var PINS_LOCAL_FILE = (0, import_node_path8.join)(".strauss", "kb-pins.local.json");
|
|
3114
3847
|
var PIN_LAYERS = ["project", "local", "user"];
|
|
3115
|
-
var pinSchema =
|
|
3848
|
+
var pinSchema = import_zod5.z.object({
|
|
3116
3849
|
/** Relative to the manifest's root, so the file is committable. */
|
|
3117
|
-
path:
|
|
3118
|
-
pinnedAt:
|
|
3850
|
+
path: import_zod5.z.string().min(1),
|
|
3851
|
+
pinnedAt: import_zod5.z.string().min(1).optional(),
|
|
3119
3852
|
/**
|
|
3120
3853
|
* How `context` renders this base. `full` preloads the whole base into
|
|
3121
3854
|
* the block regardless of the full-under threshold — for a base whose
|
|
@@ -3125,7 +3858,7 @@ var pinSchema = import_zod4.z.object({
|
|
|
3125
3858
|
* Absent: the profile's full-under threshold decides. Invalid values
|
|
3126
3859
|
* degrade to absent rather than failing the manifest.
|
|
3127
3860
|
*/
|
|
3128
|
-
mode:
|
|
3861
|
+
mode: import_zod5.z.enum(["full", "index"]).optional().catch(void 0),
|
|
3129
3862
|
/**
|
|
3130
3863
|
* Context profiles this pin surfaces in (e.g. only at session-start,
|
|
3131
3864
|
* not per turn). Absent: every profile. A run without a profile sees
|
|
@@ -3133,17 +3866,17 @@ var pinSchema = import_zod4.z.object({
|
|
|
3133
3866
|
* that skill at point of use than pinned at all — pins are what every
|
|
3134
3867
|
* session should see.
|
|
3135
3868
|
*/
|
|
3136
|
-
profiles:
|
|
3869
|
+
profiles: import_zod5.z.array(import_zod5.z.string()).optional().catch(void 0),
|
|
3137
3870
|
/**
|
|
3138
3871
|
* The base is concluded — a finished piece of research, a frozen ADR
|
|
3139
3872
|
* set. Write commands against it refuse while this workspace holds the
|
|
3140
3873
|
* pin, and `context` labels it read-only. Workspace policy, not base
|
|
3141
3874
|
* state: the base itself stays copyable and writable elsewhere.
|
|
3142
3875
|
*/
|
|
3143
|
-
frozen:
|
|
3876
|
+
frozen: import_zod5.z.boolean().optional().catch(void 0)
|
|
3144
3877
|
}).passthrough();
|
|
3145
|
-
var pinsManifestSchema =
|
|
3146
|
-
pins:
|
|
3878
|
+
var pinsManifestSchema = import_zod5.z.object({
|
|
3879
|
+
pins: import_zod5.z.array(pinSchema).default([]),
|
|
3147
3880
|
/**
|
|
3148
3881
|
* Per-repo budgets for the `context` command, keyed by profile —
|
|
3149
3882
|
* `"session-start"`, `"compact"`, `"turn"`, or `"default"` for all of
|
|
@@ -3152,18 +3885,18 @@ var pinsManifestSchema = import_zod4.z.object({
|
|
|
3152
3885
|
* the index at every session start. `contextProfileBudgets` does the
|
|
3153
3886
|
* tolerant read.
|
|
3154
3887
|
*/
|
|
3155
|
-
context:
|
|
3888
|
+
context: import_zod5.z.unknown().optional()
|
|
3156
3889
|
}).passthrough();
|
|
3157
3890
|
|
|
3158
3891
|
// src/kb-pins/layers.ts
|
|
3159
3892
|
function userRoot() {
|
|
3160
|
-
return process.env.STRAUSS_KB_USER_ROOT || (0,
|
|
3893
|
+
return process.env.STRAUSS_KB_USER_ROOT || (0, import_node_os3.homedir)();
|
|
3161
3894
|
}
|
|
3162
3895
|
function layerRoot(workspaceDir, layer) {
|
|
3163
|
-
return layer === "user" ? userRoot() : (0,
|
|
3896
|
+
return layer === "user" ? userRoot() : (0, import_node_path9.resolve)(workspaceDir);
|
|
3164
3897
|
}
|
|
3165
3898
|
function layerFile(workspaceDir, layer) {
|
|
3166
|
-
return (0,
|
|
3899
|
+
return (0, import_node_path9.join)(
|
|
3167
3900
|
layerRoot(workspaceDir, layer),
|
|
3168
3901
|
layer === "local" ? PINS_LOCAL_FILE : PINS_FILE
|
|
3169
3902
|
);
|
|
@@ -3172,7 +3905,7 @@ async function readPinsLayer(workspaceDir, layer) {
|
|
|
3172
3905
|
const file = layerFile(workspaceDir, layer);
|
|
3173
3906
|
let raw;
|
|
3174
3907
|
try {
|
|
3175
|
-
raw = await (0,
|
|
3908
|
+
raw = await (0, import_promises7.readFile)(file, "utf8");
|
|
3176
3909
|
} catch {
|
|
3177
3910
|
return { pins: [] };
|
|
3178
3911
|
}
|
|
@@ -3196,16 +3929,16 @@ async function readPinsLayer(workspaceDir, layer) {
|
|
|
3196
3929
|
}
|
|
3197
3930
|
async function writePinsLayer(workspaceDir, layer, manifest) {
|
|
3198
3931
|
const file = layerFile(workspaceDir, layer);
|
|
3199
|
-
await (0,
|
|
3200
|
-
await (0,
|
|
3932
|
+
await (0, import_promises7.mkdir)((0, import_node_path9.dirname)(file), { recursive: true });
|
|
3933
|
+
await (0, import_promises7.writeFile)(file, `${JSON.stringify(manifest, null, 2)}
|
|
3201
3934
|
`, "utf8");
|
|
3202
3935
|
}
|
|
3203
3936
|
function resolvePinPath(rootDir, path) {
|
|
3204
|
-
return (0,
|
|
3937
|
+
return (0, import_node_path9.isAbsolute)(path) ? (0, import_node_path9.resolve)(path) : (0, import_node_path9.resolve)(rootDir, path.split("/").join(import_node_path9.sep));
|
|
3205
3938
|
}
|
|
3206
3939
|
function storablePath(rootDir, bundlePath2) {
|
|
3207
|
-
const rel = (0,
|
|
3208
|
-
return (rel === "" ? "." : rel).split(
|
|
3940
|
+
const rel = (0, import_node_path9.relative)((0, import_node_path9.resolve)(rootDir), (0, import_node_path9.resolve)(bundlePath2));
|
|
3941
|
+
return (rel === "" ? "." : rel).split(import_node_path9.sep).join("/");
|
|
3209
3942
|
}
|
|
3210
3943
|
async function readMergedPins(workspaceDir) {
|
|
3211
3944
|
const manifests = {};
|
|
@@ -3233,7 +3966,7 @@ async function readMergedPins(workspaceDir) {
|
|
|
3233
3966
|
// src/kb-pins/frozen.ts
|
|
3234
3967
|
async function assertBaseNotFrozen(workspaceDir, bundlePath2) {
|
|
3235
3968
|
const merged = await readMergedPins(workspaceDir);
|
|
3236
|
-
const absolute = (0,
|
|
3969
|
+
const absolute = (0, import_node_path10.resolve)(bundlePath2);
|
|
3237
3970
|
const pin = merged.pins.find((entry) => entry.absolutePath === absolute);
|
|
3238
3971
|
if (pin?.frozen === true) {
|
|
3239
3972
|
throw new KbBaseFrozenError(pin.path, pin.layer);
|
|
@@ -3262,7 +3995,7 @@ async function listPins(store, workspaceDir) {
|
|
|
3262
3995
|
}
|
|
3263
3996
|
|
|
3264
3997
|
// src/kb-pins/pin.ts
|
|
3265
|
-
async function pinBase(store, workspaceDir, bundlePath2,
|
|
3998
|
+
async function pinBase(store, workspaceDir, bundlePath2, at2, options = {}) {
|
|
3266
3999
|
const layer = options.layer ?? "project";
|
|
3267
4000
|
const root = layerRoot(workspaceDir, layer);
|
|
3268
4001
|
const manifest = await readPinsLayer(workspaceDir, layer);
|
|
@@ -3290,7 +4023,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
|
|
|
3290
4023
|
return {
|
|
3291
4024
|
path: existing.path,
|
|
3292
4025
|
layer,
|
|
3293
|
-
pinnedAt: existing.pinnedAt ??
|
|
4026
|
+
pinnedAt: existing.pinnedAt ?? at2,
|
|
3294
4027
|
alreadyPinned: true,
|
|
3295
4028
|
...updated.mode ? { mode: updated.mode } : {},
|
|
3296
4029
|
...updated.profiles ? { profiles: updated.profiles } : {},
|
|
@@ -3300,7 +4033,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
|
|
|
3300
4033
|
}
|
|
3301
4034
|
const entry = {
|
|
3302
4035
|
path: storablePath(root, bundlePath2),
|
|
3303
|
-
pinnedAt:
|
|
4036
|
+
pinnedAt: at2,
|
|
3304
4037
|
...fields
|
|
3305
4038
|
};
|
|
3306
4039
|
await writePinsLayer(workspaceDir, layer, {
|
|
@@ -3310,7 +4043,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
|
|
|
3310
4043
|
return {
|
|
3311
4044
|
path: entry.path,
|
|
3312
4045
|
layer,
|
|
3313
|
-
pinnedAt:
|
|
4046
|
+
pinnedAt: at2,
|
|
3314
4047
|
alreadyPinned: false,
|
|
3315
4048
|
...fields,
|
|
3316
4049
|
...warning ? { warning } : {}
|
|
@@ -3318,7 +4051,7 @@ async function pinBase(store, workspaceDir, bundlePath2, at, options = {}) {
|
|
|
3318
4051
|
}
|
|
3319
4052
|
|
|
3320
4053
|
// src/kb-pins/unpin.ts
|
|
3321
|
-
var
|
|
4054
|
+
var import_node_path11 = require("path");
|
|
3322
4055
|
async function unpinBase(workspaceDir, bundlePath2) {
|
|
3323
4056
|
const layers = [];
|
|
3324
4057
|
for (const layer of PIN_LAYERS) {
|
|
@@ -3339,14 +4072,14 @@ async function unpinBase(workspaceDir, bundlePath2) {
|
|
|
3339
4072
|
}
|
|
3340
4073
|
}
|
|
3341
4074
|
return {
|
|
3342
|
-
path: storablePath((0,
|
|
4075
|
+
path: storablePath((0, import_node_path11.resolve)(workspaceDir), bundlePath2),
|
|
3343
4076
|
removed: layers.length > 0,
|
|
3344
4077
|
layers
|
|
3345
4078
|
};
|
|
3346
4079
|
}
|
|
3347
4080
|
|
|
3348
4081
|
// src/kb-context.ts
|
|
3349
|
-
var
|
|
4082
|
+
var import_promises8 = require("fs/promises");
|
|
3350
4083
|
var HEADING2 = "## Knowledge bases (pinned)";
|
|
3351
4084
|
var DEFAULT_CONTEXT_BUDGET = 4e3;
|
|
3352
4085
|
var CONTEXT_PROFILES = {
|
|
@@ -3550,13 +4283,13 @@ function toHookJson(block, event) {
|
|
|
3550
4283
|
var CONTEXT_BEGIN = "<!-- strauss-kb:begin -->";
|
|
3551
4284
|
var CONTEXT_END = "<!-- strauss-kb:end -->";
|
|
3552
4285
|
async function syncInstructions(file, block) {
|
|
3553
|
-
const existing = await (0,
|
|
4286
|
+
const existing = await (0, import_promises8.readFile)(file, "utf8").catch(() => null);
|
|
3554
4287
|
const region = block ? `${CONTEXT_BEGIN}
|
|
3555
4288
|
${block.trim()}
|
|
3556
4289
|
${CONTEXT_END}` : null;
|
|
3557
4290
|
if (existing === null) {
|
|
3558
4291
|
if (!region) return { file, action: "unchanged" };
|
|
3559
|
-
await (0,
|
|
4292
|
+
await (0, import_promises8.writeFile)(file, `${region}
|
|
3560
4293
|
`, "utf8");
|
|
3561
4294
|
return { file, action: "created" };
|
|
3562
4295
|
}
|
|
@@ -3567,11 +4300,11 @@ ${CONTEXT_END}` : null;
|
|
|
3567
4300
|
const after = existing.slice(end + CONTEXT_END.length);
|
|
3568
4301
|
const next = region ? `${before}${region}${after}` : `${before.replace(/\n+$/, "\n")}${after.replace(/^\n+/, "\n")}`;
|
|
3569
4302
|
if (next === existing) return { file, action: "unchanged" };
|
|
3570
|
-
await (0,
|
|
4303
|
+
await (0, import_promises8.writeFile)(file, next, "utf8");
|
|
3571
4304
|
return { file, action: region ? "replaced" : "removed" };
|
|
3572
4305
|
}
|
|
3573
4306
|
if (!region) return { file, action: "unchanged" };
|
|
3574
|
-
await (0,
|
|
4307
|
+
await (0, import_promises8.writeFile)(
|
|
3575
4308
|
file,
|
|
3576
4309
|
`${existing.replace(/\n*$/, "\n\n")}${region}
|
|
3577
4310
|
`,
|
|
@@ -3581,14 +4314,14 @@ ${CONTEXT_END}` : null;
|
|
|
3581
4314
|
}
|
|
3582
4315
|
|
|
3583
4316
|
// src/json-schema.ts
|
|
3584
|
-
var
|
|
4317
|
+
var import_zod6 = require("zod");
|
|
3585
4318
|
function kbJsonSchemas() {
|
|
3586
4319
|
return {
|
|
3587
|
-
recordFrontmatter:
|
|
4320
|
+
recordFrontmatter: import_zod6.z.toJSONSchema(kbRecordFrontmatterSchema, {
|
|
3588
4321
|
io: "input"
|
|
3589
4322
|
}),
|
|
3590
|
-
composeInput:
|
|
3591
|
-
logEntry:
|
|
4323
|
+
composeInput: import_zod6.z.toJSONSchema(composeInputSchema, { io: "input" }),
|
|
4324
|
+
logEntry: import_zod6.z.toJSONSchema(kbLogEntrySchema, { io: "input" })
|
|
3592
4325
|
};
|
|
3593
4326
|
}
|
|
3594
4327
|
|
|
@@ -3598,7 +4331,7 @@ function matchToDiff(files, records, options = {}) {
|
|
|
3598
4331
|
const anchored = records.filter(
|
|
3599
4332
|
(record) => (record.frontmatter.strauss_anchors ?? []).length > 0
|
|
3600
4333
|
);
|
|
3601
|
-
const
|
|
4334
|
+
const matches3 = [];
|
|
3602
4335
|
for (const file of files) {
|
|
3603
4336
|
const candidates = anchored.map((record) => ({
|
|
3604
4337
|
record,
|
|
@@ -3617,7 +4350,7 @@ function matchToDiff(files, records, options = {}) {
|
|
|
3617
4350
|
hits.push(record);
|
|
3618
4351
|
}
|
|
3619
4352
|
if (!hits.length) continue;
|
|
3620
|
-
|
|
4353
|
+
matches3.push({
|
|
3621
4354
|
filePath: file.filePath,
|
|
3622
4355
|
hunk,
|
|
3623
4356
|
records: order(adjudicate(hits, records, options.now)),
|
|
@@ -3625,7 +4358,7 @@ function matchToDiff(files, records, options = {}) {
|
|
|
3625
4358
|
});
|
|
3626
4359
|
}
|
|
3627
4360
|
}
|
|
3628
|
-
return
|
|
4361
|
+
return matches3;
|
|
3629
4362
|
}
|
|
3630
4363
|
function place(anchors, filePath, hunk, ranges) {
|
|
3631
4364
|
let fallback = "miss";
|
|
@@ -3774,6 +4507,18 @@ var CHECK_HEADLINES = {
|
|
|
3774
4507
|
unchecked: "an anchor in another repository nothing could reach"
|
|
3775
4508
|
};
|
|
3776
4509
|
var DAY_MS = 864e5;
|
|
4510
|
+
function anchorResolverCounts(bundle) {
|
|
4511
|
+
let treeSitter = 0;
|
|
4512
|
+
let regex = 0;
|
|
4513
|
+
for (const record of bundle) {
|
|
4514
|
+
for (const anchor of record.frontmatter.strauss_anchors ?? []) {
|
|
4515
|
+
if (!anchor.hash || !anchor.symbol) continue;
|
|
4516
|
+
if (anchor.resolver === "tree-sitter") treeSitter += 1;
|
|
4517
|
+
else regex += 1;
|
|
4518
|
+
}
|
|
4519
|
+
}
|
|
4520
|
+
return { total: treeSitter + regex, treeSitter, regex };
|
|
4521
|
+
}
|
|
3777
4522
|
function doctor(bundle, options = {}) {
|
|
3778
4523
|
const thresholds = {
|
|
3779
4524
|
expiringDays: options.expiringDays ?? DEFAULT_EXPIRING_DAYS,
|
|
@@ -3809,6 +4554,7 @@ function doctor(bundle, options = {}) {
|
|
|
3809
4554
|
counts,
|
|
3810
4555
|
groups,
|
|
3811
4556
|
findingCount,
|
|
4557
|
+
anchorResolvers: anchorResolverCounts(bundle),
|
|
3812
4558
|
healthy: findingCount === 0
|
|
3813
4559
|
};
|
|
3814
4560
|
}
|
|
@@ -3825,18 +4571,18 @@ function expired(hits, now) {
|
|
|
3825
4571
|
for (const hit of hits) {
|
|
3826
4572
|
const raw = hit.record.frontmatter.stale_after;
|
|
3827
4573
|
if (!raw) continue;
|
|
3828
|
-
const
|
|
3829
|
-
if (Number.isNaN(
|
|
4574
|
+
const at2 = Date.parse(raw);
|
|
4575
|
+
if (Number.isNaN(at2)) {
|
|
3830
4576
|
findings.push(
|
|
3831
4577
|
finding(hit.record, `stale_after "${raw}" is not a readable date`)
|
|
3832
4578
|
);
|
|
3833
4579
|
continue;
|
|
3834
4580
|
}
|
|
3835
|
-
if (
|
|
4581
|
+
if (at2 < now.getTime()) {
|
|
3836
4582
|
findings.push(
|
|
3837
4583
|
finding(
|
|
3838
4584
|
hit.record,
|
|
3839
|
-
`stale since ${raw} (${daysBetween(
|
|
4585
|
+
`stale since ${raw} (${daysBetween(at2, now.getTime())} days ago)`
|
|
3840
4586
|
)
|
|
3841
4587
|
);
|
|
3842
4588
|
}
|
|
@@ -3849,12 +4595,12 @@ function expiring(hits, now, withinDays) {
|
|
|
3849
4595
|
for (const hit of hits) {
|
|
3850
4596
|
const raw = hit.record.frontmatter.stale_after;
|
|
3851
4597
|
if (!raw) continue;
|
|
3852
|
-
const
|
|
3853
|
-
if (Number.isNaN(
|
|
4598
|
+
const at2 = Date.parse(raw);
|
|
4599
|
+
if (Number.isNaN(at2) || at2 < now.getTime() || at2 > horizon) continue;
|
|
3854
4600
|
findings.push(
|
|
3855
4601
|
finding(
|
|
3856
4602
|
hit.record,
|
|
3857
|
-
`goes stale ${raw} (in ${daysBetween(now.getTime(),
|
|
4603
|
+
`goes stale ${raw} (in ${daysBetween(now.getTime(), at2)} days)`
|
|
3858
4604
|
)
|
|
3859
4605
|
);
|
|
3860
4606
|
}
|
|
@@ -4024,13 +4770,16 @@ function anchorFindings(hits, kind, headline) {
|
|
|
4024
4770
|
);
|
|
4025
4771
|
}
|
|
4026
4772
|
function describeAnchor(anchor) {
|
|
4027
|
-
const
|
|
4028
|
-
if (anchor.
|
|
4773
|
+
const at2 = anchor.symbol ? `${anchor.file}:${anchor.symbol}` : anchor.file;
|
|
4774
|
+
if (anchor.class === "gone") {
|
|
4775
|
+
return `${at2} gone${anchor.reason ? ` (${anchor.reason})` : ""}`;
|
|
4776
|
+
}
|
|
4777
|
+
if (anchor.reason) return `${at2} (${anchor.reason})`;
|
|
4029
4778
|
if (anchor.remoteState === "drifted-on-default") {
|
|
4030
|
-
return `${
|
|
4779
|
+
return `${at2} (matches ref, moved on the default branch)`;
|
|
4031
4780
|
}
|
|
4032
|
-
if (anchor.diffSize === null) return `${
|
|
4033
|
-
return anchor.diffSize === 0 ? `${
|
|
4781
|
+
if (anchor.diffSize === null) return `${at2} (changed, size unrecorded)`;
|
|
4782
|
+
return anchor.diffSize === 0 ? `${at2} (content changed, same line count)` : `${at2} (${anchor.diffSize} line${anchor.diffSize === 1 ? "" : "s"} apart)`;
|
|
4034
4783
|
}
|
|
4035
4784
|
function replaces(later, earlier) {
|
|
4036
4785
|
return (later.frontmatter.strauss_supersedes ?? []).includes(earlier.conceptId) || earlier.frontmatter.strauss_superseded_by === later.conceptId;
|
|
@@ -4047,20 +4796,20 @@ function daysBetween(from, to) {
|
|
|
4047
4796
|
return Math.max(0, Math.floor((to - from) / DAY_MS));
|
|
4048
4797
|
}
|
|
4049
4798
|
function ageInDays(record, now) {
|
|
4050
|
-
const
|
|
4051
|
-
if (!
|
|
4052
|
-
const written = Date.parse(
|
|
4799
|
+
const at2 = record.frontmatter.generated?.at;
|
|
4800
|
+
if (!at2) return null;
|
|
4801
|
+
const written = Date.parse(at2);
|
|
4053
4802
|
if (Number.isNaN(written)) return null;
|
|
4054
4803
|
return daysBetween(written, now.getTime());
|
|
4055
4804
|
}
|
|
4056
4805
|
|
|
4057
4806
|
// src/decision-record.ts
|
|
4058
|
-
var
|
|
4807
|
+
var import_zod7 = require("zod");
|
|
4059
4808
|
var DECISION_TYPE = "decision";
|
|
4060
4809
|
var NO_DECISION_SLUG = "none";
|
|
4061
4810
|
var decisionInputSchema = composeInputSchema.omit({ sections: true }).extend({
|
|
4062
|
-
alternative:
|
|
4063
|
-
impact:
|
|
4811
|
+
alternative: import_zod7.z.string().min(1).optional(),
|
|
4812
|
+
impact: import_zod7.z.string().min(1).optional()
|
|
4064
4813
|
}).strict();
|
|
4065
4814
|
function composeDecisionRecord(input, writtenBy, writtenAt) {
|
|
4066
4815
|
const { alternative, impact: impact2, ...rest } = input;
|
|
@@ -4102,13 +4851,13 @@ function selectDecisions(records) {
|
|
|
4102
4851
|
}
|
|
4103
4852
|
|
|
4104
4853
|
// src/commands/anchor-resolve.ts
|
|
4105
|
-
var
|
|
4854
|
+
var import_zod9 = require("zod");
|
|
4106
4855
|
|
|
4107
4856
|
// src/commands/model.ts
|
|
4108
|
-
var
|
|
4109
|
-
var bundlePath =
|
|
4110
|
-
var conceptId =
|
|
4111
|
-
var REPO_ROOT =
|
|
4857
|
+
var import_zod8 = require("zod");
|
|
4858
|
+
var bundlePath = import_zod8.z.string().min(1).describe("Absolute path to the knowledge base directory.");
|
|
4859
|
+
var conceptId = import_zod8.z.string().min(1).describe("e.g. decision.cursor-v2");
|
|
4860
|
+
var REPO_ROOT = import_zod8.z.string().min(1).optional().describe(
|
|
4112
4861
|
"Where the anchored source lives, for the drift check. Defaults to the working directory."
|
|
4113
4862
|
);
|
|
4114
4863
|
function define(command) {
|
|
@@ -4121,9 +4870,9 @@ function argvFlag(argv, name) {
|
|
|
4121
4870
|
if (!value2) throw new KbMissingFlagValueError(name);
|
|
4122
4871
|
return value2;
|
|
4123
4872
|
}
|
|
4124
|
-
const
|
|
4125
|
-
if (
|
|
4126
|
-
const value = argv[
|
|
4873
|
+
const at2 = argv.indexOf(name);
|
|
4874
|
+
if (at2 === -1) return void 0;
|
|
4875
|
+
const value = argv[at2 + 1];
|
|
4127
4876
|
if (value === void 0 || value.startsWith("--")) {
|
|
4128
4877
|
throw new KbMissingFlagValueError(name);
|
|
4129
4878
|
}
|
|
@@ -4131,22 +4880,30 @@ function argvFlag(argv, name) {
|
|
|
4131
4880
|
}
|
|
4132
4881
|
|
|
4133
4882
|
// src/commands/anchor-resolve.ts
|
|
4883
|
+
function resolverSummary(results) {
|
|
4884
|
+
const names = [
|
|
4885
|
+
...new Set(
|
|
4886
|
+
results.flatMap((entry) => entry.resolver ? [entry.resolver] : [])
|
|
4887
|
+
)
|
|
4888
|
+
].sort();
|
|
4889
|
+
return names.length ? `${names.join(" + ")} resolver` : "whole-file";
|
|
4890
|
+
}
|
|
4134
4891
|
var anchorResolveCommand = define({
|
|
4135
4892
|
name: "anchor-resolve",
|
|
4136
4893
|
tool: "kb_anchor_resolve",
|
|
4137
4894
|
usage: "anchor-resolve <concept-id> [--repo-root <path>] [--offline] [--rebaseline] [--restamp]",
|
|
4138
4895
|
description: "Resolve a record's anchors: stamp a hash onto anchors that lack one, report drift where the code moved. An anchor naming another repository is read from that remote through a bare cache; --offline uses the cache only. kb_verify's mechanical counterpart \u2014 reach for it when the question is whether the code still is what it was. Exits non-zero on drift.",
|
|
4139
|
-
input:
|
|
4896
|
+
input: import_zod9.z.object({
|
|
4140
4897
|
bundlePath,
|
|
4141
4898
|
conceptId,
|
|
4142
|
-
repoRoot:
|
|
4143
|
-
offline:
|
|
4899
|
+
repoRoot: import_zod9.z.string().min(1).optional(),
|
|
4900
|
+
offline: import_zod9.z.boolean().optional().describe(
|
|
4144
4901
|
"Resolve foreign anchors from the local repo cache only, never fetching."
|
|
4145
4902
|
),
|
|
4146
|
-
rebaseline:
|
|
4903
|
+
rebaseline: import_zod9.z.boolean().optional().describe(
|
|
4147
4904
|
"Accept the current code as the new baseline for anchors that drifted."
|
|
4148
4905
|
),
|
|
4149
|
-
restamp:
|
|
4906
|
+
restamp: import_zod9.z.boolean().optional().describe(
|
|
4150
4907
|
"Refresh `resolved_at` on anchors that already match. Off by default, so a green run writes nothing."
|
|
4151
4908
|
)
|
|
4152
4909
|
}),
|
|
@@ -4175,6 +4932,11 @@ var anchorResolveCommand = define({
|
|
|
4175
4932
|
const updated = [];
|
|
4176
4933
|
let dirty = false;
|
|
4177
4934
|
const sources = await readSources(anchors, root, offline === true);
|
|
4935
|
+
const resolvers = defaultAnchorResolvers({ offline: offline === true });
|
|
4936
|
+
await prepareResolvers(
|
|
4937
|
+
resolvers,
|
|
4938
|
+
anchors.map((anchor) => anchor.file)
|
|
4939
|
+
);
|
|
4178
4940
|
for (const anchor of anchors) {
|
|
4179
4941
|
const base2 = {
|
|
4180
4942
|
file: anchor.file,
|
|
@@ -4191,27 +4953,39 @@ var anchorResolveCommand = define({
|
|
|
4191
4953
|
updated.push(anchor);
|
|
4192
4954
|
continue;
|
|
4193
4955
|
}
|
|
4194
|
-
const
|
|
4195
|
-
if (!
|
|
4956
|
+
const outcome = resolveAnchorSpan(source.source, anchor, resolvers);
|
|
4957
|
+
if (!outcome.ok) {
|
|
4196
4958
|
results.push({
|
|
4197
4959
|
...base2,
|
|
4198
4960
|
state: "unresolved",
|
|
4199
|
-
reason:
|
|
4961
|
+
reason: outcome.reason
|
|
4200
4962
|
});
|
|
4201
4963
|
updated.push(anchor);
|
|
4202
4964
|
continue;
|
|
4203
4965
|
}
|
|
4204
|
-
const
|
|
4966
|
+
const resolved = outcome.span;
|
|
4967
|
+
const producedBy = outcome.resolver;
|
|
4968
|
+
const { hash: currentHash, kind } = anchorHashOf(anchor, outcome);
|
|
4205
4969
|
const currentLines = resolved.endLine - resolved.startLine + 1;
|
|
4970
|
+
const stampedKind = outcome.normalized ? "ast" : "raw";
|
|
4971
|
+
const stampedHash = outcome.normalized ? anchorHashOf({ ...anchor, hash: void 0 }, outcome).hash : currentHash;
|
|
4206
4972
|
const stamped = {
|
|
4207
4973
|
...anchor,
|
|
4208
|
-
hash:
|
|
4974
|
+
hash: stampedHash,
|
|
4975
|
+
hash_kind: stampedKind,
|
|
4209
4976
|
lines: currentLines,
|
|
4210
|
-
resolved_at: now()
|
|
4977
|
+
resolved_at: now(),
|
|
4978
|
+
...producedBy ? { resolver: producedBy } : {}
|
|
4211
4979
|
};
|
|
4212
4980
|
const pinned = anchor.ref !== void 0 && source.repo !== void 0;
|
|
4213
4981
|
if (!anchor.hash) {
|
|
4214
|
-
results.push({
|
|
4982
|
+
results.push({
|
|
4983
|
+
...base2,
|
|
4984
|
+
state: "stamped",
|
|
4985
|
+
currentHash: stampedHash,
|
|
4986
|
+
hashKind: stampedKind,
|
|
4987
|
+
...producedBy ? { resolver: producedBy } : {}
|
|
4988
|
+
});
|
|
4215
4989
|
updated.push(stamped);
|
|
4216
4990
|
dirty = true;
|
|
4217
4991
|
continue;
|
|
@@ -4221,7 +4995,12 @@ var anchorResolveCommand = define({
|
|
|
4221
4995
|
...base2,
|
|
4222
4996
|
state: "drifted",
|
|
4223
4997
|
currentHash,
|
|
4998
|
+
hashKind: kind,
|
|
4224
4999
|
diffSize: lineDelta(anchor, currentLines),
|
|
5000
|
+
...producedBy ? { resolver: producedBy } : {},
|
|
5001
|
+
// A regex-stamped anchor re-read by tree-sitter drifts because the
|
|
5002
|
+
// resolver changed, not because the code did.
|
|
5003
|
+
...resolverChanged(source.source, anchor, producedBy) ? { reason: "resolver-changed" } : {},
|
|
4225
5004
|
...pinned ? { remoteState: "drifted-from-ref" } : {},
|
|
4226
5005
|
...rebaseline ? { rebaselined: true } : {}
|
|
4227
5006
|
});
|
|
@@ -4229,7 +5008,7 @@ var anchorResolveCommand = define({
|
|
|
4229
5008
|
if (rebaseline) dirty = true;
|
|
4230
5009
|
continue;
|
|
4231
5010
|
}
|
|
4232
|
-
const onDefault = pinned ? headHash(source, anchor) : void 0;
|
|
5011
|
+
const onDefault = pinned ? headHash(source, anchor, resolvers) : void 0;
|
|
4233
5012
|
if (onDefault && onDefault.hash !== anchor.hash) {
|
|
4234
5013
|
results.push({
|
|
4235
5014
|
...base2,
|
|
@@ -4245,6 +5024,8 @@ var anchorResolveCommand = define({
|
|
|
4245
5024
|
...base2,
|
|
4246
5025
|
state: "match",
|
|
4247
5026
|
currentHash,
|
|
5027
|
+
hashKind: kind,
|
|
5028
|
+
...producedBy ? { resolver: producedBy } : {},
|
|
4248
5029
|
...pinned ? { remoteState: "matches-ref" } : {}
|
|
4249
5030
|
});
|
|
4250
5031
|
const refresh = restamp || anchor.resolved_at === void 0;
|
|
@@ -4262,19 +5043,21 @@ var anchorResolveCommand = define({
|
|
|
4262
5043
|
if (!frozen) await store.updateAnchors(path, id, updated, actor);
|
|
4263
5044
|
}
|
|
4264
5045
|
const frozenNote = frozen ? { frozen: true, note: "base is frozen: nothing was stamped" } : {};
|
|
5046
|
+
const hints = grammarHints();
|
|
5047
|
+
const hintNote = hints.length ? { hints } : {};
|
|
4265
5048
|
const unreachable = results.filter(
|
|
4266
5049
|
(entry) => isUncheckedReason(entry.reason)
|
|
4267
5050
|
).length;
|
|
4268
5051
|
const checked = results.length - unreachable;
|
|
4269
|
-
const
|
|
4270
|
-
const note = `${
|
|
4271
|
-
const clean = checked > 0 &&
|
|
5052
|
+
const matches3 = results.filter((entry) => entry.state === "match").length;
|
|
5053
|
+
const note = `${matches3}/${checked} anchors match${unreachable ? `, ${unreachable} unreachable` : ""}`;
|
|
5054
|
+
const clean = checked > 0 && matches3 === checked && unreachable === 0;
|
|
4272
5055
|
if (clean) {
|
|
4273
5056
|
try {
|
|
4274
5057
|
await store.verify(
|
|
4275
5058
|
path,
|
|
4276
5059
|
id,
|
|
4277
|
-
`anchor-resolve: ${note} (
|
|
5060
|
+
`anchor-resolve: ${note} (${resolverSummary(results)})`,
|
|
4278
5061
|
actor,
|
|
4279
5062
|
now()
|
|
4280
5063
|
);
|
|
@@ -4285,17 +5068,25 @@ var anchorResolveCommand = define({
|
|
|
4285
5068
|
results,
|
|
4286
5069
|
verified: false,
|
|
4287
5070
|
verifyRefused: "self-verification",
|
|
4288
|
-
...frozenNote
|
|
5071
|
+
...frozenNote,
|
|
5072
|
+
...hintNote
|
|
4289
5073
|
};
|
|
4290
5074
|
}
|
|
4291
|
-
return {
|
|
5075
|
+
return {
|
|
5076
|
+
conceptId: id,
|
|
5077
|
+
results,
|
|
5078
|
+
verified: true,
|
|
5079
|
+
...frozenNote,
|
|
5080
|
+
...hintNote
|
|
5081
|
+
};
|
|
4292
5082
|
}
|
|
4293
5083
|
return {
|
|
4294
5084
|
conceptId: id,
|
|
4295
5085
|
results,
|
|
4296
5086
|
verified: false,
|
|
4297
5087
|
...unreachable ? { note } : {},
|
|
4298
|
-
...frozenNote
|
|
5088
|
+
...frozenNote,
|
|
5089
|
+
...hintNote
|
|
4299
5090
|
};
|
|
4300
5091
|
},
|
|
4301
5092
|
// A stored hash that no longer resolves is a broken anchor, not an absence:
|
|
@@ -4311,13 +5102,13 @@ var anchorResolveCommand = define({
|
|
|
4311
5102
|
function lineDelta(anchor, current) {
|
|
4312
5103
|
return anchor.lines === void 0 ? null : Math.abs(current - anchor.lines);
|
|
4313
5104
|
}
|
|
4314
|
-
function headHash(source, anchor) {
|
|
5105
|
+
function headHash(source, anchor, resolvers) {
|
|
4315
5106
|
if (source.head === void 0) return void 0;
|
|
4316
|
-
const
|
|
4317
|
-
if (!
|
|
5107
|
+
const outcome = resolveAnchorSpan(source.head, anchor, resolvers);
|
|
5108
|
+
if (!outcome.ok) return void 0;
|
|
4318
5109
|
return {
|
|
4319
|
-
hash: hashAnchorText(
|
|
4320
|
-
lines:
|
|
5110
|
+
hash: hashAnchorText(outcome.span.text),
|
|
5111
|
+
lines: outcome.span.endLine - outcome.span.startLine + 1
|
|
4321
5112
|
};
|
|
4322
5113
|
}
|
|
4323
5114
|
async function readSources(anchors, root, offline) {
|
|
@@ -4367,13 +5158,13 @@ async function readSources(anchors, root, offline) {
|
|
|
4367
5158
|
}
|
|
4368
5159
|
|
|
4369
5160
|
// src/commands/answer.ts
|
|
4370
|
-
var
|
|
5161
|
+
var import_zod10 = require("zod");
|
|
4371
5162
|
var answerCommand = define({
|
|
4372
5163
|
name: "answer",
|
|
4373
5164
|
tool: "kb_answer",
|
|
4374
5165
|
usage: "answer <concept-id> <answer...>",
|
|
4375
5166
|
description: "Resolve an open question: set status, stamp who and when, append an Answer section. If the answer overturns a decision or assumption, supersede that record explicitly.",
|
|
4376
|
-
input:
|
|
5167
|
+
input: import_zod10.z.object({ bundlePath, conceptId, answer: import_zod10.z.string().min(1) }),
|
|
4377
5168
|
fromArgv: (argv, path) => ({
|
|
4378
5169
|
bundlePath: path,
|
|
4379
5170
|
conceptId: argv[1],
|
|
@@ -4387,27 +5178,27 @@ var answerCommand = define({
|
|
|
4387
5178
|
});
|
|
4388
5179
|
|
|
4389
5180
|
// src/commands/backlinks.ts
|
|
4390
|
-
var
|
|
5181
|
+
var import_zod11 = require("zod");
|
|
4391
5182
|
var backlinksCommand = define({
|
|
4392
5183
|
name: "backlinks",
|
|
4393
5184
|
tool: "kb_backlinks",
|
|
4394
5185
|
usage: "backlinks <concept-id>",
|
|
4395
5186
|
description: "Who points at this record: every inbound typed causal link (`strauss_links`), one hop, every rel including `related_to`, each with its rel and the standing of the record that made it. Use it when you need the exact edges \u2014 reviewing or renaming a record.",
|
|
4396
|
-
input:
|
|
5187
|
+
input: import_zod11.z.object({ bundlePath, conceptId }),
|
|
4397
5188
|
fromArgv: (argv, path) => ({ bundlePath: path, conceptId: argv[1] }),
|
|
4398
5189
|
run: async ({ store }, { bundlePath: path, conceptId: id }) => store.backlinks(path, id)
|
|
4399
5190
|
});
|
|
4400
5191
|
|
|
4401
5192
|
// src/commands/catalog.ts
|
|
4402
|
-
var
|
|
5193
|
+
var import_zod12 = require("zod");
|
|
4403
5194
|
var catalogCommand = define({
|
|
4404
5195
|
name: "catalog",
|
|
4405
5196
|
tool: "kb_catalog",
|
|
4406
5197
|
usage: "catalog [type]",
|
|
4407
5198
|
description: "Lists every record as one line \u2014 concept id, type, title, standing, and a stale flag \u2014 at roughly thirty tokens each. Pick this over kb_load once kb_load refuses: kb_catalog never refuses. Superseded records show only their replacement; fetch bodies with kb_load, kb_pack, kb_query, or kb_trace.",
|
|
4408
|
-
input:
|
|
5199
|
+
input: import_zod12.z.object({
|
|
4409
5200
|
bundlePath,
|
|
4410
|
-
type:
|
|
5201
|
+
type: import_zod12.z.enum(KB_RECORD_TYPES).optional()
|
|
4411
5202
|
}),
|
|
4412
5203
|
fromArgv: (argv, path) => ({
|
|
4413
5204
|
bundlePath: path,
|
|
@@ -4462,26 +5253,26 @@ function count(value, noun) {
|
|
|
4462
5253
|
}
|
|
4463
5254
|
|
|
4464
5255
|
// src/commands/context.ts
|
|
4465
|
-
var
|
|
5256
|
+
var import_zod13 = require("zod");
|
|
4466
5257
|
var contextCommand = define({
|
|
4467
5258
|
name: "context",
|
|
4468
5259
|
tool: "kb_context",
|
|
4469
5260
|
usage: "context [--profile NAME] [--budget N] [--full-under N] [--format json] [--event NAME]",
|
|
4470
5261
|
description: "Index block of pinned bases (ids, titles, standing) for injection at context birth. Takes no bundlePath \u2014 reads the workspace pin manifests. Empty when nothing is pinned; refuses over budget rather than truncating. Budget precedence: flags, then the manifest `context[profile]` over `context.default`, then the built-in profile, then package defaults.",
|
|
4471
|
-
input:
|
|
4472
|
-
budgetTokens:
|
|
5262
|
+
input: import_zod13.z.object({
|
|
5263
|
+
budgetTokens: import_zod13.z.number().int().positive().optional().describe(
|
|
4473
5264
|
"Ceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000."
|
|
4474
5265
|
),
|
|
4475
|
-
fullUnderTokens:
|
|
5266
|
+
fullUnderTokens: import_zod13.z.number().int().positive().optional().describe(
|
|
4476
5267
|
"Per-base rendering threshold, applied before the budget: a base whose complete load fits under this arrives as full records instead of index lines, and the whole block still answers to budgetTokens. Off by default \u2014 index-only is the safe default at a context birth, because injected bodies outlive the qualifiers on them; the session-start profile opts tiny bases in at 1500."
|
|
4477
5268
|
),
|
|
4478
|
-
profile:
|
|
5269
|
+
profile: import_zod13.z.string().optional().describe(
|
|
4479
5270
|
"Named budget set: built-ins are session-start (full-under 1500), compact and turn (budget 2500); the manifests' `context` tables override per repo. Unknown names fall through to defaults rather than failing."
|
|
4480
5271
|
),
|
|
4481
|
-
format:
|
|
5272
|
+
format: import_zod13.z.enum(["markdown", "json"]).optional().describe(
|
|
4482
5273
|
"CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
|
|
4483
5274
|
),
|
|
4484
|
-
event:
|
|
5275
|
+
event: import_zod13.z.string().optional().describe(
|
|
4485
5276
|
"hookEventName stamped into the JSON envelope. Only meaningful with format=json."
|
|
4486
5277
|
)
|
|
4487
5278
|
}),
|
|
@@ -4517,14 +5308,567 @@ var contextCommand = define({
|
|
|
4517
5308
|
});
|
|
4518
5309
|
|
|
4519
5310
|
// src/commands/doctor.ts
|
|
4520
|
-
var
|
|
4521
|
-
|
|
5311
|
+
var import_zod15 = require("zod");
|
|
5312
|
+
|
|
5313
|
+
// src/drift/git.ts
|
|
5314
|
+
var import_node_child_process3 = require("child_process");
|
|
5315
|
+
var import_node_util3 = require("util");
|
|
5316
|
+
var execFileAsync3 = (0, import_node_util3.promisify)(import_node_child_process3.execFile);
|
|
5317
|
+
var MAX_GIT_OUTPUT_BYTES = 1048576;
|
|
5318
|
+
var GIT_TIMEOUT_MS = 5e3;
|
|
5319
|
+
async function git2(cwd, args) {
|
|
5320
|
+
const env = { ...process.env };
|
|
5321
|
+
delete env["GIT_DIR"];
|
|
5322
|
+
delete env["GIT_WORK_TREE"];
|
|
5323
|
+
delete env["GIT_INDEX_FILE"];
|
|
5324
|
+
try {
|
|
5325
|
+
const { stdout } = await execFileAsync3("git", ["-C", cwd, ...args], {
|
|
5326
|
+
timeout: GIT_TIMEOUT_MS,
|
|
5327
|
+
maxBuffer: MAX_GIT_OUTPUT_BYTES,
|
|
5328
|
+
env
|
|
5329
|
+
});
|
|
5330
|
+
return { ok: true, stdout };
|
|
5331
|
+
} catch {
|
|
5332
|
+
return { ok: false };
|
|
5333
|
+
}
|
|
5334
|
+
}
|
|
5335
|
+
async function listRepoFiles(repoRoot) {
|
|
5336
|
+
const result = await git2(repoRoot, ["ls-files", "-z", "--cached"]);
|
|
5337
|
+
if (!result.ok) return [];
|
|
5338
|
+
return result.stdout.split("\0").filter(Boolean);
|
|
5339
|
+
}
|
|
5340
|
+
async function readOldSource(repoRoot, anchor) {
|
|
5341
|
+
if (!filePathIsSafe(anchor.file))
|
|
5342
|
+
return { ok: false, reason: "unrecoverable" };
|
|
5343
|
+
if (anchor.ref && refShapeIsSafe(anchor.ref)) {
|
|
5344
|
+
const shown2 = await showFile(repoRoot, anchor.ref, anchor.file);
|
|
5345
|
+
if (shown2 !== null) {
|
|
5346
|
+
return {
|
|
5347
|
+
ok: true,
|
|
5348
|
+
source: shown2,
|
|
5349
|
+
origin: { kind: "ref", ref: anchor.ref }
|
|
5350
|
+
};
|
|
5351
|
+
}
|
|
5352
|
+
}
|
|
5353
|
+
const at2 = anchor.resolved_at;
|
|
5354
|
+
if (!at2 || Number.isNaN(Date.parse(at2))) {
|
|
5355
|
+
return { ok: false, reason: "unrecoverable" };
|
|
5356
|
+
}
|
|
5357
|
+
const found = await git2(repoRoot, [
|
|
5358
|
+
"log",
|
|
5359
|
+
"-1",
|
|
5360
|
+
"--format=%H",
|
|
5361
|
+
`--before=${at2}`,
|
|
5362
|
+
"--end-of-options",
|
|
5363
|
+
"HEAD",
|
|
5364
|
+
"--",
|
|
5365
|
+
anchor.file
|
|
5366
|
+
]);
|
|
5367
|
+
const sha = found.ok ? found.stdout.trim() : "";
|
|
5368
|
+
if (!sha || !refShapeIsSafe(sha))
|
|
5369
|
+
return { ok: false, reason: "unrecoverable" };
|
|
5370
|
+
const shown = await showFile(repoRoot, sha, anchor.file);
|
|
5371
|
+
if (shown === null) return { ok: false, reason: "unrecoverable" };
|
|
5372
|
+
return { ok: true, source: shown, origin: { kind: "history", ref: sha } };
|
|
5373
|
+
}
|
|
5374
|
+
async function showFile(repoRoot, ref, file) {
|
|
5375
|
+
const path = file.replace(/^\.\//, "");
|
|
5376
|
+
const result = await git2(repoRoot, [
|
|
5377
|
+
"show",
|
|
5378
|
+
"--end-of-options",
|
|
5379
|
+
`${ref}:${path}`
|
|
5380
|
+
]);
|
|
5381
|
+
return result.ok ? result.stdout : null;
|
|
5382
|
+
}
|
|
5383
|
+
|
|
5384
|
+
// src/drift/moved.ts
|
|
5385
|
+
var import_promises9 = require("fs/promises");
|
|
5386
|
+
var MAX_MOVED_SEARCH_FILES = 2e3;
|
|
5387
|
+
var SEARCH_BATCH = 64;
|
|
5388
|
+
function movedSearch(repoRoot, options = {}) {
|
|
5389
|
+
const read = options.reader ?? anchorFileReader(repoRoot);
|
|
5390
|
+
const sizeOf = options.sizeOf ?? diskSize(repoRoot);
|
|
5391
|
+
const resolver = new TreeSitterResolver();
|
|
5392
|
+
let repoFiles;
|
|
5393
|
+
const prepared = /* @__PURE__ */ new Set();
|
|
5394
|
+
const filesForLanguage = async (language) => {
|
|
5395
|
+
repoFiles ??= listRepoFiles(repoRoot);
|
|
5396
|
+
return (await repoFiles).filter((file) => languageForFile(file) === language).slice(0, MAX_MOVED_SEARCH_FILES);
|
|
5397
|
+
};
|
|
5398
|
+
return {
|
|
5399
|
+
async find(anchor) {
|
|
5400
|
+
const stored = anchor.hash;
|
|
5401
|
+
if (!stored) return void 0;
|
|
5402
|
+
const language = languageForFile(anchor.file);
|
|
5403
|
+
if (!language) return sameFileWindow(anchor, read, stored);
|
|
5404
|
+
const candidates = await filesForLanguage(language);
|
|
5405
|
+
if (!prepared.has(language)) {
|
|
5406
|
+
await resolver.prepare(candidates.length ? candidates : [anchor.file]);
|
|
5407
|
+
prepared.add(language);
|
|
5408
|
+
}
|
|
5409
|
+
const floor = anchor.lines ?? 0;
|
|
5410
|
+
for (let at2 = 0; at2 < candidates.length; at2 += SEARCH_BATCH) {
|
|
5411
|
+
const batch = candidates.slice(at2, at2 + SEARCH_BATCH);
|
|
5412
|
+
const hits = await mapLimit(
|
|
5413
|
+
batch,
|
|
5414
|
+
DEFAULT_IO_CONCURRENCY,
|
|
5415
|
+
async (file) => {
|
|
5416
|
+
const size2 = await sizeOf(file);
|
|
5417
|
+
if (size2 !== null && size2 < floor) return void 0;
|
|
5418
|
+
return matchIn(resolver, read, anchor, stored, file);
|
|
5419
|
+
}
|
|
5420
|
+
);
|
|
5421
|
+
const found = hits.find((hit) => hit !== void 0);
|
|
5422
|
+
if (found) return found;
|
|
5423
|
+
}
|
|
5424
|
+
return void 0;
|
|
5425
|
+
}
|
|
5426
|
+
};
|
|
5427
|
+
}
|
|
5428
|
+
async function matchIn(resolver, read, anchor, stored, file) {
|
|
5429
|
+
const source = await read(file);
|
|
5430
|
+
if (!source.ok) return void 0;
|
|
5431
|
+
const normalized = source.source.replace(/\r\n/g, "\n");
|
|
5432
|
+
for (const found of resolver.spans(normalized, file)) {
|
|
5433
|
+
const text = anchor.hash_kind === "ast" ? resolver.normalize(found.span.text, file) : found.span.text;
|
|
5434
|
+
if (text === null || hashAnchorText(text) !== stored) continue;
|
|
5435
|
+
if (file === anchor.file && found.symbol === anchor.symbol) continue;
|
|
5436
|
+
return {
|
|
5437
|
+
file,
|
|
5438
|
+
symbol: found.symbol,
|
|
5439
|
+
startLine: found.span.startLine,
|
|
5440
|
+
endLine: found.span.endLine
|
|
5441
|
+
};
|
|
5442
|
+
}
|
|
5443
|
+
return void 0;
|
|
5444
|
+
}
|
|
5445
|
+
function diskSize(repoRoot) {
|
|
5446
|
+
return async (file) => {
|
|
5447
|
+
const path = anchorFilePath(repoRoot, file);
|
|
5448
|
+
if (path === null) return null;
|
|
5449
|
+
try {
|
|
5450
|
+
return (await (0, import_promises9.stat)(path)).size;
|
|
5451
|
+
} catch {
|
|
5452
|
+
return null;
|
|
5453
|
+
}
|
|
5454
|
+
};
|
|
5455
|
+
}
|
|
5456
|
+
async function sameFileWindow(anchor, read, stored) {
|
|
5457
|
+
const height = anchor.lines;
|
|
5458
|
+
if (!height || anchor.hash_kind === "ast") return void 0;
|
|
5459
|
+
const source = await read(anchor.file);
|
|
5460
|
+
if (!source.ok) return void 0;
|
|
5461
|
+
const lines = source.source.replace(/\r\n/g, "\n").split("\n");
|
|
5462
|
+
for (let at2 = 0; at2 + height <= lines.length; at2++) {
|
|
5463
|
+
if (hashAnchorText(lines.slice(at2, at2 + height).join("\n")) !== stored) {
|
|
5464
|
+
continue;
|
|
5465
|
+
}
|
|
5466
|
+
return {
|
|
5467
|
+
file: anchor.file,
|
|
5468
|
+
...anchor.symbol ? { symbol: anchor.symbol } : {},
|
|
5469
|
+
startLine: at2 + 1,
|
|
5470
|
+
endLine: at2 + height
|
|
5471
|
+
};
|
|
5472
|
+
}
|
|
5473
|
+
return void 0;
|
|
5474
|
+
}
|
|
5475
|
+
|
|
5476
|
+
// src/drift/classify.ts
|
|
5477
|
+
async function classifyDrift(repoRoot, record, entries, options = {}) {
|
|
5478
|
+
const anchors = (record.frontmatter.strauss_anchors ?? []).filter(
|
|
5479
|
+
(anchor) => anchor.hash
|
|
5480
|
+
);
|
|
5481
|
+
const reader = options.reader ?? anchorFileReader(repoRoot);
|
|
5482
|
+
const treeSitter = new TreeSitterResolver();
|
|
5483
|
+
const resolvers = [treeSitter, regexResolver];
|
|
5484
|
+
const search = options.search ?? movedSearch(repoRoot, { ...options.reader ? { reader } : {} });
|
|
5485
|
+
const wanted = [];
|
|
5486
|
+
entries.forEach((entry, at2) => {
|
|
5487
|
+
const anchor = anchors[at2];
|
|
5488
|
+
if (!anchor) return;
|
|
5489
|
+
if (entry.state === "match" || isUncheckedReason(entry.reason)) return;
|
|
5490
|
+
wanted.push({ anchor, entry });
|
|
5491
|
+
});
|
|
5492
|
+
if (!wanted.length) return [];
|
|
5493
|
+
await prepareResolvers(
|
|
5494
|
+
resolvers,
|
|
5495
|
+
wanted.map(({ anchor }) => anchor.file)
|
|
5496
|
+
);
|
|
5497
|
+
const out = [];
|
|
5498
|
+
for (const { anchor, entry } of wanted) {
|
|
5499
|
+
const movedTo = await search.find(anchor);
|
|
5500
|
+
if (movedTo) {
|
|
5501
|
+
out.push({
|
|
5502
|
+
anchor,
|
|
5503
|
+
entry: { ...entry, class: "moved", movedTo },
|
|
5504
|
+
class: "moved"
|
|
5505
|
+
});
|
|
5506
|
+
continue;
|
|
5507
|
+
}
|
|
5508
|
+
const newText = await currentText(reader, anchor, resolvers);
|
|
5509
|
+
const old = options.withHistory === false ? { ok: false, reason: "unrecoverable" } : await readOldSource(repoRoot, anchor);
|
|
5510
|
+
const oldText = old.ok ? spanIn(old.source, anchor, resolvers) : void 0;
|
|
5511
|
+
const settled = newText !== void 0 && oldText !== void 0 && sameTokens(treeSitter, anchor.file, oldText, newText) ? "cosmetic" : entry.class ?? "changed";
|
|
5512
|
+
out.push({
|
|
5513
|
+
anchor,
|
|
5514
|
+
entry: { ...entry, class: settled },
|
|
5515
|
+
class: settled,
|
|
5516
|
+
...newText !== void 0 ? { newText } : {},
|
|
5517
|
+
...oldText !== void 0 ? { oldText } : {},
|
|
5518
|
+
...old.ok ? { oldOrigin: old.origin } : {}
|
|
5519
|
+
});
|
|
5520
|
+
}
|
|
5521
|
+
return out;
|
|
5522
|
+
}
|
|
5523
|
+
function sameTokens(resolver, file, before, after) {
|
|
5524
|
+
if (before === after) return false;
|
|
5525
|
+
const left = resolver.normalize(before, file);
|
|
5526
|
+
const right = resolver.normalize(after, file);
|
|
5527
|
+
return left !== null && left === right;
|
|
5528
|
+
}
|
|
5529
|
+
async function currentText(reader, anchor, resolvers) {
|
|
5530
|
+
const read = await reader(anchor.file);
|
|
5531
|
+
if (!read.ok) return void 0;
|
|
5532
|
+
return spanIn(read.source, anchor, resolvers);
|
|
5533
|
+
}
|
|
5534
|
+
function spanIn(source, anchor, resolvers) {
|
|
5535
|
+
const outcome = resolveAnchorSpan(source, anchor, resolvers);
|
|
5536
|
+
return outcome.ok ? outcome.span.text : void 0;
|
|
5537
|
+
}
|
|
5538
|
+
|
|
5539
|
+
// src/drift/diff.ts
|
|
5540
|
+
var MAX_ANCHOR_DIFF_LINES = 200;
|
|
5541
|
+
var PACKET_DIFF_LINE_BUDGET = 200;
|
|
5542
|
+
var MIN_ANCHOR_DIFF_LINES = 12;
|
|
5543
|
+
function diffBudget(anchors) {
|
|
5544
|
+
if (anchors <= 0) return MAX_ANCHOR_DIFF_LINES;
|
|
5545
|
+
return Math.min(
|
|
5546
|
+
MAX_ANCHOR_DIFF_LINES,
|
|
5547
|
+
Math.max(
|
|
5548
|
+
MIN_ANCHOR_DIFF_LINES,
|
|
5549
|
+
Math.floor(PACKET_DIFF_LINE_BUDGET / anchors)
|
|
5550
|
+
)
|
|
5551
|
+
);
|
|
5552
|
+
}
|
|
5553
|
+
function unifiedDiff(before, after, options = {}) {
|
|
5554
|
+
const max = options.maxLines ?? MAX_ANCHOR_DIFF_LINES;
|
|
5555
|
+
const left = before.replace(/\r\n/g, "\n").split("\n");
|
|
5556
|
+
const right = after.replace(/\r\n/g, "\n").split("\n");
|
|
5557
|
+
const body = [];
|
|
5558
|
+
let added = 0;
|
|
5559
|
+
let removed = 0;
|
|
5560
|
+
for (const edit of edits(left, right)) {
|
|
5561
|
+
if (edit.kind === "same") body.push(` ${edit.line}`);
|
|
5562
|
+
else if (edit.kind === "remove") {
|
|
5563
|
+
body.push(`-${edit.line}`);
|
|
5564
|
+
removed += 1;
|
|
5565
|
+
} else {
|
|
5566
|
+
body.push(`+${edit.line}`);
|
|
5567
|
+
added += 1;
|
|
5568
|
+
}
|
|
5569
|
+
}
|
|
5570
|
+
const truncated = body.length > max;
|
|
5571
|
+
const shown = truncated ? body.slice(0, max) : body;
|
|
5572
|
+
const header = `@@ -1,${left.length} +1,${right.length} @@${options.oldLabel ? ` ${options.oldLabel} \u2192 ${options.newLabel ?? ""}`.trimEnd() : ""}`;
|
|
5573
|
+
const lines = [header, ...shown];
|
|
5574
|
+
if (truncated) lines.push(`\u2026 ${body.length - max} more diff lines`);
|
|
5575
|
+
return { text: lines.join("\n"), added, removed, truncated };
|
|
5576
|
+
}
|
|
5577
|
+
function edits(left, right) {
|
|
5578
|
+
const rows = left.length;
|
|
5579
|
+
const cols = right.length;
|
|
5580
|
+
const table2 = Array.from(
|
|
5581
|
+
{ length: rows + 1 },
|
|
5582
|
+
() => new Array(cols + 1).fill(0)
|
|
5583
|
+
);
|
|
5584
|
+
for (let row2 = rows - 1; row2 >= 0; row2--) {
|
|
5585
|
+
for (let col2 = cols - 1; col2 >= 0; col2--) {
|
|
5586
|
+
table2[row2][col2] = left[row2] === right[col2] ? table2[row2 + 1][col2 + 1] + 1 : Math.max(
|
|
5587
|
+
table2[row2 + 1][col2],
|
|
5588
|
+
table2[row2][col2 + 1]
|
|
5589
|
+
);
|
|
5590
|
+
}
|
|
5591
|
+
}
|
|
5592
|
+
const out = [];
|
|
5593
|
+
let row = 0;
|
|
5594
|
+
let col = 0;
|
|
5595
|
+
while (row < rows && col < cols) {
|
|
5596
|
+
if (left[row] === right[col]) {
|
|
5597
|
+
out.push({ kind: "same", line: left[row] });
|
|
5598
|
+
row += 1;
|
|
5599
|
+
col += 1;
|
|
5600
|
+
} else if (table2[row + 1][col] >= table2[row][col + 1]) {
|
|
5601
|
+
out.push({ kind: "remove", line: left[row] });
|
|
5602
|
+
row += 1;
|
|
5603
|
+
} else {
|
|
5604
|
+
out.push({ kind: "add", line: right[col] });
|
|
5605
|
+
col += 1;
|
|
5606
|
+
}
|
|
5607
|
+
}
|
|
5608
|
+
for (; row < rows; row++)
|
|
5609
|
+
out.push({ kind: "remove", line: left[row] });
|
|
5610
|
+
for (; col < cols; col++)
|
|
5611
|
+
out.push({ kind: "add", line: right[col] });
|
|
5612
|
+
return out;
|
|
5613
|
+
}
|
|
5614
|
+
|
|
5615
|
+
// src/drift/packet.ts
|
|
5616
|
+
var PRESUMED_INVALID = [
|
|
5617
|
+
"fact",
|
|
5618
|
+
"constraint",
|
|
5619
|
+
"contract"
|
|
5620
|
+
];
|
|
5621
|
+
var RATIONALE_SURVIVES = ["decision", "risk"];
|
|
5622
|
+
var DEFAULT_NOTES = {
|
|
5623
|
+
"presumed-invalidated": "the code this claim was taken from changed; presume it no longer holds until re-read",
|
|
5624
|
+
"rationale-may-survive": "the reasoning may outlive the code that implemented it; check whether it does",
|
|
5625
|
+
review: "re-read the record against the new code"
|
|
5626
|
+
};
|
|
5627
|
+
async function reassessPacket(repoRoot, record, entries, options = {}) {
|
|
5628
|
+
const classified = await classifyDrift(repoRoot, record, entries, {
|
|
5629
|
+
...options.reader ? { reader: options.reader } : {},
|
|
5630
|
+
...options.search ? { search: options.search } : {},
|
|
5631
|
+
withHistory: options.withDiff !== false
|
|
5632
|
+
});
|
|
5633
|
+
const open = classified.filter(
|
|
5634
|
+
(found) => found.class === "changed" || found.class === "gone"
|
|
5635
|
+
);
|
|
5636
|
+
if (!open.length) return { packet: null, classified };
|
|
5637
|
+
const budget = diffBudget(open.length);
|
|
5638
|
+
const anchors = open.map(
|
|
5639
|
+
(found) => anchorPacket(found, options.withDiff === true, budget)
|
|
5640
|
+
);
|
|
5641
|
+
const type = record.frontmatter.type;
|
|
5642
|
+
const fallback = isKbRecordType(type) ? PRESUMED_INVALID.includes(type) ? "presumed-invalidated" : RATIONALE_SURVIVES.includes(type) ? "rationale-may-survive" : "review" : "review";
|
|
5643
|
+
return {
|
|
5644
|
+
classified,
|
|
5645
|
+
packet: {
|
|
5646
|
+
conceptId: record.conceptId,
|
|
5647
|
+
title: record.frontmatter.title ?? null,
|
|
5648
|
+
type,
|
|
5649
|
+
standing: options.standing ?? "unsettled",
|
|
5650
|
+
why: record.frontmatter.description ?? null,
|
|
5651
|
+
claim: claimOf(record),
|
|
5652
|
+
anchors,
|
|
5653
|
+
impact: (options.impact?.impacted ?? []).map((entry) => ({
|
|
5654
|
+
conceptId: entry.conceptId,
|
|
5655
|
+
title: entry.title,
|
|
5656
|
+
standing: entry.standing,
|
|
5657
|
+
depth: entry.depth
|
|
5658
|
+
})),
|
|
5659
|
+
impactTruncated: options.impact?.truncated ?? false,
|
|
5660
|
+
default: fallback,
|
|
5661
|
+
defaultNote: DEFAULT_NOTES[fallback]
|
|
5662
|
+
}
|
|
5663
|
+
};
|
|
5664
|
+
}
|
|
5665
|
+
function anchorPacket(found, withDiff, maxLines) {
|
|
5666
|
+
const { entry } = found;
|
|
5667
|
+
const base2 = {
|
|
5668
|
+
file: entry.file,
|
|
5669
|
+
...entry.symbol ? { symbol: entry.symbol } : {},
|
|
5670
|
+
class: found.class,
|
|
5671
|
+
...entry.reason ? { reason: entry.reason } : {},
|
|
5672
|
+
storedHash: entry.storedHash,
|
|
5673
|
+
...entry.currentHash ? { currentHash: entry.currentHash } : {},
|
|
5674
|
+
diffSize: entry.diffSize,
|
|
5675
|
+
...entry.movedTo ? { movedTo: entry.movedTo } : {}
|
|
5676
|
+
};
|
|
5677
|
+
if (!withDiff) return base2;
|
|
5678
|
+
if (found.oldText === void 0 || !found.oldOrigin) {
|
|
5679
|
+
return { ...base2, diff: { status: "unrecoverable" } };
|
|
5680
|
+
}
|
|
5681
|
+
const rendered = unifiedDiff(found.oldText, found.newText ?? "", {
|
|
5682
|
+
maxLines
|
|
5683
|
+
});
|
|
5684
|
+
return {
|
|
5685
|
+
...base2,
|
|
5686
|
+
diff: {
|
|
5687
|
+
status: "ok",
|
|
5688
|
+
source: found.oldOrigin.kind,
|
|
5689
|
+
ref: found.oldOrigin.ref,
|
|
5690
|
+
unified: rendered.text,
|
|
5691
|
+
added: rendered.added,
|
|
5692
|
+
removed: rendered.removed,
|
|
5693
|
+
truncated: rendered.truncated
|
|
5694
|
+
}
|
|
5695
|
+
};
|
|
5696
|
+
}
|
|
5697
|
+
function claimOf(record) {
|
|
5698
|
+
const type = record.frontmatter.type;
|
|
5699
|
+
const section = isKbRecordType(type) ? RECORD_TYPES[type].sections[0] : void 0;
|
|
5700
|
+
if (!section) return null;
|
|
5701
|
+
const lines = record.body.replace(/\r\n/g, "\n").split("\n");
|
|
5702
|
+
const start = lines.findIndex(
|
|
5703
|
+
(line) => line.trim().toLowerCase() === `## ${section}`.toLowerCase()
|
|
5704
|
+
);
|
|
5705
|
+
if (start < 0) return null;
|
|
5706
|
+
const rest = lines.slice(start + 1);
|
|
5707
|
+
const end = rest.findIndex((line) => line.startsWith("## "));
|
|
5708
|
+
const text = (end < 0 ? rest : rest.slice(0, end)).join("\n").trim();
|
|
5709
|
+
return text ? { section, text } : null;
|
|
5710
|
+
}
|
|
5711
|
+
|
|
5712
|
+
// src/commands/reassess.ts
|
|
5713
|
+
var import_zod14 = require("zod");
|
|
5714
|
+
var reassessCommand = define({
|
|
5715
|
+
name: "reassess",
|
|
5716
|
+
tool: "kb_reassess",
|
|
5717
|
+
usage: "reassess <concept-id> [--repo-root <path>] [--with-diff]",
|
|
5718
|
+
description: "One drifted record, as something to judge: its claim, each anchor's drift class, the old-vs-new span diff, and the records that depend on it. Formatting-only drift is dropped. Empty when there is nothing to reassess. Writes: relocates moved anchors, keeping their hash; never verifies, supersedes, or changes standing.",
|
|
5719
|
+
input: import_zod14.z.object({
|
|
5720
|
+
bundlePath,
|
|
5721
|
+
conceptId,
|
|
5722
|
+
repoRoot: REPO_ROOT,
|
|
5723
|
+
withDiff: import_zod14.z.boolean().optional().describe(
|
|
5724
|
+
"Recover each anchor's committed span and render the diff. Reads git history."
|
|
5725
|
+
)
|
|
5726
|
+
}),
|
|
5727
|
+
fromArgv: (argv, path) => {
|
|
5728
|
+
const repoRoot = argvFlag(argv, "--repo-root");
|
|
5729
|
+
return {
|
|
5730
|
+
bundlePath: path,
|
|
5731
|
+
conceptId: argv[1],
|
|
5732
|
+
...repoRoot !== void 0 ? { repoRoot } : {},
|
|
5733
|
+
...argv.includes("--with-diff") ? { withDiff: true } : {}
|
|
5734
|
+
};
|
|
5735
|
+
},
|
|
5736
|
+
run: async ({ store, actor }, { bundlePath: path, conceptId: id, repoRoot, withDiff }) => {
|
|
5737
|
+
const root = repoRoot ?? process.cwd();
|
|
5738
|
+
const bundle = await store.list(path);
|
|
5739
|
+
const record = bundle.find((entry) => entry.conceptId === id);
|
|
5740
|
+
if (!record) throw new KbRecordNotFoundError(id);
|
|
5741
|
+
const drift = await store.detectDrift([record], repoRoot);
|
|
5742
|
+
const entries = drift?.get(id) ?? [];
|
|
5743
|
+
if (!entries.some((entry) => entry.state !== "match")) {
|
|
5744
|
+
return { conceptId: id, packet: null, rebaselined: [], cosmetic: 0 };
|
|
5745
|
+
}
|
|
5746
|
+
const standing = adjudicate(bundle, bundle).find(
|
|
5747
|
+
(hit) => hit.record.conceptId === id
|
|
5748
|
+
)?.standing;
|
|
5749
|
+
const impact2 = await store.impact(path, id);
|
|
5750
|
+
const { packet, classified } = await reassessPacket(root, record, entries, {
|
|
5751
|
+
...withDiff ? { withDiff: true } : {},
|
|
5752
|
+
impact: impact2,
|
|
5753
|
+
...standing ? { standing } : {}
|
|
5754
|
+
});
|
|
5755
|
+
const moves = classified.filter((found) => found.class === "moved");
|
|
5756
|
+
let frozen = false;
|
|
5757
|
+
const rebaselined = [];
|
|
5758
|
+
if (moves.length) {
|
|
5759
|
+
const relocated = /* @__PURE__ */ new Map();
|
|
5760
|
+
for (const found of moves) {
|
|
5761
|
+
const to = found.entry.movedTo;
|
|
5762
|
+
if (!to) continue;
|
|
5763
|
+
relocated.set(found.anchor, {
|
|
5764
|
+
...found.anchor,
|
|
5765
|
+
file: to.file,
|
|
5766
|
+
...to.symbol ? { symbol: to.symbol } : {}
|
|
5767
|
+
});
|
|
5768
|
+
rebaselined.push({
|
|
5769
|
+
file: found.anchor.file,
|
|
5770
|
+
...found.anchor.symbol ? { symbol: found.anchor.symbol } : {},
|
|
5771
|
+
toFile: to.file,
|
|
5772
|
+
...to.symbol ? { toSymbol: to.symbol } : {}
|
|
5773
|
+
});
|
|
5774
|
+
}
|
|
5775
|
+
try {
|
|
5776
|
+
await assertBaseNotFrozen(process.cwd(), path);
|
|
5777
|
+
} catch (error) {
|
|
5778
|
+
if (!(error instanceof KbBaseFrozenError)) throw error;
|
|
5779
|
+
frozen = true;
|
|
5780
|
+
}
|
|
5781
|
+
if (!frozen) {
|
|
5782
|
+
await store.updateAnchors(
|
|
5783
|
+
path,
|
|
5784
|
+
id,
|
|
5785
|
+
(record.frontmatter.strauss_anchors ?? []).map(
|
|
5786
|
+
(anchor) => relocated.get(anchor) ?? anchor
|
|
5787
|
+
),
|
|
5788
|
+
actor
|
|
5789
|
+
);
|
|
5790
|
+
}
|
|
5791
|
+
}
|
|
5792
|
+
return {
|
|
5793
|
+
conceptId: id,
|
|
5794
|
+
packet,
|
|
5795
|
+
rebaselined: frozen ? [] : rebaselined,
|
|
5796
|
+
cosmetic: classified.filter((found) => found.class === "cosmetic").length,
|
|
5797
|
+
...frozen ? {
|
|
5798
|
+
frozen: true,
|
|
5799
|
+
note: "base is frozen: nothing was rebaselined"
|
|
5800
|
+
} : {}
|
|
5801
|
+
};
|
|
5802
|
+
},
|
|
5803
|
+
render: (result) => renderReassess(result)
|
|
5804
|
+
});
|
|
5805
|
+
function renderReassess(result) {
|
|
5806
|
+
const lines = [];
|
|
5807
|
+
for (const move of result.rebaselined) {
|
|
5808
|
+
lines.push(
|
|
5809
|
+
`rebaselined: ${at(move.file, move.symbol)} \u2192 ${at(move.toFile, move.toSymbol)} (same code, new address)`
|
|
5810
|
+
);
|
|
5811
|
+
}
|
|
5812
|
+
if (result.cosmetic) {
|
|
5813
|
+
lines.push(
|
|
5814
|
+
`${result.cosmetic} anchor${result.cosmetic === 1 ? "" : "s"} changed formatting only.`
|
|
5815
|
+
);
|
|
5816
|
+
}
|
|
5817
|
+
if (result.note) lines.push(result.note);
|
|
5818
|
+
const packet = result.packet;
|
|
5819
|
+
if (!packet) {
|
|
5820
|
+
lines.push(`${result.conceptId}: nothing to reassess.`);
|
|
5821
|
+
return lines.join("\n");
|
|
5822
|
+
}
|
|
5823
|
+
lines.push(
|
|
5824
|
+
"",
|
|
5825
|
+
`# ${packet.conceptId}${packet.title ? ` \u2014 ${packet.title}` : ""}`,
|
|
5826
|
+
`type: ${packet.type} standing: ${packet.standing}`,
|
|
5827
|
+
...packet.why ? [`why: ${packet.why}`] : [],
|
|
5828
|
+
...packet.claim ? ["", `## ${packet.claim.section}`, packet.claim.text] : [],
|
|
5829
|
+
"",
|
|
5830
|
+
`## Anchors (${packet.anchors.length})`
|
|
5831
|
+
);
|
|
5832
|
+
for (const anchor of packet.anchors) {
|
|
5833
|
+
lines.push(
|
|
5834
|
+
`- ${at(anchor.file, anchor.symbol)} \u2014 ${anchor.class}${anchor.reason ? ` (${anchor.reason})` : ""}`
|
|
5835
|
+
);
|
|
5836
|
+
if (!anchor.diff) continue;
|
|
5837
|
+
if (anchor.diff.status === "unrecoverable") {
|
|
5838
|
+
lines.push(
|
|
5839
|
+
" diff: unrecoverable \u2014 no committed span to compare against"
|
|
5840
|
+
);
|
|
5841
|
+
continue;
|
|
5842
|
+
}
|
|
5843
|
+
lines.push(
|
|
5844
|
+
` diff vs ${anchor.diff.ref} (${anchor.diff.source}): +${anchor.diff.added} \u2212${anchor.diff.removed}`,
|
|
5845
|
+
...anchor.diff.unified.split("\n").map((line) => ` ${line}`)
|
|
5846
|
+
);
|
|
5847
|
+
}
|
|
5848
|
+
if (packet.impact.length) {
|
|
5849
|
+
lines.push("", `## Impact (${packet.impact.length})`);
|
|
5850
|
+
for (const entry of packet.impact) {
|
|
5851
|
+
lines.push(
|
|
5852
|
+
`- ${entry.conceptId} [${entry.standing}]${entry.title ? ` \u2014 ${entry.title}` : ""}`
|
|
5853
|
+
);
|
|
5854
|
+
}
|
|
5855
|
+
if (packet.impactTruncated) lines.push("- \u2026 walk truncated");
|
|
5856
|
+
}
|
|
5857
|
+
lines.push("", `Default: ${packet.default} \u2014 ${packet.defaultNote}.`);
|
|
5858
|
+
return lines.join("\n");
|
|
5859
|
+
}
|
|
5860
|
+
function at(file, symbol) {
|
|
5861
|
+
return symbol ? `${file}:${symbol}` : file;
|
|
5862
|
+
}
|
|
5863
|
+
|
|
5864
|
+
// src/commands/doctor.ts
|
|
5865
|
+
var days = (what, fallback) => import_zod15.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
|
|
4522
5866
|
var doctorCommand = define({
|
|
4523
5867
|
name: "doctor",
|
|
4524
5868
|
tool: "kb_doctor",
|
|
4525
|
-
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--offline] [--strict]",
|
|
4526
|
-
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.
|
|
4527
|
-
input:
|
|
5869
|
+
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--offline] [--strict] [--drifted [--with-diff]]",
|
|
5870
|
+
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. `drifted` narrows it to a reassessment packet per drifted record, `with_diff` adding each anchor's old-vs-new span.",
|
|
5871
|
+
input: import_zod15.z.object({
|
|
4528
5872
|
bundlePath,
|
|
4529
5873
|
repoRoot: REPO_ROOT,
|
|
4530
5874
|
expiringDays: days(
|
|
@@ -4539,11 +5883,17 @@ var doctorCommand = define({
|
|
|
4539
5883
|
"How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
|
|
4540
5884
|
DEFAULT_AGING_DAYS
|
|
4541
5885
|
),
|
|
4542
|
-
offline:
|
|
5886
|
+
offline: import_zod15.z.boolean().optional().describe(
|
|
4543
5887
|
"Read foreign anchors from the local repo cache only, never fetching."
|
|
4544
5888
|
),
|
|
4545
|
-
strict:
|
|
5889
|
+
strict: import_zod15.z.boolean().optional().describe(
|
|
4546
5890
|
"Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
|
|
5891
|
+
),
|
|
5892
|
+
drifted: import_zod15.z.boolean().optional().describe(
|
|
5893
|
+
"Report only drift, as a reassessment packet per record: claim, per-anchor class, and what depends on it."
|
|
5894
|
+
),
|
|
5895
|
+
withDiff: import_zod15.z.boolean().optional().describe(
|
|
5896
|
+
"With `drifted`: recover each anchor's committed span and render the old-vs-new diff. Reads git history."
|
|
4547
5897
|
)
|
|
4548
5898
|
}),
|
|
4549
5899
|
// Presence, not truthiness: `--expiring-days ""` is a caller who meant
|
|
@@ -4562,7 +5912,9 @@ var doctorCommand = define({
|
|
|
4562
5912
|
...unverified2 !== void 0 ? { unverifiedDays: Number(unverified2) } : {},
|
|
4563
5913
|
...agingDays !== void 0 ? { agingDays: Number(agingDays) } : {},
|
|
4564
5914
|
...argv.includes("--offline") ? { offline: true } : {},
|
|
4565
|
-
...argv.includes("--strict") ? { strict: true } : {}
|
|
5915
|
+
...argv.includes("--strict") ? { strict: true } : {},
|
|
5916
|
+
...argv.includes("--drifted") ? { drifted: true } : {},
|
|
5917
|
+
...argv.includes("--with-diff") ? { withDiff: true } : {}
|
|
4566
5918
|
};
|
|
4567
5919
|
},
|
|
4568
5920
|
run: async ({ store, now }, {
|
|
@@ -4571,7 +5923,9 @@ var doctorCommand = define({
|
|
|
4571
5923
|
unverifiedDays,
|
|
4572
5924
|
agingDays,
|
|
4573
5925
|
repoRoot,
|
|
4574
|
-
offline
|
|
5926
|
+
offline,
|
|
5927
|
+
drifted: drifted2,
|
|
5928
|
+
withDiff
|
|
4575
5929
|
}) => {
|
|
4576
5930
|
const checkedAt = now();
|
|
4577
5931
|
const records = await store.list(path);
|
|
@@ -4585,7 +5939,54 @@ var doctorCommand = define({
|
|
|
4585
5939
|
...anchorDrift !== void 0 ? { anchorDrift } : {},
|
|
4586
5940
|
now: new Date(checkedAt)
|
|
4587
5941
|
});
|
|
4588
|
-
|
|
5942
|
+
const hints = grammarHints();
|
|
5943
|
+
if (!drifted2) {
|
|
5944
|
+
return {
|
|
5945
|
+
bundlePath: path,
|
|
5946
|
+
checkedAt,
|
|
5947
|
+
...report,
|
|
5948
|
+
...hints.length ? { hints } : {}
|
|
5949
|
+
};
|
|
5950
|
+
}
|
|
5951
|
+
const standings = new Map(
|
|
5952
|
+
adjudicate(records, records, new Date(checkedAt)).map((hit) => [
|
|
5953
|
+
hit.record.conceptId,
|
|
5954
|
+
hit.standing
|
|
5955
|
+
])
|
|
5956
|
+
);
|
|
5957
|
+
const packets = [];
|
|
5958
|
+
const rebaselinable = [];
|
|
5959
|
+
const search = movedSearch(repoRoot ?? process.cwd());
|
|
5960
|
+
for (const found of report.groups.find((g) => g.check === "drifted")?.findings ?? []) {
|
|
5961
|
+
const record = records.find(
|
|
5962
|
+
(entry) => entry.conceptId === found.conceptId
|
|
5963
|
+
);
|
|
5964
|
+
if (!record) continue;
|
|
5965
|
+
const standing = standings.get(record.conceptId);
|
|
5966
|
+
const built = await reassessPacket(
|
|
5967
|
+
repoRoot ?? process.cwd(),
|
|
5968
|
+
record,
|
|
5969
|
+
anchorDrift?.get(record.conceptId) ?? [],
|
|
5970
|
+
{
|
|
5971
|
+
...withDiff ? { withDiff: true } : {},
|
|
5972
|
+
impact: await store.impact(path, record.conceptId),
|
|
5973
|
+
...standing ? { standing } : {},
|
|
5974
|
+
search
|
|
5975
|
+
}
|
|
5976
|
+
);
|
|
5977
|
+
if (built.packet) packets.push(built.packet);
|
|
5978
|
+
if (built.classified.some((entry) => entry.class === "moved")) {
|
|
5979
|
+
rebaselinable.push(record.conceptId);
|
|
5980
|
+
}
|
|
5981
|
+
}
|
|
5982
|
+
return {
|
|
5983
|
+
bundlePath: path,
|
|
5984
|
+
checkedAt,
|
|
5985
|
+
...report,
|
|
5986
|
+
packets,
|
|
5987
|
+
rebaselinable,
|
|
5988
|
+
...hints.length ? { hints } : {}
|
|
5989
|
+
};
|
|
4589
5990
|
},
|
|
4590
5991
|
render: (result) => render2(result),
|
|
4591
5992
|
// Only expiry, and only under --strict. The other seven checks report debt a
|
|
@@ -4597,18 +5998,22 @@ var doctorCommand = define({
|
|
|
4597
5998
|
failsWhen: (result, input) => input.strict === true && result.counts.expired > 0
|
|
4598
5999
|
});
|
|
4599
6000
|
function render2(result) {
|
|
6001
|
+
if (result.packets) return renderPackets(result);
|
|
4600
6002
|
const { thresholds } = result;
|
|
4601
6003
|
const lines = [
|
|
4602
6004
|
`# KB Doctor \u2014 ${result.bundlePath}`,
|
|
4603
6005
|
`records: ${result.recordCount}`,
|
|
4604
6006
|
`thresholds: expiring within ${thresholds.expiringDays}d, unverified over ${thresholds.unverifiedDays}d, aging over ${thresholds.agingDays}d`,
|
|
4605
6007
|
`checked: ${result.checkedAt}`,
|
|
6008
|
+
...result.anchorResolvers.total ? [
|
|
6009
|
+
`anchors: ${result.anchorResolvers.total} hashed \u2014 ${result.anchorResolvers.treeSitter} tree-sitter, ${result.anchorResolvers.regex} regex`
|
|
6010
|
+
] : [],
|
|
4606
6011
|
""
|
|
4607
6012
|
];
|
|
4608
|
-
const
|
|
6013
|
+
const width2 = Math.max(...result.groups.map((group2) => group2.check.length));
|
|
4609
6014
|
for (const group2 of result.groups) {
|
|
4610
6015
|
lines.push(
|
|
4611
|
-
` ${group2.check.padEnd(
|
|
6016
|
+
` ${group2.check.padEnd(width2)} ${String(group2.count).padStart(3)} ${group2.headline}`
|
|
4612
6017
|
);
|
|
4613
6018
|
}
|
|
4614
6019
|
for (const group2 of result.groups) {
|
|
@@ -4620,27 +6025,52 @@ function render2(result) {
|
|
|
4620
6025
|
);
|
|
4621
6026
|
}
|
|
4622
6027
|
}
|
|
6028
|
+
for (const hint of result.hints ?? []) lines.push("", hint);
|
|
4623
6029
|
lines.push(
|
|
4624
6030
|
"",
|
|
4625
6031
|
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.`
|
|
4626
6032
|
);
|
|
4627
6033
|
return lines.join("\n");
|
|
4628
6034
|
}
|
|
6035
|
+
function renderPackets(result) {
|
|
6036
|
+
const packets = result.packets ?? [];
|
|
6037
|
+
const lines = [
|
|
6038
|
+
`# KB Drift \u2014 ${result.bundlePath}`,
|
|
6039
|
+
`checked: ${result.checkedAt}`,
|
|
6040
|
+
`${packets.length} record${packets.length === 1 ? "" : "s"} need a reading; ${result.counts.drifted} drifted in all.`
|
|
6041
|
+
];
|
|
6042
|
+
if (result.rebaselinable?.length) {
|
|
6043
|
+
lines.push(
|
|
6044
|
+
`moved, rebaseline with \`kb_reassess\`: ${result.rebaselinable.join(", ")}`
|
|
6045
|
+
);
|
|
6046
|
+
}
|
|
6047
|
+
for (const packet of packets) {
|
|
6048
|
+
lines.push(
|
|
6049
|
+
renderReassess({
|
|
6050
|
+
conceptId: packet.conceptId,
|
|
6051
|
+
packet,
|
|
6052
|
+
rebaselined: [],
|
|
6053
|
+
cosmetic: 0
|
|
6054
|
+
})
|
|
6055
|
+
);
|
|
6056
|
+
}
|
|
6057
|
+
return lines.join("\n");
|
|
6058
|
+
}
|
|
4629
6059
|
|
|
4630
6060
|
// src/commands/impact.ts
|
|
4631
|
-
var
|
|
6061
|
+
var import_zod16 = require("zod");
|
|
4632
6062
|
var impactCommand = define({
|
|
4633
6063
|
name: "impact",
|
|
4634
6064
|
tool: "kb_impact",
|
|
4635
6065
|
usage: "impact <concept-id> [--depth N] [--rels a,b]",
|
|
4636
6066
|
description: "What breaks if this record changes: its transitive set of dependants, each with its standing. Each rel declares which of its ends depends on the other, and the walk follows each rel in its own direction. Naming `related_to` or an unknown rel in `rels` is an error. kb_backlinks gives one flat hop.",
|
|
4637
|
-
input:
|
|
6067
|
+
input: import_zod16.z.object({
|
|
4638
6068
|
bundlePath,
|
|
4639
6069
|
conceptId,
|
|
4640
|
-
depth:
|
|
6070
|
+
depth: import_zod16.z.number().int().positive().optional().describe(
|
|
4641
6071
|
"Hops out from the record. Unbounded when omitted; a walk this cuts reports truncated: true."
|
|
4642
6072
|
),
|
|
4643
|
-
rels:
|
|
6073
|
+
rels: import_zod16.z.array(import_zod16.z.enum(KB_CAUSAL_LINK_RELS)).optional().describe(
|
|
4644
6074
|
"Narrow which rels the walk follows. Defaults to every rel that carries a dependence \u2014 all but related_to."
|
|
4645
6075
|
)
|
|
4646
6076
|
}),
|
|
@@ -4661,13 +6091,13 @@ var impactCommand = define({
|
|
|
4661
6091
|
});
|
|
4662
6092
|
|
|
4663
6093
|
// src/commands/list.ts
|
|
4664
|
-
var
|
|
6094
|
+
var import_zod17 = require("zod");
|
|
4665
6095
|
var listCommand = define({
|
|
4666
6096
|
name: "list",
|
|
4667
6097
|
tool: "kb_list",
|
|
4668
6098
|
usage: "list [type]",
|
|
4669
6099
|
description: "Every record, optionally one type. For enumerating; use kb_query for a question.",
|
|
4670
|
-
input:
|
|
6100
|
+
input: import_zod17.z.object({ bundlePath, type: import_zod17.z.enum(KB_RECORD_TYPES).optional() }),
|
|
4671
6101
|
fromArgv: (argv, path) => ({ bundlePath: path, type: argv[1] }),
|
|
4672
6102
|
run: async ({ store }, { bundlePath: path, type }) => (await store.list(path, type)).map((record) => ({
|
|
4673
6103
|
conceptId: record.conceptId,
|
|
@@ -4679,17 +6109,17 @@ var listCommand = define({
|
|
|
4679
6109
|
});
|
|
4680
6110
|
|
|
4681
6111
|
// src/commands/load.ts
|
|
4682
|
-
var
|
|
6112
|
+
var import_zod18 = require("zod");
|
|
4683
6113
|
var loadCommand = define({
|
|
4684
6114
|
name: "load",
|
|
4685
6115
|
tool: "kb_load",
|
|
4686
6116
|
usage: "load [type] [--budget N | --all] [--repo-root PATH]",
|
|
4687
6117
|
description: "Load the whole base, each record with its standing \u2014 call it first, at the point of use, since compaction drops it. Superseded records arrive as stubs; kb_trace has the history. Over budget it refuses: kb_catalog, then kb_pack, or narrow with `type`; `all` bypasses. Never read record files directly \u2014 only kb_* tools resolve supersession. `digest` stamps the base's content, so hooks know when to reload.",
|
|
4688
|
-
input:
|
|
6118
|
+
input: import_zod18.z.object({
|
|
4689
6119
|
bundlePath,
|
|
4690
|
-
type:
|
|
4691
|
-
budgetTokens:
|
|
4692
|
-
all:
|
|
6120
|
+
type: import_zod18.z.enum(KB_RECORD_TYPES).optional(),
|
|
6121
|
+
budgetTokens: import_zod18.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
|
|
6122
|
+
all: import_zod18.z.boolean().optional().describe(
|
|
4693
6123
|
"Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
|
|
4694
6124
|
),
|
|
4695
6125
|
repoRoot: REPO_ROOT
|
|
@@ -4731,25 +6161,25 @@ var loadCommand = define({
|
|
|
4731
6161
|
});
|
|
4732
6162
|
|
|
4733
6163
|
// src/commands/log.ts
|
|
4734
|
-
var
|
|
6164
|
+
var import_zod19 = require("zod");
|
|
4735
6165
|
var logCommand = define({
|
|
4736
6166
|
name: "log",
|
|
4737
6167
|
tool: "kb_log",
|
|
4738
6168
|
usage: "log",
|
|
4739
6169
|
description: "Who touched what, and when. Append-only; malformed lines are reported, never repaired.",
|
|
4740
|
-
input:
|
|
6170
|
+
input: import_zod19.z.object({ bundlePath }),
|
|
4741
6171
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
4742
6172
|
run: ({ store }, { bundlePath: path }) => store.readLog(path)
|
|
4743
6173
|
});
|
|
4744
6174
|
|
|
4745
6175
|
// src/commands/no-decision.ts
|
|
4746
|
-
var
|
|
6176
|
+
var import_zod20 = require("zod");
|
|
4747
6177
|
var noDecisionCommand = define({
|
|
4748
6178
|
name: "no-decision",
|
|
4749
6179
|
tool: "kb_no_decision",
|
|
4750
6180
|
usage: "no-decision <reason...>",
|
|
4751
6181
|
description: "Record in one sentence that a piece of work had nothing to decide. Idempotent.",
|
|
4752
|
-
input:
|
|
6182
|
+
input: import_zod20.z.object({ bundlePath, reason: import_zod20.z.string().min(1) }),
|
|
4753
6183
|
fromArgv: (argv, path) => ({
|
|
4754
6184
|
bundlePath: path,
|
|
4755
6185
|
reason: argv.slice(1).join(" ").trim()
|
|
@@ -4766,20 +6196,20 @@ var noDecisionCommand = define({
|
|
|
4766
6196
|
});
|
|
4767
6197
|
|
|
4768
6198
|
// src/commands/pack.ts
|
|
4769
|
-
var
|
|
6199
|
+
var import_zod21 = require("zod");
|
|
4770
6200
|
var packCommand = define({
|
|
4771
6201
|
name: "pack",
|
|
4772
6202
|
tool: "kb_pack",
|
|
4773
6203
|
usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
|
|
4774
6204
|
description: "Bounded neighbourhood around one record: within `hops`, ranked, cut to `maxNodes`, with every cut record named under Excluded. Use when the base is over kb_load's budget and the work centres on a record you can name. Refuses over budget rather than truncating. Everything below the header is byte-stable across runs. Resolves supersession like kb_load.",
|
|
4775
|
-
input:
|
|
6205
|
+
input: import_zod21.z.object({
|
|
4776
6206
|
bundlePath,
|
|
4777
6207
|
conceptId,
|
|
4778
|
-
hops:
|
|
4779
|
-
maxNodes:
|
|
6208
|
+
hops: import_zod21.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
|
|
6209
|
+
maxNodes: import_zod21.z.number().int().positive().optional().describe(
|
|
4780
6210
|
"How many records the pack may hold, root included. Defaults to 20."
|
|
4781
6211
|
),
|
|
4782
|
-
budgetTokens:
|
|
6212
|
+
budgetTokens: import_zod21.z.number().int().positive().optional().describe(
|
|
4783
6213
|
"Approximate token ceiling over what is actually emitted. Defaults to 25000."
|
|
4784
6214
|
)
|
|
4785
6215
|
}),
|
|
@@ -4804,12 +6234,12 @@ var packCommand = define({
|
|
|
4804
6234
|
return render3(result, path, now());
|
|
4805
6235
|
}
|
|
4806
6236
|
});
|
|
4807
|
-
function render3(result, bundle,
|
|
6237
|
+
function render3(result, bundle, at2) {
|
|
4808
6238
|
const lines = [
|
|
4809
6239
|
`# KB Pack \u2014 ${result.root}`,
|
|
4810
6240
|
`bundle: ${bundle}`,
|
|
4811
6241
|
`budget: ~${result.tokensLoaded} of ${result.budgetTokens} tokens, ${result.recordCount} records`,
|
|
4812
|
-
`packed: ${
|
|
6242
|
+
`packed: ${at2}`,
|
|
4813
6243
|
"",
|
|
4814
6244
|
`## Records (${result.records.length})`
|
|
4815
6245
|
];
|
|
@@ -4866,22 +6296,22 @@ function warningLabel(warning) {
|
|
|
4866
6296
|
}
|
|
4867
6297
|
|
|
4868
6298
|
// src/commands/pin.ts
|
|
4869
|
-
var
|
|
6299
|
+
var import_zod22 = require("zod");
|
|
4870
6300
|
var pinCommand = define({
|
|
4871
6301
|
name: "pin",
|
|
4872
6302
|
tool: "kb_pin",
|
|
4873
6303
|
usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
|
|
4874
6304
|
description: "Pin a base into a workspace manifest so kb_context surfaces it. Layers, nearest wins: project `.strauss/kb-pins.json` (default), `--local` (personal, gitignored), `--user` (`~/.strauss`). Idempotent; `--mode full|index`, `--profiles`, `--frozen`/`--unfreeze` update only those fields. A path with no records pins with a warning. Never touches the base itself.",
|
|
4875
|
-
input:
|
|
6305
|
+
input: import_zod22.z.object({
|
|
4876
6306
|
bundlePath,
|
|
4877
|
-
mode:
|
|
6307
|
+
mode: import_zod22.z.enum(["full", "index"]).optional().describe(
|
|
4878
6308
|
"full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
|
|
4879
6309
|
),
|
|
4880
|
-
profiles:
|
|
4881
|
-
layer:
|
|
6310
|
+
profiles: import_zod22.z.array(import_zod22.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
|
|
6311
|
+
layer: import_zod22.z.enum(["project", "local", "user"]).optional().describe(
|
|
4882
6312
|
"Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
|
|
4883
6313
|
),
|
|
4884
|
-
frozen:
|
|
6314
|
+
frozen: import_zod22.z.boolean().optional().describe(
|
|
4885
6315
|
"true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
|
|
4886
6316
|
)
|
|
4887
6317
|
}),
|
|
@@ -4910,29 +6340,29 @@ var pinCommand = define({
|
|
|
4910
6340
|
});
|
|
4911
6341
|
|
|
4912
6342
|
// src/commands/pins.ts
|
|
4913
|
-
var
|
|
6343
|
+
var import_zod23 = require("zod");
|
|
4914
6344
|
var pinsCommand = define({
|
|
4915
6345
|
name: "pins",
|
|
4916
6346
|
tool: "kb_pins",
|
|
4917
6347
|
usage: "pins",
|
|
4918
6348
|
description: "Every pinned base across the manifest layers, with its layer and whether it resolves to records. Takes no bundlePath.",
|
|
4919
|
-
input:
|
|
6349
|
+
input: import_zod23.z.object({}),
|
|
4920
6350
|
fromArgv: () => ({}),
|
|
4921
6351
|
run: ({ store }) => listPins(store, process.cwd())
|
|
4922
6352
|
});
|
|
4923
6353
|
|
|
4924
6354
|
// src/commands/query.ts
|
|
4925
|
-
var
|
|
6355
|
+
var import_zod24 = require("zod");
|
|
4926
6356
|
var queryCommand = define({
|
|
4927
6357
|
name: "query",
|
|
4928
6358
|
tool: "kb_query",
|
|
4929
6359
|
usage: "query <text...> [--repo-root PATH]",
|
|
4930
6360
|
description: "Search; every hit carries its standing. Flagged, never filtered: a superseded hit returns with its replacement, a rejected one is marked. Prefer kb_load when the base fits its budget \u2014 a full read beats search. Results are volatile: place them at the tail, not the cached prefix. Never read record files directly.",
|
|
4931
|
-
input:
|
|
6361
|
+
input: import_zod24.z.object({
|
|
4932
6362
|
bundlePath,
|
|
4933
|
-
text:
|
|
4934
|
-
type:
|
|
4935
|
-
includeNonCurrent:
|
|
6363
|
+
text: import_zod24.z.string().optional(),
|
|
6364
|
+
type: import_zod24.z.enum(KB_RECORD_TYPES).optional(),
|
|
6365
|
+
includeNonCurrent: import_zod24.z.boolean().optional(),
|
|
4936
6366
|
repoRoot: REPO_ROOT
|
|
4937
6367
|
}),
|
|
4938
6368
|
// `--repo-root` is a flag, so its value must not fall into the search text.
|
|
@@ -4964,43 +6394,43 @@ var queryCommand = define({
|
|
|
4964
6394
|
});
|
|
4965
6395
|
|
|
4966
6396
|
// src/commands/read-index.ts
|
|
4967
|
-
var
|
|
6397
|
+
var import_zod25 = require("zod");
|
|
4968
6398
|
var readIndexCommand = define({
|
|
4969
6399
|
name: "index",
|
|
4970
6400
|
tool: "kb_index",
|
|
4971
6401
|
usage: "index",
|
|
4972
6402
|
description: "The index \u2014 title, type, status, description per record \u2014 rebuilt if stale. Cheapest re-orientation after compaction: call it (or kb_context) first, then kb_load or fetch by id.",
|
|
4973
|
-
input:
|
|
6403
|
+
input: import_zod25.z.object({ bundlePath }),
|
|
4974
6404
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
4975
6405
|
run: ({ store }, { bundlePath: path }) => store.readIndex(path)
|
|
4976
6406
|
});
|
|
4977
6407
|
|
|
4978
6408
|
// src/commands/schema.ts
|
|
4979
|
-
var
|
|
6409
|
+
var import_zod26 = require("zod");
|
|
4980
6410
|
var schemaCommand = define({
|
|
4981
6411
|
name: "schema",
|
|
4982
6412
|
tool: "kb_schema",
|
|
4983
6413
|
usage: "schema",
|
|
4984
6414
|
description: "JSON Schema for frontmatter, write input, and log entries, generated from the enforcing code.",
|
|
4985
|
-
input:
|
|
6415
|
+
input: import_zod26.z.object({}),
|
|
4986
6416
|
fromArgv: () => ({}),
|
|
4987
6417
|
run: () => Promise.resolve(kbJsonSchemas())
|
|
4988
6418
|
});
|
|
4989
6419
|
|
|
4990
6420
|
// src/commands/stamp.ts
|
|
4991
|
-
var
|
|
4992
|
-
var
|
|
6421
|
+
var import_promises10 = require("fs/promises");
|
|
6422
|
+
var import_zod27 = require("zod");
|
|
4993
6423
|
var DIGEST = /^[0-9a-f]{64}$/;
|
|
4994
6424
|
var stampCommand = define({
|
|
4995
6425
|
name: "stamp",
|
|
4996
6426
|
tool: "kb_stamp",
|
|
4997
6427
|
usage: "stamp [--bundle PATH] [--since DIGEST|FILE]",
|
|
4998
|
-
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
|
|
4999
|
-
input:
|
|
5000
|
-
bundlePath:
|
|
6428
|
+
description: "Content stamp of a base \u2014 `load`'s digest, record counts, per-record digests, how many records have drifted anchors \u2014 without any bodies. Takes no bundlePath to stamp every pinned base. With `since`, reports only the bases that moved, naming the changed ids. Reads, never writes.",
|
|
6429
|
+
input: import_zod27.z.object({
|
|
6430
|
+
bundlePath: import_zod27.z.string().min(1).optional().describe(
|
|
5001
6431
|
"Absolute path to one knowledge base. Omit to stamp every pinned base."
|
|
5002
6432
|
),
|
|
5003
|
-
since:
|
|
6433
|
+
since: import_zod27.z.string().min(1).optional().describe(
|
|
5004
6434
|
"Prior digest, or path to a prior `stamp --json`; only moved bases return, with changed ids when the baseline is a file."
|
|
5005
6435
|
)
|
|
5006
6436
|
}),
|
|
@@ -5039,7 +6469,7 @@ var stampCommand = define({
|
|
|
5039
6469
|
return reports;
|
|
5040
6470
|
},
|
|
5041
6471
|
render: (result) => result.map((report) => {
|
|
5042
|
-
const counts = `${report.recordCount} record(s), ${report.superseded} superseded`;
|
|
6472
|
+
const counts = `${report.recordCount} record(s), ${report.superseded} superseded${report.drifted ? `, ${report.drifted} drifted` : ""}`;
|
|
5043
6473
|
const head = `${report.path} ${report.digest} ${counts}${report.newestAt ? ` newest ${report.newestAt}` : ""}`;
|
|
5044
6474
|
return report.changed?.length ? `${head}
|
|
5045
6475
|
changed: ${report.changed.join(", ")}` : head;
|
|
@@ -5062,7 +6492,7 @@ async function readBaseline(since) {
|
|
|
5062
6492
|
if (DIGEST.test(since)) return { digest: since, byPath: /* @__PURE__ */ new Map() };
|
|
5063
6493
|
let parsed;
|
|
5064
6494
|
try {
|
|
5065
|
-
parsed = JSON.parse(await (0,
|
|
6495
|
+
parsed = JSON.parse(await (0, import_promises10.readFile)(since, "utf8"));
|
|
5066
6496
|
} catch {
|
|
5067
6497
|
throw new KbStampBaselineError(since);
|
|
5068
6498
|
}
|
|
@@ -5086,16 +6516,16 @@ async function readBaseline(since) {
|
|
|
5086
6516
|
}
|
|
5087
6517
|
|
|
5088
6518
|
// src/commands/status.ts
|
|
5089
|
-
var
|
|
6519
|
+
var import_zod28 = require("zod");
|
|
5090
6520
|
var statusCommand = define({
|
|
5091
6521
|
name: "status",
|
|
5092
6522
|
tool: "kb_status",
|
|
5093
6523
|
usage: "status <concept-id> <status>",
|
|
5094
6524
|
description: "Move a record's status. Compare-and-swap: a concurrent change fails instead of being overwritten.",
|
|
5095
|
-
input:
|
|
6525
|
+
input: import_zod28.z.object({
|
|
5096
6526
|
bundlePath,
|
|
5097
6527
|
conceptId,
|
|
5098
|
-
status:
|
|
6528
|
+
status: import_zod28.z.enum(KB_RECORD_STATUSES)
|
|
5099
6529
|
}),
|
|
5100
6530
|
fromArgv: (argv, path) => ({
|
|
5101
6531
|
bundlePath: path,
|
|
@@ -5110,13 +6540,13 @@ var statusCommand = define({
|
|
|
5110
6540
|
});
|
|
5111
6541
|
|
|
5112
6542
|
// src/commands/supersede.ts
|
|
5113
|
-
var
|
|
6543
|
+
var import_zod29 = require("zod");
|
|
5114
6544
|
var supersedeCommand = define({
|
|
5115
6545
|
name: "supersede",
|
|
5116
6546
|
tool: "kb_supersede",
|
|
5117
6547
|
usage: "supersede <concept-id> <replacement-id>",
|
|
5118
6548
|
description: "Mark a record superseded by another, linked in both directions. Use instead of editing a record whose meaning changed.",
|
|
5119
|
-
input:
|
|
6549
|
+
input: import_zod29.z.object({ bundlePath, conceptId, replacementId: conceptId }),
|
|
5120
6550
|
fromArgv: (argv, path) => ({
|
|
5121
6551
|
bundlePath: path,
|
|
5122
6552
|
conceptId: argv[1],
|
|
@@ -5130,16 +6560,16 @@ var supersedeCommand = define({
|
|
|
5130
6560
|
});
|
|
5131
6561
|
|
|
5132
6562
|
// src/commands/sync-instructions.ts
|
|
5133
|
-
var
|
|
6563
|
+
var import_zod30 = require("zod");
|
|
5134
6564
|
var syncInstructionsCommand = define({
|
|
5135
6565
|
name: "sync-instructions",
|
|
5136
6566
|
usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
|
|
5137
6567
|
description: "CLI-only: plant the kb_context block between sentinel comments in AGENTS.md or CLAUDE.md, idempotently.",
|
|
5138
|
-
input:
|
|
5139
|
-
file:
|
|
5140
|
-
budgetTokens:
|
|
5141
|
-
fullUnderTokens:
|
|
5142
|
-
profile:
|
|
6568
|
+
input: import_zod30.z.object({
|
|
6569
|
+
file: import_zod30.z.string().min(1).describe("The instruction file to edit in place."),
|
|
6570
|
+
budgetTokens: import_zod30.z.number().int().positive().optional(),
|
|
6571
|
+
fullUnderTokens: import_zod30.z.number().int().positive().optional(),
|
|
6572
|
+
profile: import_zod30.z.string().optional()
|
|
5143
6573
|
}),
|
|
5144
6574
|
fromArgv: (argv) => {
|
|
5145
6575
|
const budget = argvFlag(argv, "--budget");
|
|
@@ -5165,17 +6595,17 @@ var syncInstructionsCommand = define({
|
|
|
5165
6595
|
});
|
|
5166
6596
|
|
|
5167
6597
|
// src/commands/trace.ts
|
|
5168
|
-
var
|
|
6598
|
+
var import_zod31 = require("zod");
|
|
5169
6599
|
var traceCommand = define({
|
|
5170
6600
|
name: "trace",
|
|
5171
6601
|
tool: "kb_trace",
|
|
5172
6602
|
usage: "trace <concept-id> [edges...]",
|
|
5173
6603
|
description: 'Timeline of how a position was reached, ordered by write time, following supersession, shared anchors and shared sources. Includes rejected, draft and superseded records \u2014 in a history they are the content. For "why is it like this"; kb_load answers "what holds now".',
|
|
5174
|
-
input:
|
|
6604
|
+
input: import_zod31.z.object({
|
|
5175
6605
|
bundlePath,
|
|
5176
6606
|
conceptId,
|
|
5177
|
-
edges:
|
|
5178
|
-
depth:
|
|
6607
|
+
edges: import_zod31.z.array(import_zod31.z.enum(TRACE_EDGES)).optional(),
|
|
6608
|
+
depth: import_zod31.z.number().int().positive().optional()
|
|
5179
6609
|
}),
|
|
5180
6610
|
fromArgv: (argv, path) => ({
|
|
5181
6611
|
bundlePath: path,
|
|
@@ -5197,37 +6627,37 @@ var traceCommand = define({
|
|
|
5197
6627
|
});
|
|
5198
6628
|
|
|
5199
6629
|
// src/commands/types.ts
|
|
5200
|
-
var
|
|
6630
|
+
var import_zod32 = require("zod");
|
|
5201
6631
|
var typesCommand = define({
|
|
5202
6632
|
name: "types",
|
|
5203
6633
|
tool: "kb_types",
|
|
5204
6634
|
usage: "types",
|
|
5205
6635
|
description: "The twelve record types with their purpose, body sections, and starting status. Read this before writing rather than guessing headings \u2014 a section the type does not define is rejected.",
|
|
5206
|
-
input:
|
|
6636
|
+
input: import_zod32.z.object({}),
|
|
5207
6637
|
fromArgv: () => ({}),
|
|
5208
6638
|
run: () => Promise.resolve(RECORD_TYPES)
|
|
5209
6639
|
});
|
|
5210
6640
|
|
|
5211
6641
|
// src/commands/unpin.ts
|
|
5212
|
-
var
|
|
6642
|
+
var import_zod33 = require("zod");
|
|
5213
6643
|
var unpinCommand = define({
|
|
5214
6644
|
name: "unpin",
|
|
5215
6645
|
tool: "kb_unpin",
|
|
5216
6646
|
usage: "unpin [bundle-path]",
|
|
5217
6647
|
description: "Remove a base from every manifest layer that holds it. Reports the layers touched.",
|
|
5218
|
-
input:
|
|
6648
|
+
input: import_zod33.z.object({ bundlePath }),
|
|
5219
6649
|
fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
|
|
5220
6650
|
run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
|
|
5221
6651
|
});
|
|
5222
6652
|
|
|
5223
6653
|
// src/commands/validate.ts
|
|
5224
|
-
var
|
|
6654
|
+
var import_zod34 = require("zod");
|
|
5225
6655
|
var validateCommand = define({
|
|
5226
6656
|
name: "validate",
|
|
5227
6657
|
tool: "kb_validate",
|
|
5228
6658
|
usage: "validate",
|
|
5229
6659
|
description: "Check pointers no single record can see: supersession links that disagree between the two records, typed causal links, and assumptions that cite sources. Each finding carries a severity: errors fail the exit code, warnings do not.",
|
|
5230
|
-
input:
|
|
6660
|
+
input: import_zod34.z.object({ bundlePath }),
|
|
5231
6661
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
5232
6662
|
run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
|
|
5233
6663
|
// Warnings never fail the exit code; every other severity does.
|
|
@@ -5237,16 +6667,16 @@ var validateCommand = define({
|
|
|
5237
6667
|
});
|
|
5238
6668
|
|
|
5239
6669
|
// src/commands/verify.ts
|
|
5240
|
-
var
|
|
6670
|
+
var import_zod35 = require("zod");
|
|
5241
6671
|
var verifyCommand = define({
|
|
5242
6672
|
name: "verify",
|
|
5243
6673
|
tool: "kb_verify",
|
|
5244
6674
|
usage: "verify <concept-id> --note <text>",
|
|
5245
6675
|
description: "Append a verified[] event: who checked, when, and what was found. Append-only. A record's own generator is refused unless the actor is `human:`-prefixed.",
|
|
5246
|
-
input:
|
|
6676
|
+
input: import_zod35.z.object({
|
|
5247
6677
|
bundlePath,
|
|
5248
6678
|
conceptId,
|
|
5249
|
-
note:
|
|
6679
|
+
note: import_zod35.z.string().refine((s) => s.trim().length > 0, {
|
|
5250
6680
|
message: "note must say what the check found"
|
|
5251
6681
|
})
|
|
5252
6682
|
}),
|
|
@@ -5266,15 +6696,15 @@ var verifyCommand = define({
|
|
|
5266
6696
|
});
|
|
5267
6697
|
|
|
5268
6698
|
// src/commands/write.ts
|
|
5269
|
-
var
|
|
6699
|
+
var import_zod36 = require("zod");
|
|
5270
6700
|
var writeCommand = define({
|
|
5271
6701
|
name: "write",
|
|
5272
6702
|
tool: "kb_write",
|
|
5273
6703
|
usage: "write <type> < record.json",
|
|
5274
6704
|
description: "Write one record. Search first \u2014 a duplicate concept id is rejected, not overwritten; kb_types lists each type's sections. An unsourced claim is an `assumption` with assumption: true, never a vague `fact`. Conflicting records get a `risk`, `open-question`, or superseding `decision`. Prefer a new short record over overloading one. Never delete; supersede.",
|
|
5275
|
-
input:
|
|
6705
|
+
input: import_zod36.z.object({
|
|
5276
6706
|
bundlePath,
|
|
5277
|
-
type:
|
|
6707
|
+
type: import_zod36.z.enum(KB_RECORD_TYPES),
|
|
5278
6708
|
input: composeInputSchema
|
|
5279
6709
|
}),
|
|
5280
6710
|
fromArgv: async (argv, path, stdin) => ({
|
|
@@ -5298,13 +6728,13 @@ var writeCommand = define({
|
|
|
5298
6728
|
});
|
|
5299
6729
|
|
|
5300
6730
|
// src/commands/write-decision.ts
|
|
5301
|
-
var
|
|
6731
|
+
var import_zod37 = require("zod");
|
|
5302
6732
|
var writeDecisionCommand = define({
|
|
5303
6733
|
name: "write-decision",
|
|
5304
6734
|
tool: "kb_write_decision",
|
|
5305
6735
|
usage: "write-decision < decision.json",
|
|
5306
6736
|
description: "Write a decision, with `alternative` (what was rejected and why) and `impact` as fields. Record one when a later reader would otherwise simplify the constraint away; skip when the diff already answers it. `sources` for material read, `anchors` for code, `relatedConceptIds` for records.",
|
|
5307
|
-
input:
|
|
6737
|
+
input: import_zod37.z.object({ bundlePath, input: decisionInputSchema }),
|
|
5308
6738
|
fromArgv: async (_argv, path, stdin) => ({
|
|
5309
6739
|
bundlePath: path,
|
|
5310
6740
|
input: JSON.parse(await stdin())
|
|
@@ -5334,6 +6764,7 @@ var KB_COMMANDS = [
|
|
|
5334
6764
|
answerCommand,
|
|
5335
6765
|
verifyCommand,
|
|
5336
6766
|
anchorResolveCommand,
|
|
6767
|
+
reassessCommand,
|
|
5337
6768
|
loadCommand,
|
|
5338
6769
|
catalogCommand,
|
|
5339
6770
|
packCommand,
|
|
@@ -5364,7 +6795,7 @@ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
|
5364
6795
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
5365
6796
|
|
|
5366
6797
|
// src/version.ts
|
|
5367
|
-
var VERSION = true ? "0.1.
|
|
6798
|
+
var VERSION = true ? "0.1.18" : "0.0.0-dev";
|
|
5368
6799
|
|
|
5369
6800
|
// src/mcp.ts
|
|
5370
6801
|
function createKbMcpServer() {
|
|
@@ -5403,7 +6834,7 @@ async function runKbMcpServer() {
|
|
|
5403
6834
|
}
|
|
5404
6835
|
|
|
5405
6836
|
// src/cli.ts
|
|
5406
|
-
var
|
|
6837
|
+
var import_node_path12 = require("path");
|
|
5407
6838
|
async function runKbCli(argv) {
|
|
5408
6839
|
const { flags, literal } = takeLiteral(argv);
|
|
5409
6840
|
const {
|
|
@@ -5458,21 +6889,21 @@ async function runKbCli(argv) {
|
|
|
5458
6889
|
`);
|
|
5459
6890
|
}
|
|
5460
6891
|
function takeLiteral(argv) {
|
|
5461
|
-
const
|
|
5462
|
-
if (
|
|
5463
|
-
return { flags: argv.slice(0,
|
|
6892
|
+
const at2 = argv.indexOf("--");
|
|
6893
|
+
if (at2 === -1) return { flags: argv, literal: [] };
|
|
6894
|
+
return { flags: argv.slice(0, at2), literal: argv.slice(at2 + 1) };
|
|
5464
6895
|
}
|
|
5465
6896
|
function takeBundle(argv) {
|
|
5466
|
-
const
|
|
5467
|
-
if (
|
|
5468
|
-
return { bundle: (0,
|
|
6897
|
+
const at2 = argv.indexOf("--bundle");
|
|
6898
|
+
if (at2 === -1) {
|
|
6899
|
+
return { bundle: (0, import_node_path12.join)(process.cwd(), KB_DIR), explicit: false, rest: argv };
|
|
5469
6900
|
}
|
|
5470
|
-
const bundle = argv[
|
|
6901
|
+
const bundle = argv[at2 + 1];
|
|
5471
6902
|
if (!bundle) die("--bundle requires a path");
|
|
5472
6903
|
return {
|
|
5473
6904
|
bundle,
|
|
5474
6905
|
explicit: true,
|
|
5475
|
-
rest: [...argv.slice(0,
|
|
6906
|
+
rest: [...argv.slice(0, at2), ...argv.slice(at2 + 2)]
|
|
5476
6907
|
};
|
|
5477
6908
|
}
|
|
5478
6909
|
function readStdin() {
|
|
@@ -5495,14 +6926,14 @@ function summarise(description) {
|
|
|
5495
6926
|
return sentence.length > 78 ? `${sentence.slice(0, 75)}\u2026` : sentence;
|
|
5496
6927
|
}
|
|
5497
6928
|
function usage() {
|
|
5498
|
-
const
|
|
6929
|
+
const width2 = Math.max(...KB_COMMANDS.map((command) => command.usage.length));
|
|
5499
6930
|
return [
|
|
5500
6931
|
"strauss-kb \u2014 knowledge base commands",
|
|
5501
6932
|
"",
|
|
5502
6933
|
"Usage: strauss-kb [--bundle PATH] <command> [args]",
|
|
5503
6934
|
"",
|
|
5504
6935
|
...KB_COMMANDS.map(
|
|
5505
|
-
(command) => ` ${command.usage.padEnd(
|
|
6936
|
+
(command) => ` ${command.usage.padEnd(width2)} ${summarise(command.description)}`
|
|
5506
6937
|
),
|
|
5507
6938
|
"",
|
|
5508
6939
|
` --bundle PATH defaults to ./${KB_DIR}`,
|
|
@@ -5563,12 +6994,14 @@ function usage() {
|
|
|
5563
6994
|
RECORD_TYPES,
|
|
5564
6995
|
SEARCH_INDEX_FILE,
|
|
5565
6996
|
TRACE_EDGES,
|
|
6997
|
+
TreeSitterResolver,
|
|
5566
6998
|
adjudicate,
|
|
5567
6999
|
anchorFilePath,
|
|
5568
7000
|
assertBaseNotFrozen,
|
|
5569
7001
|
backlinks,
|
|
5570
7002
|
buildContext,
|
|
5571
7003
|
catalog,
|
|
7004
|
+
classifyDrift,
|
|
5572
7005
|
composeDecisionRecord,
|
|
5573
7006
|
composeInputSchema,
|
|
5574
7007
|
composeLinkSchema,
|
|
@@ -5577,9 +7010,14 @@ function usage() {
|
|
|
5577
7010
|
contextProfileBudgets,
|
|
5578
7011
|
createKbMcpServer,
|
|
5579
7012
|
decisionInputSchema,
|
|
7013
|
+
defaultAnchorResolvers,
|
|
5580
7014
|
detectAnchorDrift,
|
|
5581
7015
|
doctor,
|
|
5582
7016
|
edgeNeighbours,
|
|
7017
|
+
ensureGrammar,
|
|
7018
|
+
grammarHints,
|
|
7019
|
+
grammarManifest,
|
|
7020
|
+
grammarsCacheRoot,
|
|
5583
7021
|
hashAnchorText,
|
|
5584
7022
|
impact,
|
|
5585
7023
|
inboundIndex,
|
|
@@ -5597,6 +7035,7 @@ function usage() {
|
|
|
5597
7035
|
kbRecordFrontmatterSchema,
|
|
5598
7036
|
kbSourceSchema,
|
|
5599
7037
|
kbVerifiedEventSchema,
|
|
7038
|
+
languageForFile,
|
|
5600
7039
|
listPins,
|
|
5601
7040
|
loadQmd,
|
|
5602
7041
|
matchToDiff,
|
|
@@ -5606,9 +7045,11 @@ function usage() {
|
|
|
5606
7045
|
parseLog,
|
|
5607
7046
|
parseMarkdownWithFrontmatter,
|
|
5608
7047
|
pinBase,
|
|
7048
|
+
prepareResolvers,
|
|
5609
7049
|
readMergedPins,
|
|
5610
7050
|
readPinsLayer,
|
|
5611
7051
|
readRemoteAnchors,
|
|
7052
|
+
reassessPacket,
|
|
5612
7053
|
regexResolver,
|
|
5613
7054
|
renderCatalogLine,
|
|
5614
7055
|
renderIndex,
|
|
@@ -5616,6 +7057,7 @@ function usage() {
|
|
|
5616
7057
|
renderLogEntry,
|
|
5617
7058
|
repoCacheDir,
|
|
5618
7059
|
resolveAnchor,
|
|
7060
|
+
resolveAnchorSpan,
|
|
5619
7061
|
resolveHeads,
|
|
5620
7062
|
resolveHits,
|
|
5621
7063
|
resolvePinPath,
|
|
@@ -5628,6 +7070,8 @@ function usage() {
|
|
|
5628
7070
|
syncInstructions,
|
|
5629
7071
|
toHookJson,
|
|
5630
7072
|
trace,
|
|
7073
|
+
treeSitterLanguages,
|
|
7074
|
+
unifiedDiff,
|
|
5631
7075
|
unpinBase,
|
|
5632
7076
|
validateBundle
|
|
5633
7077
|
});
|