@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
|
@@ -934,6 +934,369 @@ ${CONTEXT_END}` : null;
|
|
|
934
934
|
return { file, action: "appended" };
|
|
935
935
|
}
|
|
936
936
|
|
|
937
|
+
// src/kb-edges.ts
|
|
938
|
+
var KB_EDGE_KINDS = [
|
|
939
|
+
"body-link",
|
|
940
|
+
"supersession",
|
|
941
|
+
"anchor",
|
|
942
|
+
"source"
|
|
943
|
+
];
|
|
944
|
+
var BODY_LINK_TARGET = new RegExp(
|
|
945
|
+
`\\]\\((${KB_CONCEPT_ID_PATTERN.source.replace(/^\^|\$$/g, "")})\\.md\\)`,
|
|
946
|
+
"g"
|
|
947
|
+
);
|
|
948
|
+
function neighbours(from, bundle, kinds = KB_EDGE_KINDS) {
|
|
949
|
+
const found = /* @__PURE__ */ new Map();
|
|
950
|
+
for (const kind of kinds) {
|
|
951
|
+
for (const record of edgeNeighbours(from, bundle, kind)) {
|
|
952
|
+
const existing = found.get(record.conceptId);
|
|
953
|
+
if (existing) {
|
|
954
|
+
if (!existing.via.includes(kind)) existing.via.push(kind);
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
found.set(record.conceptId, { record, via: [kind] });
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
return [...found.values()];
|
|
961
|
+
}
|
|
962
|
+
function edgeNeighbours(from, bundle, kind) {
|
|
963
|
+
switch (kind) {
|
|
964
|
+
// A link whose target is not in the bundle is legal per compose.ts —
|
|
965
|
+
// records are routinely written before the ones they point at exist — so
|
|
966
|
+
// missing targets are skipped, never an error.
|
|
967
|
+
case "body-link": {
|
|
968
|
+
const targets = new Set(
|
|
969
|
+
[...from.body.matchAll(BODY_LINK_TARGET)].map((match) => match[1])
|
|
970
|
+
);
|
|
971
|
+
if (!targets.size) return [];
|
|
972
|
+
return bundle.filter(
|
|
973
|
+
(candidate) => candidate.conceptId !== from.conceptId && targets.has(candidate.conceptId)
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
// Both directions and both pointers: `supersede()` writes the pair, but a
|
|
977
|
+
// hand-edit can leave one side behind, and a walk trusting one pointer
|
|
978
|
+
// would miss a replacement the bundle openly declares.
|
|
979
|
+
case "supersession":
|
|
980
|
+
return bundle.filter(
|
|
981
|
+
(candidate) => candidate.conceptId !== from.conceptId && (candidate.conceptId === from.frontmatter.strauss_superseded_by || from.frontmatter.strauss_supersedes?.includes(
|
|
982
|
+
candidate.conceptId
|
|
983
|
+
) || candidate.frontmatter.strauss_superseded_by === from.conceptId || candidate.frontmatter.strauss_supersedes?.includes(from.conceptId))
|
|
984
|
+
);
|
|
985
|
+
// The edge that answers "why is this code shaped this way": every record
|
|
986
|
+
// attached to the same file or symbol, whatever its standing.
|
|
987
|
+
case "anchor": {
|
|
988
|
+
const mine = from.frontmatter.strauss_anchors ?? [];
|
|
989
|
+
if (!mine.length) return [];
|
|
990
|
+
return bundle.filter(
|
|
991
|
+
(candidate) => candidate.conceptId !== from.conceptId && (candidate.frontmatter.strauss_anchors ?? []).some(
|
|
992
|
+
(theirs) => mine.some((ours) => anchorsTouch(ours, theirs))
|
|
993
|
+
)
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
case "source": {
|
|
997
|
+
const mine = new Set((from.frontmatter.sources ?? []).map((s) => s.id));
|
|
998
|
+
if (!mine.size) return [];
|
|
999
|
+
return bundle.filter(
|
|
1000
|
+
(candidate) => candidate.conceptId !== from.conceptId && (candidate.frontmatter.sources ?? []).some(
|
|
1001
|
+
(source) => mine.has(source.id)
|
|
1002
|
+
)
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
function anchorsTouch(left, right) {
|
|
1008
|
+
if (left.file !== right.file) return false;
|
|
1009
|
+
if (!left.symbol || !right.symbol) return true;
|
|
1010
|
+
return left.symbol === right.symbol;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// src/validate.ts
|
|
1014
|
+
function validateBundle(records) {
|
|
1015
|
+
const byId = new Map(records.map((record) => [record.conceptId, record]));
|
|
1016
|
+
const problems = [];
|
|
1017
|
+
const report = (check, conceptId2, note) => problems.push({ check, conceptId: conceptId2, note });
|
|
1018
|
+
for (const record of records) {
|
|
1019
|
+
const { conceptId: conceptId2, frontmatter: fm } = record;
|
|
1020
|
+
if (!isKbRecordType(fm.type)) {
|
|
1021
|
+
report("type", conceptId2, `unrecognised type "${fm.type}"`);
|
|
1022
|
+
}
|
|
1023
|
+
if (fm.strauss_status === "superseded") {
|
|
1024
|
+
const by = fm.strauss_superseded_by;
|
|
1025
|
+
if (!by) {
|
|
1026
|
+
report("superseded_by", conceptId2, "superseded with no replacement");
|
|
1027
|
+
} else if (!byId.has(by)) {
|
|
1028
|
+
report("superseded_by", conceptId2, `replacement ${by} is missing`);
|
|
1029
|
+
} else if (!byId.get(by)?.frontmatter.strauss_supersedes?.includes(conceptId2)) {
|
|
1030
|
+
report("backlink", by, `does not list ${conceptId2} in supersedes`);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
for (const old of fm.strauss_supersedes ?? []) {
|
|
1034
|
+
const previous = byId.get(old);
|
|
1035
|
+
if (!previous) {
|
|
1036
|
+
report("supersedes", conceptId2, `target ${old} is missing`);
|
|
1037
|
+
} else if (previous.frontmatter.strauss_status !== "superseded") {
|
|
1038
|
+
report("supersedes", conceptId2, `${old} is not marked superseded`);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
if (fm.strauss_assumption && fm.sources?.length) {
|
|
1042
|
+
report("assumption", conceptId2, "marked an assumption but cites sources");
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
return problems;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// src/doctor.ts
|
|
1049
|
+
var DEFAULT_EXPIRING_DAYS = 30;
|
|
1050
|
+
var DEFAULT_UNVERIFIED_DAYS = 90;
|
|
1051
|
+
var DEFAULT_AGING_DAYS = 90;
|
|
1052
|
+
var KB_DOCTOR_CHECKS = [
|
|
1053
|
+
"expired",
|
|
1054
|
+
"expiring",
|
|
1055
|
+
"unverified",
|
|
1056
|
+
"aging",
|
|
1057
|
+
"orphaned",
|
|
1058
|
+
"broken-supersession",
|
|
1059
|
+
"superseded-but-cited"
|
|
1060
|
+
];
|
|
1061
|
+
var CHECK_HEADLINES = {
|
|
1062
|
+
expired: "past its stale_after date",
|
|
1063
|
+
expiring: "stale_after falls within the window",
|
|
1064
|
+
unverified: "nobody has ever confirmed it, and it is old enough to matter",
|
|
1065
|
+
aging: "still open or still proposed long after it was written",
|
|
1066
|
+
orphaned: "no other record links to it",
|
|
1067
|
+
"broken-supersession": "the supersession pointers do not resolve",
|
|
1068
|
+
"superseded-but-cited": "a live record's body links to one that no longer holds"
|
|
1069
|
+
};
|
|
1070
|
+
var DAY_MS = 864e5;
|
|
1071
|
+
function doctor(bundle, options = {}) {
|
|
1072
|
+
const thresholds = {
|
|
1073
|
+
expiringDays: options.expiringDays ?? DEFAULT_EXPIRING_DAYS,
|
|
1074
|
+
unverifiedDays: options.unverifiedDays ?? DEFAULT_UNVERIFIED_DAYS,
|
|
1075
|
+
agingDays: options.agingDays ?? DEFAULT_AGING_DAYS
|
|
1076
|
+
};
|
|
1077
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
1078
|
+
const adjudicated = adjudicate(bundle, bundle, now);
|
|
1079
|
+
const standings = new Map(
|
|
1080
|
+
adjudicated.map((hit) => [hit.record.conceptId, hit.standing])
|
|
1081
|
+
);
|
|
1082
|
+
const inForce = adjudicated.filter(
|
|
1083
|
+
(hit) => hit.standing !== "superseded" && hit.standing !== "rejected"
|
|
1084
|
+
);
|
|
1085
|
+
const groups = [
|
|
1086
|
+
group("expired", expired(inForce, now)),
|
|
1087
|
+
group("expiring", expiring(inForce, now, thresholds.expiringDays)),
|
|
1088
|
+
group("unverified", unverified(inForce, now, thresholds.unverifiedDays)),
|
|
1089
|
+
group("aging", aging(inForce, now, thresholds.agingDays)),
|
|
1090
|
+
group("orphaned", orphaned(bundle)),
|
|
1091
|
+
group("broken-supersession", brokenSupersession(bundle, adjudicated)),
|
|
1092
|
+
group("superseded-but-cited", supersededButCited(bundle, standings))
|
|
1093
|
+
];
|
|
1094
|
+
const counts = Object.fromEntries(
|
|
1095
|
+
groups.map((entry) => [entry.check, entry.count])
|
|
1096
|
+
);
|
|
1097
|
+
const findingCount = groups.reduce((total, entry) => total + entry.count, 0);
|
|
1098
|
+
return {
|
|
1099
|
+
recordCount: bundle.length,
|
|
1100
|
+
thresholds,
|
|
1101
|
+
counts,
|
|
1102
|
+
groups,
|
|
1103
|
+
findingCount,
|
|
1104
|
+
healthy: findingCount === 0
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
function group(check, findings) {
|
|
1108
|
+
return {
|
|
1109
|
+
check,
|
|
1110
|
+
headline: CHECK_HEADLINES[check],
|
|
1111
|
+
count: findings.length,
|
|
1112
|
+
findings
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
function expired(hits, now) {
|
|
1116
|
+
const findings = [];
|
|
1117
|
+
for (const hit of hits) {
|
|
1118
|
+
const raw = hit.record.frontmatter.stale_after;
|
|
1119
|
+
if (!raw) continue;
|
|
1120
|
+
const at = Date.parse(raw);
|
|
1121
|
+
if (Number.isNaN(at)) {
|
|
1122
|
+
findings.push(
|
|
1123
|
+
finding(hit.record, `stale_after "${raw}" is not a readable date`)
|
|
1124
|
+
);
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
1127
|
+
if (at < now.getTime()) {
|
|
1128
|
+
findings.push(
|
|
1129
|
+
finding(
|
|
1130
|
+
hit.record,
|
|
1131
|
+
`stale since ${raw} (${daysBetween(at, now.getTime())} days ago)`
|
|
1132
|
+
)
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
return findings;
|
|
1137
|
+
}
|
|
1138
|
+
function expiring(hits, now, withinDays) {
|
|
1139
|
+
const horizon = now.getTime() + withinDays * DAY_MS;
|
|
1140
|
+
const findings = [];
|
|
1141
|
+
for (const hit of hits) {
|
|
1142
|
+
const raw = hit.record.frontmatter.stale_after;
|
|
1143
|
+
if (!raw) continue;
|
|
1144
|
+
const at = Date.parse(raw);
|
|
1145
|
+
if (Number.isNaN(at) || at < now.getTime() || at > horizon) continue;
|
|
1146
|
+
findings.push(
|
|
1147
|
+
finding(
|
|
1148
|
+
hit.record,
|
|
1149
|
+
`goes stale ${raw} (in ${daysBetween(now.getTime(), at)} days)`
|
|
1150
|
+
)
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
return findings;
|
|
1154
|
+
}
|
|
1155
|
+
function unverified(hits, now, olderThanDays) {
|
|
1156
|
+
const findings = [];
|
|
1157
|
+
for (const hit of hits) {
|
|
1158
|
+
if (hit.record.frontmatter.verified?.length) continue;
|
|
1159
|
+
const age = ageInDays(hit.record, now);
|
|
1160
|
+
if (age === null || age <= olderThanDays) continue;
|
|
1161
|
+
findings.push(
|
|
1162
|
+
finding(hit.record, `never verified, written ${age} days ago`)
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
return findings;
|
|
1166
|
+
}
|
|
1167
|
+
function aging(hits, now, olderThanDays) {
|
|
1168
|
+
const findings = [];
|
|
1169
|
+
for (const hit of hits) {
|
|
1170
|
+
const status = hit.record.frontmatter.strauss_status;
|
|
1171
|
+
if (status !== "open" && status !== "proposed") continue;
|
|
1172
|
+
const age = ageInDays(hit.record, now);
|
|
1173
|
+
if (age === null || age <= olderThanDays) continue;
|
|
1174
|
+
findings.push(
|
|
1175
|
+
finding(
|
|
1176
|
+
hit.record,
|
|
1177
|
+
status === "open" ? `open for ${age} days` : `proposed ${age} days ago and still unsettled`
|
|
1178
|
+
)
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
return findings.sort(
|
|
1182
|
+
(left, right) => left.conceptId.localeCompare(right.conceptId)
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
function orphaned(bundle) {
|
|
1186
|
+
const present = new Set(bundle.map((record) => record.conceptId));
|
|
1187
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
1188
|
+
for (const record of bundle) {
|
|
1189
|
+
for (const neighbour of edgeNeighbours(record, bundle, "body-link")) {
|
|
1190
|
+
referenced.add(neighbour.conceptId);
|
|
1191
|
+
}
|
|
1192
|
+
for (const replaced of record.frontmatter.strauss_supersedes ?? []) {
|
|
1193
|
+
referenced.add(replaced);
|
|
1194
|
+
}
|
|
1195
|
+
const replacement = record.frontmatter.strauss_superseded_by;
|
|
1196
|
+
if (replacement && present.has(replacement)) {
|
|
1197
|
+
referenced.add(record.conceptId);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
return bundle.filter((record) => !referenced.has(record.conceptId)).map((record) => finding(record, "no other record links to it"));
|
|
1201
|
+
}
|
|
1202
|
+
var SUPERSESSION_CHECKS = /* @__PURE__ */ new Set([
|
|
1203
|
+
"superseded_by",
|
|
1204
|
+
"supersedes",
|
|
1205
|
+
"backlink"
|
|
1206
|
+
]);
|
|
1207
|
+
function brokenSupersession(bundle, adjudicated) {
|
|
1208
|
+
const byId = new Map(bundle.map((record) => [record.conceptId, record]));
|
|
1209
|
+
const findings = [];
|
|
1210
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1211
|
+
const add = (record, note) => {
|
|
1212
|
+
const key = `${record.conceptId}\0${note}`;
|
|
1213
|
+
if (seen.has(key)) return;
|
|
1214
|
+
seen.add(key);
|
|
1215
|
+
findings.push(finding(record, note));
|
|
1216
|
+
};
|
|
1217
|
+
for (const problem of validateBundle(bundle)) {
|
|
1218
|
+
if (!SUPERSESSION_CHECKS.has(problem.check)) continue;
|
|
1219
|
+
const record = byId.get(problem.conceptId);
|
|
1220
|
+
if (record) add(record, problem.note);
|
|
1221
|
+
}
|
|
1222
|
+
for (const record of bundle) {
|
|
1223
|
+
const replacement = record.frontmatter.strauss_superseded_by;
|
|
1224
|
+
if (!replacement) continue;
|
|
1225
|
+
if (!byId.has(replacement)) {
|
|
1226
|
+
add(record, `replacement ${replacement} is missing`);
|
|
1227
|
+
} else if (record.frontmatter.strauss_status !== "superseded") {
|
|
1228
|
+
add(
|
|
1229
|
+
record,
|
|
1230
|
+
`names ${replacement} as its replacement but is not marked superseded`
|
|
1231
|
+
);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
for (const hit of adjudicated) {
|
|
1235
|
+
for (const warning of hit.warnings) {
|
|
1236
|
+
if (warning.kind === "broken-chain") {
|
|
1237
|
+
add(hit.record, `replacement ${warning.missing} is missing`);
|
|
1238
|
+
} else if (warning.kind === "chain-cycle") {
|
|
1239
|
+
add(
|
|
1240
|
+
hit.record,
|
|
1241
|
+
`supersession chain cycles through ${warning.through.join(" \u2192 ")}`
|
|
1242
|
+
);
|
|
1243
|
+
} else if (warning.kind === "forked-chain") {
|
|
1244
|
+
add(
|
|
1245
|
+
hit.record,
|
|
1246
|
+
`two records claim to replace it: ${warning.heads.join(", ")}`
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
return findings.sort(
|
|
1252
|
+
(left, right) => left.conceptId.localeCompare(right.conceptId)
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
function supersededButCited(bundle, standings) {
|
|
1256
|
+
const byId = new Map(bundle.map((record) => [record.conceptId, record]));
|
|
1257
|
+
const findings = [];
|
|
1258
|
+
for (const record of bundle) {
|
|
1259
|
+
const standing = standings.get(record.conceptId);
|
|
1260
|
+
if (standing === "superseded" || standing === "rejected") continue;
|
|
1261
|
+
for (const target of edgeNeighbours(record, bundle, "body-link")) {
|
|
1262
|
+
const targetStanding = standings.get(target.conceptId);
|
|
1263
|
+
if (targetStanding !== "superseded" && targetStanding !== "rejected") {
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
if (replaces(record, target)) continue;
|
|
1267
|
+
const replacement = target.frontmatter.strauss_superseded_by;
|
|
1268
|
+
findings.push(
|
|
1269
|
+
finding(
|
|
1270
|
+
record,
|
|
1271
|
+
`cites ${targetStanding} ${target.conceptId}${targetStanding === "superseded" && replacement && byId.has(replacement) ? ` \u2014 replaced by ${replacement}` : ""}`
|
|
1272
|
+
)
|
|
1273
|
+
);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
return findings;
|
|
1277
|
+
}
|
|
1278
|
+
function replaces(later, earlier) {
|
|
1279
|
+
return (later.frontmatter.strauss_supersedes ?? []).includes(earlier.conceptId) || earlier.frontmatter.strauss_superseded_by === later.conceptId;
|
|
1280
|
+
}
|
|
1281
|
+
function finding(record, note) {
|
|
1282
|
+
return {
|
|
1283
|
+
conceptId: record.conceptId,
|
|
1284
|
+
title: record.frontmatter.title ?? null,
|
|
1285
|
+
status: record.frontmatter.strauss_status,
|
|
1286
|
+
note
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
function daysBetween(from, to) {
|
|
1290
|
+
return Math.max(0, Math.floor((to - from) / DAY_MS));
|
|
1291
|
+
}
|
|
1292
|
+
function ageInDays(record, now) {
|
|
1293
|
+
const at = record.frontmatter.generated?.at;
|
|
1294
|
+
if (!at) return null;
|
|
1295
|
+
const written = Date.parse(at);
|
|
1296
|
+
if (Number.isNaN(written)) return null;
|
|
1297
|
+
return daysBetween(written, now.getTime());
|
|
1298
|
+
}
|
|
1299
|
+
|
|
937
1300
|
// src/kb-log.ts
|
|
938
1301
|
import { z as z5 } from "zod";
|
|
939
1302
|
var LOG_FILE = "log.jsonl";
|
|
@@ -999,7 +1362,7 @@ function trace(seedId, bundle, options = {}) {
|
|
|
999
1362
|
const next = [];
|
|
1000
1363
|
for (const from of frontier) {
|
|
1001
1364
|
for (const edge of edges) {
|
|
1002
|
-
for (const record of
|
|
1365
|
+
for (const record of edgeNeighbours(from, bundle, edge)) {
|
|
1003
1366
|
const existing = reached.get(record.conceptId);
|
|
1004
1367
|
if (existing) {
|
|
1005
1368
|
if (existing.depth > 0 && !existing.via.includes(edge)) {
|
|
@@ -1016,81 +1379,11 @@ function trace(seedId, bundle, options = {}) {
|
|
|
1016
1379
|
}
|
|
1017
1380
|
return [...reached.values()].sort(byGeneratedAt);
|
|
1018
1381
|
}
|
|
1019
|
-
function neighbours(from, bundle, edge) {
|
|
1020
|
-
switch (edge) {
|
|
1021
|
-
case "supersession":
|
|
1022
|
-
return bundle.filter(
|
|
1023
|
-
(candidate) => candidate.conceptId !== from.conceptId && (candidate.conceptId === from.frontmatter.strauss_superseded_by || from.frontmatter.strauss_supersedes?.includes(
|
|
1024
|
-
candidate.conceptId
|
|
1025
|
-
) || candidate.frontmatter.strauss_superseded_by === from.conceptId || candidate.frontmatter.strauss_supersedes?.includes(from.conceptId))
|
|
1026
|
-
);
|
|
1027
|
-
// The edge that answers "why is this code shaped this way": every record
|
|
1028
|
-
// attached to the same file or symbol, whatever its standing.
|
|
1029
|
-
case "anchor": {
|
|
1030
|
-
const mine = from.frontmatter.strauss_anchors ?? [];
|
|
1031
|
-
if (!mine.length) return [];
|
|
1032
|
-
return bundle.filter(
|
|
1033
|
-
(candidate) => candidate.conceptId !== from.conceptId && (candidate.frontmatter.strauss_anchors ?? []).some(
|
|
1034
|
-
(theirs) => mine.some((ours) => anchorsTouch(ours, theirs))
|
|
1035
|
-
)
|
|
1036
|
-
);
|
|
1037
|
-
}
|
|
1038
|
-
case "source": {
|
|
1039
|
-
const mine = new Set((from.frontmatter.sources ?? []).map((s) => s.id));
|
|
1040
|
-
if (!mine.size) return [];
|
|
1041
|
-
return bundle.filter(
|
|
1042
|
-
(candidate) => candidate.conceptId !== from.conceptId && (candidate.frontmatter.sources ?? []).some(
|
|
1043
|
-
(source) => mine.has(source.id)
|
|
1044
|
-
)
|
|
1045
|
-
);
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1049
|
-
function anchorsTouch(left, right) {
|
|
1050
|
-
if (left.file !== right.file) return false;
|
|
1051
|
-
if (!left.symbol || !right.symbol) return true;
|
|
1052
|
-
return left.symbol === right.symbol;
|
|
1053
|
-
}
|
|
1054
1382
|
function byGeneratedAt(left, right) {
|
|
1055
1383
|
const at = (step) => step.record.frontmatter.generated?.at ?? "";
|
|
1056
1384
|
return at(left).localeCompare(at(right)) || left.depth - right.depth;
|
|
1057
1385
|
}
|
|
1058
1386
|
|
|
1059
|
-
// src/validate.ts
|
|
1060
|
-
function validateBundle(records) {
|
|
1061
|
-
const byId = new Map(records.map((record) => [record.conceptId, record]));
|
|
1062
|
-
const problems = [];
|
|
1063
|
-
const report = (check, conceptId2, note) => problems.push({ check, conceptId: conceptId2, note });
|
|
1064
|
-
for (const record of records) {
|
|
1065
|
-
const { conceptId: conceptId2, frontmatter: fm } = record;
|
|
1066
|
-
if (!isKbRecordType(fm.type)) {
|
|
1067
|
-
report("type", conceptId2, `unrecognised type "${fm.type}"`);
|
|
1068
|
-
}
|
|
1069
|
-
if (fm.strauss_status === "superseded") {
|
|
1070
|
-
const by = fm.strauss_superseded_by;
|
|
1071
|
-
if (!by) {
|
|
1072
|
-
report("superseded_by", conceptId2, "superseded with no replacement");
|
|
1073
|
-
} else if (!byId.has(by)) {
|
|
1074
|
-
report("superseded_by", conceptId2, `replacement ${by} is missing`);
|
|
1075
|
-
} else if (!byId.get(by)?.frontmatter.strauss_supersedes?.includes(conceptId2)) {
|
|
1076
|
-
report("backlink", by, `does not list ${conceptId2} in supersedes`);
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
for (const old of fm.strauss_supersedes ?? []) {
|
|
1080
|
-
const previous = byId.get(old);
|
|
1081
|
-
if (!previous) {
|
|
1082
|
-
report("supersedes", conceptId2, `target ${old} is missing`);
|
|
1083
|
-
} else if (previous.frontmatter.strauss_status !== "superseded") {
|
|
1084
|
-
report("supersedes", conceptId2, `${old} is not marked superseded`);
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
if (fm.strauss_assumption && fm.sources?.length) {
|
|
1088
|
-
report("assumption", conceptId2, "marked an assumption but cites sources");
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
return problems;
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
1387
|
// src/commands/answer.ts
|
|
1095
1388
|
import { z as z8 } from "zod";
|
|
1096
1389
|
|
|
@@ -1180,14 +1473,104 @@ var contextCommand = define({
|
|
|
1180
1473
|
}
|
|
1181
1474
|
});
|
|
1182
1475
|
|
|
1183
|
-
// src/commands/
|
|
1476
|
+
// src/commands/doctor.ts
|
|
1184
1477
|
import { z as z10 } from "zod";
|
|
1478
|
+
var days = (what, fallback) => z10.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
|
|
1479
|
+
var doctorCommand = define({
|
|
1480
|
+
name: "doctor",
|
|
1481
|
+
tool: "kb_doctor",
|
|
1482
|
+
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--strict]",
|
|
1483
|
+
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.",
|
|
1484
|
+
input: z10.object({
|
|
1485
|
+
bundlePath,
|
|
1486
|
+
expiringDays: days(
|
|
1487
|
+
"How far ahead `expiring` looks, in days.",
|
|
1488
|
+
DEFAULT_EXPIRING_DAYS
|
|
1489
|
+
),
|
|
1490
|
+
unverifiedDays: days(
|
|
1491
|
+
"How old an unconfirmed record must be before `unverified` reports it, in days.",
|
|
1492
|
+
DEFAULT_UNVERIFIED_DAYS
|
|
1493
|
+
),
|
|
1494
|
+
agingDays: days(
|
|
1495
|
+
"How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
|
|
1496
|
+
DEFAULT_AGING_DAYS
|
|
1497
|
+
),
|
|
1498
|
+
strict: z10.boolean().optional().describe(
|
|
1499
|
+
"Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
|
|
1500
|
+
)
|
|
1501
|
+
}),
|
|
1502
|
+
// Presence, not truthiness: `--expiring-days ""` is a caller who meant
|
|
1503
|
+
// something and mistyped it, and a falsy test would answer by quietly
|
|
1504
|
+
// sweeping at the default. Passed through as given, the schema rejects it
|
|
1505
|
+
// and says which field.
|
|
1506
|
+
fromArgv: (argv, path) => {
|
|
1507
|
+
const expiring2 = argvFlag(argv, "--expiring-days");
|
|
1508
|
+
const unverified2 = argvFlag(argv, "--unverified-days");
|
|
1509
|
+
const agingDays = argvFlag(argv, "--aging-days");
|
|
1510
|
+
return {
|
|
1511
|
+
bundlePath: path,
|
|
1512
|
+
...expiring2 !== void 0 ? { expiringDays: Number(expiring2) } : {},
|
|
1513
|
+
...unverified2 !== void 0 ? { unverifiedDays: Number(unverified2) } : {},
|
|
1514
|
+
...agingDays !== void 0 ? { agingDays: Number(agingDays) } : {},
|
|
1515
|
+
...argv.includes("--strict") ? { strict: true } : {}
|
|
1516
|
+
};
|
|
1517
|
+
},
|
|
1518
|
+
run: async ({ store, now }, { bundlePath: path, expiringDays, unverifiedDays, agingDays }) => {
|
|
1519
|
+
const checkedAt = now();
|
|
1520
|
+
const report = doctor(await store.list(path), {
|
|
1521
|
+
...expiringDays !== void 0 ? { expiringDays } : {},
|
|
1522
|
+
...unverifiedDays !== void 0 ? { unverifiedDays } : {},
|
|
1523
|
+
...agingDays !== void 0 ? { agingDays } : {},
|
|
1524
|
+
now: new Date(checkedAt)
|
|
1525
|
+
});
|
|
1526
|
+
return { bundlePath: path, checkedAt, ...report };
|
|
1527
|
+
},
|
|
1528
|
+
render: (result) => render(result),
|
|
1529
|
+
// Only expiry, and only under --strict. The other six checks report debt a
|
|
1530
|
+
// reader decides about; an expired record is the base asserting something it
|
|
1531
|
+
// already said it would stop standing behind, which is the one finding a
|
|
1532
|
+
// pipeline can act on without a judgment call.
|
|
1533
|
+
failsWhen: (result, input) => input.strict === true && result.counts.expired > 0
|
|
1534
|
+
});
|
|
1535
|
+
function render(result) {
|
|
1536
|
+
const { thresholds } = result;
|
|
1537
|
+
const lines = [
|
|
1538
|
+
`# KB Doctor \u2014 ${result.bundlePath}`,
|
|
1539
|
+
`records: ${result.recordCount}`,
|
|
1540
|
+
`thresholds: expiring within ${thresholds.expiringDays}d, unverified over ${thresholds.unverifiedDays}d, aging over ${thresholds.agingDays}d`,
|
|
1541
|
+
`checked: ${result.checkedAt}`,
|
|
1542
|
+
""
|
|
1543
|
+
];
|
|
1544
|
+
const width = Math.max(...result.groups.map((group2) => group2.check.length));
|
|
1545
|
+
for (const group2 of result.groups) {
|
|
1546
|
+
lines.push(
|
|
1547
|
+
` ${group2.check.padEnd(width)} ${String(group2.count).padStart(3)} ${group2.headline}`
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1550
|
+
for (const group2 of result.groups) {
|
|
1551
|
+
if (!group2.count) continue;
|
|
1552
|
+
lines.push("", `## ${group2.check} (${group2.count})`);
|
|
1553
|
+
for (const found of group2.findings) {
|
|
1554
|
+
lines.push(
|
|
1555
|
+
`- ${found.conceptId}${found.title ? ` \u2014 ${found.title}` : ""}: ${found.note}`
|
|
1556
|
+
);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
lines.push(
|
|
1560
|
+
"",
|
|
1561
|
+
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.`
|
|
1562
|
+
);
|
|
1563
|
+
return lines.join("\n");
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
// src/commands/list.ts
|
|
1567
|
+
import { z as z11 } from "zod";
|
|
1185
1568
|
var listCommand = define({
|
|
1186
1569
|
name: "list",
|
|
1187
1570
|
tool: "kb_list",
|
|
1188
1571
|
usage: "list [type]",
|
|
1189
1572
|
description: "Every record, optionally narrowed to one type. Use kb_query when you have a question; this is for enumerating.",
|
|
1190
|
-
input:
|
|
1573
|
+
input: z11.object({ bundlePath, type: z11.enum(KB_RECORD_TYPES).optional() }),
|
|
1191
1574
|
fromArgv: (argv, path) => ({ bundlePath: path, type: argv[1] }),
|
|
1192
1575
|
run: async ({ store }, { bundlePath: path, type }) => (await store.list(path, type)).map((record) => ({
|
|
1193
1576
|
conceptId: record.conceptId,
|
|
@@ -1199,17 +1582,17 @@ var listCommand = define({
|
|
|
1199
1582
|
});
|
|
1200
1583
|
|
|
1201
1584
|
// src/commands/load.ts
|
|
1202
|
-
import { z as
|
|
1585
|
+
import { z as z12 } from "zod";
|
|
1203
1586
|
var loadCommand = define({
|
|
1204
1587
|
name: "load",
|
|
1205
1588
|
tool: "kb_load",
|
|
1206
1589
|
usage: "load [type] [--budget N | --all]",
|
|
1207
1590
|
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.",
|
|
1208
|
-
input:
|
|
1591
|
+
input: z12.object({
|
|
1209
1592
|
bundlePath,
|
|
1210
|
-
type:
|
|
1211
|
-
budgetTokens:
|
|
1212
|
-
all:
|
|
1593
|
+
type: z12.enum(KB_RECORD_TYPES).optional(),
|
|
1594
|
+
budgetTokens: z12.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
|
|
1595
|
+
all: z12.boolean().optional().describe(
|
|
1213
1596
|
"Load the entire base regardless of size. The deliberate-operator escape hatch; mutually exclusive with budgetTokens."
|
|
1214
1597
|
)
|
|
1215
1598
|
}).refine((value) => !(value.all && value.budgetTokens !== void 0), {
|
|
@@ -1247,25 +1630,25 @@ var loadCommand = define({
|
|
|
1247
1630
|
});
|
|
1248
1631
|
|
|
1249
1632
|
// src/commands/log.ts
|
|
1250
|
-
import { z as
|
|
1633
|
+
import { z as z13 } from "zod";
|
|
1251
1634
|
var logCommand = define({
|
|
1252
1635
|
name: "log",
|
|
1253
1636
|
tool: "kb_log",
|
|
1254
1637
|
usage: "log",
|
|
1255
1638
|
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.",
|
|
1256
|
-
input:
|
|
1639
|
+
input: z13.object({ bundlePath }),
|
|
1257
1640
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
1258
1641
|
run: ({ store }, { bundlePath: path }) => store.readLog(path)
|
|
1259
1642
|
});
|
|
1260
1643
|
|
|
1261
1644
|
// src/commands/no-decision.ts
|
|
1262
|
-
import { z as
|
|
1645
|
+
import { z as z14 } from "zod";
|
|
1263
1646
|
var noDecisionCommand = define({
|
|
1264
1647
|
name: "no-decision",
|
|
1265
1648
|
tool: "kb_no_decision",
|
|
1266
1649
|
usage: "no-decision <reason...>",
|
|
1267
1650
|
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.',
|
|
1268
|
-
input:
|
|
1651
|
+
input: z14.object({ bundlePath, reason: z14.string().min(1) }),
|
|
1269
1652
|
fromArgv: (argv, path) => ({
|
|
1270
1653
|
bundlePath: path,
|
|
1271
1654
|
reason: argv.slice(1).join(" ").trim()
|
|
@@ -1281,23 +1664,123 @@ var noDecisionCommand = define({
|
|
|
1281
1664
|
}
|
|
1282
1665
|
});
|
|
1283
1666
|
|
|
1667
|
+
// src/commands/pack.ts
|
|
1668
|
+
import { z as z15 } from "zod";
|
|
1669
|
+
var packCommand = define({
|
|
1670
|
+
name: "pack",
|
|
1671
|
+
tool: "kb_pack",
|
|
1672
|
+
usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
|
|
1673
|
+
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.",
|
|
1674
|
+
input: z15.object({
|
|
1675
|
+
bundlePath,
|
|
1676
|
+
conceptId,
|
|
1677
|
+
hops: z15.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
|
|
1678
|
+
maxNodes: z15.number().int().positive().optional().describe(
|
|
1679
|
+
"How many records the pack may hold, root included. Defaults to 20."
|
|
1680
|
+
),
|
|
1681
|
+
budgetTokens: z15.number().int().positive().optional().describe(
|
|
1682
|
+
"Approximate token ceiling over what is actually emitted. Defaults to 25000."
|
|
1683
|
+
)
|
|
1684
|
+
}),
|
|
1685
|
+
fromArgv: (argv, path) => {
|
|
1686
|
+
const hops = argvFlag(argv, "--hops");
|
|
1687
|
+
const maxNodes = argvFlag(argv, "--max-nodes");
|
|
1688
|
+
const budget = argvFlag(argv, "--budget");
|
|
1689
|
+
return {
|
|
1690
|
+
bundlePath: path,
|
|
1691
|
+
conceptId: argv[1],
|
|
1692
|
+
...hops ? { hops: Number(hops) } : {},
|
|
1693
|
+
...maxNodes ? { maxNodes: Number(maxNodes) } : {},
|
|
1694
|
+
...budget ? { budgetTokens: Number(budget) } : {}
|
|
1695
|
+
};
|
|
1696
|
+
},
|
|
1697
|
+
run: async ({ store, now }, { bundlePath: path, conceptId: root, hops, maxNodes, budgetTokens }) => {
|
|
1698
|
+
const result = await store.pack(path, root, {
|
|
1699
|
+
...hops !== void 0 ? { hops } : {},
|
|
1700
|
+
...maxNodes !== void 0 ? { maxNodes } : {},
|
|
1701
|
+
...budgetTokens !== void 0 ? { budgetTokens } : {}
|
|
1702
|
+
});
|
|
1703
|
+
return render2(result, path, now());
|
|
1704
|
+
}
|
|
1705
|
+
});
|
|
1706
|
+
function render2(result, bundle, at) {
|
|
1707
|
+
const lines = [
|
|
1708
|
+
`# KB Pack \u2014 ${result.root}`,
|
|
1709
|
+
`bundle: ${bundle}`,
|
|
1710
|
+
`budget: ~${result.tokensLoaded} of ${result.budgetTokens} tokens, ${result.recordCount} records`,
|
|
1711
|
+
`packed: ${at}`,
|
|
1712
|
+
"",
|
|
1713
|
+
`## Records (${result.records.length})`
|
|
1714
|
+
];
|
|
1715
|
+
for (const record of result.records) {
|
|
1716
|
+
lines.push(
|
|
1717
|
+
"",
|
|
1718
|
+
`### ${record.conceptId}${record.title ? ` \u2014 ${record.title}` : ""} [${record.standing}]`
|
|
1719
|
+
);
|
|
1720
|
+
if (record.warnings.length) {
|
|
1721
|
+
lines.push(`warnings: ${record.warnings.map(warningLabel).join("; ")}`);
|
|
1722
|
+
}
|
|
1723
|
+
if (record.anchors.length) {
|
|
1724
|
+
lines.push(
|
|
1725
|
+
`anchors: ${record.anchors.map(
|
|
1726
|
+
(anchor) => anchor.symbol ? `${anchor.file}#${anchor.symbol}` : anchor.file
|
|
1727
|
+
).join(", ")}`
|
|
1728
|
+
);
|
|
1729
|
+
}
|
|
1730
|
+
lines.push("", record.body.trimEnd());
|
|
1731
|
+
}
|
|
1732
|
+
if (result.superseded.length) {
|
|
1733
|
+
lines.push("", `## Superseded (${result.superseded.length})`);
|
|
1734
|
+
for (const entry of result.superseded) {
|
|
1735
|
+
lines.push(
|
|
1736
|
+
`- ${entry.conceptId} \u2192 ${entry.supersededBy.join(", ") || "(no surviving head)"}${entry.at ? ` (${entry.at})` : ""}`
|
|
1737
|
+
);
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
if (result.excluded.length) {
|
|
1741
|
+
lines.push("", `## Excluded (${result.excluded.length})`);
|
|
1742
|
+
for (const cut of result.excluded) lines.push(`- ${cut}`);
|
|
1743
|
+
}
|
|
1744
|
+
return lines.join("\n");
|
|
1745
|
+
}
|
|
1746
|
+
function warningLabel(warning) {
|
|
1747
|
+
switch (warning.kind) {
|
|
1748
|
+
case "superseded":
|
|
1749
|
+
return `superseded by ${warning.by.join(", ")}`;
|
|
1750
|
+
case "unsettled":
|
|
1751
|
+
return `unsettled (${warning.status})`;
|
|
1752
|
+
case "broken-chain":
|
|
1753
|
+
return `broken chain \u2014 ${warning.missing} is not in the bundle`;
|
|
1754
|
+
case "chain-cycle":
|
|
1755
|
+
return `chain cycle through ${warning.through.join(" \u2192 ")}`;
|
|
1756
|
+
case "forked-chain":
|
|
1757
|
+
return `forked chain \u2014 heads ${warning.heads.join(", ")}`;
|
|
1758
|
+
case "stale":
|
|
1759
|
+
return `stale since ${warning.staleAfter}`;
|
|
1760
|
+
case "unresolved-question":
|
|
1761
|
+
return "unresolved question";
|
|
1762
|
+
default:
|
|
1763
|
+
return warning.kind;
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1284
1767
|
// src/commands/pin.ts
|
|
1285
|
-
import { z as
|
|
1768
|
+
import { z as z16 } from "zod";
|
|
1286
1769
|
var pinCommand = define({
|
|
1287
1770
|
name: "pin",
|
|
1288
1771
|
tool: "kb_pin",
|
|
1289
1772
|
usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
|
|
1290
1773
|
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.",
|
|
1291
|
-
input:
|
|
1774
|
+
input: z16.object({
|
|
1292
1775
|
bundlePath,
|
|
1293
|
-
mode:
|
|
1776
|
+
mode: z16.enum(["full", "index"]).optional().describe(
|
|
1294
1777
|
"full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
|
|
1295
1778
|
),
|
|
1296
|
-
profiles:
|
|
1297
|
-
layer:
|
|
1779
|
+
profiles: z16.array(z16.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
|
|
1780
|
+
layer: z16.enum(["project", "local", "user"]).optional().describe(
|
|
1298
1781
|
"Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
|
|
1299
1782
|
),
|
|
1300
|
-
frozen:
|
|
1783
|
+
frozen: z16.boolean().optional().describe(
|
|
1301
1784
|
"true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
|
|
1302
1785
|
)
|
|
1303
1786
|
}),
|
|
@@ -1326,29 +1809,29 @@ var pinCommand = define({
|
|
|
1326
1809
|
});
|
|
1327
1810
|
|
|
1328
1811
|
// src/commands/pins.ts
|
|
1329
|
-
import { z as
|
|
1812
|
+
import { z as z17 } from "zod";
|
|
1330
1813
|
var pinsCommand = define({
|
|
1331
1814
|
name: "pins",
|
|
1332
1815
|
tool: "kb_pins",
|
|
1333
1816
|
usage: "pins",
|
|
1334
1817
|
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.",
|
|
1335
|
-
input:
|
|
1818
|
+
input: z17.object({}),
|
|
1336
1819
|
fromArgv: () => ({}),
|
|
1337
1820
|
run: ({ store }) => listPins(store, process.cwd())
|
|
1338
1821
|
});
|
|
1339
1822
|
|
|
1340
1823
|
// src/commands/query.ts
|
|
1341
|
-
import { z as
|
|
1824
|
+
import { z as z18 } from "zod";
|
|
1342
1825
|
var queryCommand = define({
|
|
1343
1826
|
name: "query",
|
|
1344
1827
|
tool: "kb_query",
|
|
1345
1828
|
usage: "query <text...>",
|
|
1346
1829
|
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.",
|
|
1347
|
-
input:
|
|
1830
|
+
input: z18.object({
|
|
1348
1831
|
bundlePath,
|
|
1349
|
-
text:
|
|
1350
|
-
type:
|
|
1351
|
-
includeNonCurrent:
|
|
1832
|
+
text: z18.string().optional(),
|
|
1833
|
+
type: z18.enum(KB_RECORD_TYPES).optional(),
|
|
1834
|
+
includeNonCurrent: z18.boolean().optional()
|
|
1352
1835
|
}),
|
|
1353
1836
|
fromArgv: (argv, path) => ({
|
|
1354
1837
|
bundlePath: path,
|
|
@@ -1370,40 +1853,40 @@ var queryCommand = define({
|
|
|
1370
1853
|
});
|
|
1371
1854
|
|
|
1372
1855
|
// src/commands/read-index.ts
|
|
1373
|
-
import { z as
|
|
1856
|
+
import { z as z19 } from "zod";
|
|
1374
1857
|
var readIndexCommand = define({
|
|
1375
1858
|
name: "index",
|
|
1376
1859
|
tool: "kb_index",
|
|
1377
1860
|
usage: "index",
|
|
1378
1861
|
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.",
|
|
1379
|
-
input:
|
|
1862
|
+
input: z19.object({ bundlePath }),
|
|
1380
1863
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
1381
1864
|
run: ({ store }, { bundlePath: path }) => store.readIndex(path)
|
|
1382
1865
|
});
|
|
1383
1866
|
|
|
1384
1867
|
// src/commands/schema.ts
|
|
1385
|
-
import { z as
|
|
1868
|
+
import { z as z20 } from "zod";
|
|
1386
1869
|
var schemaCommand = define({
|
|
1387
1870
|
name: "schema",
|
|
1388
1871
|
tool: "kb_schema",
|
|
1389
1872
|
usage: "schema",
|
|
1390
1873
|
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.",
|
|
1391
|
-
input:
|
|
1874
|
+
input: z20.object({}),
|
|
1392
1875
|
fromArgv: () => ({}),
|
|
1393
1876
|
run: () => Promise.resolve(kbJsonSchemas())
|
|
1394
1877
|
});
|
|
1395
1878
|
|
|
1396
1879
|
// src/commands/status.ts
|
|
1397
|
-
import { z as
|
|
1880
|
+
import { z as z21 } from "zod";
|
|
1398
1881
|
var statusCommand = define({
|
|
1399
1882
|
name: "status",
|
|
1400
1883
|
tool: "kb_status",
|
|
1401
1884
|
usage: "status <concept-id> <status>",
|
|
1402
1885
|
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.",
|
|
1403
|
-
input:
|
|
1886
|
+
input: z21.object({
|
|
1404
1887
|
bundlePath,
|
|
1405
1888
|
conceptId,
|
|
1406
|
-
status:
|
|
1889
|
+
status: z21.enum(KB_RECORD_STATUSES)
|
|
1407
1890
|
}),
|
|
1408
1891
|
fromArgv: (argv, path) => ({
|
|
1409
1892
|
bundlePath: path,
|
|
@@ -1418,13 +1901,13 @@ var statusCommand = define({
|
|
|
1418
1901
|
});
|
|
1419
1902
|
|
|
1420
1903
|
// src/commands/supersede.ts
|
|
1421
|
-
import { z as
|
|
1904
|
+
import { z as z22 } from "zod";
|
|
1422
1905
|
var supersedeCommand = define({
|
|
1423
1906
|
name: "supersede",
|
|
1424
1907
|
tool: "kb_supersede",
|
|
1425
1908
|
usage: "supersede <concept-id> <replacement-id>",
|
|
1426
1909
|
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.",
|
|
1427
|
-
input:
|
|
1910
|
+
input: z22.object({ bundlePath, conceptId, replacementId: conceptId }),
|
|
1428
1911
|
fromArgv: (argv, path) => ({
|
|
1429
1912
|
bundlePath: path,
|
|
1430
1913
|
conceptId: argv[1],
|
|
@@ -1438,16 +1921,16 @@ var supersedeCommand = define({
|
|
|
1438
1921
|
});
|
|
1439
1922
|
|
|
1440
1923
|
// src/commands/sync-instructions.ts
|
|
1441
|
-
import { z as
|
|
1924
|
+
import { z as z23 } from "zod";
|
|
1442
1925
|
var syncInstructionsCommand = define({
|
|
1443
1926
|
name: "sync-instructions",
|
|
1444
1927
|
usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
|
|
1445
1928
|
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.",
|
|
1446
|
-
input:
|
|
1447
|
-
file:
|
|
1448
|
-
budgetTokens:
|
|
1449
|
-
fullUnderTokens:
|
|
1450
|
-
profile:
|
|
1929
|
+
input: z23.object({
|
|
1930
|
+
file: z23.string().min(1).describe("The instruction file to edit in place."),
|
|
1931
|
+
budgetTokens: z23.number().int().positive().optional(),
|
|
1932
|
+
fullUnderTokens: z23.number().int().positive().optional(),
|
|
1933
|
+
profile: z23.string().optional()
|
|
1451
1934
|
}),
|
|
1452
1935
|
fromArgv: (argv) => {
|
|
1453
1936
|
const budget = argvFlag(argv, "--budget");
|
|
@@ -1473,17 +1956,17 @@ var syncInstructionsCommand = define({
|
|
|
1473
1956
|
});
|
|
1474
1957
|
|
|
1475
1958
|
// src/commands/trace.ts
|
|
1476
|
-
import { z as
|
|
1959
|
+
import { z as z24 } from "zod";
|
|
1477
1960
|
var traceCommand = define({
|
|
1478
1961
|
name: "trace",
|
|
1479
1962
|
tool: "kb_trace",
|
|
1480
1963
|
usage: "trace <concept-id> [edges...]",
|
|
1481
1964
|
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.',
|
|
1482
|
-
input:
|
|
1965
|
+
input: z24.object({
|
|
1483
1966
|
bundlePath,
|
|
1484
1967
|
conceptId,
|
|
1485
|
-
edges:
|
|
1486
|
-
depth:
|
|
1968
|
+
edges: z24.array(z24.enum(TRACE_EDGES)).optional(),
|
|
1969
|
+
depth: z24.number().int().positive().optional()
|
|
1487
1970
|
}),
|
|
1488
1971
|
fromArgv: (argv, path) => ({
|
|
1489
1972
|
bundlePath: path,
|
|
@@ -1505,53 +1988,53 @@ var traceCommand = define({
|
|
|
1505
1988
|
});
|
|
1506
1989
|
|
|
1507
1990
|
// src/commands/types.ts
|
|
1508
|
-
import { z as
|
|
1991
|
+
import { z as z25 } from "zod";
|
|
1509
1992
|
var typesCommand = define({
|
|
1510
1993
|
name: "types",
|
|
1511
1994
|
tool: "kb_types",
|
|
1512
1995
|
usage: "types",
|
|
1513
1996
|
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.",
|
|
1514
|
-
input:
|
|
1997
|
+
input: z25.object({}),
|
|
1515
1998
|
fromArgv: () => ({}),
|
|
1516
1999
|
run: () => Promise.resolve(RECORD_TYPES)
|
|
1517
2000
|
});
|
|
1518
2001
|
|
|
1519
2002
|
// src/commands/unpin.ts
|
|
1520
|
-
import { z as
|
|
2003
|
+
import { z as z26 } from "zod";
|
|
1521
2004
|
var unpinCommand = define({
|
|
1522
2005
|
name: "unpin",
|
|
1523
2006
|
tool: "kb_unpin",
|
|
1524
2007
|
usage: "unpin [bundle-path]",
|
|
1525
2008
|
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.",
|
|
1526
|
-
input:
|
|
2009
|
+
input: z26.object({ bundlePath }),
|
|
1527
2010
|
fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
|
|
1528
2011
|
run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
|
|
1529
2012
|
});
|
|
1530
2013
|
|
|
1531
2014
|
// src/commands/validate.ts
|
|
1532
|
-
import { z as
|
|
2015
|
+
import { z as z27 } from "zod";
|
|
1533
2016
|
var validateCommand = define({
|
|
1534
2017
|
name: "validate",
|
|
1535
2018
|
tool: "kb_validate",
|
|
1536
2019
|
usage: "validate",
|
|
1537
2020
|
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.",
|
|
1538
|
-
input:
|
|
2021
|
+
input: z27.object({ bundlePath }),
|
|
1539
2022
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
1540
2023
|
run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
|
|
1541
2024
|
failsWhen: (result) => Array.isArray(result) && result.length > 0
|
|
1542
2025
|
});
|
|
1543
2026
|
|
|
1544
2027
|
// src/commands/verify.ts
|
|
1545
|
-
import { z as
|
|
2028
|
+
import { z as z28 } from "zod";
|
|
1546
2029
|
var verifyCommand = define({
|
|
1547
2030
|
name: "verify",
|
|
1548
2031
|
tool: "kb_verify",
|
|
1549
2032
|
usage: "verify <concept-id> --note <text>",
|
|
1550
2033
|
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.",
|
|
1551
|
-
input:
|
|
2034
|
+
input: z28.object({
|
|
1552
2035
|
bundlePath,
|
|
1553
2036
|
conceptId,
|
|
1554
|
-
note:
|
|
2037
|
+
note: z28.string().refine((s) => s.trim().length > 0, {
|
|
1555
2038
|
message: "note must say what the check found"
|
|
1556
2039
|
})
|
|
1557
2040
|
}),
|
|
@@ -1571,7 +2054,7 @@ var verifyCommand = define({
|
|
|
1571
2054
|
});
|
|
1572
2055
|
|
|
1573
2056
|
// src/commands/write.ts
|
|
1574
|
-
import { z as
|
|
2057
|
+
import { z as z29 } from "zod";
|
|
1575
2058
|
var writeCommand = define({
|
|
1576
2059
|
name: "write",
|
|
1577
2060
|
tool: "kb_write",
|
|
@@ -1585,9 +2068,9 @@ var writeCommand = define({
|
|
|
1585
2068
|
"- Prefer a new record over overloading an existing one, and keep each short. A record nobody finishes reading is not durable memory.",
|
|
1586
2069
|
"- Records are never deleted; supersede instead, so the earlier reasoning stays inspectable."
|
|
1587
2070
|
].join("\n"),
|
|
1588
|
-
input:
|
|
2071
|
+
input: z29.object({
|
|
1589
2072
|
bundlePath,
|
|
1590
|
-
type:
|
|
2073
|
+
type: z29.enum(KB_RECORD_TYPES),
|
|
1591
2074
|
input: composeInputSchema
|
|
1592
2075
|
}),
|
|
1593
2076
|
fromArgv: async (argv, path, stdin) => ({
|
|
@@ -1611,7 +2094,7 @@ var writeCommand = define({
|
|
|
1611
2094
|
});
|
|
1612
2095
|
|
|
1613
2096
|
// src/commands/write-decision.ts
|
|
1614
|
-
import { z as
|
|
2097
|
+
import { z as z30 } from "zod";
|
|
1615
2098
|
var writeDecisionCommand = define({
|
|
1616
2099
|
name: "write-decision",
|
|
1617
2100
|
tool: "kb_write_decision",
|
|
@@ -1624,7 +2107,7 @@ var writeDecisionCommand = define({
|
|
|
1624
2107
|
"- `alternative` is what you turned down and why, not a list of everything considered.",
|
|
1625
2108
|
"- A reference to material you read goes in `sources`; a reference to code goes in `anchors`; a reference to another record goes in `relatedConceptIds`."
|
|
1626
2109
|
].join("\n"),
|
|
1627
|
-
input:
|
|
2110
|
+
input: z30.object({ bundlePath, input: decisionInputSchema }),
|
|
1628
2111
|
fromArgv: async (_argv, path, stdin) => ({
|
|
1629
2112
|
bundlePath: path,
|
|
1630
2113
|
input: JSON.parse(await stdin())
|
|
@@ -1654,12 +2137,14 @@ var KB_COMMANDS = [
|
|
|
1654
2137
|
answerCommand,
|
|
1655
2138
|
verifyCommand,
|
|
1656
2139
|
loadCommand,
|
|
2140
|
+
packCommand,
|
|
1657
2141
|
queryCommand,
|
|
1658
2142
|
traceCommand,
|
|
1659
2143
|
listCommand,
|
|
1660
2144
|
readIndexCommand,
|
|
1661
2145
|
logCommand,
|
|
1662
2146
|
validateCommand,
|
|
2147
|
+
doctorCommand,
|
|
1663
2148
|
schemaCommand,
|
|
1664
2149
|
pinCommand,
|
|
1665
2150
|
unpinCommand,
|
|
@@ -1707,6 +2192,7 @@ var Fault = /* @__PURE__ */ ((Fault2) => {
|
|
|
1707
2192
|
var ErrorTypes = /* @__PURE__ */ ((ErrorTypes2) => {
|
|
1708
2193
|
ErrorTypes2["KbRecordAlreadyExists"] = "KbRecordAlreadyExists";
|
|
1709
2194
|
ErrorTypes2["KbInvalidConceptId"] = "KbInvalidConceptId";
|
|
2195
|
+
ErrorTypes2["KbPackBudgetExceeded"] = "KbPackBudgetExceeded";
|
|
1710
2196
|
ErrorTypes2["KbRecordNotFound"] = "KbRecordNotFound";
|
|
1711
2197
|
ErrorTypes2["KbSelfVerification"] = "KbSelfVerification";
|
|
1712
2198
|
ErrorTypes2["KbWriteConflict"] = "KbWriteConflict";
|
|
@@ -1796,6 +2282,27 @@ var KbSelfVerificationError = class extends BaseError {
|
|
|
1796
2282
|
actor;
|
|
1797
2283
|
generatedBy;
|
|
1798
2284
|
};
|
|
2285
|
+
var KbPackBudgetExceededError = class extends BaseError {
|
|
2286
|
+
constructor(recordCount, approxTokens2, budgetTokens, excluded) {
|
|
2287
|
+
super({
|
|
2288
|
+
message: `kb: a pack of ${recordCount} records is ~${approxTokens2} tokens against a budget of ${budgetTokens} \u2014 lower hops or maxNodes, or raise the budget`,
|
|
2289
|
+
errorType: "KbPackBudgetExceeded" /* KbPackBudgetExceeded */,
|
|
2290
|
+
code: 400,
|
|
2291
|
+
fault: "User" /* User */,
|
|
2292
|
+
retriable: false,
|
|
2293
|
+
reportToUser: true,
|
|
2294
|
+
details: { recordCount, approxTokens: approxTokens2, budgetTokens, excluded }
|
|
2295
|
+
});
|
|
2296
|
+
this.recordCount = recordCount;
|
|
2297
|
+
this.approxTokens = approxTokens2;
|
|
2298
|
+
this.budgetTokens = budgetTokens;
|
|
2299
|
+
this.excluded = excluded;
|
|
2300
|
+
}
|
|
2301
|
+
recordCount;
|
|
2302
|
+
approxTokens;
|
|
2303
|
+
budgetTokens;
|
|
2304
|
+
excluded;
|
|
2305
|
+
};
|
|
1799
2306
|
var KbInvalidConceptIdError = class extends BaseError {
|
|
1800
2307
|
constructor(message, details) {
|
|
1801
2308
|
super({
|
|
@@ -2197,6 +2704,10 @@ ${answer}
|
|
|
2197
2704
|
async trace(bundlePath2, seedId, options = {}) {
|
|
2198
2705
|
return trace(seedId, await this.list(bundlePath2), options);
|
|
2199
2706
|
}
|
|
2707
|
+
/** A bounded neighbourhood around one record. See `pack.ts`. */
|
|
2708
|
+
async pack(bundlePath2, rootId, options = {}) {
|
|
2709
|
+
return pack(await this.list(bundlePath2), rootId, options);
|
|
2710
|
+
}
|
|
2200
2711
|
/**
|
|
2201
2712
|
* The stored index, rebuilt if it disagrees with the records.
|
|
2202
2713
|
*
|
|
@@ -2403,6 +2914,93 @@ function digest(contents) {
|
|
|
2403
2914
|
return createHash("sha256").update(contents).digest("hex");
|
|
2404
2915
|
}
|
|
2405
2916
|
|
|
2917
|
+
// src/pack.ts
|
|
2918
|
+
var DEFAULT_PACK_HOPS = 2;
|
|
2919
|
+
var DEFAULT_PACK_MAX_NODES = 20;
|
|
2920
|
+
var TYPE_PRIORITY = [
|
|
2921
|
+
"decision",
|
|
2922
|
+
"constraint",
|
|
2923
|
+
"requirement",
|
|
2924
|
+
...KB_RECORD_TYPES.filter(
|
|
2925
|
+
(type) => !["decision", "constraint", "requirement"].includes(type)
|
|
2926
|
+
)
|
|
2927
|
+
];
|
|
2928
|
+
function pack(bundle, rootId, options = {}) {
|
|
2929
|
+
const hops = options.hops ?? DEFAULT_PACK_HOPS;
|
|
2930
|
+
const maxNodes = options.maxNodes ?? DEFAULT_PACK_MAX_NODES;
|
|
2931
|
+
const budgetTokens = options.budgetTokens ?? DEFAULT_LOAD_BUDGET;
|
|
2932
|
+
const byId = new Map(bundle.map((record) => [record.conceptId, record]));
|
|
2933
|
+
const root = byId.get(rootId);
|
|
2934
|
+
if (!root) throw new KbRecordNotFoundError(rootId);
|
|
2935
|
+
const reached = [{ record: root, depth: 0 }];
|
|
2936
|
+
const seen = /* @__PURE__ */ new Set([rootId]);
|
|
2937
|
+
let frontier = [root];
|
|
2938
|
+
for (let depth = 1; frontier.length; depth += 1) {
|
|
2939
|
+
const next = [];
|
|
2940
|
+
for (const from of frontier) {
|
|
2941
|
+
for (const { record } of neighbours(from, bundle)) {
|
|
2942
|
+
if (seen.has(record.conceptId)) continue;
|
|
2943
|
+
seen.add(record.conceptId);
|
|
2944
|
+
reached.push({ record, depth });
|
|
2945
|
+
next.push(record);
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
frontier = next;
|
|
2949
|
+
}
|
|
2950
|
+
reached.sort(byRank);
|
|
2951
|
+
const within = reached.filter((entry) => entry.depth <= hops);
|
|
2952
|
+
const kept = within.slice(0, maxNodes);
|
|
2953
|
+
const excluded = [
|
|
2954
|
+
...within.slice(maxNodes),
|
|
2955
|
+
...reached.filter((entry) => entry.depth > hops)
|
|
2956
|
+
].map((entry) => entry.record.conceptId).sort();
|
|
2957
|
+
const adjudicated = adjudicate(
|
|
2958
|
+
kept.map((entry) => entry.record),
|
|
2959
|
+
bundle
|
|
2960
|
+
);
|
|
2961
|
+
const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(stub);
|
|
2962
|
+
const whole = adjudicated.filter((hit) => hit.standing !== "superseded");
|
|
2963
|
+
const tokensLoaded = whole.reduce((total, hit) => total + estimateTokens(hit.record), 0) + superseded.reduce((total, entry) => total + estimateStubTokens(entry), 0);
|
|
2964
|
+
const recordCount = adjudicated.length;
|
|
2965
|
+
if (tokensLoaded > budgetTokens) {
|
|
2966
|
+
throw new KbPackBudgetExceededError(
|
|
2967
|
+
recordCount,
|
|
2968
|
+
tokensLoaded,
|
|
2969
|
+
budgetTokens,
|
|
2970
|
+
excluded
|
|
2971
|
+
);
|
|
2972
|
+
}
|
|
2973
|
+
return {
|
|
2974
|
+
root: rootId,
|
|
2975
|
+
records: whole.map((hit) => ({
|
|
2976
|
+
conceptId: hit.record.conceptId,
|
|
2977
|
+
title: hit.record.frontmatter.title ?? null,
|
|
2978
|
+
standing: hit.standing,
|
|
2979
|
+
supersededBy: hit.heads.map((head) => head.conceptId),
|
|
2980
|
+
warnings: hit.warnings,
|
|
2981
|
+
anchors: hit.record.frontmatter.strauss_anchors ?? [],
|
|
2982
|
+
body: hit.record.body
|
|
2983
|
+
})),
|
|
2984
|
+
superseded,
|
|
2985
|
+
excluded,
|
|
2986
|
+
recordCount,
|
|
2987
|
+
tokensLoaded,
|
|
2988
|
+
budgetTokens
|
|
2989
|
+
};
|
|
2990
|
+
}
|
|
2991
|
+
function byRank(left, right) {
|
|
2992
|
+
return left.depth - right.depth || typeRank(left.record) - typeRank(right.record) || (left.record.frontmatter.title ?? "").localeCompare(
|
|
2993
|
+
right.record.frontmatter.title ?? ""
|
|
2994
|
+
) || left.record.conceptId.localeCompare(right.record.conceptId);
|
|
2995
|
+
}
|
|
2996
|
+
function typeRank(record) {
|
|
2997
|
+
const index = TYPE_PRIORITY.indexOf(record.frontmatter.type);
|
|
2998
|
+
return index === -1 ? TYPE_PRIORITY.length : index;
|
|
2999
|
+
}
|
|
3000
|
+
|
|
3001
|
+
// src/version.ts
|
|
3002
|
+
var VERSION = true ? "0.1.8" : "0.0.0-dev";
|
|
3003
|
+
|
|
2406
3004
|
export {
|
|
2407
3005
|
kbSourceSchema,
|
|
2408
3006
|
kbActorStampSchema,
|
|
@@ -2453,6 +3051,15 @@ export {
|
|
|
2453
3051
|
CONTEXT_BEGIN,
|
|
2454
3052
|
CONTEXT_END,
|
|
2455
3053
|
syncInstructions,
|
|
3054
|
+
KB_EDGE_KINDS,
|
|
3055
|
+
neighbours,
|
|
3056
|
+
edgeNeighbours,
|
|
3057
|
+
validateBundle,
|
|
3058
|
+
DEFAULT_EXPIRING_DAYS,
|
|
3059
|
+
DEFAULT_UNVERIFIED_DAYS,
|
|
3060
|
+
DEFAULT_AGING_DAYS,
|
|
3061
|
+
KB_DOCTOR_CHECKS,
|
|
3062
|
+
doctor,
|
|
2456
3063
|
LOG_FILE,
|
|
2457
3064
|
kbLogEntrySchema,
|
|
2458
3065
|
renderLogEntry,
|
|
@@ -2460,7 +3067,6 @@ export {
|
|
|
2460
3067
|
kbJsonSchemas,
|
|
2461
3068
|
TRACE_EDGES,
|
|
2462
3069
|
trace,
|
|
2463
|
-
validateBundle,
|
|
2464
3070
|
KB_COMMANDS,
|
|
2465
3071
|
KB_COMMANDS_BY_NAME,
|
|
2466
3072
|
stringifyMarkdownWithFrontmatter,
|
|
@@ -2473,12 +3079,18 @@ export {
|
|
|
2473
3079
|
KbRecordNotFoundError,
|
|
2474
3080
|
KbWriteConflictError,
|
|
2475
3081
|
KbSelfVerificationError,
|
|
3082
|
+
KbPackBudgetExceededError,
|
|
2476
3083
|
KbInvalidConceptIdError,
|
|
2477
3084
|
SEARCH_INDEX_FILE,
|
|
2478
3085
|
searchBase,
|
|
2479
3086
|
resolveHits,
|
|
2480
3087
|
loadQmd,
|
|
3088
|
+
DEFAULT_PACK_HOPS,
|
|
3089
|
+
DEFAULT_PACK_MAX_NODES,
|
|
3090
|
+
pack,
|
|
2481
3091
|
KB_DIR,
|
|
2482
|
-
|
|
3092
|
+
DEFAULT_LOAD_BUDGET,
|
|
3093
|
+
KbStore,
|
|
3094
|
+
VERSION
|
|
2483
3095
|
};
|
|
2484
|
-
//# sourceMappingURL=chunk-
|
|
3096
|
+
//# sourceMappingURL=chunk-YJK7KGHN.js.map
|