@saasontools/strauss-kb 0.1.6 → 0.1.7
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 +21 -2
- package/dist/{chunk-V7TRZ2ER.js → chunk-GKCG4P3L.js} +10 -3
- package/dist/chunk-GKCG4P3L.js.map +1 -0
- package/dist/{chunk-PNSRTKYN.js → chunk-GKUQOJEK.js} +343 -79
- package/dist/chunk-GKUQOJEK.js.map +1 -0
- package/dist/{chunk-BVF7X5VO.js → chunk-LCQKARFK.js} +5 -4
- package/dist/chunk-LCQKARFK.js.map +1 -0
- package/dist/cli-main.cjs +348 -88
- package/dist/cli-main.cjs.map +1 -1
- package/dist/cli-main.js +2 -2
- package/dist/index.cjs +357 -78
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +104 -7
- package/dist/index.d.ts +104 -7
- package/dist/index.js +19 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp-main.cjs +343 -89
- 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/mcp-main.cjs
CHANGED
|
@@ -1157,23 +1157,123 @@ var noDecisionCommand = define({
|
|
|
1157
1157
|
}
|
|
1158
1158
|
});
|
|
1159
1159
|
|
|
1160
|
-
// src/commands/
|
|
1160
|
+
// src/commands/pack.ts
|
|
1161
1161
|
var import_zod12 = require("zod");
|
|
1162
|
+
var packCommand = define({
|
|
1163
|
+
name: "pack",
|
|
1164
|
+
tool: "kb_pack",
|
|
1165
|
+
usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
|
|
1166
|
+
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.",
|
|
1167
|
+
input: import_zod12.z.object({
|
|
1168
|
+
bundlePath,
|
|
1169
|
+
conceptId,
|
|
1170
|
+
hops: import_zod12.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
|
|
1171
|
+
maxNodes: import_zod12.z.number().int().positive().optional().describe(
|
|
1172
|
+
"How many records the pack may hold, root included. Defaults to 20."
|
|
1173
|
+
),
|
|
1174
|
+
budgetTokens: import_zod12.z.number().int().positive().optional().describe(
|
|
1175
|
+
"Approximate token ceiling over what is actually emitted. Defaults to 25000."
|
|
1176
|
+
)
|
|
1177
|
+
}),
|
|
1178
|
+
fromArgv: (argv, path) => {
|
|
1179
|
+
const hops = argvFlag(argv, "--hops");
|
|
1180
|
+
const maxNodes = argvFlag(argv, "--max-nodes");
|
|
1181
|
+
const budget = argvFlag(argv, "--budget");
|
|
1182
|
+
return {
|
|
1183
|
+
bundlePath: path,
|
|
1184
|
+
conceptId: argv[1],
|
|
1185
|
+
...hops ? { hops: Number(hops) } : {},
|
|
1186
|
+
...maxNodes ? { maxNodes: Number(maxNodes) } : {},
|
|
1187
|
+
...budget ? { budgetTokens: Number(budget) } : {}
|
|
1188
|
+
};
|
|
1189
|
+
},
|
|
1190
|
+
run: async ({ store, now }, { bundlePath: path, conceptId: root, hops, maxNodes, budgetTokens }) => {
|
|
1191
|
+
const result = await store.pack(path, root, {
|
|
1192
|
+
...hops !== void 0 ? { hops } : {},
|
|
1193
|
+
...maxNodes !== void 0 ? { maxNodes } : {},
|
|
1194
|
+
...budgetTokens !== void 0 ? { budgetTokens } : {}
|
|
1195
|
+
});
|
|
1196
|
+
return render(result, path, now());
|
|
1197
|
+
}
|
|
1198
|
+
});
|
|
1199
|
+
function render(result, bundle, at) {
|
|
1200
|
+
const lines = [
|
|
1201
|
+
`# KB Pack \u2014 ${result.root}`,
|
|
1202
|
+
`bundle: ${bundle}`,
|
|
1203
|
+
`budget: ~${result.tokensLoaded} of ${result.budgetTokens} tokens, ${result.recordCount} records`,
|
|
1204
|
+
`packed: ${at}`,
|
|
1205
|
+
"",
|
|
1206
|
+
`## Records (${result.records.length})`
|
|
1207
|
+
];
|
|
1208
|
+
for (const record of result.records) {
|
|
1209
|
+
lines.push(
|
|
1210
|
+
"",
|
|
1211
|
+
`### ${record.conceptId}${record.title ? ` \u2014 ${record.title}` : ""} [${record.standing}]`
|
|
1212
|
+
);
|
|
1213
|
+
if (record.warnings.length) {
|
|
1214
|
+
lines.push(`warnings: ${record.warnings.map(warningLabel).join("; ")}`);
|
|
1215
|
+
}
|
|
1216
|
+
if (record.anchors.length) {
|
|
1217
|
+
lines.push(
|
|
1218
|
+
`anchors: ${record.anchors.map(
|
|
1219
|
+
(anchor) => anchor.symbol ? `${anchor.file}#${anchor.symbol}` : anchor.file
|
|
1220
|
+
).join(", ")}`
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
lines.push("", record.body.trimEnd());
|
|
1224
|
+
}
|
|
1225
|
+
if (result.superseded.length) {
|
|
1226
|
+
lines.push("", `## Superseded (${result.superseded.length})`);
|
|
1227
|
+
for (const entry of result.superseded) {
|
|
1228
|
+
lines.push(
|
|
1229
|
+
`- ${entry.conceptId} \u2192 ${entry.supersededBy.join(", ") || "(no surviving head)"}${entry.at ? ` (${entry.at})` : ""}`
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
if (result.excluded.length) {
|
|
1234
|
+
lines.push("", `## Excluded (${result.excluded.length})`);
|
|
1235
|
+
for (const cut of result.excluded) lines.push(`- ${cut}`);
|
|
1236
|
+
}
|
|
1237
|
+
return lines.join("\n");
|
|
1238
|
+
}
|
|
1239
|
+
function warningLabel(warning) {
|
|
1240
|
+
switch (warning.kind) {
|
|
1241
|
+
case "superseded":
|
|
1242
|
+
return `superseded by ${warning.by.join(", ")}`;
|
|
1243
|
+
case "unsettled":
|
|
1244
|
+
return `unsettled (${warning.status})`;
|
|
1245
|
+
case "broken-chain":
|
|
1246
|
+
return `broken chain \u2014 ${warning.missing} is not in the bundle`;
|
|
1247
|
+
case "chain-cycle":
|
|
1248
|
+
return `chain cycle through ${warning.through.join(" \u2192 ")}`;
|
|
1249
|
+
case "forked-chain":
|
|
1250
|
+
return `forked chain \u2014 heads ${warning.heads.join(", ")}`;
|
|
1251
|
+
case "stale":
|
|
1252
|
+
return `stale since ${warning.staleAfter}`;
|
|
1253
|
+
case "unresolved-question":
|
|
1254
|
+
return "unresolved question";
|
|
1255
|
+
default:
|
|
1256
|
+
return warning.kind;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// src/commands/pin.ts
|
|
1261
|
+
var import_zod13 = require("zod");
|
|
1162
1262
|
var pinCommand = define({
|
|
1163
1263
|
name: "pin",
|
|
1164
1264
|
tool: "kb_pin",
|
|
1165
1265
|
usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
|
|
1166
1266
|
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.",
|
|
1167
|
-
input:
|
|
1267
|
+
input: import_zod13.z.object({
|
|
1168
1268
|
bundlePath,
|
|
1169
|
-
mode:
|
|
1269
|
+
mode: import_zod13.z.enum(["full", "index"]).optional().describe(
|
|
1170
1270
|
"full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
|
|
1171
1271
|
),
|
|
1172
|
-
profiles:
|
|
1173
|
-
layer:
|
|
1272
|
+
profiles: import_zod13.z.array(import_zod13.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
|
|
1273
|
+
layer: import_zod13.z.enum(["project", "local", "user"]).optional().describe(
|
|
1174
1274
|
"Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
|
|
1175
1275
|
),
|
|
1176
|
-
frozen:
|
|
1276
|
+
frozen: import_zod13.z.boolean().optional().describe(
|
|
1177
1277
|
"true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
|
|
1178
1278
|
)
|
|
1179
1279
|
}),
|
|
@@ -1202,29 +1302,29 @@ var pinCommand = define({
|
|
|
1202
1302
|
});
|
|
1203
1303
|
|
|
1204
1304
|
// src/commands/pins.ts
|
|
1205
|
-
var
|
|
1305
|
+
var import_zod14 = require("zod");
|
|
1206
1306
|
var pinsCommand = define({
|
|
1207
1307
|
name: "pins",
|
|
1208
1308
|
tool: "kb_pins",
|
|
1209
1309
|
usage: "pins",
|
|
1210
1310
|
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.",
|
|
1211
|
-
input:
|
|
1311
|
+
input: import_zod14.z.object({}),
|
|
1212
1312
|
fromArgv: () => ({}),
|
|
1213
1313
|
run: ({ store }) => listPins(store, process.cwd())
|
|
1214
1314
|
});
|
|
1215
1315
|
|
|
1216
1316
|
// src/commands/query.ts
|
|
1217
|
-
var
|
|
1317
|
+
var import_zod15 = require("zod");
|
|
1218
1318
|
var queryCommand = define({
|
|
1219
1319
|
name: "query",
|
|
1220
1320
|
tool: "kb_query",
|
|
1221
1321
|
usage: "query <text...>",
|
|
1222
1322
|
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.",
|
|
1223
|
-
input:
|
|
1323
|
+
input: import_zod15.z.object({
|
|
1224
1324
|
bundlePath,
|
|
1225
|
-
text:
|
|
1226
|
-
type:
|
|
1227
|
-
includeNonCurrent:
|
|
1325
|
+
text: import_zod15.z.string().optional(),
|
|
1326
|
+
type: import_zod15.z.enum(KB_RECORD_TYPES).optional(),
|
|
1327
|
+
includeNonCurrent: import_zod15.z.boolean().optional()
|
|
1228
1328
|
}),
|
|
1229
1329
|
fromArgv: (argv, path) => ({
|
|
1230
1330
|
bundlePath: path,
|
|
@@ -1246,33 +1346,33 @@ var queryCommand = define({
|
|
|
1246
1346
|
});
|
|
1247
1347
|
|
|
1248
1348
|
// src/commands/read-index.ts
|
|
1249
|
-
var
|
|
1349
|
+
var import_zod16 = require("zod");
|
|
1250
1350
|
var readIndexCommand = define({
|
|
1251
1351
|
name: "index",
|
|
1252
1352
|
tool: "kb_index",
|
|
1253
1353
|
usage: "index",
|
|
1254
1354
|
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.",
|
|
1255
|
-
input:
|
|
1355
|
+
input: import_zod16.z.object({ bundlePath }),
|
|
1256
1356
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
1257
1357
|
run: ({ store }, { bundlePath: path }) => store.readIndex(path)
|
|
1258
1358
|
});
|
|
1259
1359
|
|
|
1260
1360
|
// src/commands/schema.ts
|
|
1261
|
-
var
|
|
1361
|
+
var import_zod19 = require("zod");
|
|
1262
1362
|
|
|
1263
1363
|
// src/json-schema.ts
|
|
1264
|
-
var
|
|
1364
|
+
var import_zod18 = require("zod");
|
|
1265
1365
|
|
|
1266
1366
|
// src/kb-log.ts
|
|
1267
|
-
var
|
|
1367
|
+
var import_zod17 = require("zod");
|
|
1268
1368
|
var LOG_FILE = "log.jsonl";
|
|
1269
|
-
var kbLogEntrySchema =
|
|
1270
|
-
at:
|
|
1271
|
-
by:
|
|
1272
|
-
operation:
|
|
1273
|
-
conceptId:
|
|
1369
|
+
var kbLogEntrySchema = import_zod17.z.object({
|
|
1370
|
+
at: import_zod17.z.string().min(1),
|
|
1371
|
+
by: import_zod17.z.string().min(1),
|
|
1372
|
+
operation: import_zod17.z.string().min(1),
|
|
1373
|
+
conceptId: import_zod17.z.string().min(1),
|
|
1274
1374
|
/** Second concept id, where the operation relates two — supersession. */
|
|
1275
|
-
target:
|
|
1375
|
+
target: import_zod17.z.string().min(1).optional()
|
|
1276
1376
|
}).strict();
|
|
1277
1377
|
function renderLogEntry(entry) {
|
|
1278
1378
|
return `${JSON.stringify(kbLogEntrySchema.parse(entry))}
|
|
@@ -1303,11 +1403,11 @@ function parseLog(raw) {
|
|
|
1303
1403
|
// src/json-schema.ts
|
|
1304
1404
|
function kbJsonSchemas() {
|
|
1305
1405
|
return {
|
|
1306
|
-
recordFrontmatter:
|
|
1406
|
+
recordFrontmatter: import_zod18.z.toJSONSchema(kbRecordFrontmatterSchema, {
|
|
1307
1407
|
io: "input"
|
|
1308
1408
|
}),
|
|
1309
|
-
composeInput:
|
|
1310
|
-
logEntry:
|
|
1409
|
+
composeInput: import_zod18.z.toJSONSchema(composeInputSchema, { io: "input" }),
|
|
1410
|
+
logEntry: import_zod18.z.toJSONSchema(kbLogEntrySchema, { io: "input" })
|
|
1311
1411
|
};
|
|
1312
1412
|
}
|
|
1313
1413
|
|
|
@@ -1317,22 +1417,22 @@ var schemaCommand = define({
|
|
|
1317
1417
|
tool: "kb_schema",
|
|
1318
1418
|
usage: "schema",
|
|
1319
1419
|
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.",
|
|
1320
|
-
input:
|
|
1420
|
+
input: import_zod19.z.object({}),
|
|
1321
1421
|
fromArgv: () => ({}),
|
|
1322
1422
|
run: () => Promise.resolve(kbJsonSchemas())
|
|
1323
1423
|
});
|
|
1324
1424
|
|
|
1325
1425
|
// src/commands/status.ts
|
|
1326
|
-
var
|
|
1426
|
+
var import_zod20 = require("zod");
|
|
1327
1427
|
var statusCommand = define({
|
|
1328
1428
|
name: "status",
|
|
1329
1429
|
tool: "kb_status",
|
|
1330
1430
|
usage: "status <concept-id> <status>",
|
|
1331
1431
|
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.",
|
|
1332
|
-
input:
|
|
1432
|
+
input: import_zod20.z.object({
|
|
1333
1433
|
bundlePath,
|
|
1334
1434
|
conceptId,
|
|
1335
|
-
status:
|
|
1435
|
+
status: import_zod20.z.enum(KB_RECORD_STATUSES)
|
|
1336
1436
|
}),
|
|
1337
1437
|
fromArgv: (argv, path) => ({
|
|
1338
1438
|
bundlePath: path,
|
|
@@ -1347,13 +1447,13 @@ var statusCommand = define({
|
|
|
1347
1447
|
});
|
|
1348
1448
|
|
|
1349
1449
|
// src/commands/supersede.ts
|
|
1350
|
-
var
|
|
1450
|
+
var import_zod21 = require("zod");
|
|
1351
1451
|
var supersedeCommand = define({
|
|
1352
1452
|
name: "supersede",
|
|
1353
1453
|
tool: "kb_supersede",
|
|
1354
1454
|
usage: "supersede <concept-id> <replacement-id>",
|
|
1355
1455
|
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.",
|
|
1356
|
-
input:
|
|
1456
|
+
input: import_zod21.z.object({ bundlePath, conceptId, replacementId: conceptId }),
|
|
1357
1457
|
fromArgv: (argv, path) => ({
|
|
1358
1458
|
bundlePath: path,
|
|
1359
1459
|
conceptId: argv[1],
|
|
@@ -1367,16 +1467,16 @@ var supersedeCommand = define({
|
|
|
1367
1467
|
});
|
|
1368
1468
|
|
|
1369
1469
|
// src/commands/sync-instructions.ts
|
|
1370
|
-
var
|
|
1470
|
+
var import_zod22 = require("zod");
|
|
1371
1471
|
var syncInstructionsCommand = define({
|
|
1372
1472
|
name: "sync-instructions",
|
|
1373
1473
|
usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
|
|
1374
1474
|
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.",
|
|
1375
|
-
input:
|
|
1376
|
-
file:
|
|
1377
|
-
budgetTokens:
|
|
1378
|
-
fullUnderTokens:
|
|
1379
|
-
profile:
|
|
1475
|
+
input: import_zod22.z.object({
|
|
1476
|
+
file: import_zod22.z.string().min(1).describe("The instruction file to edit in place."),
|
|
1477
|
+
budgetTokens: import_zod22.z.number().int().positive().optional(),
|
|
1478
|
+
fullUnderTokens: import_zod22.z.number().int().positive().optional(),
|
|
1479
|
+
profile: import_zod22.z.string().optional()
|
|
1380
1480
|
}),
|
|
1381
1481
|
fromArgv: (argv) => {
|
|
1382
1482
|
const budget = argvFlag(argv, "--budget");
|
|
@@ -1402,43 +1502,50 @@ var syncInstructionsCommand = define({
|
|
|
1402
1502
|
});
|
|
1403
1503
|
|
|
1404
1504
|
// src/commands/trace.ts
|
|
1405
|
-
var
|
|
1505
|
+
var import_zod23 = require("zod");
|
|
1406
1506
|
|
|
1407
|
-
// src/
|
|
1408
|
-
var
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
for (const
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
if (existing.depth > 0 && !existing.via.includes(edge)) {
|
|
1427
|
-
existing.via.push(edge);
|
|
1428
|
-
}
|
|
1429
|
-
continue;
|
|
1430
|
-
}
|
|
1431
|
-
reached.set(record.conceptId, { record, depth, via: [edge] });
|
|
1432
|
-
next.push(record);
|
|
1433
|
-
}
|
|
1507
|
+
// src/kb-edges.ts
|
|
1508
|
+
var KB_EDGE_KINDS = [
|
|
1509
|
+
"body-link",
|
|
1510
|
+
"supersession",
|
|
1511
|
+
"anchor",
|
|
1512
|
+
"source"
|
|
1513
|
+
];
|
|
1514
|
+
var BODY_LINK_TARGET = new RegExp(
|
|
1515
|
+
`\\]\\((${KB_CONCEPT_ID_PATTERN.source.replace(/^\^|\$$/g, "")})\\.md\\)`,
|
|
1516
|
+
"g"
|
|
1517
|
+
);
|
|
1518
|
+
function neighbours(from, bundle, kinds = KB_EDGE_KINDS) {
|
|
1519
|
+
const found = /* @__PURE__ */ new Map();
|
|
1520
|
+
for (const kind of kinds) {
|
|
1521
|
+
for (const record of edgeNeighbours(from, bundle, kind)) {
|
|
1522
|
+
const existing = found.get(record.conceptId);
|
|
1523
|
+
if (existing) {
|
|
1524
|
+
if (!existing.via.includes(kind)) existing.via.push(kind);
|
|
1525
|
+
continue;
|
|
1434
1526
|
}
|
|
1527
|
+
found.set(record.conceptId, { record, via: [kind] });
|
|
1435
1528
|
}
|
|
1436
|
-
frontier = next;
|
|
1437
1529
|
}
|
|
1438
|
-
return [...
|
|
1530
|
+
return [...found.values()];
|
|
1439
1531
|
}
|
|
1440
|
-
function
|
|
1441
|
-
switch (
|
|
1532
|
+
function edgeNeighbours(from, bundle, kind) {
|
|
1533
|
+
switch (kind) {
|
|
1534
|
+
// A link whose target is not in the bundle is legal per compose.ts —
|
|
1535
|
+
// records are routinely written before the ones they point at exist — so
|
|
1536
|
+
// missing targets are skipped, never an error.
|
|
1537
|
+
case "body-link": {
|
|
1538
|
+
const targets = new Set(
|
|
1539
|
+
[...from.body.matchAll(BODY_LINK_TARGET)].map((match) => match[1])
|
|
1540
|
+
);
|
|
1541
|
+
if (!targets.size) return [];
|
|
1542
|
+
return bundle.filter(
|
|
1543
|
+
(candidate) => candidate.conceptId !== from.conceptId && targets.has(candidate.conceptId)
|
|
1544
|
+
);
|
|
1545
|
+
}
|
|
1546
|
+
// Both directions and both pointers: `supersede()` writes the pair, but a
|
|
1547
|
+
// hand-edit can leave one side behind, and a walk trusting one pointer
|
|
1548
|
+
// would miss a replacement the bundle openly declares.
|
|
1442
1549
|
case "supersession":
|
|
1443
1550
|
return bundle.filter(
|
|
1444
1551
|
(candidate) => candidate.conceptId !== from.conceptId && (candidate.conceptId === from.frontmatter.strauss_superseded_by || from.frontmatter.strauss_supersedes?.includes(
|
|
@@ -1472,6 +1579,40 @@ function anchorsTouch(left, right) {
|
|
|
1472
1579
|
if (!left.symbol || !right.symbol) return true;
|
|
1473
1580
|
return left.symbol === right.symbol;
|
|
1474
1581
|
}
|
|
1582
|
+
|
|
1583
|
+
// src/trace.ts
|
|
1584
|
+
var TRACE_EDGES = ["supersession", "anchor", "source"];
|
|
1585
|
+
function trace(seedId, bundle, options = {}) {
|
|
1586
|
+
const edges = options.edges?.length ? options.edges : TRACE_EDGES;
|
|
1587
|
+
const maxDepth = options.depth ?? 3;
|
|
1588
|
+
const byId = new Map(bundle.map((record) => [record.conceptId, record]));
|
|
1589
|
+
const seed = byId.get(seedId);
|
|
1590
|
+
if (!seed) return [];
|
|
1591
|
+
const reached = /* @__PURE__ */ new Map([
|
|
1592
|
+
[seedId, { record: seed, depth: 0, via: [] }]
|
|
1593
|
+
]);
|
|
1594
|
+
let frontier = [seed];
|
|
1595
|
+
for (let depth = 1; depth <= maxDepth && frontier.length; depth += 1) {
|
|
1596
|
+
const next = [];
|
|
1597
|
+
for (const from of frontier) {
|
|
1598
|
+
for (const edge of edges) {
|
|
1599
|
+
for (const record of edgeNeighbours(from, bundle, edge)) {
|
|
1600
|
+
const existing = reached.get(record.conceptId);
|
|
1601
|
+
if (existing) {
|
|
1602
|
+
if (existing.depth > 0 && !existing.via.includes(edge)) {
|
|
1603
|
+
existing.via.push(edge);
|
|
1604
|
+
}
|
|
1605
|
+
continue;
|
|
1606
|
+
}
|
|
1607
|
+
reached.set(record.conceptId, { record, depth, via: [edge] });
|
|
1608
|
+
next.push(record);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
frontier = next;
|
|
1613
|
+
}
|
|
1614
|
+
return [...reached.values()].sort(byGeneratedAt);
|
|
1615
|
+
}
|
|
1475
1616
|
function byGeneratedAt(left, right) {
|
|
1476
1617
|
const at = (step) => step.record.frontmatter.generated?.at ?? "";
|
|
1477
1618
|
return at(left).localeCompare(at(right)) || left.depth - right.depth;
|
|
@@ -1483,11 +1624,11 @@ var traceCommand = define({
|
|
|
1483
1624
|
tool: "kb_trace",
|
|
1484
1625
|
usage: "trace <concept-id> [edges...]",
|
|
1485
1626
|
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.',
|
|
1486
|
-
input:
|
|
1627
|
+
input: import_zod23.z.object({
|
|
1487
1628
|
bundlePath,
|
|
1488
1629
|
conceptId,
|
|
1489
|
-
edges:
|
|
1490
|
-
depth:
|
|
1630
|
+
edges: import_zod23.z.array(import_zod23.z.enum(TRACE_EDGES)).optional(),
|
|
1631
|
+
depth: import_zod23.z.number().int().positive().optional()
|
|
1491
1632
|
}),
|
|
1492
1633
|
fromArgv: (argv, path) => ({
|
|
1493
1634
|
bundlePath: path,
|
|
@@ -1509,31 +1650,31 @@ var traceCommand = define({
|
|
|
1509
1650
|
});
|
|
1510
1651
|
|
|
1511
1652
|
// src/commands/types.ts
|
|
1512
|
-
var
|
|
1653
|
+
var import_zod24 = require("zod");
|
|
1513
1654
|
var typesCommand = define({
|
|
1514
1655
|
name: "types",
|
|
1515
1656
|
tool: "kb_types",
|
|
1516
1657
|
usage: "types",
|
|
1517
1658
|
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.",
|
|
1518
|
-
input:
|
|
1659
|
+
input: import_zod24.z.object({}),
|
|
1519
1660
|
fromArgv: () => ({}),
|
|
1520
1661
|
run: () => Promise.resolve(RECORD_TYPES)
|
|
1521
1662
|
});
|
|
1522
1663
|
|
|
1523
1664
|
// src/commands/unpin.ts
|
|
1524
|
-
var
|
|
1665
|
+
var import_zod25 = require("zod");
|
|
1525
1666
|
var unpinCommand = define({
|
|
1526
1667
|
name: "unpin",
|
|
1527
1668
|
tool: "kb_unpin",
|
|
1528
1669
|
usage: "unpin [bundle-path]",
|
|
1529
1670
|
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.",
|
|
1530
|
-
input:
|
|
1671
|
+
input: import_zod25.z.object({ bundlePath }),
|
|
1531
1672
|
fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
|
|
1532
1673
|
run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
|
|
1533
1674
|
});
|
|
1534
1675
|
|
|
1535
1676
|
// src/commands/validate.ts
|
|
1536
|
-
var
|
|
1677
|
+
var import_zod26 = require("zod");
|
|
1537
1678
|
|
|
1538
1679
|
// src/validate.ts
|
|
1539
1680
|
function validateBundle(records) {
|
|
@@ -1576,23 +1717,23 @@ var validateCommand = define({
|
|
|
1576
1717
|
tool: "kb_validate",
|
|
1577
1718
|
usage: "validate",
|
|
1578
1719
|
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.",
|
|
1579
|
-
input:
|
|
1720
|
+
input: import_zod26.z.object({ bundlePath }),
|
|
1580
1721
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
1581
1722
|
run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
|
|
1582
1723
|
failsWhen: (result) => Array.isArray(result) && result.length > 0
|
|
1583
1724
|
});
|
|
1584
1725
|
|
|
1585
1726
|
// src/commands/verify.ts
|
|
1586
|
-
var
|
|
1727
|
+
var import_zod27 = require("zod");
|
|
1587
1728
|
var verifyCommand = define({
|
|
1588
1729
|
name: "verify",
|
|
1589
1730
|
tool: "kb_verify",
|
|
1590
1731
|
usage: "verify <concept-id> --note <text>",
|
|
1591
1732
|
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.",
|
|
1592
|
-
input:
|
|
1733
|
+
input: import_zod27.z.object({
|
|
1593
1734
|
bundlePath,
|
|
1594
1735
|
conceptId,
|
|
1595
|
-
note:
|
|
1736
|
+
note: import_zod27.z.string().refine((s) => s.trim().length > 0, {
|
|
1596
1737
|
message: "note must say what the check found"
|
|
1597
1738
|
})
|
|
1598
1739
|
}),
|
|
@@ -1612,7 +1753,7 @@ var verifyCommand = define({
|
|
|
1612
1753
|
});
|
|
1613
1754
|
|
|
1614
1755
|
// src/commands/write.ts
|
|
1615
|
-
var
|
|
1756
|
+
var import_zod28 = require("zod");
|
|
1616
1757
|
var writeCommand = define({
|
|
1617
1758
|
name: "write",
|
|
1618
1759
|
tool: "kb_write",
|
|
@@ -1626,9 +1767,9 @@ var writeCommand = define({
|
|
|
1626
1767
|
"- Prefer a new record over overloading an existing one, and keep each short. A record nobody finishes reading is not durable memory.",
|
|
1627
1768
|
"- Records are never deleted; supersede instead, so the earlier reasoning stays inspectable."
|
|
1628
1769
|
].join("\n"),
|
|
1629
|
-
input:
|
|
1770
|
+
input: import_zod28.z.object({
|
|
1630
1771
|
bundlePath,
|
|
1631
|
-
type:
|
|
1772
|
+
type: import_zod28.z.enum(KB_RECORD_TYPES),
|
|
1632
1773
|
input: composeInputSchema
|
|
1633
1774
|
}),
|
|
1634
1775
|
fromArgv: async (argv, path, stdin) => ({
|
|
@@ -1652,7 +1793,7 @@ var writeCommand = define({
|
|
|
1652
1793
|
});
|
|
1653
1794
|
|
|
1654
1795
|
// src/commands/write-decision.ts
|
|
1655
|
-
var
|
|
1796
|
+
var import_zod29 = require("zod");
|
|
1656
1797
|
var writeDecisionCommand = define({
|
|
1657
1798
|
name: "write-decision",
|
|
1658
1799
|
tool: "kb_write_decision",
|
|
@@ -1665,7 +1806,7 @@ var writeDecisionCommand = define({
|
|
|
1665
1806
|
"- `alternative` is what you turned down and why, not a list of everything considered.",
|
|
1666
1807
|
"- A reference to material you read goes in `sources`; a reference to code goes in `anchors`; a reference to another record goes in `relatedConceptIds`."
|
|
1667
1808
|
].join("\n"),
|
|
1668
|
-
input:
|
|
1809
|
+
input: import_zod29.z.object({ bundlePath, input: decisionInputSchema }),
|
|
1669
1810
|
fromArgv: async (_argv, path, stdin) => ({
|
|
1670
1811
|
bundlePath: path,
|
|
1671
1812
|
input: JSON.parse(await stdin())
|
|
@@ -1695,6 +1836,7 @@ var KB_COMMANDS = [
|
|
|
1695
1836
|
answerCommand,
|
|
1696
1837
|
verifyCommand,
|
|
1697
1838
|
loadCommand,
|
|
1839
|
+
packCommand,
|
|
1698
1840
|
queryCommand,
|
|
1699
1841
|
traceCommand,
|
|
1700
1842
|
listCommand,
|
|
@@ -1828,6 +1970,27 @@ var KbSelfVerificationError = class extends BaseError {
|
|
|
1828
1970
|
actor;
|
|
1829
1971
|
generatedBy;
|
|
1830
1972
|
};
|
|
1973
|
+
var KbPackBudgetExceededError = class extends BaseError {
|
|
1974
|
+
constructor(recordCount, approxTokens2, budgetTokens, excluded) {
|
|
1975
|
+
super({
|
|
1976
|
+
message: `kb: a pack of ${recordCount} records is ~${approxTokens2} tokens against a budget of ${budgetTokens} \u2014 lower hops or maxNodes, or raise the budget`,
|
|
1977
|
+
errorType: "KbPackBudgetExceeded" /* KbPackBudgetExceeded */,
|
|
1978
|
+
code: 400,
|
|
1979
|
+
fault: "User" /* User */,
|
|
1980
|
+
retriable: false,
|
|
1981
|
+
reportToUser: true,
|
|
1982
|
+
details: { recordCount, approxTokens: approxTokens2, budgetTokens, excluded }
|
|
1983
|
+
});
|
|
1984
|
+
this.recordCount = recordCount;
|
|
1985
|
+
this.approxTokens = approxTokens2;
|
|
1986
|
+
this.budgetTokens = budgetTokens;
|
|
1987
|
+
this.excluded = excluded;
|
|
1988
|
+
}
|
|
1989
|
+
recordCount;
|
|
1990
|
+
approxTokens;
|
|
1991
|
+
budgetTokens;
|
|
1992
|
+
excluded;
|
|
1993
|
+
};
|
|
1831
1994
|
var KbInvalidConceptIdError = class extends BaseError {
|
|
1832
1995
|
constructor(message, details) {
|
|
1833
1996
|
super({
|
|
@@ -1928,6 +2091,90 @@ async function loadQmd(logger) {
|
|
|
1928
2091
|
}
|
|
1929
2092
|
}
|
|
1930
2093
|
|
|
2094
|
+
// src/pack.ts
|
|
2095
|
+
var DEFAULT_PACK_HOPS = 2;
|
|
2096
|
+
var DEFAULT_PACK_MAX_NODES = 20;
|
|
2097
|
+
var TYPE_PRIORITY = [
|
|
2098
|
+
"decision",
|
|
2099
|
+
"constraint",
|
|
2100
|
+
"requirement",
|
|
2101
|
+
...KB_RECORD_TYPES.filter(
|
|
2102
|
+
(type) => !["decision", "constraint", "requirement"].includes(type)
|
|
2103
|
+
)
|
|
2104
|
+
];
|
|
2105
|
+
function pack(bundle, rootId, options = {}) {
|
|
2106
|
+
const hops = options.hops ?? DEFAULT_PACK_HOPS;
|
|
2107
|
+
const maxNodes = options.maxNodes ?? DEFAULT_PACK_MAX_NODES;
|
|
2108
|
+
const budgetTokens = options.budgetTokens ?? DEFAULT_LOAD_BUDGET;
|
|
2109
|
+
const byId = new Map(bundle.map((record) => [record.conceptId, record]));
|
|
2110
|
+
const root = byId.get(rootId);
|
|
2111
|
+
if (!root) throw new KbRecordNotFoundError(rootId);
|
|
2112
|
+
const reached = [{ record: root, depth: 0 }];
|
|
2113
|
+
const seen = /* @__PURE__ */ new Set([rootId]);
|
|
2114
|
+
let frontier = [root];
|
|
2115
|
+
for (let depth = 1; frontier.length; depth += 1) {
|
|
2116
|
+
const next = [];
|
|
2117
|
+
for (const from of frontier) {
|
|
2118
|
+
for (const { record } of neighbours(from, bundle)) {
|
|
2119
|
+
if (seen.has(record.conceptId)) continue;
|
|
2120
|
+
seen.add(record.conceptId);
|
|
2121
|
+
reached.push({ record, depth });
|
|
2122
|
+
next.push(record);
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
frontier = next;
|
|
2126
|
+
}
|
|
2127
|
+
reached.sort(byRank);
|
|
2128
|
+
const within = reached.filter((entry) => entry.depth <= hops);
|
|
2129
|
+
const kept = within.slice(0, maxNodes);
|
|
2130
|
+
const excluded = [
|
|
2131
|
+
...within.slice(maxNodes),
|
|
2132
|
+
...reached.filter((entry) => entry.depth > hops)
|
|
2133
|
+
].map((entry) => entry.record.conceptId).sort();
|
|
2134
|
+
const adjudicated = adjudicate(
|
|
2135
|
+
kept.map((entry) => entry.record),
|
|
2136
|
+
bundle
|
|
2137
|
+
);
|
|
2138
|
+
const superseded = adjudicated.filter((hit) => hit.standing === "superseded").map(stub);
|
|
2139
|
+
const whole = adjudicated.filter((hit) => hit.standing !== "superseded");
|
|
2140
|
+
const tokensLoaded = whole.reduce((total, hit) => total + estimateTokens(hit.record), 0) + superseded.reduce((total, entry) => total + estimateStubTokens(entry), 0);
|
|
2141
|
+
const recordCount = adjudicated.length;
|
|
2142
|
+
if (tokensLoaded > budgetTokens) {
|
|
2143
|
+
throw new KbPackBudgetExceededError(
|
|
2144
|
+
recordCount,
|
|
2145
|
+
tokensLoaded,
|
|
2146
|
+
budgetTokens,
|
|
2147
|
+
excluded
|
|
2148
|
+
);
|
|
2149
|
+
}
|
|
2150
|
+
return {
|
|
2151
|
+
root: rootId,
|
|
2152
|
+
records: whole.map((hit) => ({
|
|
2153
|
+
conceptId: hit.record.conceptId,
|
|
2154
|
+
title: hit.record.frontmatter.title ?? null,
|
|
2155
|
+
standing: hit.standing,
|
|
2156
|
+
supersededBy: hit.heads.map((head) => head.conceptId),
|
|
2157
|
+
warnings: hit.warnings,
|
|
2158
|
+
anchors: hit.record.frontmatter.strauss_anchors ?? [],
|
|
2159
|
+
body: hit.record.body
|
|
2160
|
+
})),
|
|
2161
|
+
superseded,
|
|
2162
|
+
excluded,
|
|
2163
|
+
recordCount,
|
|
2164
|
+
tokensLoaded,
|
|
2165
|
+
budgetTokens
|
|
2166
|
+
};
|
|
2167
|
+
}
|
|
2168
|
+
function byRank(left, right) {
|
|
2169
|
+
return left.depth - right.depth || typeRank(left.record) - typeRank(right.record) || (left.record.frontmatter.title ?? "").localeCompare(
|
|
2170
|
+
right.record.frontmatter.title ?? ""
|
|
2171
|
+
) || left.record.conceptId.localeCompare(right.record.conceptId);
|
|
2172
|
+
}
|
|
2173
|
+
function typeRank(record) {
|
|
2174
|
+
const index = TYPE_PRIORITY.indexOf(record.frontmatter.type);
|
|
2175
|
+
return index === -1 ? TYPE_PRIORITY.length : index;
|
|
2176
|
+
}
|
|
2177
|
+
|
|
1931
2178
|
// src/kb-store.ts
|
|
1932
2179
|
var KB_DIR = (0, import_node_path6.join)(".strauss", "kb");
|
|
1933
2180
|
var STORE_OWNED = /* @__PURE__ */ new Set([INDEX_FILE, LOG_FILE, SEARCH_INDEX_FILE]);
|
|
@@ -2217,6 +2464,10 @@ ${answer}
|
|
|
2217
2464
|
async trace(bundlePath2, seedId, options = {}) {
|
|
2218
2465
|
return trace(seedId, await this.list(bundlePath2), options);
|
|
2219
2466
|
}
|
|
2467
|
+
/** A bounded neighbourhood around one record. See `pack.ts`. */
|
|
2468
|
+
async pack(bundlePath2, rootId, options = {}) {
|
|
2469
|
+
return pack(await this.list(bundlePath2), rootId, options);
|
|
2470
|
+
}
|
|
2220
2471
|
/**
|
|
2221
2472
|
* The stored index, rebuilt if it disagrees with the records.
|
|
2222
2473
|
*
|
|
@@ -2423,9 +2674,12 @@ function digest(contents) {
|
|
|
2423
2674
|
return (0, import_node_crypto.createHash)("sha256").update(contents).digest("hex");
|
|
2424
2675
|
}
|
|
2425
2676
|
|
|
2677
|
+
// src/version.ts
|
|
2678
|
+
var VERSION = true ? "0.1.7" : "0.0.0-dev";
|
|
2679
|
+
|
|
2426
2680
|
// src/mcp.ts
|
|
2427
2681
|
function createKbMcpServer() {
|
|
2428
|
-
const server = new import_mcp.McpServer({ name: "strauss-kb", version:
|
|
2682
|
+
const server = new import_mcp.McpServer({ name: "strauss-kb", version: VERSION });
|
|
2429
2683
|
const store = new KbStore({
|
|
2430
2684
|
warn: (entry) => process.stderr.write(`${JSON.stringify(entry)}
|
|
2431
2685
|
`)
|