@saasontools/strauss-kb 0.1.6 → 0.1.8
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/README.md +111 -6
- package/dist/{chunk-V7TRZ2ER.js → chunk-MGPYUZOM.js} +32 -11
- package/dist/chunk-MGPYUZOM.js.map +1 -0
- package/dist/{chunk-PNSRTKYN.js → chunk-YJK7KGHN.js} +743 -131
- package/dist/chunk-YJK7KGHN.js.map +1 -0
- package/dist/{chunk-BVF7X5VO.js → chunk-YYX6CX6V.js} +5 -4
- package/dist/chunk-YYX6CX6V.js.map +1 -0
- package/dist/cli-main.cjs +757 -149
- package/dist/cli-main.cjs.map +1 -1
- package/dist/cli-main.js +2 -2
- package/dist/index.cjs +743 -97
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +181 -8
- package/dist/index.d.ts +181 -8
- package/dist/index.js +29 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp-main.cjs +730 -142
- package/dist/mcp-main.cjs.map +1 -1
- package/dist/mcp-main.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-BVF7X5VO.js.map +0 -1
- package/dist/chunk-PNSRTKYN.js.map +0 -1
- package/dist/chunk-V7TRZ2ER.js.map +0 -1
package/dist/cli-main.cjs
CHANGED
|
@@ -1055,14 +1055,460 @@ var contextCommand = define({
|
|
|
1055
1055
|
}
|
|
1056
1056
|
});
|
|
1057
1057
|
|
|
1058
|
-
// src/commands/
|
|
1058
|
+
// src/commands/doctor.ts
|
|
1059
1059
|
var import_zod8 = require("zod");
|
|
1060
|
+
|
|
1061
|
+
// src/kb-edges.ts
|
|
1062
|
+
var KB_EDGE_KINDS = [
|
|
1063
|
+
"body-link",
|
|
1064
|
+
"supersession",
|
|
1065
|
+
"anchor",
|
|
1066
|
+
"source"
|
|
1067
|
+
];
|
|
1068
|
+
var BODY_LINK_TARGET = new RegExp(
|
|
1069
|
+
`\\]\\((${KB_CONCEPT_ID_PATTERN.source.replace(/^\^|\$$/g, "")})\\.md\\)`,
|
|
1070
|
+
"g"
|
|
1071
|
+
);
|
|
1072
|
+
function neighbours(from, bundle, kinds = KB_EDGE_KINDS) {
|
|
1073
|
+
const found = /* @__PURE__ */ new Map();
|
|
1074
|
+
for (const kind of kinds) {
|
|
1075
|
+
for (const record of edgeNeighbours(from, bundle, kind)) {
|
|
1076
|
+
const existing = found.get(record.conceptId);
|
|
1077
|
+
if (existing) {
|
|
1078
|
+
if (!existing.via.includes(kind)) existing.via.push(kind);
|
|
1079
|
+
continue;
|
|
1080
|
+
}
|
|
1081
|
+
found.set(record.conceptId, { record, via: [kind] });
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
return [...found.values()];
|
|
1085
|
+
}
|
|
1086
|
+
function edgeNeighbours(from, bundle, kind) {
|
|
1087
|
+
switch (kind) {
|
|
1088
|
+
// A link whose target is not in the bundle is legal per compose.ts —
|
|
1089
|
+
// records are routinely written before the ones they point at exist — so
|
|
1090
|
+
// missing targets are skipped, never an error.
|
|
1091
|
+
case "body-link": {
|
|
1092
|
+
const targets = new Set(
|
|
1093
|
+
[...from.body.matchAll(BODY_LINK_TARGET)].map((match) => match[1])
|
|
1094
|
+
);
|
|
1095
|
+
if (!targets.size) return [];
|
|
1096
|
+
return bundle.filter(
|
|
1097
|
+
(candidate) => candidate.conceptId !== from.conceptId && targets.has(candidate.conceptId)
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
// Both directions and both pointers: `supersede()` writes the pair, but a
|
|
1101
|
+
// hand-edit can leave one side behind, and a walk trusting one pointer
|
|
1102
|
+
// would miss a replacement the bundle openly declares.
|
|
1103
|
+
case "supersession":
|
|
1104
|
+
return bundle.filter(
|
|
1105
|
+
(candidate) => candidate.conceptId !== from.conceptId && (candidate.conceptId === from.frontmatter.strauss_superseded_by || from.frontmatter.strauss_supersedes?.includes(
|
|
1106
|
+
candidate.conceptId
|
|
1107
|
+
) || candidate.frontmatter.strauss_superseded_by === from.conceptId || candidate.frontmatter.strauss_supersedes?.includes(from.conceptId))
|
|
1108
|
+
);
|
|
1109
|
+
// The edge that answers "why is this code shaped this way": every record
|
|
1110
|
+
// attached to the same file or symbol, whatever its standing.
|
|
1111
|
+
case "anchor": {
|
|
1112
|
+
const mine = from.frontmatter.strauss_anchors ?? [];
|
|
1113
|
+
if (!mine.length) return [];
|
|
1114
|
+
return bundle.filter(
|
|
1115
|
+
(candidate) => candidate.conceptId !== from.conceptId && (candidate.frontmatter.strauss_anchors ?? []).some(
|
|
1116
|
+
(theirs) => mine.some((ours) => anchorsTouch(ours, theirs))
|
|
1117
|
+
)
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
case "source": {
|
|
1121
|
+
const mine = new Set((from.frontmatter.sources ?? []).map((s) => s.id));
|
|
1122
|
+
if (!mine.size) return [];
|
|
1123
|
+
return bundle.filter(
|
|
1124
|
+
(candidate) => candidate.conceptId !== from.conceptId && (candidate.frontmatter.sources ?? []).some(
|
|
1125
|
+
(source) => mine.has(source.id)
|
|
1126
|
+
)
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
function anchorsTouch(left, right) {
|
|
1132
|
+
if (left.file !== right.file) return false;
|
|
1133
|
+
if (!left.symbol || !right.symbol) return true;
|
|
1134
|
+
return left.symbol === right.symbol;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// src/validate.ts
|
|
1138
|
+
function validateBundle(records) {
|
|
1139
|
+
const byId = new Map(records.map((record) => [record.conceptId, record]));
|
|
1140
|
+
const problems = [];
|
|
1141
|
+
const report = (check, conceptId2, note) => problems.push({ check, conceptId: conceptId2, note });
|
|
1142
|
+
for (const record of records) {
|
|
1143
|
+
const { conceptId: conceptId2, frontmatter: fm } = record;
|
|
1144
|
+
if (!isKbRecordType(fm.type)) {
|
|
1145
|
+
report("type", conceptId2, `unrecognised type "${fm.type}"`);
|
|
1146
|
+
}
|
|
1147
|
+
if (fm.strauss_status === "superseded") {
|
|
1148
|
+
const by = fm.strauss_superseded_by;
|
|
1149
|
+
if (!by) {
|
|
1150
|
+
report("superseded_by", conceptId2, "superseded with no replacement");
|
|
1151
|
+
} else if (!byId.has(by)) {
|
|
1152
|
+
report("superseded_by", conceptId2, `replacement ${by} is missing`);
|
|
1153
|
+
} else if (!byId.get(by)?.frontmatter.strauss_supersedes?.includes(conceptId2)) {
|
|
1154
|
+
report("backlink", by, `does not list ${conceptId2} in supersedes`);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
for (const old of fm.strauss_supersedes ?? []) {
|
|
1158
|
+
const previous = byId.get(old);
|
|
1159
|
+
if (!previous) {
|
|
1160
|
+
report("supersedes", conceptId2, `target ${old} is missing`);
|
|
1161
|
+
} else if (previous.frontmatter.strauss_status !== "superseded") {
|
|
1162
|
+
report("supersedes", conceptId2, `${old} is not marked superseded`);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
if (fm.strauss_assumption && fm.sources?.length) {
|
|
1166
|
+
report("assumption", conceptId2, "marked an assumption but cites sources");
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
return problems;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// src/doctor.ts
|
|
1173
|
+
var DEFAULT_EXPIRING_DAYS = 30;
|
|
1174
|
+
var DEFAULT_UNVERIFIED_DAYS = 90;
|
|
1175
|
+
var DEFAULT_AGING_DAYS = 90;
|
|
1176
|
+
var CHECK_HEADLINES = {
|
|
1177
|
+
expired: "past its stale_after date",
|
|
1178
|
+
expiring: "stale_after falls within the window",
|
|
1179
|
+
unverified: "nobody has ever confirmed it, and it is old enough to matter",
|
|
1180
|
+
aging: "still open or still proposed long after it was written",
|
|
1181
|
+
orphaned: "no other record links to it",
|
|
1182
|
+
"broken-supersession": "the supersession pointers do not resolve",
|
|
1183
|
+
"superseded-but-cited": "a live record's body links to one that no longer holds"
|
|
1184
|
+
};
|
|
1185
|
+
var DAY_MS = 864e5;
|
|
1186
|
+
function doctor(bundle, options = {}) {
|
|
1187
|
+
const thresholds = {
|
|
1188
|
+
expiringDays: options.expiringDays ?? DEFAULT_EXPIRING_DAYS,
|
|
1189
|
+
unverifiedDays: options.unverifiedDays ?? DEFAULT_UNVERIFIED_DAYS,
|
|
1190
|
+
agingDays: options.agingDays ?? DEFAULT_AGING_DAYS
|
|
1191
|
+
};
|
|
1192
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
1193
|
+
const adjudicated = adjudicate(bundle, bundle, now);
|
|
1194
|
+
const standings = new Map(
|
|
1195
|
+
adjudicated.map((hit) => [hit.record.conceptId, hit.standing])
|
|
1196
|
+
);
|
|
1197
|
+
const inForce = adjudicated.filter(
|
|
1198
|
+
(hit) => hit.standing !== "superseded" && hit.standing !== "rejected"
|
|
1199
|
+
);
|
|
1200
|
+
const groups = [
|
|
1201
|
+
group("expired", expired(inForce, now)),
|
|
1202
|
+
group("expiring", expiring(inForce, now, thresholds.expiringDays)),
|
|
1203
|
+
group("unverified", unverified(inForce, now, thresholds.unverifiedDays)),
|
|
1204
|
+
group("aging", aging(inForce, now, thresholds.agingDays)),
|
|
1205
|
+
group("orphaned", orphaned(bundle)),
|
|
1206
|
+
group("broken-supersession", brokenSupersession(bundle, adjudicated)),
|
|
1207
|
+
group("superseded-but-cited", supersededButCited(bundle, standings))
|
|
1208
|
+
];
|
|
1209
|
+
const counts = Object.fromEntries(
|
|
1210
|
+
groups.map((entry) => [entry.check, entry.count])
|
|
1211
|
+
);
|
|
1212
|
+
const findingCount = groups.reduce((total, entry) => total + entry.count, 0);
|
|
1213
|
+
return {
|
|
1214
|
+
recordCount: bundle.length,
|
|
1215
|
+
thresholds,
|
|
1216
|
+
counts,
|
|
1217
|
+
groups,
|
|
1218
|
+
findingCount,
|
|
1219
|
+
healthy: findingCount === 0
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
function group(check, findings) {
|
|
1223
|
+
return {
|
|
1224
|
+
check,
|
|
1225
|
+
headline: CHECK_HEADLINES[check],
|
|
1226
|
+
count: findings.length,
|
|
1227
|
+
findings
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
function expired(hits, now) {
|
|
1231
|
+
const findings = [];
|
|
1232
|
+
for (const hit of hits) {
|
|
1233
|
+
const raw = hit.record.frontmatter.stale_after;
|
|
1234
|
+
if (!raw) continue;
|
|
1235
|
+
const at = Date.parse(raw);
|
|
1236
|
+
if (Number.isNaN(at)) {
|
|
1237
|
+
findings.push(
|
|
1238
|
+
finding(hit.record, `stale_after "${raw}" is not a readable date`)
|
|
1239
|
+
);
|
|
1240
|
+
continue;
|
|
1241
|
+
}
|
|
1242
|
+
if (at < now.getTime()) {
|
|
1243
|
+
findings.push(
|
|
1244
|
+
finding(
|
|
1245
|
+
hit.record,
|
|
1246
|
+
`stale since ${raw} (${daysBetween(at, now.getTime())} days ago)`
|
|
1247
|
+
)
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
return findings;
|
|
1252
|
+
}
|
|
1253
|
+
function expiring(hits, now, withinDays) {
|
|
1254
|
+
const horizon = now.getTime() + withinDays * DAY_MS;
|
|
1255
|
+
const findings = [];
|
|
1256
|
+
for (const hit of hits) {
|
|
1257
|
+
const raw = hit.record.frontmatter.stale_after;
|
|
1258
|
+
if (!raw) continue;
|
|
1259
|
+
const at = Date.parse(raw);
|
|
1260
|
+
if (Number.isNaN(at) || at < now.getTime() || at > horizon) continue;
|
|
1261
|
+
findings.push(
|
|
1262
|
+
finding(
|
|
1263
|
+
hit.record,
|
|
1264
|
+
`goes stale ${raw} (in ${daysBetween(now.getTime(), at)} days)`
|
|
1265
|
+
)
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
return findings;
|
|
1269
|
+
}
|
|
1270
|
+
function unverified(hits, now, olderThanDays) {
|
|
1271
|
+
const findings = [];
|
|
1272
|
+
for (const hit of hits) {
|
|
1273
|
+
if (hit.record.frontmatter.verified?.length) continue;
|
|
1274
|
+
const age = ageInDays(hit.record, now);
|
|
1275
|
+
if (age === null || age <= olderThanDays) continue;
|
|
1276
|
+
findings.push(
|
|
1277
|
+
finding(hit.record, `never verified, written ${age} days ago`)
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
return findings;
|
|
1281
|
+
}
|
|
1282
|
+
function aging(hits, now, olderThanDays) {
|
|
1283
|
+
const findings = [];
|
|
1284
|
+
for (const hit of hits) {
|
|
1285
|
+
const status = hit.record.frontmatter.strauss_status;
|
|
1286
|
+
if (status !== "open" && status !== "proposed") continue;
|
|
1287
|
+
const age = ageInDays(hit.record, now);
|
|
1288
|
+
if (age === null || age <= olderThanDays) continue;
|
|
1289
|
+
findings.push(
|
|
1290
|
+
finding(
|
|
1291
|
+
hit.record,
|
|
1292
|
+
status === "open" ? `open for ${age} days` : `proposed ${age} days ago and still unsettled`
|
|
1293
|
+
)
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
return findings.sort(
|
|
1297
|
+
(left, right) => left.conceptId.localeCompare(right.conceptId)
|
|
1298
|
+
);
|
|
1299
|
+
}
|
|
1300
|
+
function orphaned(bundle) {
|
|
1301
|
+
const present = new Set(bundle.map((record) => record.conceptId));
|
|
1302
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
1303
|
+
for (const record of bundle) {
|
|
1304
|
+
for (const neighbour of edgeNeighbours(record, bundle, "body-link")) {
|
|
1305
|
+
referenced.add(neighbour.conceptId);
|
|
1306
|
+
}
|
|
1307
|
+
for (const replaced of record.frontmatter.strauss_supersedes ?? []) {
|
|
1308
|
+
referenced.add(replaced);
|
|
1309
|
+
}
|
|
1310
|
+
const replacement = record.frontmatter.strauss_superseded_by;
|
|
1311
|
+
if (replacement && present.has(replacement)) {
|
|
1312
|
+
referenced.add(record.conceptId);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
return bundle.filter((record) => !referenced.has(record.conceptId)).map((record) => finding(record, "no other record links to it"));
|
|
1316
|
+
}
|
|
1317
|
+
var SUPERSESSION_CHECKS = /* @__PURE__ */ new Set([
|
|
1318
|
+
"superseded_by",
|
|
1319
|
+
"supersedes",
|
|
1320
|
+
"backlink"
|
|
1321
|
+
]);
|
|
1322
|
+
function brokenSupersession(bundle, adjudicated) {
|
|
1323
|
+
const byId = new Map(bundle.map((record) => [record.conceptId, record]));
|
|
1324
|
+
const findings = [];
|
|
1325
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1326
|
+
const add = (record, note) => {
|
|
1327
|
+
const key = `${record.conceptId}\0${note}`;
|
|
1328
|
+
if (seen.has(key)) return;
|
|
1329
|
+
seen.add(key);
|
|
1330
|
+
findings.push(finding(record, note));
|
|
1331
|
+
};
|
|
1332
|
+
for (const problem of validateBundle(bundle)) {
|
|
1333
|
+
if (!SUPERSESSION_CHECKS.has(problem.check)) continue;
|
|
1334
|
+
const record = byId.get(problem.conceptId);
|
|
1335
|
+
if (record) add(record, problem.note);
|
|
1336
|
+
}
|
|
1337
|
+
for (const record of bundle) {
|
|
1338
|
+
const replacement = record.frontmatter.strauss_superseded_by;
|
|
1339
|
+
if (!replacement) continue;
|
|
1340
|
+
if (!byId.has(replacement)) {
|
|
1341
|
+
add(record, `replacement ${replacement} is missing`);
|
|
1342
|
+
} else if (record.frontmatter.strauss_status !== "superseded") {
|
|
1343
|
+
add(
|
|
1344
|
+
record,
|
|
1345
|
+
`names ${replacement} as its replacement but is not marked superseded`
|
|
1346
|
+
);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
for (const hit of adjudicated) {
|
|
1350
|
+
for (const warning of hit.warnings) {
|
|
1351
|
+
if (warning.kind === "broken-chain") {
|
|
1352
|
+
add(hit.record, `replacement ${warning.missing} is missing`);
|
|
1353
|
+
} else if (warning.kind === "chain-cycle") {
|
|
1354
|
+
add(
|
|
1355
|
+
hit.record,
|
|
1356
|
+
`supersession chain cycles through ${warning.through.join(" \u2192 ")}`
|
|
1357
|
+
);
|
|
1358
|
+
} else if (warning.kind === "forked-chain") {
|
|
1359
|
+
add(
|
|
1360
|
+
hit.record,
|
|
1361
|
+
`two records claim to replace it: ${warning.heads.join(", ")}`
|
|
1362
|
+
);
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
return findings.sort(
|
|
1367
|
+
(left, right) => left.conceptId.localeCompare(right.conceptId)
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
function supersededButCited(bundle, standings) {
|
|
1371
|
+
const byId = new Map(bundle.map((record) => [record.conceptId, record]));
|
|
1372
|
+
const findings = [];
|
|
1373
|
+
for (const record of bundle) {
|
|
1374
|
+
const standing = standings.get(record.conceptId);
|
|
1375
|
+
if (standing === "superseded" || standing === "rejected") continue;
|
|
1376
|
+
for (const target of edgeNeighbours(record, bundle, "body-link")) {
|
|
1377
|
+
const targetStanding = standings.get(target.conceptId);
|
|
1378
|
+
if (targetStanding !== "superseded" && targetStanding !== "rejected") {
|
|
1379
|
+
continue;
|
|
1380
|
+
}
|
|
1381
|
+
if (replaces(record, target)) continue;
|
|
1382
|
+
const replacement = target.frontmatter.strauss_superseded_by;
|
|
1383
|
+
findings.push(
|
|
1384
|
+
finding(
|
|
1385
|
+
record,
|
|
1386
|
+
`cites ${targetStanding} ${target.conceptId}${targetStanding === "superseded" && replacement && byId.has(replacement) ? ` \u2014 replaced by ${replacement}` : ""}`
|
|
1387
|
+
)
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
return findings;
|
|
1392
|
+
}
|
|
1393
|
+
function replaces(later, earlier) {
|
|
1394
|
+
return (later.frontmatter.strauss_supersedes ?? []).includes(earlier.conceptId) || earlier.frontmatter.strauss_superseded_by === later.conceptId;
|
|
1395
|
+
}
|
|
1396
|
+
function finding(record, note) {
|
|
1397
|
+
return {
|
|
1398
|
+
conceptId: record.conceptId,
|
|
1399
|
+
title: record.frontmatter.title ?? null,
|
|
1400
|
+
status: record.frontmatter.strauss_status,
|
|
1401
|
+
note
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
function daysBetween(from, to) {
|
|
1405
|
+
return Math.max(0, Math.floor((to - from) / DAY_MS));
|
|
1406
|
+
}
|
|
1407
|
+
function ageInDays(record, now) {
|
|
1408
|
+
const at = record.frontmatter.generated?.at;
|
|
1409
|
+
if (!at) return null;
|
|
1410
|
+
const written = Date.parse(at);
|
|
1411
|
+
if (Number.isNaN(written)) return null;
|
|
1412
|
+
return daysBetween(written, now.getTime());
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
// src/commands/doctor.ts
|
|
1416
|
+
var days = (what, fallback) => import_zod8.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
|
|
1417
|
+
var doctorCommand = define({
|
|
1418
|
+
name: "doctor",
|
|
1419
|
+
tool: "kb_doctor",
|
|
1420
|
+
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--strict]",
|
|
1421
|
+
description: "A health sweep over a whole base: what the calendar has already retired, what nobody ever confirmed, what has been open or proposed long enough that the status is now the answer, and what the graph has dropped on the floor. Read-only \u2014 it never writes, never supersedes, and never re-dates anything; every finding names a record for a person to repair. Seven checks, grouped and counted: expired (past `stale_after`), expiring (inside the window), unverified (an empty `verified[]` on a record old enough to matter), aging (still `open` or `proposed`), orphaned (no other record links to it), broken supersession (a chain that does not resolve), and superseded-but-cited (a live record whose body links to a record that no longer holds). Every group is reported even when empty, because a check that found nothing and a check that never ran look identical in a report that only lists findings.\n\nThis is the question no reader thinks to ask, which is why it needs a command: decay is invisible from inside a single record \u2014 a stale one reads exactly like a live one, and a question nobody answered reads exactly like one nobody asked. Reach for it when picking up a base someone else kept, before trusting a base you have not touched in months, or on a schedule; kb_validate is the narrower neighbour, checking only whether pointers between records agree.",
|
|
1422
|
+
input: import_zod8.z.object({
|
|
1423
|
+
bundlePath,
|
|
1424
|
+
expiringDays: days(
|
|
1425
|
+
"How far ahead `expiring` looks, in days.",
|
|
1426
|
+
DEFAULT_EXPIRING_DAYS
|
|
1427
|
+
),
|
|
1428
|
+
unverifiedDays: days(
|
|
1429
|
+
"How old an unconfirmed record must be before `unverified` reports it, in days.",
|
|
1430
|
+
DEFAULT_UNVERIFIED_DAYS
|
|
1431
|
+
),
|
|
1432
|
+
agingDays: days(
|
|
1433
|
+
"How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
|
|
1434
|
+
DEFAULT_AGING_DAYS
|
|
1435
|
+
),
|
|
1436
|
+
strict: import_zod8.z.boolean().optional().describe(
|
|
1437
|
+
"Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
|
|
1438
|
+
)
|
|
1439
|
+
}),
|
|
1440
|
+
// Presence, not truthiness: `--expiring-days ""` is a caller who meant
|
|
1441
|
+
// something and mistyped it, and a falsy test would answer by quietly
|
|
1442
|
+
// sweeping at the default. Passed through as given, the schema rejects it
|
|
1443
|
+
// and says which field.
|
|
1444
|
+
fromArgv: (argv, path) => {
|
|
1445
|
+
const expiring2 = argvFlag(argv, "--expiring-days");
|
|
1446
|
+
const unverified2 = argvFlag(argv, "--unverified-days");
|
|
1447
|
+
const agingDays = argvFlag(argv, "--aging-days");
|
|
1448
|
+
return {
|
|
1449
|
+
bundlePath: path,
|
|
1450
|
+
...expiring2 !== void 0 ? { expiringDays: Number(expiring2) } : {},
|
|
1451
|
+
...unverified2 !== void 0 ? { unverifiedDays: Number(unverified2) } : {},
|
|
1452
|
+
...agingDays !== void 0 ? { agingDays: Number(agingDays) } : {},
|
|
1453
|
+
...argv.includes("--strict") ? { strict: true } : {}
|
|
1454
|
+
};
|
|
1455
|
+
},
|
|
1456
|
+
run: async ({ store, now }, { bundlePath: path, expiringDays, unverifiedDays, agingDays }) => {
|
|
1457
|
+
const checkedAt = now();
|
|
1458
|
+
const report = doctor(await store.list(path), {
|
|
1459
|
+
...expiringDays !== void 0 ? { expiringDays } : {},
|
|
1460
|
+
...unverifiedDays !== void 0 ? { unverifiedDays } : {},
|
|
1461
|
+
...agingDays !== void 0 ? { agingDays } : {},
|
|
1462
|
+
now: new Date(checkedAt)
|
|
1463
|
+
});
|
|
1464
|
+
return { bundlePath: path, checkedAt, ...report };
|
|
1465
|
+
},
|
|
1466
|
+
render: (result) => render(result),
|
|
1467
|
+
// Only expiry, and only under --strict. The other six checks report debt a
|
|
1468
|
+
// reader decides about; an expired record is the base asserting something it
|
|
1469
|
+
// already said it would stop standing behind, which is the one finding a
|
|
1470
|
+
// pipeline can act on without a judgment call.
|
|
1471
|
+
failsWhen: (result, input) => input.strict === true && result.counts.expired > 0
|
|
1472
|
+
});
|
|
1473
|
+
function render(result) {
|
|
1474
|
+
const { thresholds } = result;
|
|
1475
|
+
const lines = [
|
|
1476
|
+
`# KB Doctor \u2014 ${result.bundlePath}`,
|
|
1477
|
+
`records: ${result.recordCount}`,
|
|
1478
|
+
`thresholds: expiring within ${thresholds.expiringDays}d, unverified over ${thresholds.unverifiedDays}d, aging over ${thresholds.agingDays}d`,
|
|
1479
|
+
`checked: ${result.checkedAt}`,
|
|
1480
|
+
""
|
|
1481
|
+
];
|
|
1482
|
+
const width = Math.max(...result.groups.map((group2) => group2.check.length));
|
|
1483
|
+
for (const group2 of result.groups) {
|
|
1484
|
+
lines.push(
|
|
1485
|
+
` ${group2.check.padEnd(width)} ${String(group2.count).padStart(3)} ${group2.headline}`
|
|
1486
|
+
);
|
|
1487
|
+
}
|
|
1488
|
+
for (const group2 of result.groups) {
|
|
1489
|
+
if (!group2.count) continue;
|
|
1490
|
+
lines.push("", `## ${group2.check} (${group2.count})`);
|
|
1491
|
+
for (const found of group2.findings) {
|
|
1492
|
+
lines.push(
|
|
1493
|
+
`- ${found.conceptId}${found.title ? ` \u2014 ${found.title}` : ""}: ${found.note}`
|
|
1494
|
+
);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
lines.push(
|
|
1498
|
+
"",
|
|
1499
|
+
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.`
|
|
1500
|
+
);
|
|
1501
|
+
return lines.join("\n");
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
// src/commands/list.ts
|
|
1505
|
+
var import_zod9 = require("zod");
|
|
1060
1506
|
var listCommand = define({
|
|
1061
1507
|
name: "list",
|
|
1062
1508
|
tool: "kb_list",
|
|
1063
1509
|
usage: "list [type]",
|
|
1064
1510
|
description: "Every record, optionally narrowed to one type. Use kb_query when you have a question; this is for enumerating.",
|
|
1065
|
-
input:
|
|
1511
|
+
input: import_zod9.z.object({ bundlePath, type: import_zod9.z.enum(KB_RECORD_TYPES).optional() }),
|
|
1066
1512
|
fromArgv: (argv, path) => ({ bundlePath: path, type: argv[1] }),
|
|
1067
1513
|
run: async ({ store }, { bundlePath: path, type }) => (await store.list(path, type)).map((record) => ({
|
|
1068
1514
|
conceptId: record.conceptId,
|
|
@@ -1074,17 +1520,17 @@ var listCommand = define({
|
|
|
1074
1520
|
});
|
|
1075
1521
|
|
|
1076
1522
|
// src/commands/load.ts
|
|
1077
|
-
var
|
|
1523
|
+
var import_zod10 = require("zod");
|
|
1078
1524
|
var loadCommand = define({
|
|
1079
1525
|
name: "load",
|
|
1080
1526
|
tool: "kb_load",
|
|
1081
1527
|
usage: "load [type] [--budget N | --all]",
|
|
1082
1528
|
description: "Load the whole knowledge base at once, each record with its standing. Prefer this over searching: these bases run to a few thousand tokens, and a reader holding all of it has perfect recall and knows why it is asking, which no ranker does. Superseded records arrive under `superseded` as name, replacement and date only \u2014 their bodies no longer hold, and reading one later in a long session is the mistake this prevents; pass the id to kb_trace when you need the history. Rejected and unresolved records arrive whole: what was turned down, and what is still open, is the part a diff cannot show you. Refuses with a count rather than truncating when the base is too large \u2014 a truncated base is indistinguishable from a complete one, and would have you conclude something was never decided from a slice you did not know was a slice. Call at the point of use, not once per session: a base loaded early is summarised away by compaction, so if the visible context holds no records from this base and the question at hand is one it might govern, load before answering \u2014 never conclude nothing was decided from a context with no KB content in it. This tool (with kb_query and kb_trace) is the only supported way to read a base; a raw file read bypasses supersession resolution and returns replaced records as if current.\n\nThat refusal is the default guardrail, meant for an agent that would otherwise burn its whole context on one call. `all` bypasses it and loads everything regardless of size: a deliberate operator with the budget to spend, not something to reach for automatically. It is mutually exclusive with `budgetTokens`. When the reader does not need everything, kb_query or a narrower `type` filter is the better fit than either.",
|
|
1083
|
-
input:
|
|
1529
|
+
input: import_zod10.z.object({
|
|
1084
1530
|
bundlePath,
|
|
1085
|
-
type:
|
|
1086
|
-
budgetTokens:
|
|
1087
|
-
all:
|
|
1531
|
+
type: import_zod10.z.enum(KB_RECORD_TYPES).optional(),
|
|
1532
|
+
budgetTokens: import_zod10.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
|
|
1533
|
+
all: import_zod10.z.boolean().optional().describe(
|
|
1088
1534
|
"Load the entire base regardless of size. The deliberate-operator escape hatch; mutually exclusive with budgetTokens."
|
|
1089
1535
|
)
|
|
1090
1536
|
}).refine((value) => !(value.all && value.budgetTokens !== void 0), {
|
|
@@ -1122,25 +1568,25 @@ var loadCommand = define({
|
|
|
1122
1568
|
});
|
|
1123
1569
|
|
|
1124
1570
|
// src/commands/log.ts
|
|
1125
|
-
var
|
|
1571
|
+
var import_zod11 = require("zod");
|
|
1126
1572
|
var logCommand = define({
|
|
1127
1573
|
name: "log",
|
|
1128
1574
|
tool: "kb_log",
|
|
1129
1575
|
usage: "log",
|
|
1130
1576
|
description: "What touched what, and when. The only artifact here that cannot be reconstructed from the records, so malformed lines are reported rather than repaired.",
|
|
1131
|
-
input:
|
|
1577
|
+
input: import_zod11.z.object({ bundlePath }),
|
|
1132
1578
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
1133
1579
|
run: ({ store }, { bundlePath: path }) => store.readLog(path)
|
|
1134
1580
|
});
|
|
1135
1581
|
|
|
1136
1582
|
// src/commands/no-decision.ts
|
|
1137
|
-
var
|
|
1583
|
+
var import_zod12 = require("zod");
|
|
1138
1584
|
var noDecisionCommand = define({
|
|
1139
1585
|
name: "no-decision",
|
|
1140
1586
|
tool: "kb_no_decision",
|
|
1141
1587
|
usage: "no-decision <reason...>",
|
|
1142
1588
|
description: 'Claim in one sentence that there was nothing to decide. Gating on "did you write a decision?" rewards writing a junk one; gating on "did you answer?" does not, so silence has to be expressible. Idempotent \u2014 restating it is not a collision.',
|
|
1143
|
-
input:
|
|
1589
|
+
input: import_zod12.z.object({ bundlePath, reason: import_zod12.z.string().min(1) }),
|
|
1144
1590
|
fromArgv: (argv, path) => ({
|
|
1145
1591
|
bundlePath: path,
|
|
1146
1592
|
reason: argv.slice(1).join(" ").trim()
|
|
@@ -1156,23 +1602,123 @@ var noDecisionCommand = define({
|
|
|
1156
1602
|
}
|
|
1157
1603
|
});
|
|
1158
1604
|
|
|
1605
|
+
// src/commands/pack.ts
|
|
1606
|
+
var import_zod13 = require("zod");
|
|
1607
|
+
var packCommand = define({
|
|
1608
|
+
name: "pack",
|
|
1609
|
+
tool: "kb_pack",
|
|
1610
|
+
usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
|
|
1611
|
+
description: "The bounded neighbourhood around one record: everything within `hops` of the root, ranked and cut to `maxNodes`, with every cut record named under Excluded \u2014 a named gap is knowable, a silent one is not. Prefer this over kb_load when the base is too large to hold whole and the work centres on one record; prefer it over kb_query when the question needs the governed neighbourhood \u2014 what was settled and what binds near this record \u2014 rather than a lookup by wording. Superseded records arrive as name, replacement and date stubs exactly as kb_load emits them: their bodies no longer hold, and kb_trace has the history. Refuses outright rather than truncating when the pack would exceed its token budget \u2014 a partial pack is indistinguishable from a complete one \u2014 reporting the record count and every already-cut id so the caller can lower hops or maxNodes, or raise the budget. The header carries the bundle, root, budget and a timestamp; everything below the header is byte-identical across runs over an unchanged base, so two packs can be diffed and a changed byte means changed knowledge. This tool (with kb_load, kb_query and kb_trace) is the only supported way to read a base; a raw file read bypasses supersession resolution and returns replaced records as if current.",
|
|
1612
|
+
input: import_zod13.z.object({
|
|
1613
|
+
bundlePath,
|
|
1614
|
+
conceptId,
|
|
1615
|
+
hops: import_zod13.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
|
|
1616
|
+
maxNodes: import_zod13.z.number().int().positive().optional().describe(
|
|
1617
|
+
"How many records the pack may hold, root included. Defaults to 20."
|
|
1618
|
+
),
|
|
1619
|
+
budgetTokens: import_zod13.z.number().int().positive().optional().describe(
|
|
1620
|
+
"Approximate token ceiling over what is actually emitted. Defaults to 25000."
|
|
1621
|
+
)
|
|
1622
|
+
}),
|
|
1623
|
+
fromArgv: (argv, path) => {
|
|
1624
|
+
const hops = argvFlag(argv, "--hops");
|
|
1625
|
+
const maxNodes = argvFlag(argv, "--max-nodes");
|
|
1626
|
+
const budget = argvFlag(argv, "--budget");
|
|
1627
|
+
return {
|
|
1628
|
+
bundlePath: path,
|
|
1629
|
+
conceptId: argv[1],
|
|
1630
|
+
...hops ? { hops: Number(hops) } : {},
|
|
1631
|
+
...maxNodes ? { maxNodes: Number(maxNodes) } : {},
|
|
1632
|
+
...budget ? { budgetTokens: Number(budget) } : {}
|
|
1633
|
+
};
|
|
1634
|
+
},
|
|
1635
|
+
run: async ({ store, now }, { bundlePath: path, conceptId: root, hops, maxNodes, budgetTokens }) => {
|
|
1636
|
+
const result = await store.pack(path, root, {
|
|
1637
|
+
...hops !== void 0 ? { hops } : {},
|
|
1638
|
+
...maxNodes !== void 0 ? { maxNodes } : {},
|
|
1639
|
+
...budgetTokens !== void 0 ? { budgetTokens } : {}
|
|
1640
|
+
});
|
|
1641
|
+
return render2(result, path, now());
|
|
1642
|
+
}
|
|
1643
|
+
});
|
|
1644
|
+
function render2(result, bundle, at) {
|
|
1645
|
+
const lines = [
|
|
1646
|
+
`# KB Pack \u2014 ${result.root}`,
|
|
1647
|
+
`bundle: ${bundle}`,
|
|
1648
|
+
`budget: ~${result.tokensLoaded} of ${result.budgetTokens} tokens, ${result.recordCount} records`,
|
|
1649
|
+
`packed: ${at}`,
|
|
1650
|
+
"",
|
|
1651
|
+
`## Records (${result.records.length})`
|
|
1652
|
+
];
|
|
1653
|
+
for (const record of result.records) {
|
|
1654
|
+
lines.push(
|
|
1655
|
+
"",
|
|
1656
|
+
`### ${record.conceptId}${record.title ? ` \u2014 ${record.title}` : ""} [${record.standing}]`
|
|
1657
|
+
);
|
|
1658
|
+
if (record.warnings.length) {
|
|
1659
|
+
lines.push(`warnings: ${record.warnings.map(warningLabel).join("; ")}`);
|
|
1660
|
+
}
|
|
1661
|
+
if (record.anchors.length) {
|
|
1662
|
+
lines.push(
|
|
1663
|
+
`anchors: ${record.anchors.map(
|
|
1664
|
+
(anchor) => anchor.symbol ? `${anchor.file}#${anchor.symbol}` : anchor.file
|
|
1665
|
+
).join(", ")}`
|
|
1666
|
+
);
|
|
1667
|
+
}
|
|
1668
|
+
lines.push("", record.body.trimEnd());
|
|
1669
|
+
}
|
|
1670
|
+
if (result.superseded.length) {
|
|
1671
|
+
lines.push("", `## Superseded (${result.superseded.length})`);
|
|
1672
|
+
for (const entry of result.superseded) {
|
|
1673
|
+
lines.push(
|
|
1674
|
+
`- ${entry.conceptId} \u2192 ${entry.supersededBy.join(", ") || "(no surviving head)"}${entry.at ? ` (${entry.at})` : ""}`
|
|
1675
|
+
);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
if (result.excluded.length) {
|
|
1679
|
+
lines.push("", `## Excluded (${result.excluded.length})`);
|
|
1680
|
+
for (const cut of result.excluded) lines.push(`- ${cut}`);
|
|
1681
|
+
}
|
|
1682
|
+
return lines.join("\n");
|
|
1683
|
+
}
|
|
1684
|
+
function warningLabel(warning) {
|
|
1685
|
+
switch (warning.kind) {
|
|
1686
|
+
case "superseded":
|
|
1687
|
+
return `superseded by ${warning.by.join(", ")}`;
|
|
1688
|
+
case "unsettled":
|
|
1689
|
+
return `unsettled (${warning.status})`;
|
|
1690
|
+
case "broken-chain":
|
|
1691
|
+
return `broken chain \u2014 ${warning.missing} is not in the bundle`;
|
|
1692
|
+
case "chain-cycle":
|
|
1693
|
+
return `chain cycle through ${warning.through.join(" \u2192 ")}`;
|
|
1694
|
+
case "forked-chain":
|
|
1695
|
+
return `forked chain \u2014 heads ${warning.heads.join(", ")}`;
|
|
1696
|
+
case "stale":
|
|
1697
|
+
return `stale since ${warning.staleAfter}`;
|
|
1698
|
+
case "unresolved-question":
|
|
1699
|
+
return "unresolved question";
|
|
1700
|
+
default:
|
|
1701
|
+
return warning.kind;
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1159
1705
|
// src/commands/pin.ts
|
|
1160
|
-
var
|
|
1706
|
+
var import_zod14 = require("zod");
|
|
1161
1707
|
var pinCommand = define({
|
|
1162
1708
|
name: "pin",
|
|
1163
1709
|
tool: "kb_pin",
|
|
1164
1710
|
usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
|
|
1165
1711
|
description: "Pin a base into a workspace pin manifest, so `context` surfaces it at every context birth. Three layers, nearest wins: the committed project manifest (.strauss/kb-pins.json, the default), `--local` (.strauss/kb-pins.local.json, personal and gitignored), and `--user` (~/.strauss/kb-pins.json, every workspace). Idempotent \u2014 re-pinning changes nothing unless --mode, --profiles, or --frozen/--unfreeze are given, which update just those fields. `--mode full` preloads the whole base into the block regardless of the full-under threshold; `--mode index` never upgrades. `--profiles` scopes the pin to named context profiles. `--frozen` marks the base concluded: write commands against it refuse and `context` labels it read-only. A path with no records yet succeeds with a warning; bases are routinely pinned before they are populated. Pins are workspace state: the pinned base itself is never touched.",
|
|
1166
|
-
input:
|
|
1712
|
+
input: import_zod14.z.object({
|
|
1167
1713
|
bundlePath,
|
|
1168
|
-
mode:
|
|
1714
|
+
mode: import_zod14.z.enum(["full", "index"]).optional().describe(
|
|
1169
1715
|
"full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
|
|
1170
1716
|
),
|
|
1171
|
-
profiles:
|
|
1172
|
-
layer:
|
|
1717
|
+
profiles: import_zod14.z.array(import_zod14.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
|
|
1718
|
+
layer: import_zod14.z.enum(["project", "local", "user"]).optional().describe(
|
|
1173
1719
|
"Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
|
|
1174
1720
|
),
|
|
1175
|
-
frozen:
|
|
1721
|
+
frozen: import_zod14.z.boolean().optional().describe(
|
|
1176
1722
|
"true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
|
|
1177
1723
|
)
|
|
1178
1724
|
}),
|
|
@@ -1201,29 +1747,29 @@ var pinCommand = define({
|
|
|
1201
1747
|
});
|
|
1202
1748
|
|
|
1203
1749
|
// src/commands/pins.ts
|
|
1204
|
-
var
|
|
1750
|
+
var import_zod15 = require("zod");
|
|
1205
1751
|
var pinsCommand = define({
|
|
1206
1752
|
name: "pins",
|
|
1207
1753
|
tool: "kb_pins",
|
|
1208
1754
|
usage: "pins",
|
|
1209
1755
|
description: "Every pinned base across the manifest layers, each with its layer and whether it currently resolves to readable records. Reads the workspace manifests rather than any one base, like kb_context.",
|
|
1210
|
-
input:
|
|
1756
|
+
input: import_zod15.z.object({}),
|
|
1211
1757
|
fromArgv: () => ({}),
|
|
1212
1758
|
run: ({ store }) => listPins(store, process.cwd())
|
|
1213
1759
|
});
|
|
1214
1760
|
|
|
1215
1761
|
// src/commands/query.ts
|
|
1216
|
-
var
|
|
1762
|
+
var import_zod16 = require("zod");
|
|
1217
1763
|
var queryCommand = define({
|
|
1218
1764
|
name: "query",
|
|
1219
1765
|
tool: "kb_query",
|
|
1220
1766
|
usage: "query <text...>",
|
|
1221
1767
|
description: "Search and return each match with its standing. Results are flagged, never filtered: a superseded record comes back alongside whatever replaced it, and a rejected one is marked as something explicitly not adopted. Prefer kb_load when the base fits its budget: on this package's measurements, a reader holding the whole base answered eight of nine questions whose wording appears in no record, where embedding search answered four. Never read record files directly \u2014 this tool (with kb_load and kb_trace) is the only supported way to read a base; a file read bypasses supersession resolution and returns replaced records as if current.",
|
|
1222
|
-
input:
|
|
1768
|
+
input: import_zod16.z.object({
|
|
1223
1769
|
bundlePath,
|
|
1224
|
-
text:
|
|
1225
|
-
type:
|
|
1226
|
-
includeNonCurrent:
|
|
1770
|
+
text: import_zod16.z.string().optional(),
|
|
1771
|
+
type: import_zod16.z.enum(KB_RECORD_TYPES).optional(),
|
|
1772
|
+
includeNonCurrent: import_zod16.z.boolean().optional()
|
|
1227
1773
|
}),
|
|
1228
1774
|
fromArgv: (argv, path) => ({
|
|
1229
1775
|
bundlePath: path,
|
|
@@ -1245,33 +1791,33 @@ var queryCommand = define({
|
|
|
1245
1791
|
});
|
|
1246
1792
|
|
|
1247
1793
|
// src/commands/read-index.ts
|
|
1248
|
-
var
|
|
1794
|
+
var import_zod17 = require("zod");
|
|
1249
1795
|
var readIndexCommand = define({
|
|
1250
1796
|
name: "index",
|
|
1251
1797
|
tool: "kb_index",
|
|
1252
1798
|
usage: "index",
|
|
1253
1799
|
description: "The index, rebuilt if it disagrees with the records. One call gives the whole shape of the base: title, type, status, and description per record. The cheap re-orientation call after compaction or deep in a long session \u2014 a few hundred tokens; call it (or kb_context, when bases are pinned) first, then kb_load or fetch by concept id.",
|
|
1254
|
-
input:
|
|
1800
|
+
input: import_zod17.z.object({ bundlePath }),
|
|
1255
1801
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
1256
1802
|
run: ({ store }, { bundlePath: path }) => store.readIndex(path)
|
|
1257
1803
|
});
|
|
1258
1804
|
|
|
1259
1805
|
// src/commands/schema.ts
|
|
1260
|
-
var
|
|
1806
|
+
var import_zod20 = require("zod");
|
|
1261
1807
|
|
|
1262
1808
|
// src/json-schema.ts
|
|
1263
|
-
var
|
|
1809
|
+
var import_zod19 = require("zod");
|
|
1264
1810
|
|
|
1265
1811
|
// src/kb-log.ts
|
|
1266
|
-
var
|
|
1812
|
+
var import_zod18 = require("zod");
|
|
1267
1813
|
var LOG_FILE = "log.jsonl";
|
|
1268
|
-
var kbLogEntrySchema =
|
|
1269
|
-
at:
|
|
1270
|
-
by:
|
|
1271
|
-
operation:
|
|
1272
|
-
conceptId:
|
|
1814
|
+
var kbLogEntrySchema = import_zod18.z.object({
|
|
1815
|
+
at: import_zod18.z.string().min(1),
|
|
1816
|
+
by: import_zod18.z.string().min(1),
|
|
1817
|
+
operation: import_zod18.z.string().min(1),
|
|
1818
|
+
conceptId: import_zod18.z.string().min(1),
|
|
1273
1819
|
/** Second concept id, where the operation relates two — supersession. */
|
|
1274
|
-
target:
|
|
1820
|
+
target: import_zod18.z.string().min(1).optional()
|
|
1275
1821
|
}).strict();
|
|
1276
1822
|
function renderLogEntry(entry) {
|
|
1277
1823
|
return `${JSON.stringify(kbLogEntrySchema.parse(entry))}
|
|
@@ -1302,11 +1848,11 @@ function parseLog(raw) {
|
|
|
1302
1848
|
// src/json-schema.ts
|
|
1303
1849
|
function kbJsonSchemas() {
|
|
1304
1850
|
return {
|
|
1305
|
-
recordFrontmatter:
|
|
1851
|
+
recordFrontmatter: import_zod19.z.toJSONSchema(kbRecordFrontmatterSchema, {
|
|
1306
1852
|
io: "input"
|
|
1307
1853
|
}),
|
|
1308
|
-
composeInput:
|
|
1309
|
-
logEntry:
|
|
1854
|
+
composeInput: import_zod19.z.toJSONSchema(composeInputSchema, { io: "input" }),
|
|
1855
|
+
logEntry: import_zod19.z.toJSONSchema(kbLogEntrySchema, { io: "input" })
|
|
1310
1856
|
};
|
|
1311
1857
|
}
|
|
1312
1858
|
|
|
@@ -1316,22 +1862,22 @@ var schemaCommand = define({
|
|
|
1316
1862
|
tool: "kb_schema",
|
|
1317
1863
|
usage: "schema",
|
|
1318
1864
|
description: "JSON Schema for the frontmatter, the write input, and log entries \u2014 generated from the code that enforces them, so it cannot drift from what a write will accept.",
|
|
1319
|
-
input:
|
|
1865
|
+
input: import_zod20.z.object({}),
|
|
1320
1866
|
fromArgv: () => ({}),
|
|
1321
1867
|
run: () => Promise.resolve(kbJsonSchemas())
|
|
1322
1868
|
});
|
|
1323
1869
|
|
|
1324
1870
|
// src/commands/status.ts
|
|
1325
|
-
var
|
|
1871
|
+
var import_zod21 = require("zod");
|
|
1326
1872
|
var statusCommand = define({
|
|
1327
1873
|
name: "status",
|
|
1328
1874
|
tool: "kb_status",
|
|
1329
1875
|
usage: "status <concept-id> <status>",
|
|
1330
1876
|
description: "Move a record's status, leaving everything else alone. Uses a compare-and-swap, so a concurrent change fails loudly rather than being overwritten.",
|
|
1331
|
-
input:
|
|
1877
|
+
input: import_zod21.z.object({
|
|
1332
1878
|
bundlePath,
|
|
1333
1879
|
conceptId,
|
|
1334
|
-
status:
|
|
1880
|
+
status: import_zod21.z.enum(KB_RECORD_STATUSES)
|
|
1335
1881
|
}),
|
|
1336
1882
|
fromArgv: (argv, path) => ({
|
|
1337
1883
|
bundlePath: path,
|
|
@@ -1346,13 +1892,13 @@ var statusCommand = define({
|
|
|
1346
1892
|
});
|
|
1347
1893
|
|
|
1348
1894
|
// src/commands/supersede.ts
|
|
1349
|
-
var
|
|
1895
|
+
var import_zod22 = require("zod");
|
|
1350
1896
|
var supersedeCommand = define({
|
|
1351
1897
|
name: "supersede",
|
|
1352
1898
|
tool: "kb_supersede",
|
|
1353
1899
|
usage: "supersede <concept-id> <replacement-id>",
|
|
1354
1900
|
description: "Mark a record superseded by another, linking both directions. Use this rather than editing a record whose meaning changed \u2014 a record that quietly becomes something else invalidates every reference to it, and the earlier understanding is what a later trace needs.",
|
|
1355
|
-
input:
|
|
1901
|
+
input: import_zod22.z.object({ bundlePath, conceptId, replacementId: conceptId }),
|
|
1356
1902
|
fromArgv: (argv, path) => ({
|
|
1357
1903
|
bundlePath: path,
|
|
1358
1904
|
conceptId: argv[1],
|
|
@@ -1366,16 +1912,16 @@ var supersedeCommand = define({
|
|
|
1366
1912
|
});
|
|
1367
1913
|
|
|
1368
1914
|
// src/commands/sync-instructions.ts
|
|
1369
|
-
var
|
|
1915
|
+
var import_zod23 = require("zod");
|
|
1370
1916
|
var syncInstructionsCommand = define({
|
|
1371
1917
|
name: "sync-instructions",
|
|
1372
1918
|
usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
|
|
1373
1919
|
description: "Idempotently plant the `context` block between sentinel comments in an instruction file (AGENTS.md, CLAUDE.md), creating the block when absent and leaving everything outside the sentinels alone. CLI-only: this is file plumbing for runtimes whose instruction files are re-read where their conversations are not, not an agent capability \u2014 the capability is kb_context.",
|
|
1374
|
-
input:
|
|
1375
|
-
file:
|
|
1376
|
-
budgetTokens:
|
|
1377
|
-
fullUnderTokens:
|
|
1378
|
-
profile:
|
|
1920
|
+
input: import_zod23.z.object({
|
|
1921
|
+
file: import_zod23.z.string().min(1).describe("The instruction file to edit in place."),
|
|
1922
|
+
budgetTokens: import_zod23.z.number().int().positive().optional(),
|
|
1923
|
+
fullUnderTokens: import_zod23.z.number().int().positive().optional(),
|
|
1924
|
+
profile: import_zod23.z.string().optional()
|
|
1379
1925
|
}),
|
|
1380
1926
|
fromArgv: (argv) => {
|
|
1381
1927
|
const budget = argvFlag(argv, "--budget");
|
|
@@ -1401,7 +1947,7 @@ var syncInstructionsCommand = define({
|
|
|
1401
1947
|
});
|
|
1402
1948
|
|
|
1403
1949
|
// src/commands/trace.ts
|
|
1404
|
-
var
|
|
1950
|
+
var import_zod24 = require("zod");
|
|
1405
1951
|
|
|
1406
1952
|
// src/trace.ts
|
|
1407
1953
|
var TRACE_EDGES = ["supersession", "anchor", "source"];
|
|
@@ -1419,7 +1965,7 @@ function trace(seedId, bundle, options = {}) {
|
|
|
1419
1965
|
const next = [];
|
|
1420
1966
|
for (const from of frontier) {
|
|
1421
1967
|
for (const edge of edges) {
|
|
1422
|
-
for (const record of
|
|
1968
|
+
for (const record of edgeNeighbours(from, bundle, edge)) {
|
|
1423
1969
|
const existing = reached.get(record.conceptId);
|
|
1424
1970
|
if (existing) {
|
|
1425
1971
|
if (existing.depth > 0 && !existing.via.includes(edge)) {
|
|
@@ -1436,41 +1982,6 @@ function trace(seedId, bundle, options = {}) {
|
|
|
1436
1982
|
}
|
|
1437
1983
|
return [...reached.values()].sort(byGeneratedAt);
|
|
1438
1984
|
}
|
|
1439
|
-
function neighbours(from, bundle, edge) {
|
|
1440
|
-
switch (edge) {
|
|
1441
|
-
case "supersession":
|
|
1442
|
-
return bundle.filter(
|
|
1443
|
-
(candidate) => candidate.conceptId !== from.conceptId && (candidate.conceptId === from.frontmatter.strauss_superseded_by || from.frontmatter.strauss_supersedes?.includes(
|
|
1444
|
-
candidate.conceptId
|
|
1445
|
-
) || candidate.frontmatter.strauss_superseded_by === from.conceptId || candidate.frontmatter.strauss_supersedes?.includes(from.conceptId))
|
|
1446
|
-
);
|
|
1447
|
-
// The edge that answers "why is this code shaped this way": every record
|
|
1448
|
-
// attached to the same file or symbol, whatever its standing.
|
|
1449
|
-
case "anchor": {
|
|
1450
|
-
const mine = from.frontmatter.strauss_anchors ?? [];
|
|
1451
|
-
if (!mine.length) return [];
|
|
1452
|
-
return bundle.filter(
|
|
1453
|
-
(candidate) => candidate.conceptId !== from.conceptId && (candidate.frontmatter.strauss_anchors ?? []).some(
|
|
1454
|
-
(theirs) => mine.some((ours) => anchorsTouch(ours, theirs))
|
|
1455
|
-
)
|
|
1456
|
-
);
|
|
1457
|
-
}
|
|
1458
|
-
case "source": {
|
|
1459
|
-
const mine = new Set((from.frontmatter.sources ?? []).map((s) => s.id));
|
|
1460
|
-
if (!mine.size) return [];
|
|
1461
|
-
return bundle.filter(
|
|
1462
|
-
(candidate) => candidate.conceptId !== from.conceptId && (candidate.frontmatter.sources ?? []).some(
|
|
1463
|
-
(source) => mine.has(source.id)
|
|
1464
|
-
)
|
|
1465
|
-
);
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1468
|
-
}
|
|
1469
|
-
function anchorsTouch(left, right) {
|
|
1470
|
-
if (left.file !== right.file) return false;
|
|
1471
|
-
if (!left.symbol || !right.symbol) return true;
|
|
1472
|
-
return left.symbol === right.symbol;
|
|
1473
|
-
}
|
|
1474
1985
|
function byGeneratedAt(left, right) {
|
|
1475
1986
|
const at = (step) => step.record.frontmatter.generated?.at ?? "";
|
|
1476
1987
|
return at(left).localeCompare(at(right)) || left.depth - right.depth;
|
|
@@ -1482,11 +1993,11 @@ var traceCommand = define({
|
|
|
1482
1993
|
tool: "kb_trace",
|
|
1483
1994
|
usage: "trace <concept-id> [edges...]",
|
|
1484
1995
|
description: 'How a position was arrived at, as a timeline ordered by when each record was written. Deliberately includes rejected, draft, and superseded records \u2014 in a history those are the content, not noise. Follows supersession, shared code anchors, and shared sources. Use when the question is "why is this the way it is" rather than "what do we hold now". This tool (with kb_load and kb_query) is the only supported way to read a base; a raw file read bypasses supersession resolution and returns replaced records as if current.',
|
|
1485
|
-
input:
|
|
1996
|
+
input: import_zod24.z.object({
|
|
1486
1997
|
bundlePath,
|
|
1487
1998
|
conceptId,
|
|
1488
|
-
edges:
|
|
1489
|
-
depth:
|
|
1999
|
+
edges: import_zod24.z.array(import_zod24.z.enum(TRACE_EDGES)).optional(),
|
|
2000
|
+
depth: import_zod24.z.number().int().positive().optional()
|
|
1490
2001
|
}),
|
|
1491
2002
|
fromArgv: (argv, path) => ({
|
|
1492
2003
|
bundlePath: path,
|
|
@@ -1508,90 +2019,53 @@ var traceCommand = define({
|
|
|
1508
2019
|
});
|
|
1509
2020
|
|
|
1510
2021
|
// src/commands/types.ts
|
|
1511
|
-
var
|
|
2022
|
+
var import_zod25 = require("zod");
|
|
1512
2023
|
var typesCommand = define({
|
|
1513
2024
|
name: "types",
|
|
1514
2025
|
tool: "kb_types",
|
|
1515
2026
|
usage: "types",
|
|
1516
2027
|
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.",
|
|
1517
|
-
input:
|
|
2028
|
+
input: import_zod25.z.object({}),
|
|
1518
2029
|
fromArgv: () => ({}),
|
|
1519
2030
|
run: () => Promise.resolve(RECORD_TYPES)
|
|
1520
2031
|
});
|
|
1521
2032
|
|
|
1522
2033
|
// src/commands/unpin.ts
|
|
1523
|
-
var
|
|
2034
|
+
var import_zod26 = require("zod");
|
|
1524
2035
|
var unpinCommand = define({
|
|
1525
2036
|
name: "unpin",
|
|
1526
2037
|
tool: "kb_unpin",
|
|
1527
2038
|
usage: "unpin [bundle-path]",
|
|
1528
2039
|
description: "Remove a base from every pin manifest layer that holds it \u2014 project, local, and user \u2014 because unpinned means gone, not still injected from another file. Reports which layers were touched.",
|
|
1529
|
-
input:
|
|
2040
|
+
input: import_zod26.z.object({ bundlePath }),
|
|
1530
2041
|
fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
|
|
1531
2042
|
run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
|
|
1532
2043
|
});
|
|
1533
2044
|
|
|
1534
2045
|
// src/commands/validate.ts
|
|
1535
|
-
var
|
|
1536
|
-
|
|
1537
|
-
// src/validate.ts
|
|
1538
|
-
function validateBundle(records) {
|
|
1539
|
-
const byId = new Map(records.map((record) => [record.conceptId, record]));
|
|
1540
|
-
const problems = [];
|
|
1541
|
-
const report = (check, conceptId2, note) => problems.push({ check, conceptId: conceptId2, note });
|
|
1542
|
-
for (const record of records) {
|
|
1543
|
-
const { conceptId: conceptId2, frontmatter: fm } = record;
|
|
1544
|
-
if (!isKbRecordType(fm.type)) {
|
|
1545
|
-
report("type", conceptId2, `unrecognised type "${fm.type}"`);
|
|
1546
|
-
}
|
|
1547
|
-
if (fm.strauss_status === "superseded") {
|
|
1548
|
-
const by = fm.strauss_superseded_by;
|
|
1549
|
-
if (!by) {
|
|
1550
|
-
report("superseded_by", conceptId2, "superseded with no replacement");
|
|
1551
|
-
} else if (!byId.has(by)) {
|
|
1552
|
-
report("superseded_by", conceptId2, `replacement ${by} is missing`);
|
|
1553
|
-
} else if (!byId.get(by)?.frontmatter.strauss_supersedes?.includes(conceptId2)) {
|
|
1554
|
-
report("backlink", by, `does not list ${conceptId2} in supersedes`);
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
for (const old of fm.strauss_supersedes ?? []) {
|
|
1558
|
-
const previous = byId.get(old);
|
|
1559
|
-
if (!previous) {
|
|
1560
|
-
report("supersedes", conceptId2, `target ${old} is missing`);
|
|
1561
|
-
} else if (previous.frontmatter.strauss_status !== "superseded") {
|
|
1562
|
-
report("supersedes", conceptId2, `${old} is not marked superseded`);
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1565
|
-
if (fm.strauss_assumption && fm.sources?.length) {
|
|
1566
|
-
report("assumption", conceptId2, "marked an assumption but cites sources");
|
|
1567
|
-
}
|
|
1568
|
-
}
|
|
1569
|
-
return problems;
|
|
1570
|
-
}
|
|
1571
|
-
|
|
1572
|
-
// src/commands/validate.ts
|
|
2046
|
+
var import_zod27 = require("zod");
|
|
1573
2047
|
var validateCommand = define({
|
|
1574
2048
|
name: "validate",
|
|
1575
2049
|
tool: "kb_validate",
|
|
1576
2050
|
usage: "validate",
|
|
1577
2051
|
description: "Check pointers no single record can see: supersession links that disagree between the two records, and assumptions that cite sources. Per-record shape is enforced on every read, so a problem here means someone edited a file by hand.",
|
|
1578
|
-
input:
|
|
2052
|
+
input: import_zod27.z.object({ bundlePath }),
|
|
1579
2053
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
1580
2054
|
run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
|
|
1581
2055
|
failsWhen: (result) => Array.isArray(result) && result.length > 0
|
|
1582
2056
|
});
|
|
1583
2057
|
|
|
1584
2058
|
// src/commands/verify.ts
|
|
1585
|
-
var
|
|
2059
|
+
var import_zod28 = require("zod");
|
|
1586
2060
|
var verifyCommand = define({
|
|
1587
2061
|
name: "verify",
|
|
1588
2062
|
tool: "kb_verify",
|
|
1589
2063
|
usage: "verify <concept-id> --note <text>",
|
|
1590
2064
|
description: "Append one verified[] event \u2014 who checked the record, when, and what the check found. Appends only; prior events are never rewritten. A record's own generator is refused unless the actor is human: re-reading your own output is not an independent check.",
|
|
1591
|
-
input:
|
|
2065
|
+
input: import_zod28.z.object({
|
|
1592
2066
|
bundlePath,
|
|
1593
2067
|
conceptId,
|
|
1594
|
-
note:
|
|
2068
|
+
note: import_zod28.z.string().refine((s) => s.trim().length > 0, {
|
|
1595
2069
|
message: "note must say what the check found"
|
|
1596
2070
|
})
|
|
1597
2071
|
}),
|
|
@@ -1611,7 +2085,7 @@ var verifyCommand = define({
|
|
|
1611
2085
|
});
|
|
1612
2086
|
|
|
1613
2087
|
// src/commands/write.ts
|
|
1614
|
-
var
|
|
2088
|
+
var import_zod29 = require("zod");
|
|
1615
2089
|
var writeCommand = define({
|
|
1616
2090
|
name: "write",
|
|
1617
2091
|
tool: "kb_write",
|
|
@@ -1625,9 +2099,9 @@ var writeCommand = define({
|
|
|
1625
2099
|
"- Prefer a new record over overloading an existing one, and keep each short. A record nobody finishes reading is not durable memory.",
|
|
1626
2100
|
"- Records are never deleted; supersede instead, so the earlier reasoning stays inspectable."
|
|
1627
2101
|
].join("\n"),
|
|
1628
|
-
input:
|
|
2102
|
+
input: import_zod29.z.object({
|
|
1629
2103
|
bundlePath,
|
|
1630
|
-
type:
|
|
2104
|
+
type: import_zod29.z.enum(KB_RECORD_TYPES),
|
|
1631
2105
|
input: composeInputSchema
|
|
1632
2106
|
}),
|
|
1633
2107
|
fromArgv: async (argv, path, stdin) => ({
|
|
@@ -1651,7 +2125,7 @@ var writeCommand = define({
|
|
|
1651
2125
|
});
|
|
1652
2126
|
|
|
1653
2127
|
// src/commands/write-decision.ts
|
|
1654
|
-
var
|
|
2128
|
+
var import_zod30 = require("zod");
|
|
1655
2129
|
var writeDecisionCommand = define({
|
|
1656
2130
|
name: "write-decision",
|
|
1657
2131
|
tool: "kb_write_decision",
|
|
@@ -1664,7 +2138,7 @@ var writeDecisionCommand = define({
|
|
|
1664
2138
|
"- `alternative` is what you turned down and why, not a list of everything considered.",
|
|
1665
2139
|
"- A reference to material you read goes in `sources`; a reference to code goes in `anchors`; a reference to another record goes in `relatedConceptIds`."
|
|
1666
2140
|
].join("\n"),
|
|
1667
|
-
input:
|
|
2141
|
+
input: import_zod30.z.object({ bundlePath, input: decisionInputSchema }),
|
|
1668
2142
|
fromArgv: async (_argv, path, stdin) => ({
|
|
1669
2143
|
bundlePath: path,
|
|
1670
2144
|
input: JSON.parse(await stdin())
|
|
@@ -1694,12 +2168,14 @@ var KB_COMMANDS = [
|
|
|
1694
2168
|
answerCommand,
|
|
1695
2169
|
verifyCommand,
|
|
1696
2170
|
loadCommand,
|
|
2171
|
+
packCommand,
|
|
1697
2172
|
queryCommand,
|
|
1698
2173
|
traceCommand,
|
|
1699
2174
|
listCommand,
|
|
1700
2175
|
readIndexCommand,
|
|
1701
2176
|
logCommand,
|
|
1702
2177
|
validateCommand,
|
|
2178
|
+
doctorCommand,
|
|
1703
2179
|
schemaCommand,
|
|
1704
2180
|
pinCommand,
|
|
1705
2181
|
unpinCommand,
|
|
@@ -1827,6 +2303,27 @@ var KbSelfVerificationError = class extends BaseError {
|
|
|
1827
2303
|
actor;
|
|
1828
2304
|
generatedBy;
|
|
1829
2305
|
};
|
|
2306
|
+
var KbPackBudgetExceededError = class extends BaseError {
|
|
2307
|
+
constructor(recordCount, approxTokens2, budgetTokens, excluded) {
|
|
2308
|
+
super({
|
|
2309
|
+
message: `kb: a pack of ${recordCount} records is ~${approxTokens2} tokens against a budget of ${budgetTokens} \u2014 lower hops or maxNodes, or raise the budget`,
|
|
2310
|
+
errorType: "KbPackBudgetExceeded" /* KbPackBudgetExceeded */,
|
|
2311
|
+
code: 400,
|
|
2312
|
+
fault: "User" /* User */,
|
|
2313
|
+
retriable: false,
|
|
2314
|
+
reportToUser: true,
|
|
2315
|
+
details: { recordCount, approxTokens: approxTokens2, budgetTokens, excluded }
|
|
2316
|
+
});
|
|
2317
|
+
this.recordCount = recordCount;
|
|
2318
|
+
this.approxTokens = approxTokens2;
|
|
2319
|
+
this.budgetTokens = budgetTokens;
|
|
2320
|
+
this.excluded = excluded;
|
|
2321
|
+
}
|
|
2322
|
+
recordCount;
|
|
2323
|
+
approxTokens;
|
|
2324
|
+
budgetTokens;
|
|
2325
|
+
excluded;
|
|
2326
|
+
};
|
|
1830
2327
|
var KbInvalidConceptIdError = class extends BaseError {
|
|
1831
2328
|
constructor(message, details) {
|
|
1832
2329
|
super({
|
|
@@ -1927,6 +2424,90 @@ async function loadQmd(logger) {
|
|
|
1927
2424
|
}
|
|
1928
2425
|
}
|
|
1929
2426
|
|
|
2427
|
+
// src/pack.ts
|
|
2428
|
+
var DEFAULT_PACK_HOPS = 2;
|
|
2429
|
+
var DEFAULT_PACK_MAX_NODES = 20;
|
|
2430
|
+
var TYPE_PRIORITY = [
|
|
2431
|
+
"decision",
|
|
2432
|
+
"constraint",
|
|
2433
|
+
"requirement",
|
|
2434
|
+
...KB_RECORD_TYPES.filter(
|
|
2435
|
+
(type) => !["decision", "constraint", "requirement"].includes(type)
|
|
2436
|
+
)
|
|
2437
|
+
];
|
|
2438
|
+
function pack(bundle, rootId, options = {}) {
|
|
2439
|
+
const hops = options.hops ?? DEFAULT_PACK_HOPS;
|
|
2440
|
+
const maxNodes = options.maxNodes ?? DEFAULT_PACK_MAX_NODES;
|
|
2441
|
+
const budgetTokens = options.budgetTokens ?? DEFAULT_LOAD_BUDGET;
|
|
2442
|
+
const byId = new Map(bundle.map((record) => [record.conceptId, record]));
|
|
2443
|
+
const root = byId.get(rootId);
|
|
2444
|
+
if (!root) throw new KbRecordNotFoundError(rootId);
|
|
2445
|
+
const reached = [{ record: root, depth: 0 }];
|
|
2446
|
+
const seen = /* @__PURE__ */ new Set([rootId]);
|
|
2447
|
+
let frontier = [root];
|
|
2448
|
+
for (let depth = 1; frontier.length; depth += 1) {
|
|
2449
|
+
const next = [];
|
|
2450
|
+
for (const from of frontier) {
|
|
2451
|
+
for (const { record } of neighbours(from, bundle)) {
|
|
2452
|
+
if (seen.has(record.conceptId)) continue;
|
|
2453
|
+
seen.add(record.conceptId);
|
|
2454
|
+
reached.push({ record, depth });
|
|
2455
|
+
next.push(record);
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
frontier = next;
|
|
2459
|
+
}
|
|
2460
|
+
reached.sort(byRank);
|
|
2461
|
+
const within = reached.filter((entry) => entry.depth <= hops);
|
|
2462
|
+
const kept = within.slice(0, maxNodes);
|
|
2463
|
+
const excluded = [
|
|
2464
|
+
...within.slice(maxNodes),
|
|
2465
|
+
...reached.filter((entry) => entry.depth > hops)
|
|
2466
|
+
].map((entry) => entry.record.conceptId).sort();
|
|
2467
|
+
const adjudicated = adjudicate(
|
|
2468
|
+
kept.map((entry) => entry.record),
|
|
2469
|
+
bundle
|
|
2470
|
+
);
|
|
2471
|
+
const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(stub);
|
|
2472
|
+
const whole = adjudicated.filter((hit) => hit.standing !== "superseded");
|
|
2473
|
+
const tokensLoaded = whole.reduce((total, hit) => total + estimateTokens(hit.record), 0) + superseded.reduce((total, entry) => total + estimateStubTokens(entry), 0);
|
|
2474
|
+
const recordCount = adjudicated.length;
|
|
2475
|
+
if (tokensLoaded > budgetTokens) {
|
|
2476
|
+
throw new KbPackBudgetExceededError(
|
|
2477
|
+
recordCount,
|
|
2478
|
+
tokensLoaded,
|
|
2479
|
+
budgetTokens,
|
|
2480
|
+
excluded
|
|
2481
|
+
);
|
|
2482
|
+
}
|
|
2483
|
+
return {
|
|
2484
|
+
root: rootId,
|
|
2485
|
+
records: whole.map((hit) => ({
|
|
2486
|
+
conceptId: hit.record.conceptId,
|
|
2487
|
+
title: hit.record.frontmatter.title ?? null,
|
|
2488
|
+
standing: hit.standing,
|
|
2489
|
+
supersededBy: hit.heads.map((head) => head.conceptId),
|
|
2490
|
+
warnings: hit.warnings,
|
|
2491
|
+
anchors: hit.record.frontmatter.strauss_anchors ?? [],
|
|
2492
|
+
body: hit.record.body
|
|
2493
|
+
})),
|
|
2494
|
+
superseded,
|
|
2495
|
+
excluded,
|
|
2496
|
+
recordCount,
|
|
2497
|
+
tokensLoaded,
|
|
2498
|
+
budgetTokens
|
|
2499
|
+
};
|
|
2500
|
+
}
|
|
2501
|
+
function byRank(left, right) {
|
|
2502
|
+
return left.depth - right.depth || typeRank(left.record) - typeRank(right.record) || (left.record.frontmatter.title ?? "").localeCompare(
|
|
2503
|
+
right.record.frontmatter.title ?? ""
|
|
2504
|
+
) || left.record.conceptId.localeCompare(right.record.conceptId);
|
|
2505
|
+
}
|
|
2506
|
+
function typeRank(record) {
|
|
2507
|
+
const index = TYPE_PRIORITY.indexOf(record.frontmatter.type);
|
|
2508
|
+
return index === -1 ? TYPE_PRIORITY.length : index;
|
|
2509
|
+
}
|
|
2510
|
+
|
|
1930
2511
|
// src/kb-store.ts
|
|
1931
2512
|
var KB_DIR = (0, import_node_path6.join)(".strauss", "kb");
|
|
1932
2513
|
var STORE_OWNED = /* @__PURE__ */ new Set([INDEX_FILE, LOG_FILE, SEARCH_INDEX_FILE]);
|
|
@@ -2216,6 +2797,10 @@ ${answer}
|
|
|
2216
2797
|
async trace(bundlePath2, seedId, options = {}) {
|
|
2217
2798
|
return trace(seedId, await this.list(bundlePath2), options);
|
|
2218
2799
|
}
|
|
2800
|
+
/** A bounded neighbourhood around one record. See `pack.ts`. */
|
|
2801
|
+
async pack(bundlePath2, rootId, options = {}) {
|
|
2802
|
+
return pack(await this.list(bundlePath2), rootId, options);
|
|
2803
|
+
}
|
|
2219
2804
|
/**
|
|
2220
2805
|
* The stored index, rebuilt if it disagrees with the records.
|
|
2221
2806
|
*
|
|
@@ -2422,16 +3007,33 @@ function digest(contents) {
|
|
|
2422
3007
|
return (0, import_node_crypto.createHash)("sha256").update(contents).digest("hex");
|
|
2423
3008
|
}
|
|
2424
3009
|
|
|
3010
|
+
// src/version.ts
|
|
3011
|
+
var VERSION = true ? "0.1.8" : "0.0.0-dev";
|
|
3012
|
+
|
|
2425
3013
|
// src/cli.ts
|
|
2426
3014
|
async function runKbCli(argv) {
|
|
2427
|
-
const {
|
|
2428
|
-
const
|
|
3015
|
+
const { flags, literal } = takeLiteral(argv);
|
|
3016
|
+
const { bundle, rest: withFlags } = takeBundle(flags);
|
|
3017
|
+
const name = withFlags[0] ?? "";
|
|
2429
3018
|
if (!name || name === "-h" || name === "--help") {
|
|
2430
3019
|
process.stdout.write(usage());
|
|
2431
3020
|
return;
|
|
2432
3021
|
}
|
|
3022
|
+
if (name === "--version" || name === "-v") {
|
|
3023
|
+
process.stdout.write(`${VERSION}
|
|
3024
|
+
`);
|
|
3025
|
+
return;
|
|
3026
|
+
}
|
|
2433
3027
|
const command = KB_COMMANDS_BY_NAME.get(name);
|
|
2434
3028
|
if (!command) die(`unknown command ${name}`);
|
|
3029
|
+
const json = withFlags.includes("--json");
|
|
3030
|
+
if (json && !command.render) {
|
|
3031
|
+
die(`${name} takes no --json: its result is already the machine shape`);
|
|
3032
|
+
}
|
|
3033
|
+
const rest = [
|
|
3034
|
+
...json ? withFlags.filter((argument) => argument !== "--json") : withFlags,
|
|
3035
|
+
...literal
|
|
3036
|
+
];
|
|
2435
3037
|
const raw = await command.fromArgv(rest, bundle, readStdin);
|
|
2436
3038
|
const parsed = command.input.safeParse(raw);
|
|
2437
3039
|
if (!parsed.success) {
|
|
@@ -2451,13 +3053,16 @@ async function runKbCli(argv) {
|
|
|
2451
3053
|
},
|
|
2452
3054
|
parsed.data
|
|
2453
3055
|
);
|
|
2454
|
-
if (command.failsWhen?.(result)) process.exitCode = 1;
|
|
3056
|
+
if (command.failsWhen?.(result, parsed.data)) process.exitCode = 1;
|
|
2455
3057
|
if (result === "") return;
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
`
|
|
2459
|
-
|
|
2460
|
-
|
|
3058
|
+
const text = command.render && !json ? command.render(result) : typeof result === "string" ? result : JSON.stringify(result, null, 2);
|
|
3059
|
+
process.stdout.write(text.endsWith("\n") ? text : `${text}
|
|
3060
|
+
`);
|
|
3061
|
+
}
|
|
3062
|
+
function takeLiteral(argv) {
|
|
3063
|
+
const at = argv.indexOf("--");
|
|
3064
|
+
if (at === -1) return { flags: argv, literal: [] };
|
|
3065
|
+
return { flags: argv.slice(0, at), literal: argv.slice(at + 1) };
|
|
2461
3066
|
}
|
|
2462
3067
|
function takeBundle(argv) {
|
|
2463
3068
|
const at = argv.indexOf("--bundle");
|
|
@@ -2499,6 +3104,9 @@ function usage() {
|
|
|
2499
3104
|
),
|
|
2500
3105
|
"",
|
|
2501
3106
|
` --bundle PATH defaults to ./${KB_DIR}`,
|
|
3107
|
+
" --json the machine shape, where a command prints a table",
|
|
3108
|
+
" -- everything after it is text, not flags",
|
|
3109
|
+
" --version the installed package version",
|
|
2502
3110
|
" STRAUSS_KB_ACTOR names the writer in the log",
|
|
2503
3111
|
""
|
|
2504
3112
|
].join("\n");
|