@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/dist/index.d.cts CHANGED
@@ -225,12 +225,11 @@ declare function resolveHeads(from: KbRecord, byId: Map<string, KbRecord>): {
225
225
  };
226
226
 
227
227
  /**
228
- * Edges a trace may follow.
229
- *
230
- * Two more are conceivable and absent: `strauss_answered` carries no target id,
231
- * so a question's resolution lives in its own body rather than in another
232
- * record; and following OKF's body markdown links would need a markdown AST
233
- * pass this package does not yet do.
228
+ * Edges a trace may follow — the shared kb-edges.ts definitions, minus
229
+ * `body-link`: body links can reach most of a bundle from anywhere, which
230
+ * suits a bounded pack but floods a timeline. Also absent by design:
231
+ * `strauss_answered` carries no target id, so a question's resolution lives
232
+ * in its own body rather than in another record.
234
233
  */
235
234
  declare const TRACE_EDGES: readonly ["supersession", "anchor", "source"];
236
235
  type KbTraceEdge = (typeof TRACE_EDGES)[number];
@@ -260,6 +259,58 @@ type KbTraceOptions = {
260
259
  */
261
260
  declare function trace(seedId: string, bundle: KbRecord[], options?: KbTraceOptions): KbTraceStep[];
262
261
 
262
+ declare const DEFAULT_PACK_HOPS = 2;
263
+ declare const DEFAULT_PACK_MAX_NODES = 20;
264
+ type KbPackOptions = {
265
+ /** How far from the root the walk may reach. */
266
+ hops?: number;
267
+ /** How many records the pack may hold, root included. */
268
+ maxNodes?: number;
269
+ /** Approximate token ceiling over what is actually emitted. */
270
+ budgetTokens?: number;
271
+ };
272
+ /** One record as the pack emits it — the same mapping `kb_load` hands back. */
273
+ type KbPackedRecord = {
274
+ conceptId: string;
275
+ title: string | null;
276
+ standing: KbStanding;
277
+ supersededBy: string[];
278
+ warnings: KbWarning[];
279
+ anchors: KbAnchor[];
280
+ body: string;
281
+ };
282
+ /**
283
+ * Deliberately timestamp-free: the same bundle and root must produce the same
284
+ * bytes on every run, so a caller can diff two packs and trust that a changed
285
+ * byte means changed knowledge.
286
+ */
287
+ type KbPackResult = {
288
+ root: string;
289
+ records: KbPackedRecord[];
290
+ /** Named only, exactly as `load` stubs them. Bodies reachable via trace. */
291
+ superseded: KbSupersededStub[];
292
+ /** Every reachable record the hop and node limits cut, never summarized. */
293
+ excluded: string[];
294
+ recordCount: number;
295
+ tokensLoaded: number;
296
+ budgetTokens: number;
297
+ };
298
+ /**
299
+ * A bounded, progressively disclosed neighbourhood around one record.
300
+ *
301
+ * Where `load` hands over a whole base and `trace` a timeline, a pack is the
302
+ * subgraph a reader needs to act near one record: everything within `hops`
303
+ * of the root, cut to `maxNodes` by ranking, with every cut id listed —
304
+ * a named gap is knowable, a silent one is not.
305
+ *
306
+ * Adjudication runs against the whole bundle, not the reached slice, so a
307
+ * record's standing cannot depend on whether its replacement happened to be
308
+ * within reach. Superseded records become the same stubs `load` emits, costed
309
+ * as stubs, and the budget is a refusal rather than a truncation: a partial
310
+ * pack looks complete to its reader.
311
+ */
312
+ declare function pack(bundle: KbRecord[], rootId: string, options?: KbPackOptions): KbPackResult;
313
+
263
314
  declare const LOG_FILE = "log.jsonl";
264
315
  declare const kbLogEntrySchema: z.ZodObject<{
265
316
  at: z.ZodString;
@@ -310,6 +361,8 @@ type KbLogger = {
310
361
  info?(entry: Record<string, unknown>): void;
311
362
  warn?(entry: Record<string, unknown>): void;
312
363
  };
364
+ /** Roughly an eighth of a large context window — generous, and overridable. */
365
+ declare const DEFAULT_LOAD_BUDGET = 25000;
313
366
  /**
314
367
  * A superseded record, named but not spelled out.
315
368
  *
@@ -467,6 +520,8 @@ declare class KbStore {
467
520
  }): Promise<KbLoadResult>;
468
521
  /** How a position was arrived at, as a timeline. See `trace.ts`. */
469
522
  trace(bundlePath: string, seedId: string, options?: KbTraceOptions): Promise<KbTraceStep[]>;
523
+ /** A bounded neighbourhood around one record. See `pack.ts`. */
524
+ pack(bundlePath: string, rootId: string, options?: KbPackOptions): Promise<KbPackResult>;
470
525
  /**
471
526
  * The stored index, rebuilt if it disagrees with the records.
472
527
  *
@@ -545,6 +600,7 @@ declare enum Fault {
545
600
  declare enum ErrorTypes {
546
601
  KbRecordAlreadyExists = "KbRecordAlreadyExists",
547
602
  KbInvalidConceptId = "KbInvalidConceptId",
603
+ KbPackBudgetExceeded = "KbPackBudgetExceeded",
548
604
  KbRecordNotFound = "KbRecordNotFound",
549
605
  KbSelfVerification = "KbSelfVerification",
550
606
  KbWriteConflict = "KbWriteConflict"
@@ -596,6 +652,20 @@ declare class KbSelfVerificationError extends BaseError {
596
652
  readonly generatedBy: string;
597
653
  constructor(conceptId: string, actor: string, generatedBy: string);
598
654
  }
655
+ /**
656
+ * A pack that will not fit its token budget. Refusal, not truncation: a
657
+ * partial pack is indistinguishable from a complete one, so the caller gets
658
+ * the full picture — how many records, how many tokens, and every id the
659
+ * walk's own limits already cut — and decides whether to raise the budget or
660
+ * tighten the walk.
661
+ */
662
+ declare class KbPackBudgetExceededError extends BaseError {
663
+ readonly recordCount: number;
664
+ readonly approxTokens: number;
665
+ readonly budgetTokens: number;
666
+ readonly excluded: string[];
667
+ constructor(recordCount: number, approxTokens: number, budgetTokens: number, excluded: string[]);
668
+ }
599
669
  declare class KbInvalidConceptIdError extends BaseError {
600
670
  constructor(message: string, details: Record<string, string>);
601
671
  }
@@ -1018,6 +1088,33 @@ type MatchOptions = {
1018
1088
  };
1019
1089
  declare function matchToDiff(files: DiffFile[], records: KbRecord[], options?: MatchOptions): DiffMatch[];
1020
1090
 
1091
+ /**
1092
+ * The edges between records in one bundle, defined once.
1093
+ *
1094
+ * Both walks — `trace` and `pack` — consume this module, so they cannot drift
1095
+ * into disagreeing about what makes two records neighbours, and a diagnostic
1096
+ * pass over the graph can reuse the same definition.
1097
+ *
1098
+ * There is no separate `related` kind: compose.ts renders `relatedConceptIds`
1099
+ * as body links (`Relates to [id](id.md).`), so in stored form a related edge
1100
+ * IS a body link, and a distinct kind would count the same markdown twice.
1101
+ */
1102
+ declare const KB_EDGE_KINDS: readonly ["body-link", "supersession", "anchor", "source"];
1103
+ type KbEdgeKind = (typeof KB_EDGE_KINDS)[number];
1104
+ type KbNeighbour = {
1105
+ record: KbRecord;
1106
+ /** Every edge kind that connects it to the record asked about. */
1107
+ via: KbEdgeKind[];
1108
+ };
1109
+ /**
1110
+ * Every record `from` touches, each carrying the full set of edge kinds that
1111
+ * connect the pair. Order is deterministic: bundle order per kind, kinds in
1112
+ * the order given.
1113
+ */
1114
+ declare function neighbours(from: KbRecord, bundle: KbRecord[], kinds?: readonly KbEdgeKind[]): KbNeighbour[];
1115
+ /** The records one edge kind connects `from` to, in bundle order. */
1116
+ declare function edgeNeighbours(from: KbRecord, bundle: KbRecord[], kind: KbEdgeKind): KbRecord[];
1117
+
1021
1118
  type KbValidationProblem = {
1022
1119
  check: string;
1023
1120
  conceptId: string;
@@ -1033,6 +1130,68 @@ type KbValidationProblem = {
1033
1130
  */
1034
1131
  declare function validateBundle(records: KbRecord[]): KbValidationProblem[];
1035
1132
 
1133
+ /**
1134
+ * A health sweep over a whole base — read-only, and never a mutation.
1135
+ *
1136
+ * Every other read answers a question a caller already had. This one asks the
1137
+ * questions nobody thinks to: which records the calendar has already retired,
1138
+ * which nobody ever confirmed, which have been open long enough that "open" is
1139
+ * now the answer, and which the graph has quietly dropped on the floor. Those
1140
+ * decay silently, because a stale record reads exactly like a live one and a
1141
+ * question nobody answered reads exactly like one nobody asked.
1142
+ *
1143
+ * Grouped and counted rather than merged into one list: the seven checks are
1144
+ * seven different repairs — re-verify, re-date, answer, link, or supersede —
1145
+ * and a flat list of "problems" would leave the reader sorting them again.
1146
+ *
1147
+ * Every group is emitted even when empty. A check that found nothing and a
1148
+ * check that never ran look identical in a report that only lists findings,
1149
+ * and the difference is the whole value of a sweep.
1150
+ */
1151
+ declare const DEFAULT_EXPIRING_DAYS = 30;
1152
+ declare const DEFAULT_UNVERIFIED_DAYS = 90;
1153
+ declare const DEFAULT_AGING_DAYS = 90;
1154
+ declare const KB_DOCTOR_CHECKS: readonly ["expired", "expiring", "unverified", "aging", "orphaned", "broken-supersession", "superseded-but-cited"];
1155
+ type KbDoctorCheck = (typeof KB_DOCTOR_CHECKS)[number];
1156
+ type KbDoctorFinding = {
1157
+ conceptId: string;
1158
+ title: string | null;
1159
+ status: KbRecordStatus;
1160
+ /** Why this record is in this group, in one phrase a reader can act on. */
1161
+ note: string;
1162
+ };
1163
+ type KbDoctorGroup = {
1164
+ check: KbDoctorCheck;
1165
+ /** What the check looks for, so a zero count still says something. */
1166
+ headline: string;
1167
+ count: number;
1168
+ findings: KbDoctorFinding[];
1169
+ };
1170
+ type KbDoctorThresholds = {
1171
+ expiringDays: number;
1172
+ unverifiedDays: number;
1173
+ agingDays: number;
1174
+ };
1175
+ type KbDoctorReport = {
1176
+ recordCount: number;
1177
+ thresholds: KbDoctorThresholds;
1178
+ counts: Record<KbDoctorCheck, number>;
1179
+ /** All seven, in `KB_DOCTOR_CHECKS` order, empty ones included. */
1180
+ groups: KbDoctorGroup[];
1181
+ findingCount: number;
1182
+ healthy: boolean;
1183
+ };
1184
+ type KbDoctorOptions = {
1185
+ /** How far ahead `expiring` looks. */
1186
+ expiringDays?: number;
1187
+ /** How old an unconfirmed record must be before `unverified` reports it. */
1188
+ unverifiedDays?: number;
1189
+ /** How long `open` or `proposed` may stand before `aging` reports it. */
1190
+ agingDays?: number;
1191
+ now?: Date;
1192
+ };
1193
+ declare function doctor(bundle: KbRecord[], options?: KbDoctorOptions): KbDoctorReport;
1194
+
1036
1195
  /**
1037
1196
  * The record written while a change is being made: why it is shaped the way it
1038
1197
  * is, anchored to the symbols it touches.
@@ -1147,12 +1306,26 @@ type KbCommand<Shape extends z.ZodRawShape = z.ZodRawShape> = {
1147
1306
  /** Positional argv → the same object MCP receives. */
1148
1307
  fromArgv(argv: string[], bundlePath: string, stdin: () => Promise<string>): Promise<unknown> | unknown;
1149
1308
  run(ctx: KbCommandContext, input: z.infer<z.ZodObject<Shape>>): Promise<unknown>;
1309
+ /**
1310
+ * A human-readable form of the result, for the CLI. Where it exists the CLI
1311
+ * prints it and `--json` asks for the machine shape instead; MCP always gets
1312
+ * the machine shape, since a tool result is parsed rather than read.
1313
+ *
1314
+ * Separate from `run` rather than rendered inside it — as `pack` does, whose
1315
+ * result *is* a document — because a command whose result is a report needs
1316
+ * both forms: the table for a person, and the object for `failsWhen` and for
1317
+ * anything downstream.
1318
+ */
1319
+ render?(result: unknown): string;
1150
1320
  /**
1151
1321
  * Turns a result into a non-zero exit for the CLI. A check that reports a
1152
1322
  * problem has succeeded as a command and failed as a check, and a shell
1153
1323
  * caller can only see the difference through the exit code.
1324
+ *
1325
+ * The input comes too, so a command can make the exit conditional on a flag
1326
+ * the caller passed rather than on the result alone.
1154
1327
  */
1155
- failsWhen?(result: unknown): boolean;
1328
+ failsWhen?(result: unknown, input: z.infer<z.ZodObject<Shape>>): boolean;
1156
1329
  };
1157
1330
 
1158
1331
  /**
@@ -1297,4 +1470,4 @@ declare function parseMarkdownWithFrontmatter<S extends z.ZodType>(text: string,
1297
1470
  frontmatter: ReturnType<S["safeParse"]>;
1298
1471
  };
1299
1472
 
1300
- export { BaseError, CONTEXT_BEGIN, CONTEXT_END, CONTEXT_PROFILES, type ComposeInput, type ComposedRecord, DECISION_TYPE, type DecisionInput, type DiffFile, type DiffHunk, type DiffMatch, type ErrorDetails, type ErrorProps, ErrorTypes, Fault, INDEX_FILE, KB_COMMANDS, KB_COMMANDS_BY_NAME, KB_CONCEPT_ID_PATTERN, KB_CONFIDENCES, KB_DIR, KB_MATERIALITIES, KB_RECORD_STATUSES, KB_RECORD_TYPES, KB_SLUG_PATTERN, type KbActorStamp, type KbAdjudicated, type KbAnchor, KbBaseFrozenError, type KbCommand, type KbCommandContext, type KbContextBudgets, type KbContextOptions, type KbContextResult, KbInvalidConceptIdError, type KbLoadResult, type KbLogEntry, type KbLogReadResult, type KbLogger, type KbMergedPin, type KbMergedPins, type KbPin, type KbPinLayer, type KbPinOptions, type KbPinResult, type KbPinStatus, KbPinsMalformedError, type KbPinsManifest, type KbRecord, KbRecordAlreadyExistsError, type KbRecordFrontmatter, KbRecordNotFoundError, type KbRecordStatus, type KbRecordType, type KbRecordTypeSpec, type KbSearchLogger, KbSelfVerificationError, type KbSource, type KbStanding, KbStore, type KbSupersededStub, type KbSyncResult, type KbTraceEdge, type KbTraceOptions, type KbTraceStep, type KbValidationProblem, type KbVerifiedEvent, type KbWarning, KbWriteConflictError, type KbWriteInput, LOG_FILE, type MatchOptions, NO_DECISION_SLUG, PINS_FILE, PINS_LOCAL_FILE, PIN_LAYERS, type QmdModule, RECORD_TYPES, SEARCH_INDEX_FILE, type SearchHit, type SearchOptions, type SymbolRange, TRACE_EDGES, adjudicate, assertBaseNotFrozen, buildContext, composeDecisionRecord, composeInputSchema, composeNoDecisionRecord, composeRecord, contextProfileBudgets, createKbMcpServer, decisionInputSchema, indexIsStale, isKbRecordType, isNoDecisionRecord, kbActorStampSchema, kbAnchorSchema, kbConceptIdSchema, kbJsonSchemas, kbLogEntrySchema, kbRecordFrontmatterSchema, kbSourceSchema, kbVerifiedEventSchema, listPins, loadQmd, matchToDiff, mergedContextBudgets, parseLog, parseMarkdownWithFrontmatter, pinBase, readMergedPins, readPinsLayer, renderIndex, renderIndexLine, renderLogEntry, resolveHeads, resolveHits, resolvePinPath, runKbCli, runKbMcpServer, searchBase, selectDecisions, splitMarkdownFrontmatter, stringifyMarkdownWithFrontmatter, syncInstructions, toHookJson, trace, unpinBase, validateBundle };
1473
+ export { BaseError, CONTEXT_BEGIN, CONTEXT_END, CONTEXT_PROFILES, type ComposeInput, type ComposedRecord, DECISION_TYPE, DEFAULT_AGING_DAYS, DEFAULT_EXPIRING_DAYS, DEFAULT_LOAD_BUDGET, DEFAULT_PACK_HOPS, DEFAULT_PACK_MAX_NODES, DEFAULT_UNVERIFIED_DAYS, type DecisionInput, type DiffFile, type DiffHunk, type DiffMatch, type ErrorDetails, type ErrorProps, ErrorTypes, Fault, INDEX_FILE, KB_COMMANDS, KB_COMMANDS_BY_NAME, KB_CONCEPT_ID_PATTERN, KB_CONFIDENCES, KB_DIR, KB_DOCTOR_CHECKS, KB_EDGE_KINDS, KB_MATERIALITIES, KB_RECORD_STATUSES, KB_RECORD_TYPES, KB_SLUG_PATTERN, type KbActorStamp, type KbAdjudicated, type KbAnchor, KbBaseFrozenError, type KbCommand, type KbCommandContext, type KbContextBudgets, type KbContextOptions, type KbContextResult, type KbDoctorCheck, type KbDoctorFinding, type KbDoctorGroup, type KbDoctorOptions, type KbDoctorReport, type KbDoctorThresholds, type KbEdgeKind, KbInvalidConceptIdError, type KbLoadResult, type KbLogEntry, type KbLogReadResult, type KbLogger, type KbMergedPin, type KbMergedPins, type KbNeighbour, KbPackBudgetExceededError, type KbPackOptions, type KbPackResult, type KbPackedRecord, type KbPin, type KbPinLayer, type KbPinOptions, type KbPinResult, type KbPinStatus, KbPinsMalformedError, type KbPinsManifest, type KbRecord, KbRecordAlreadyExistsError, type KbRecordFrontmatter, KbRecordNotFoundError, type KbRecordStatus, type KbRecordType, type KbRecordTypeSpec, type KbSearchLogger, KbSelfVerificationError, type KbSource, type KbStanding, KbStore, type KbSupersededStub, type KbSyncResult, type KbTraceEdge, type KbTraceOptions, type KbTraceStep, type KbValidationProblem, type KbVerifiedEvent, type KbWarning, KbWriteConflictError, type KbWriteInput, LOG_FILE, type MatchOptions, NO_DECISION_SLUG, PINS_FILE, PINS_LOCAL_FILE, PIN_LAYERS, type QmdModule, RECORD_TYPES, SEARCH_INDEX_FILE, type SearchHit, type SearchOptions, type SymbolRange, TRACE_EDGES, adjudicate, assertBaseNotFrozen, buildContext, composeDecisionRecord, composeInputSchema, composeNoDecisionRecord, composeRecord, contextProfileBudgets, createKbMcpServer, decisionInputSchema, doctor, edgeNeighbours, indexIsStale, isKbRecordType, isNoDecisionRecord, kbActorStampSchema, kbAnchorSchema, kbConceptIdSchema, kbJsonSchemas, kbLogEntrySchema, kbRecordFrontmatterSchema, kbSourceSchema, kbVerifiedEventSchema, listPins, loadQmd, matchToDiff, mergedContextBudgets, neighbours, pack, parseLog, parseMarkdownWithFrontmatter, pinBase, readMergedPins, readPinsLayer, renderIndex, renderIndexLine, renderLogEntry, resolveHeads, resolveHits, resolvePinPath, runKbCli, runKbMcpServer, searchBase, selectDecisions, splitMarkdownFrontmatter, stringifyMarkdownWithFrontmatter, syncInstructions, toHookJson, trace, unpinBase, validateBundle };
package/dist/index.d.ts CHANGED
@@ -225,12 +225,11 @@ declare function resolveHeads(from: KbRecord, byId: Map<string, KbRecord>): {
225
225
  };
226
226
 
227
227
  /**
228
- * Edges a trace may follow.
229
- *
230
- * Two more are conceivable and absent: `strauss_answered` carries no target id,
231
- * so a question's resolution lives in its own body rather than in another
232
- * record; and following OKF's body markdown links would need a markdown AST
233
- * pass this package does not yet do.
228
+ * Edges a trace may follow — the shared kb-edges.ts definitions, minus
229
+ * `body-link`: body links can reach most of a bundle from anywhere, which
230
+ * suits a bounded pack but floods a timeline. Also absent by design:
231
+ * `strauss_answered` carries no target id, so a question's resolution lives
232
+ * in its own body rather than in another record.
234
233
  */
235
234
  declare const TRACE_EDGES: readonly ["supersession", "anchor", "source"];
236
235
  type KbTraceEdge = (typeof TRACE_EDGES)[number];
@@ -260,6 +259,58 @@ type KbTraceOptions = {
260
259
  */
261
260
  declare function trace(seedId: string, bundle: KbRecord[], options?: KbTraceOptions): KbTraceStep[];
262
261
 
262
+ declare const DEFAULT_PACK_HOPS = 2;
263
+ declare const DEFAULT_PACK_MAX_NODES = 20;
264
+ type KbPackOptions = {
265
+ /** How far from the root the walk may reach. */
266
+ hops?: number;
267
+ /** How many records the pack may hold, root included. */
268
+ maxNodes?: number;
269
+ /** Approximate token ceiling over what is actually emitted. */
270
+ budgetTokens?: number;
271
+ };
272
+ /** One record as the pack emits it — the same mapping `kb_load` hands back. */
273
+ type KbPackedRecord = {
274
+ conceptId: string;
275
+ title: string | null;
276
+ standing: KbStanding;
277
+ supersededBy: string[];
278
+ warnings: KbWarning[];
279
+ anchors: KbAnchor[];
280
+ body: string;
281
+ };
282
+ /**
283
+ * Deliberately timestamp-free: the same bundle and root must produce the same
284
+ * bytes on every run, so a caller can diff two packs and trust that a changed
285
+ * byte means changed knowledge.
286
+ */
287
+ type KbPackResult = {
288
+ root: string;
289
+ records: KbPackedRecord[];
290
+ /** Named only, exactly as `load` stubs them. Bodies reachable via trace. */
291
+ superseded: KbSupersededStub[];
292
+ /** Every reachable record the hop and node limits cut, never summarized. */
293
+ excluded: string[];
294
+ recordCount: number;
295
+ tokensLoaded: number;
296
+ budgetTokens: number;
297
+ };
298
+ /**
299
+ * A bounded, progressively disclosed neighbourhood around one record.
300
+ *
301
+ * Where `load` hands over a whole base and `trace` a timeline, a pack is the
302
+ * subgraph a reader needs to act near one record: everything within `hops`
303
+ * of the root, cut to `maxNodes` by ranking, with every cut id listed —
304
+ * a named gap is knowable, a silent one is not.
305
+ *
306
+ * Adjudication runs against the whole bundle, not the reached slice, so a
307
+ * record's standing cannot depend on whether its replacement happened to be
308
+ * within reach. Superseded records become the same stubs `load` emits, costed
309
+ * as stubs, and the budget is a refusal rather than a truncation: a partial
310
+ * pack looks complete to its reader.
311
+ */
312
+ declare function pack(bundle: KbRecord[], rootId: string, options?: KbPackOptions): KbPackResult;
313
+
263
314
  declare const LOG_FILE = "log.jsonl";
264
315
  declare const kbLogEntrySchema: z.ZodObject<{
265
316
  at: z.ZodString;
@@ -310,6 +361,8 @@ type KbLogger = {
310
361
  info?(entry: Record<string, unknown>): void;
311
362
  warn?(entry: Record<string, unknown>): void;
312
363
  };
364
+ /** Roughly an eighth of a large context window — generous, and overridable. */
365
+ declare const DEFAULT_LOAD_BUDGET = 25000;
313
366
  /**
314
367
  * A superseded record, named but not spelled out.
315
368
  *
@@ -467,6 +520,8 @@ declare class KbStore {
467
520
  }): Promise<KbLoadResult>;
468
521
  /** How a position was arrived at, as a timeline. See `trace.ts`. */
469
522
  trace(bundlePath: string, seedId: string, options?: KbTraceOptions): Promise<KbTraceStep[]>;
523
+ /** A bounded neighbourhood around one record. See `pack.ts`. */
524
+ pack(bundlePath: string, rootId: string, options?: KbPackOptions): Promise<KbPackResult>;
470
525
  /**
471
526
  * The stored index, rebuilt if it disagrees with the records.
472
527
  *
@@ -545,6 +600,7 @@ declare enum Fault {
545
600
  declare enum ErrorTypes {
546
601
  KbRecordAlreadyExists = "KbRecordAlreadyExists",
547
602
  KbInvalidConceptId = "KbInvalidConceptId",
603
+ KbPackBudgetExceeded = "KbPackBudgetExceeded",
548
604
  KbRecordNotFound = "KbRecordNotFound",
549
605
  KbSelfVerification = "KbSelfVerification",
550
606
  KbWriteConflict = "KbWriteConflict"
@@ -596,6 +652,20 @@ declare class KbSelfVerificationError extends BaseError {
596
652
  readonly generatedBy: string;
597
653
  constructor(conceptId: string, actor: string, generatedBy: string);
598
654
  }
655
+ /**
656
+ * A pack that will not fit its token budget. Refusal, not truncation: a
657
+ * partial pack is indistinguishable from a complete one, so the caller gets
658
+ * the full picture — how many records, how many tokens, and every id the
659
+ * walk's own limits already cut — and decides whether to raise the budget or
660
+ * tighten the walk.
661
+ */
662
+ declare class KbPackBudgetExceededError extends BaseError {
663
+ readonly recordCount: number;
664
+ readonly approxTokens: number;
665
+ readonly budgetTokens: number;
666
+ readonly excluded: string[];
667
+ constructor(recordCount: number, approxTokens: number, budgetTokens: number, excluded: string[]);
668
+ }
599
669
  declare class KbInvalidConceptIdError extends BaseError {
600
670
  constructor(message: string, details: Record<string, string>);
601
671
  }
@@ -1018,6 +1088,33 @@ type MatchOptions = {
1018
1088
  };
1019
1089
  declare function matchToDiff(files: DiffFile[], records: KbRecord[], options?: MatchOptions): DiffMatch[];
1020
1090
 
1091
+ /**
1092
+ * The edges between records in one bundle, defined once.
1093
+ *
1094
+ * Both walks — `trace` and `pack` — consume this module, so they cannot drift
1095
+ * into disagreeing about what makes two records neighbours, and a diagnostic
1096
+ * pass over the graph can reuse the same definition.
1097
+ *
1098
+ * There is no separate `related` kind: compose.ts renders `relatedConceptIds`
1099
+ * as body links (`Relates to [id](id.md).`), so in stored form a related edge
1100
+ * IS a body link, and a distinct kind would count the same markdown twice.
1101
+ */
1102
+ declare const KB_EDGE_KINDS: readonly ["body-link", "supersession", "anchor", "source"];
1103
+ type KbEdgeKind = (typeof KB_EDGE_KINDS)[number];
1104
+ type KbNeighbour = {
1105
+ record: KbRecord;
1106
+ /** Every edge kind that connects it to the record asked about. */
1107
+ via: KbEdgeKind[];
1108
+ };
1109
+ /**
1110
+ * Every record `from` touches, each carrying the full set of edge kinds that
1111
+ * connect the pair. Order is deterministic: bundle order per kind, kinds in
1112
+ * the order given.
1113
+ */
1114
+ declare function neighbours(from: KbRecord, bundle: KbRecord[], kinds?: readonly KbEdgeKind[]): KbNeighbour[];
1115
+ /** The records one edge kind connects `from` to, in bundle order. */
1116
+ declare function edgeNeighbours(from: KbRecord, bundle: KbRecord[], kind: KbEdgeKind): KbRecord[];
1117
+
1021
1118
  type KbValidationProblem = {
1022
1119
  check: string;
1023
1120
  conceptId: string;
@@ -1033,6 +1130,68 @@ type KbValidationProblem = {
1033
1130
  */
1034
1131
  declare function validateBundle(records: KbRecord[]): KbValidationProblem[];
1035
1132
 
1133
+ /**
1134
+ * A health sweep over a whole base — read-only, and never a mutation.
1135
+ *
1136
+ * Every other read answers a question a caller already had. This one asks the
1137
+ * questions nobody thinks to: which records the calendar has already retired,
1138
+ * which nobody ever confirmed, which have been open long enough that "open" is
1139
+ * now the answer, and which the graph has quietly dropped on the floor. Those
1140
+ * decay silently, because a stale record reads exactly like a live one and a
1141
+ * question nobody answered reads exactly like one nobody asked.
1142
+ *
1143
+ * Grouped and counted rather than merged into one list: the seven checks are
1144
+ * seven different repairs — re-verify, re-date, answer, link, or supersede —
1145
+ * and a flat list of "problems" would leave the reader sorting them again.
1146
+ *
1147
+ * Every group is emitted even when empty. A check that found nothing and a
1148
+ * check that never ran look identical in a report that only lists findings,
1149
+ * and the difference is the whole value of a sweep.
1150
+ */
1151
+ declare const DEFAULT_EXPIRING_DAYS = 30;
1152
+ declare const DEFAULT_UNVERIFIED_DAYS = 90;
1153
+ declare const DEFAULT_AGING_DAYS = 90;
1154
+ declare const KB_DOCTOR_CHECKS: readonly ["expired", "expiring", "unverified", "aging", "orphaned", "broken-supersession", "superseded-but-cited"];
1155
+ type KbDoctorCheck = (typeof KB_DOCTOR_CHECKS)[number];
1156
+ type KbDoctorFinding = {
1157
+ conceptId: string;
1158
+ title: string | null;
1159
+ status: KbRecordStatus;
1160
+ /** Why this record is in this group, in one phrase a reader can act on. */
1161
+ note: string;
1162
+ };
1163
+ type KbDoctorGroup = {
1164
+ check: KbDoctorCheck;
1165
+ /** What the check looks for, so a zero count still says something. */
1166
+ headline: string;
1167
+ count: number;
1168
+ findings: KbDoctorFinding[];
1169
+ };
1170
+ type KbDoctorThresholds = {
1171
+ expiringDays: number;
1172
+ unverifiedDays: number;
1173
+ agingDays: number;
1174
+ };
1175
+ type KbDoctorReport = {
1176
+ recordCount: number;
1177
+ thresholds: KbDoctorThresholds;
1178
+ counts: Record<KbDoctorCheck, number>;
1179
+ /** All seven, in `KB_DOCTOR_CHECKS` order, empty ones included. */
1180
+ groups: KbDoctorGroup[];
1181
+ findingCount: number;
1182
+ healthy: boolean;
1183
+ };
1184
+ type KbDoctorOptions = {
1185
+ /** How far ahead `expiring` looks. */
1186
+ expiringDays?: number;
1187
+ /** How old an unconfirmed record must be before `unverified` reports it. */
1188
+ unverifiedDays?: number;
1189
+ /** How long `open` or `proposed` may stand before `aging` reports it. */
1190
+ agingDays?: number;
1191
+ now?: Date;
1192
+ };
1193
+ declare function doctor(bundle: KbRecord[], options?: KbDoctorOptions): KbDoctorReport;
1194
+
1036
1195
  /**
1037
1196
  * The record written while a change is being made: why it is shaped the way it
1038
1197
  * is, anchored to the symbols it touches.
@@ -1147,12 +1306,26 @@ type KbCommand<Shape extends z.ZodRawShape = z.ZodRawShape> = {
1147
1306
  /** Positional argv → the same object MCP receives. */
1148
1307
  fromArgv(argv: string[], bundlePath: string, stdin: () => Promise<string>): Promise<unknown> | unknown;
1149
1308
  run(ctx: KbCommandContext, input: z.infer<z.ZodObject<Shape>>): Promise<unknown>;
1309
+ /**
1310
+ * A human-readable form of the result, for the CLI. Where it exists the CLI
1311
+ * prints it and `--json` asks for the machine shape instead; MCP always gets
1312
+ * the machine shape, since a tool result is parsed rather than read.
1313
+ *
1314
+ * Separate from `run` rather than rendered inside it — as `pack` does, whose
1315
+ * result *is* a document — because a command whose result is a report needs
1316
+ * both forms: the table for a person, and the object for `failsWhen` and for
1317
+ * anything downstream.
1318
+ */
1319
+ render?(result: unknown): string;
1150
1320
  /**
1151
1321
  * Turns a result into a non-zero exit for the CLI. A check that reports a
1152
1322
  * problem has succeeded as a command and failed as a check, and a shell
1153
1323
  * caller can only see the difference through the exit code.
1324
+ *
1325
+ * The input comes too, so a command can make the exit conditional on a flag
1326
+ * the caller passed rather than on the result alone.
1154
1327
  */
1155
- failsWhen?(result: unknown): boolean;
1328
+ failsWhen?(result: unknown, input: z.infer<z.ZodObject<Shape>>): boolean;
1156
1329
  };
1157
1330
 
1158
1331
  /**
@@ -1297,4 +1470,4 @@ declare function parseMarkdownWithFrontmatter<S extends z.ZodType>(text: string,
1297
1470
  frontmatter: ReturnType<S["safeParse"]>;
1298
1471
  };
1299
1472
 
1300
- export { BaseError, CONTEXT_BEGIN, CONTEXT_END, CONTEXT_PROFILES, type ComposeInput, type ComposedRecord, DECISION_TYPE, type DecisionInput, type DiffFile, type DiffHunk, type DiffMatch, type ErrorDetails, type ErrorProps, ErrorTypes, Fault, INDEX_FILE, KB_COMMANDS, KB_COMMANDS_BY_NAME, KB_CONCEPT_ID_PATTERN, KB_CONFIDENCES, KB_DIR, KB_MATERIALITIES, KB_RECORD_STATUSES, KB_RECORD_TYPES, KB_SLUG_PATTERN, type KbActorStamp, type KbAdjudicated, type KbAnchor, KbBaseFrozenError, type KbCommand, type KbCommandContext, type KbContextBudgets, type KbContextOptions, type KbContextResult, KbInvalidConceptIdError, type KbLoadResult, type KbLogEntry, type KbLogReadResult, type KbLogger, type KbMergedPin, type KbMergedPins, type KbPin, type KbPinLayer, type KbPinOptions, type KbPinResult, type KbPinStatus, KbPinsMalformedError, type KbPinsManifest, type KbRecord, KbRecordAlreadyExistsError, type KbRecordFrontmatter, KbRecordNotFoundError, type KbRecordStatus, type KbRecordType, type KbRecordTypeSpec, type KbSearchLogger, KbSelfVerificationError, type KbSource, type KbStanding, KbStore, type KbSupersededStub, type KbSyncResult, type KbTraceEdge, type KbTraceOptions, type KbTraceStep, type KbValidationProblem, type KbVerifiedEvent, type KbWarning, KbWriteConflictError, type KbWriteInput, LOG_FILE, type MatchOptions, NO_DECISION_SLUG, PINS_FILE, PINS_LOCAL_FILE, PIN_LAYERS, type QmdModule, RECORD_TYPES, SEARCH_INDEX_FILE, type SearchHit, type SearchOptions, type SymbolRange, TRACE_EDGES, adjudicate, assertBaseNotFrozen, buildContext, composeDecisionRecord, composeInputSchema, composeNoDecisionRecord, composeRecord, contextProfileBudgets, createKbMcpServer, decisionInputSchema, indexIsStale, isKbRecordType, isNoDecisionRecord, kbActorStampSchema, kbAnchorSchema, kbConceptIdSchema, kbJsonSchemas, kbLogEntrySchema, kbRecordFrontmatterSchema, kbSourceSchema, kbVerifiedEventSchema, listPins, loadQmd, matchToDiff, mergedContextBudgets, parseLog, parseMarkdownWithFrontmatter, pinBase, readMergedPins, readPinsLayer, renderIndex, renderIndexLine, renderLogEntry, resolveHeads, resolveHits, resolvePinPath, runKbCli, runKbMcpServer, searchBase, selectDecisions, splitMarkdownFrontmatter, stringifyMarkdownWithFrontmatter, syncInstructions, toHookJson, trace, unpinBase, validateBundle };
1473
+ export { BaseError, CONTEXT_BEGIN, CONTEXT_END, CONTEXT_PROFILES, type ComposeInput, type ComposedRecord, DECISION_TYPE, DEFAULT_AGING_DAYS, DEFAULT_EXPIRING_DAYS, DEFAULT_LOAD_BUDGET, DEFAULT_PACK_HOPS, DEFAULT_PACK_MAX_NODES, DEFAULT_UNVERIFIED_DAYS, type DecisionInput, type DiffFile, type DiffHunk, type DiffMatch, type ErrorDetails, type ErrorProps, ErrorTypes, Fault, INDEX_FILE, KB_COMMANDS, KB_COMMANDS_BY_NAME, KB_CONCEPT_ID_PATTERN, KB_CONFIDENCES, KB_DIR, KB_DOCTOR_CHECKS, KB_EDGE_KINDS, KB_MATERIALITIES, KB_RECORD_STATUSES, KB_RECORD_TYPES, KB_SLUG_PATTERN, type KbActorStamp, type KbAdjudicated, type KbAnchor, KbBaseFrozenError, type KbCommand, type KbCommandContext, type KbContextBudgets, type KbContextOptions, type KbContextResult, type KbDoctorCheck, type KbDoctorFinding, type KbDoctorGroup, type KbDoctorOptions, type KbDoctorReport, type KbDoctorThresholds, type KbEdgeKind, KbInvalidConceptIdError, type KbLoadResult, type KbLogEntry, type KbLogReadResult, type KbLogger, type KbMergedPin, type KbMergedPins, type KbNeighbour, KbPackBudgetExceededError, type KbPackOptions, type KbPackResult, type KbPackedRecord, type KbPin, type KbPinLayer, type KbPinOptions, type KbPinResult, type KbPinStatus, KbPinsMalformedError, type KbPinsManifest, type KbRecord, KbRecordAlreadyExistsError, type KbRecordFrontmatter, KbRecordNotFoundError, type KbRecordStatus, type KbRecordType, type KbRecordTypeSpec, type KbSearchLogger, KbSelfVerificationError, type KbSource, type KbStanding, KbStore, type KbSupersededStub, type KbSyncResult, type KbTraceEdge, type KbTraceOptions, type KbTraceStep, type KbValidationProblem, type KbVerifiedEvent, type KbWarning, KbWriteConflictError, type KbWriteInput, LOG_FILE, type MatchOptions, NO_DECISION_SLUG, PINS_FILE, PINS_LOCAL_FILE, PIN_LAYERS, type QmdModule, RECORD_TYPES, SEARCH_INDEX_FILE, type SearchHit, type SearchOptions, type SymbolRange, TRACE_EDGES, adjudicate, assertBaseNotFrozen, buildContext, composeDecisionRecord, composeInputSchema, composeNoDecisionRecord, composeRecord, contextProfileBudgets, createKbMcpServer, decisionInputSchema, doctor, edgeNeighbours, indexIsStale, isKbRecordType, isNoDecisionRecord, kbActorStampSchema, kbAnchorSchema, kbConceptIdSchema, kbJsonSchemas, kbLogEntrySchema, kbRecordFrontmatterSchema, kbSourceSchema, kbVerifiedEventSchema, listPins, loadQmd, matchToDiff, mergedContextBudgets, neighbours, pack, parseLog, parseMarkdownWithFrontmatter, pinBase, readMergedPins, readPinsLayer, renderIndex, renderIndexLine, renderLogEntry, resolveHeads, resolveHits, resolvePinPath, runKbCli, runKbMcpServer, searchBase, selectDecisions, splitMarkdownFrontmatter, stringifyMarkdownWithFrontmatter, syncInstructions, toHookJson, trace, unpinBase, validateBundle };
package/dist/index.js CHANGED
@@ -1,16 +1,22 @@
1
1
  import {
2
2
  runKbCli
3
- } from "./chunk-V7TRZ2ER.js";
3
+ } from "./chunk-MGPYUZOM.js";
4
4
  import {
5
5
  createKbMcpServer,
6
6
  runKbMcpServer
7
- } from "./chunk-BVF7X5VO.js";
7
+ } from "./chunk-YYX6CX6V.js";
8
8
  import {
9
9
  BaseError,
10
10
  CONTEXT_BEGIN,
11
11
  CONTEXT_END,
12
12
  CONTEXT_PROFILES,
13
13
  DECISION_TYPE,
14
+ DEFAULT_AGING_DAYS,
15
+ DEFAULT_EXPIRING_DAYS,
16
+ DEFAULT_LOAD_BUDGET,
17
+ DEFAULT_PACK_HOPS,
18
+ DEFAULT_PACK_MAX_NODES,
19
+ DEFAULT_UNVERIFIED_DAYS,
14
20
  ErrorTypes,
15
21
  Fault,
16
22
  INDEX_FILE,
@@ -19,12 +25,15 @@ import {
19
25
  KB_CONCEPT_ID_PATTERN,
20
26
  KB_CONFIDENCES,
21
27
  KB_DIR,
28
+ KB_DOCTOR_CHECKS,
29
+ KB_EDGE_KINDS,
22
30
  KB_MATERIALITIES,
23
31
  KB_RECORD_STATUSES,
24
32
  KB_RECORD_TYPES,
25
33
  KB_SLUG_PATTERN,
26
34
  KbBaseFrozenError,
27
35
  KbInvalidConceptIdError,
36
+ KbPackBudgetExceededError,
28
37
  KbPinsMalformedError,
29
38
  KbRecordAlreadyExistsError,
30
39
  KbRecordNotFoundError,
@@ -48,6 +57,8 @@ import {
48
57
  composeRecord,
49
58
  contextProfileBudgets,
50
59
  decisionInputSchema,
60
+ doctor,
61
+ edgeNeighbours,
51
62
  indexIsStale,
52
63
  isKbRecordType,
53
64
  isNoDecisionRecord,
@@ -62,6 +73,8 @@ import {
62
73
  listPins,
63
74
  loadQmd,
64
75
  mergedContextBudgets,
76
+ neighbours,
77
+ pack,
65
78
  parseLog,
66
79
  parseMarkdownWithFrontmatter,
67
80
  pinBase,
@@ -82,7 +95,7 @@ import {
82
95
  trace,
83
96
  unpinBase,
84
97
  validateBundle
85
- } from "./chunk-PNSRTKYN.js";
98
+ } from "./chunk-YJK7KGHN.js";
86
99
 
87
100
  // src/match-diff.ts
88
101
  function matchToDiff(files, records, options = {}) {
@@ -169,6 +182,12 @@ export {
169
182
  CONTEXT_END,
170
183
  CONTEXT_PROFILES,
171
184
  DECISION_TYPE,
185
+ DEFAULT_AGING_DAYS,
186
+ DEFAULT_EXPIRING_DAYS,
187
+ DEFAULT_LOAD_BUDGET,
188
+ DEFAULT_PACK_HOPS,
189
+ DEFAULT_PACK_MAX_NODES,
190
+ DEFAULT_UNVERIFIED_DAYS,
172
191
  ErrorTypes,
173
192
  Fault,
174
193
  INDEX_FILE,
@@ -177,12 +196,15 @@ export {
177
196
  KB_CONCEPT_ID_PATTERN,
178
197
  KB_CONFIDENCES,
179
198
  KB_DIR,
199
+ KB_DOCTOR_CHECKS,
200
+ KB_EDGE_KINDS,
180
201
  KB_MATERIALITIES,
181
202
  KB_RECORD_STATUSES,
182
203
  KB_RECORD_TYPES,
183
204
  KB_SLUG_PATTERN,
184
205
  KbBaseFrozenError,
185
206
  KbInvalidConceptIdError,
207
+ KbPackBudgetExceededError,
186
208
  KbPinsMalformedError,
187
209
  KbRecordAlreadyExistsError,
188
210
  KbRecordNotFoundError,
@@ -207,6 +229,8 @@ export {
207
229
  contextProfileBudgets,
208
230
  createKbMcpServer,
209
231
  decisionInputSchema,
232
+ doctor,
233
+ edgeNeighbours,
210
234
  indexIsStale,
211
235
  isKbRecordType,
212
236
  isNoDecisionRecord,
@@ -222,6 +246,8 @@ export {
222
246
  loadQmd,
223
247
  matchToDiff,
224
248
  mergedContextBudgets,
249
+ neighbours,
250
+ pack,
225
251
  parseLog,
226
252
  parseMarkdownWithFrontmatter,
227
253
  pinBase,