@saasontools/strauss-kb 0.1.0

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.
@@ -0,0 +1,977 @@
1
+ import { z } from 'zod';
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+
4
+ /**
5
+ * Knowledge records, shaped as OKF v0.2 concepts.
6
+ *
7
+ * OKF (Google Cloud `knowledge-catalog`) requires exactly one key — `type` —
8
+ * and explicitly permits extension: "Producers MAY include any additional keys.
9
+ * Consumers SHOULD preserve unknown keys when round-tripping and MUST NOT
10
+ * reject documents with unrecognized fields."
11
+ *
12
+ * A record's identity is its `concept_id`: the file path within the bundle with
13
+ * `.md` removed. `<type>.<slug>.md` therefore yields `decision.some-slug`.
14
+ *
15
+ * Keys prefixed `strauss_` are this package's extensions rather than
16
+ * conformance, and are namespaced so a later OKF version defining the same
17
+ * names cannot collide: OKF names files through path-valued `resource` fields
18
+ * and has no notion of a span, so anchoring a concept to a range of code has no
19
+ * standard spelling, and standing has none either.
20
+ */
21
+ /** A source the record draws on. Footnotes in the body key to `id`. */
22
+ declare const kbSourceSchema: z.ZodObject<{
23
+ id: z.ZodString;
24
+ resource: z.ZodString;
25
+ title: z.ZodOptional<z.ZodString>;
26
+ author: z.ZodOptional<z.ZodString>;
27
+ last_modified: z.ZodOptional<z.ZodString>;
28
+ }, z.core.$loose>;
29
+ /** An actor/time pair — OKF's shape for both `generated` and `verified[]`. */
30
+ declare const kbActorStampSchema: z.ZodObject<{
31
+ by: z.ZodString;
32
+ at: z.ZodString;
33
+ }, z.core.$loose>;
34
+ /**
35
+ * Where a record attaches in the code.
36
+ *
37
+ * Symbolic on purpose. These are written while the code is still moving: a
38
+ * `line: 379` recorded at minute five is wrong by minute forty, but
39
+ * `OrderService.cancel` survives every edit that does not rename it. A later
40
+ * pass resolves symbols to line ranges once the change has settled, and records
41
+ * that resolution as a `verified[]` entry.
42
+ */
43
+ declare const kbAnchorSchema: z.ZodObject<{
44
+ file: z.ZodString;
45
+ symbol: z.ZodOptional<z.ZodString>;
46
+ }, z.core.$strict>;
47
+ declare const KB_RECORD_TYPES: readonly ["fact", "requirement", "constraint", "decision", "assumption", "open-question", "risk", "contract", "flow", "affected-system", "test-obligation", "source-note"];
48
+ type KbRecordType = (typeof KB_RECORD_TYPES)[number];
49
+ /** Both halves of `<type>.<slug>` are kebab-case, and neither may be empty. */
50
+ declare const KB_SLUG_PATTERN: RegExp;
51
+ declare const KB_CONCEPT_ID_PATTERN: RegExp;
52
+ /**
53
+ * Concept ids are rendered into markdown links unescaped, so an id carrying a
54
+ * `]` or `)` would emit a broken edge rather than fail. Validating at the entry
55
+ * point keeps the renderer from having to care.
56
+ */
57
+ declare const kbConceptIdSchema: z.ZodString;
58
+ /**
59
+ * Standing, not freshness.
60
+ *
61
+ * OKF's `verified[]` and `stale_after` answer "is this still true?"; nothing in
62
+ * the spec answers "is this settled, and does it still apply?". A base
63
+ * supersedes its own conclusions as work proceeds, so that second question
64
+ * needs an answer, and it is this package's to define — hence `strauss_`.
65
+ */
66
+ declare const KB_RECORD_STATUSES: readonly ["draft", "proposed", "accepted", "open", "resolved", "rejected", "superseded"];
67
+ type KbRecordStatus = (typeof KB_RECORD_STATUSES)[number];
68
+ declare const KB_MATERIALITIES: readonly ["blocking", "important", "non-blocking"];
69
+ declare const KB_CONFIDENCES: readonly ["low", "medium", "high"];
70
+ declare const kbRecordFrontmatterSchema: z.ZodObject<{
71
+ type: z.ZodString;
72
+ title: z.ZodOptional<z.ZodString>;
73
+ description: z.ZodOptional<z.ZodString>;
74
+ resource: z.ZodOptional<z.ZodString>;
75
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
76
+ sources: z.ZodOptional<z.ZodArray<z.ZodObject<{
77
+ id: z.ZodString;
78
+ resource: z.ZodString;
79
+ title: z.ZodOptional<z.ZodString>;
80
+ author: z.ZodOptional<z.ZodString>;
81
+ last_modified: z.ZodOptional<z.ZodString>;
82
+ }, z.core.$loose>>>;
83
+ generated: z.ZodOptional<z.ZodObject<{
84
+ by: z.ZodString;
85
+ at: z.ZodString;
86
+ }, z.core.$loose>>;
87
+ verified: z.ZodOptional<z.ZodArray<z.ZodObject<{
88
+ by: z.ZodString;
89
+ at: z.ZodString;
90
+ }, z.core.$loose>>>;
91
+ stale_after: z.ZodOptional<z.ZodString>;
92
+ strauss_anchors: z.ZodOptional<z.ZodArray<z.ZodObject<{
93
+ file: z.ZodString;
94
+ symbol: z.ZodOptional<z.ZodString>;
95
+ }, z.core.$strict>>>;
96
+ strauss_verify: z.ZodOptional<z.ZodArray<z.ZodString>>;
97
+ strauss_status: z.ZodDefault<z.ZodEnum<{
98
+ draft: "draft";
99
+ proposed: "proposed";
100
+ accepted: "accepted";
101
+ open: "open";
102
+ resolved: "resolved";
103
+ rejected: "rejected";
104
+ superseded: "superseded";
105
+ }>>;
106
+ strauss_supersedes: z.ZodOptional<z.ZodArray<z.ZodString>>;
107
+ strauss_superseded_by: z.ZodOptional<z.ZodString>;
108
+ strauss_answered: z.ZodOptional<z.ZodObject<{
109
+ by: z.ZodString;
110
+ at: z.ZodString;
111
+ }, z.core.$loose>>;
112
+ strauss_materiality: z.ZodOptional<z.ZodEnum<{
113
+ blocking: "blocking";
114
+ important: "important";
115
+ "non-blocking": "non-blocking";
116
+ }>>;
117
+ strauss_confidence: z.ZodOptional<z.ZodEnum<{
118
+ low: "low";
119
+ medium: "medium";
120
+ high: "high";
121
+ }>>;
122
+ strauss_owner: z.ZodOptional<z.ZodString>;
123
+ strauss_assumption: z.ZodOptional<z.ZodBoolean>;
124
+ }, z.core.$loose>;
125
+ type KbSource = z.infer<typeof kbSourceSchema>;
126
+ type KbActorStamp = z.infer<typeof kbActorStampSchema>;
127
+ type KbAnchor = z.infer<typeof kbAnchorSchema>;
128
+ type KbRecordFrontmatter = z.infer<typeof kbRecordFrontmatterSchema>;
129
+ type KbRecord = {
130
+ /** Path minus `.md`, relative to the bundle root. OKF's concept identity. */
131
+ conceptId: string;
132
+ frontmatter: KbRecordFrontmatter;
133
+ body: string;
134
+ };
135
+
136
+ /**
137
+ * Why a matched record must not be read as a plain answer.
138
+ *
139
+ * Relevance and standing are different questions, and ranking answers only the
140
+ * first. A superseded record is usually the older, longer, more general one and
141
+ * its replacement is usually a narrowing, so any similarity measure favours the
142
+ * record that is no longer true. Every hit therefore carries its standing, and
143
+ * the caller is never handed a bare match.
144
+ */
145
+ type KbWarning =
146
+ /** Explicitly not adopted. The most dangerous status to return unmarked: a
147
+ * well-formed assertion of what someone decided *not* to do. */
148
+ {
149
+ kind: "rejected";
150
+ } | {
151
+ kind: "superseded";
152
+ by: string[];
153
+ }
154
+ /** Not settled. Acting on a proposal as though it were a decision is a defect. */
155
+ | {
156
+ kind: "unsettled";
157
+ status: KbRecordStatus;
158
+ }
159
+ /** Says a matter is unresolved. Valuable as a result, never as an answer. */
160
+ | {
161
+ kind: "unresolved-question";
162
+ }
163
+ /** `strauss_superseded_by` names a record that is not in the bundle. */
164
+ | {
165
+ kind: "broken-chain";
166
+ missing: string;
167
+ } | {
168
+ kind: "chain-cycle";
169
+ through: string[];
170
+ }
171
+ /** Two records claim to replace this one; picking either would be a guess. */
172
+ | {
173
+ kind: "forked-chain";
174
+ heads: string[];
175
+ } | {
176
+ kind: "stale";
177
+ staleAfter: string;
178
+ } | {
179
+ kind: "unverified";
180
+ };
181
+ type KbStanding = "current" | "superseded" | "rejected" | "unsettled" | "open";
182
+ type KbAdjudicated = {
183
+ record: KbRecord;
184
+ standing: KbStanding;
185
+ /** Where the supersession chain ends. Empty when it is broken or cyclic. */
186
+ heads: KbRecord[];
187
+ warnings: KbWarning[];
188
+ };
189
+ /**
190
+ * Attaches standing to records a search returned.
191
+ *
192
+ * Adjudicating rather than filtering, deliberately. A filtered result set is
193
+ * invisible: the caller cannot tell it missed anything, so a dropped record is
194
+ * worse than a flagged one — it turns a knowable gap into an unknowable one.
195
+ */
196
+ declare function adjudicate(hits: KbRecord[], bundle: KbRecord[], now?: Date): KbAdjudicated[];
197
+ /**
198
+ * Walks a supersession chain to whatever currently stands in its place.
199
+ *
200
+ * Both directions are followed, not just `strauss_superseded_by`. `supersede()`
201
+ * writes the pair, but a hand-edit can leave one side behind, and a walk that
202
+ * trusts only the forward pointer would silently return a record that something
203
+ * in the bundle openly claims to replace.
204
+ *
205
+ * Resolution happens here rather than being denormalised onto records at write
206
+ * time: a stored head would have to be rewritten on every ancestor whenever a
207
+ * chain grows, which is derived state that goes stale — the failure this design
208
+ * keeps avoiding elsewhere.
209
+ */
210
+ declare function resolveHeads(from: KbRecord, byId: Map<string, KbRecord>): {
211
+ heads: KbRecord[];
212
+ warnings: KbWarning[];
213
+ };
214
+
215
+ /**
216
+ * Edges a trace may follow.
217
+ *
218
+ * Two more are conceivable and absent: `strauss_answered` carries no target id,
219
+ * so a question's resolution lives in its own body rather than in another
220
+ * record; and following OKF's body markdown links would need a markdown AST
221
+ * pass this package does not yet do.
222
+ */
223
+ declare const TRACE_EDGES: readonly ["supersession", "anchor", "source"];
224
+ type KbTraceEdge = (typeof TRACE_EDGES)[number];
225
+ type KbTraceStep = {
226
+ record: KbRecord;
227
+ /** Hops from the seed. 0 is the seed itself. */
228
+ depth: number;
229
+ /** Why this record was reached. Empty for the seed. */
230
+ via: KbTraceEdge[];
231
+ };
232
+ type KbTraceOptions = {
233
+ edges?: readonly KbTraceEdge[];
234
+ /** Body links alone can reach the whole bundle, so a trace is always bounded. */
235
+ depth?: number;
236
+ };
237
+ /**
238
+ * How a position was arrived at, as a timeline.
239
+ *
240
+ * The inverse of a point query, and the reason the two cannot be one call with
241
+ * a flag: there, a `rejected` record is the most dangerous thing retrievable —
242
+ * here it is the content. A trace that drops the rejected alternatives and the
243
+ * superseded earlier understanding has removed the answer and kept the
244
+ * conclusion, which is what reading a diff already gives you.
245
+ *
246
+ * Ordered by `generated.at` rather than by relevance. Ranking a history is
247
+ * meaningless when the sequence is the point.
248
+ */
249
+ declare function trace(seedId: string, bundle: KbRecord[], options?: KbTraceOptions): KbTraceStep[];
250
+
251
+ declare const LOG_FILE = "log.jsonl";
252
+ declare const kbLogEntrySchema: z.ZodObject<{
253
+ at: z.ZodString;
254
+ by: z.ZodString;
255
+ operation: z.ZodString;
256
+ conceptId: z.ZodString;
257
+ target: z.ZodOptional<z.ZodString>;
258
+ }, z.core.$strict>;
259
+ type KbLogEntry = z.infer<typeof kbLogEntrySchema>;
260
+ /**
261
+ * The log is the bundle's only primary artifact, and the reason it is handled
262
+ * unlike `INDEX.md`.
263
+ *
264
+ * The index is derived: lose it and the records rebuild it. The log records
265
+ * events — which agent wrote what, and when — that leave no trace in the record
266
+ * set, so it cannot be regenerated from anything. Repair therefore means detect
267
+ * and report, never rewrite: rewriting an append-only log destroys the only
268
+ * copy of what it holds.
269
+ *
270
+ * JSONL rather than a markdown list. An earlier version rendered entries as
271
+ * `- <at> · <by> · <op> · <id>` and parsed them by splitting on the separator —
272
+ * a hand-written parser for a format invented here, which fails the first time
273
+ * a value contains the separator. JSON needs no parser and the schema below
274
+ * needs no separator to be unambiguous. Humans read the log through
275
+ * `strauss-kb log`, as they read everything else.
276
+ *
277
+ * One line per entry, appended with `O_APPEND`: POSIX makes the offset update
278
+ * atomic, and writes this size do not interleave on a local filesystem.
279
+ */
280
+ declare function renderLogEntry(entry: KbLogEntry): string;
281
+ type KbLogReadResult = {
282
+ entries: KbLogEntry[];
283
+ /** Lines that did not parse, with their 1-based position. Never rewritten. */
284
+ malformed: {
285
+ line: number;
286
+ text: string;
287
+ }[];
288
+ };
289
+ declare function parseLog(raw: string): KbLogReadResult;
290
+
291
+ /**
292
+ * Default bundle, relative to the working directory. A scratch base lives here
293
+ * and is meant to be gitignored; a base worth keeping is written to a committed
294
+ * path instead, passed explicitly. Nothing promotes one to the other.
295
+ */
296
+ declare const KB_DIR: string;
297
+ type KbLogger = {
298
+ info?(entry: Record<string, unknown>): void;
299
+ warn?(entry: Record<string, unknown>): void;
300
+ };
301
+ /**
302
+ * A superseded record, named but not spelled out.
303
+ *
304
+ * Standing is a qualifier on a body, and over a long session the body outlives
305
+ * the qualifier — the reader keeps what the record said and loses that it no
306
+ * longer holds. A stub has nothing left to act on, so the failure cannot occur,
307
+ * and `trace` still reaches the content through the id.
308
+ */
309
+ type KbSupersededStub = {
310
+ conceptId: string;
311
+ title: string | null;
312
+ supersededBy: string[];
313
+ at: string | null;
314
+ };
315
+ type KbLoadResult = {
316
+ loaded: true;
317
+ records: KbAdjudicated[];
318
+ /** Named only. Their bodies are reachable through `trace`. */
319
+ superseded: KbSupersededStub[];
320
+ recordCount: number;
321
+ approxTokens: number;
322
+ budgetTokens: number;
323
+ } | {
324
+ loaded: false;
325
+ recordCount: number;
326
+ approxTokens: number;
327
+ budgetTokens: number;
328
+ };
329
+ type KbWriteInput = {
330
+ type: string;
331
+ slug: string;
332
+ frontmatter: Omit<KbRecordFrontmatter, "type">;
333
+ body: string;
334
+ /** Replace an existing record rather than failing on the collision. */
335
+ overwrite?: boolean;
336
+ };
337
+ /**
338
+ * Reads and writes a knowledge bundle.
339
+ *
340
+ * One record per file, deliberately. Several agents run in parallel against the
341
+ * same bundle, and a shared file would need merging — a file-per-record store
342
+ * has no write conflict to resolve, only distinct filenames to choose.
343
+ *
344
+ * The bundle is addressed by path rather than fixed to one directory. A base
345
+ * belongs to whatever prompted it — a worktree, an investigation, a document —
346
+ * and one hardcoded location cannot be all of those.
347
+ *
348
+ * Framework-free on purpose. The consumers are a library caller, a CLI an agent
349
+ * shells out to, and an MCP server; the store takes a logger rather than
350
+ * reaching for one, because only some of those have anything to reach into.
351
+ */
352
+ declare class KbStore {
353
+ private readonly logger;
354
+ constructor(logger?: KbLogger);
355
+ /**
356
+ * Writes one record. `type` and `slug` compose both the filename and the
357
+ * concept id, so a caller cannot produce a file whose identity disagrees with
358
+ * its contents.
359
+ */
360
+ write(bundlePath: string, input: KbWriteInput, actor?: string): Promise<KbRecord>;
361
+ /** One record by concept id, or null when it does not exist. */
362
+ read(bundlePath: string, conceptId: string): Promise<KbRecord | null>;
363
+ /**
364
+ * Every record in the bundle, optionally narrowed to one type.
365
+ *
366
+ * A file that fails to parse is skipped and logged rather than thrown: one
367
+ * malformed record — hand-edited, or written by a producer we don't know —
368
+ * must not make the whole bundle unreadable.
369
+ */
370
+ list(bundlePath: string, type?: string): Promise<KbRecord[]>;
371
+ /**
372
+ * Moves a record's status, preserving everything else.
373
+ *
374
+ * Read-modify-write on one file is the one place two agents genuinely race,
375
+ * and the fix is a compare-and-swap rather than a lock: hash on read, verify
376
+ * the file is unchanged immediately before writing, fail if it moved. A lock
377
+ * would buy the same guarantee and add a stale-lock failure mode — a writer
378
+ * killed mid-hold blocks every later one until someone reasons about
379
+ * timeouts.
380
+ */
381
+ setStatus(bundlePath: string, conceptId: string, status: KbRecordStatus, actor?: string): Promise<KbRecord>;
382
+ /**
383
+ * Marks `conceptId` superseded by `replacementId`, and links both directions.
384
+ *
385
+ * Writing one side and letting a validator notice the other is missing was
386
+ * the previous arrangement; doing both here means the backlink cannot drift
387
+ * in normal use, and validation drops to catching hand-edits.
388
+ */
389
+ supersede(bundlePath: string, conceptId: string, replacementId: string, actor?: string): Promise<KbRecord>;
390
+ /** Resolves an open question, stamping who answered and when. */
391
+ answer(bundlePath: string, conceptId: string, answer: string, actor?: string, at?: string): Promise<KbRecord>;
392
+ /**
393
+ * Records matching a text query, each carrying its standing.
394
+ *
395
+ * Relevance comes from qmd's BM25 where an index is available and from a
396
+ * substring scan where it is not. What never moves to the ranker is the
397
+ * adjudication below it: a ranker answers relevance, and relevance is not
398
+ * standing — a superseded record is the older, longer, more general one, so
399
+ * ranking alone prefers what is no longer true.
400
+ *
401
+ * The fallback is deliberate. A search index is an optimisation, so losing it
402
+ * degrades recall and must never change the answer's shape or fail the call.
403
+ */
404
+ query(bundlePath: string, text: string, options?: {
405
+ type?: string;
406
+ includeNonCurrent?: boolean;
407
+ }): Promise<KbAdjudicated[]>;
408
+ private rank;
409
+ /**
410
+ * The whole base, adjudicated, when it is small enough to hand over.
411
+ *
412
+ * At the sizes these reach — twenty records is about three thousand tokens —
413
+ * loading everything beats searching it, and measurably: on nine questions
414
+ * whose wording appears in no record, a reader holding the base answered
415
+ * eight against an embedding search's four. Two of those differences are
416
+ * structural. A reader can say no record answers the question; vector search
417
+ * returns its nearest neighbour whatever the distance. And a reader picks the
418
+ * record that answers the question rather than the one nearest the topic.
419
+ * See the README's retrieval section for the measurements.
420
+ *
421
+ * Load it for a question, not for a session — a base read into a long
422
+ * conversation is summarised away by the end of it.
423
+ *
424
+ * Refuses rather than truncates when the base is too large. A truncated base
425
+ * is indistinguishable from a complete one, so a caller would answer "that
426
+ * was never decided" from a slice it did not know was a slice.
427
+ */
428
+ load(bundlePath: string, options?: {
429
+ budgetTokens?: number;
430
+ type?: string;
431
+ }): Promise<KbLoadResult>;
432
+ /** How a position was arrived at, as a timeline. See `trace.ts`. */
433
+ trace(bundlePath: string, seedId: string, options?: KbTraceOptions): Promise<KbTraceStep[]>;
434
+ /**
435
+ * The stored index, rebuilt if it disagrees with the records.
436
+ *
437
+ * Repair on read is what makes the lock-free write path safe: a writer whose
438
+ * scan predated another writer's record publishes a momentarily stale index,
439
+ * and the next reader through here settles it.
440
+ */
441
+ readIndex(bundlePath: string): Promise<string>;
442
+ /**
443
+ * The log, with unparseable lines reported rather than repaired.
444
+ *
445
+ * The log is the bundle's only artifact that cannot be reconstructed — the
446
+ * records rebuild the index, and the code outlives both, but nothing else
447
+ * knows which agent touched what. So a bad line is surfaced and left alone.
448
+ */
449
+ readLog(bundlePath: string): Promise<ReturnType<typeof parseLog>>;
450
+ private mutate;
451
+ /**
452
+ * Two guarantees, both about writers running in parallel.
453
+ *
454
+ * The record is written to a staging file and only then published, so a
455
+ * concurrent reader sees the whole record or no record — never half of one. A
456
+ * plain write is not atomic, and `list()` skips what it cannot parse, so a
457
+ * torn read would be silently reported as a malformed record.
458
+ *
459
+ * Publishing uses `link` rather than `rename` unless the caller asked to
460
+ * overwrite: `link` fails with EEXIST instead of replacing, which turns "two
461
+ * writers chose the same concept id" from silent data loss into a collision
462
+ * the caller has to answer. Both are atomic; only `rename` clobbers.
463
+ *
464
+ * The staging name deliberately does not end in `.md` — `list()` would
465
+ * otherwise try to read it mid-write.
466
+ */
467
+ private publish;
468
+ /** Appends one log line. Failing to log must not fail the mutation. */
469
+ private record;
470
+ private parse;
471
+ private root;
472
+ private recordPath;
473
+ }
474
+
475
+ /**
476
+ * The error shape the store throws.
477
+ *
478
+ * Every caller here is an agent, reached through a CLI or a stdio MCP server,
479
+ * so a bare `Error` arrives as an opaque string. What a caller has to act on is
480
+ * carried as fields instead: `code` separates "pick a different slug and retry"
481
+ * from "this input was never going to work", and `retriable` says whether
482
+ * re-running the same call could succeed.
483
+ *
484
+ * No dependency, deliberately. This is four fields and a constructor; an error
485
+ * library would be a runtime dependency in aid of that.
486
+ */
487
+ declare enum Fault {
488
+ /** The environment is wrong — a path, a permission, a missing directory. */
489
+ Configuration = "Configuration",
490
+ /** Nothing the caller did; retrying may work. */
491
+ System = "System",
492
+ /** The call was malformed or asked for something impossible. */
493
+ User = "User"
494
+ }
495
+ /** Machine-readable discriminant, stable across message rewording. */
496
+ declare enum ErrorTypes {
497
+ KbRecordAlreadyExists = "KbRecordAlreadyExists",
498
+ KbInvalidConceptId = "KbInvalidConceptId",
499
+ KbRecordNotFound = "KbRecordNotFound",
500
+ KbWriteConflict = "KbWriteConflict"
501
+ }
502
+ type ErrorDetails = Record<string, string | boolean | number | string[] | boolean[] | number[]>;
503
+ interface ErrorProps {
504
+ message: string;
505
+ errorType?: ErrorTypes;
506
+ details?: ErrorDetails;
507
+ name?: string;
508
+ code?: number;
509
+ fault?: Fault;
510
+ retriable?: boolean;
511
+ reportToUser?: boolean;
512
+ }
513
+ declare class BaseError extends Error {
514
+ code: number;
515
+ errorType?: ErrorTypes;
516
+ fault?: Fault;
517
+ retriable: boolean;
518
+ reportToUser: boolean;
519
+ details?: ErrorDetails;
520
+ constructor(props: ErrorProps);
521
+ }
522
+
523
+ /**
524
+ * Every caller of this store is an agent, reached through a CLI or a stdio MCP
525
+ * server, so a bare `Error` reaches it as an opaque string. The `code` carries
526
+ * the distinction the caller has to act on: 409 means pick a different slug and
527
+ * retry, 400 means the input was never going to work.
528
+ */
529
+ declare class KbRecordAlreadyExistsError extends BaseError {
530
+ readonly conceptId: string;
531
+ constructor(conceptId: string);
532
+ }
533
+ declare class KbRecordNotFoundError extends BaseError {
534
+ readonly conceptId: string;
535
+ constructor(conceptId: string);
536
+ }
537
+ /** Retriable, unlike the others: re-reading and re-applying usually succeeds. */
538
+ declare class KbWriteConflictError extends BaseError {
539
+ readonly conceptId: string;
540
+ constructor(conceptId: string);
541
+ }
542
+ declare class KbInvalidConceptIdError extends BaseError {
543
+ constructor(message: string, details: Record<string, string>);
544
+ }
545
+
546
+ /**
547
+ * What each record type is for, and the shape of its body.
548
+ *
549
+ * A table rather than twelve composer modules. The types differ only in which
550
+ * questions their body answers and where they start in the lifecycle; encoding
551
+ * that as data keeps the one composer honest and makes adding a type an edit
552
+ * rather than a file.
553
+ *
554
+ * `sections` are ordered. A section the caller leaves empty is omitted rather
555
+ * than rendered with a placeholder — an empty "## Evidence" reads as evidence
556
+ * that was looked for and not found.
557
+ */
558
+ type KbRecordTypeSpec = {
559
+ /** One line, for `INDEX.md` legends and CLI help. */
560
+ purpose: string;
561
+ /** Ordered body headings. The first is the record's central claim. */
562
+ sections: readonly string[];
563
+ /** Where a freshly written record of this type starts. */
564
+ initialStatus: KbRecordStatus;
565
+ };
566
+ declare const RECORD_TYPES: Readonly<Record<KbRecordType, KbRecordTypeSpec>>;
567
+ declare function isKbRecordType(value: string): value is KbRecordType;
568
+
569
+ declare const composeInputSchema: z.ZodObject<{
570
+ slug: z.ZodString;
571
+ title: z.ZodString;
572
+ why: z.ZodString;
573
+ sections: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
574
+ anchors: z.ZodOptional<z.ZodArray<z.ZodObject<{
575
+ file: z.ZodString;
576
+ symbol: z.ZodOptional<z.ZodString>;
577
+ }, z.core.$strict>>>;
578
+ sources: z.ZodOptional<z.ZodArray<z.ZodObject<{
579
+ id: z.ZodString;
580
+ resource: z.ZodString;
581
+ title: z.ZodOptional<z.ZodString>;
582
+ author: z.ZodOptional<z.ZodString>;
583
+ last_modified: z.ZodOptional<z.ZodString>;
584
+ }, z.core.$loose>>>;
585
+ assumption: z.ZodOptional<z.ZodBoolean>;
586
+ verify: z.ZodOptional<z.ZodArray<z.ZodString>>;
587
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
588
+ relatedConceptIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
589
+ supersedes: z.ZodOptional<z.ZodArray<z.ZodString>>;
590
+ materiality: z.ZodOptional<z.ZodEnum<{
591
+ blocking: "blocking";
592
+ important: "important";
593
+ "non-blocking": "non-blocking";
594
+ }>>;
595
+ confidence: z.ZodOptional<z.ZodEnum<{
596
+ low: "low";
597
+ medium: "medium";
598
+ high: "high";
599
+ }>>;
600
+ owner: z.ZodOptional<z.ZodString>;
601
+ }, z.core.$strict>;
602
+ type ComposeInput = z.infer<typeof composeInputSchema>;
603
+ type ComposedRecord = {
604
+ type: string;
605
+ slug: string;
606
+ frontmatter: Omit<KbRecordFrontmatter, "type">;
607
+ body: string;
608
+ };
609
+ /**
610
+ * Builds one record's frontmatter and body from its type's spec.
611
+ *
612
+ * Edges go in the body rather than frontmatter because that is where OKF puts
613
+ * them: consumers read markdown links as directed but untyped relationships,
614
+ * with "the specific kind conveyed by the surrounding prose, not by the link
615
+ * itself". Broken links are explicitly legal, which matters here — records are
616
+ * routinely written before the ones they point at exist.
617
+ */
618
+ declare function composeRecord(type: KbRecordType, input: ComposeInput, writtenBy: string, writtenAt: string): ComposedRecord;
619
+
620
+ declare const INDEX_FILE = "INDEX.md";
621
+ /**
622
+ * `INDEX.md` is a projection — every byte recomputable from record frontmatter.
623
+ *
624
+ * That is what lets parallel writers regenerate it without a lock: they compute
625
+ * the same function of the same records, so two concurrent regenerations differ
626
+ * only in how recent each writer's scan was, and the next read settles it. The
627
+ * file is therefore eventually correct rather than always correct, which is the
628
+ * right trade for something nothing reads transactionally.
629
+ *
630
+ * Lines carry `description`, not just a title. A reader consults the index to
631
+ * decide what is worth opening, and a list of titles does not answer that.
632
+ */
633
+ declare function renderIndex(records: KbRecord[]): string;
634
+ /** Whether the stored projection still matches the records it claims to index. */
635
+ declare function indexIsStale(stored: string | null, expected: string): boolean;
636
+
637
+ /**
638
+ * The frontmatter contract, emitted rather than restated.
639
+ *
640
+ * Prose describing a schema drifts from the code that enforces it; a generated
641
+ * artifact cannot. Documentation points at this, a YAML language server
642
+ * validates hand-edited records against it, and a consumer that is not
643
+ * TypeScript has something to check.
644
+ *
645
+ * `io: 'input'` on purpose. `strauss_status` carries a default, so the output
646
+ * type marks it required while the *document* may legitimately omit it — and
647
+ * the document is what this schema is used to validate.
648
+ */
649
+ declare function kbJsonSchemas(): Record<string, unknown>;
650
+
651
+ /**
652
+ * Which records apply to which part of a change.
653
+ *
654
+ * Takes a structural description of a diff rather than a patch, so this package
655
+ * carries no diff parser: callers already have one, and a knowledge base has no
656
+ * business preferring a particular flavour of unified diff.
657
+ *
658
+ * Deterministic on purpose. Every step here is mechanical — the one judgment,
659
+ * whether a matched record is worth showing a reviewer, is deliberately absent.
660
+ * A model placed here would sit between the reviewer and their diff on every
661
+ * review, to answer a question nobody has yet shown needs asking.
662
+ *
663
+ * Distinct from `load()`, which hands a reader the whole base. That answers
664
+ * "does anything address this question"; this answers "what is attached to this
665
+ * code", and an anchor is the author's own statement rather than an inference
666
+ * from one. A reader guessing which record relates to a hunk would be guessing
667
+ * at something already written down — and a diff has dozens of hunks, which is
668
+ * dozens of reader calls against microseconds of matching. Where they compose:
669
+ * this narrows a hunk to a few records, and a reader asked to explain them gets
670
+ * those, not the base.
671
+ */
672
+ type DiffHunk = {
673
+ /** 1-based, inclusive, in the file's post-change line numbering. */
674
+ startLine: number;
675
+ endLine: number;
676
+ };
677
+ type DiffFile = {
678
+ /** Repo-relative, matching how anchors are written. */
679
+ filePath: string;
680
+ hunks: DiffHunk[];
681
+ };
682
+ /**
683
+ * A symbol resolved to lines. Supplied by whatever the caller uses to index
684
+ * symbols; absence is tolerated — see `place()`.
685
+ */
686
+ type SymbolRange = {
687
+ file: string;
688
+ symbol: string;
689
+ startLine: number;
690
+ endLine: number;
691
+ };
692
+ type DiffMatch = {
693
+ filePath: string;
694
+ hunk: DiffHunk;
695
+ /** Current records first — what still holds should be read before what does not. */
696
+ records: KbAdjudicated[];
697
+ /**
698
+ * `symbol` when every record here was placed by a resolved symbol range,
699
+ * `file` when at least one fell back to the whole file. Reported rather than
700
+ * hidden: a caller showing a file-level match as though it were pinned to
701
+ * these lines is claiming a precision it does not have.
702
+ */
703
+ precision: "symbol" | "file";
704
+ };
705
+ type MatchOptions = {
706
+ /** Without these, symbol anchors degrade to file level rather than vanishing. */
707
+ symbolRanges?: SymbolRange[];
708
+ now?: Date;
709
+ };
710
+ declare function matchToDiff(files: DiffFile[], records: KbRecord[], options?: MatchOptions): DiffMatch[];
711
+
712
+ type KbValidationProblem = {
713
+ check: string;
714
+ conceptId: string;
715
+ note: string;
716
+ };
717
+ /**
718
+ * Checks that only hold across the whole bundle.
719
+ *
720
+ * Per-record shape is the schema's job and is enforced on every read, so
721
+ * nothing here re-states it. What a schema cannot see is whether one record's
722
+ * pointers agree with another's — and since `supersede()` now writes both
723
+ * directions, a disagreement means someone edited a file by hand.
724
+ */
725
+ declare function validateBundle(records: KbRecord[]): KbValidationProblem[];
726
+
727
+ /**
728
+ * The record written while a change is being made: why it is shaped the way it
729
+ * is, anchored to the symbols it touches.
730
+ *
731
+ * A decision is the one thing a later pass cannot recover. The diff shows what
732
+ * changed; nothing in it says which alternative was rejected, or which
733
+ * constraint a future reader would otherwise "simplify" away. Everything else a
734
+ * review needs — categories, moves, formatting — is derivable from the finished
735
+ * diff and does not belong here.
736
+ */
737
+ declare const DECISION_TYPE = "decision";
738
+ /**
739
+ * Slug for the explicit "nothing to record here" answer.
740
+ *
741
+ * Gating on "did you write a decision?" rewards writing a junk decision. Gating
742
+ * on "did you answer?" does not, so silence has to be expressible as a claim:
743
+ * one record, one sentence, auditable after the fact. Work that genuinely
744
+ * needed no decision says so; work that says nothing at all is the case worth
745
+ * surfacing.
746
+ */
747
+ declare const NO_DECISION_SLUG = "none";
748
+ /**
749
+ * Decisions keep a typed input of their own where the generic composer takes a
750
+ * section map. `alternative` is not a nicety here — "what was rejected" is the
751
+ * part of a decision that a later reader cannot reconstruct, so it gets a field
752
+ * rather than a heading a writer may forget to fill.
753
+ */
754
+ declare const decisionInputSchema: z.ZodObject<{
755
+ title: z.ZodString;
756
+ assumption: z.ZodOptional<z.ZodBoolean>;
757
+ tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
758
+ sources: z.ZodOptional<z.ZodArray<z.ZodObject<{
759
+ id: z.ZodString;
760
+ resource: z.ZodString;
761
+ title: z.ZodOptional<z.ZodString>;
762
+ author: z.ZodOptional<z.ZodString>;
763
+ last_modified: z.ZodOptional<z.ZodString>;
764
+ }, z.core.$loose>>>;
765
+ slug: z.ZodString;
766
+ why: z.ZodString;
767
+ anchors: z.ZodOptional<z.ZodArray<z.ZodObject<{
768
+ file: z.ZodString;
769
+ symbol: z.ZodOptional<z.ZodString>;
770
+ }, z.core.$strict>>>;
771
+ verify: z.ZodOptional<z.ZodArray<z.ZodString>>;
772
+ relatedConceptIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
773
+ supersedes: z.ZodOptional<z.ZodArray<z.ZodString>>;
774
+ materiality: z.ZodOptional<z.ZodEnum<{
775
+ blocking: "blocking";
776
+ important: "important";
777
+ "non-blocking": "non-blocking";
778
+ }>>;
779
+ confidence: z.ZodOptional<z.ZodEnum<{
780
+ low: "low";
781
+ medium: "medium";
782
+ high: "high";
783
+ }>>;
784
+ owner: z.ZodOptional<z.ZodString>;
785
+ alternative: z.ZodOptional<z.ZodString>;
786
+ impact: z.ZodOptional<z.ZodString>;
787
+ }, z.core.$strict>;
788
+ type DecisionInput = z.infer<typeof decisionInputSchema>;
789
+ declare function composeDecisionRecord(input: DecisionInput, writtenBy: string, writtenAt: string): ComposedRecord;
790
+ /** The explicit no-decision claim, so an absence and an answer stay distinct. */
791
+ declare function composeNoDecisionRecord(reason: string, writtenBy: string, writtenAt: string): ComposedRecord;
792
+ /** Whether a record is the explicit no-decision claim rather than a decision. */
793
+ declare function isNoDecisionRecord(record: KbRecord): boolean;
794
+ /**
795
+ * Decisions in the bundle, excluding the no-decision claim.
796
+ *
797
+ * Callers asking "what was decided" must not be handed the record that exists
798
+ * precisely to say nothing was.
799
+ */
800
+ declare function selectDecisions(records: KbRecord[]): KbRecord[];
801
+
802
+ /**
803
+ * Every operation a knowledge base exposes, defined once.
804
+ *
805
+ * The CLI and the MCP server are both projections of this list. Kept apart they
806
+ * drift within a day — fourteen commands against six tools — which is the same
807
+ * failure as a schema restated in prose beside the code that enforces it, one
808
+ * level up. A command added here appears in both surfaces or in neither, and a
809
+ * test asserts exactly that.
810
+ *
811
+ * The two differ only in how arguments arrive: MCP passes an object matching
812
+ * `input`, while the CLI has to turn positional argv into the same object.
813
+ * `fromArgv` is that adapter and is the only per-surface code a command needs.
814
+ */
815
+ type KbCommandContext = {
816
+ store: KbStore;
817
+ actor: string;
818
+ now: () => string;
819
+ };
820
+ type KbCommand<Shape extends z.ZodRawShape = z.ZodRawShape> = {
821
+ /** CLI verb. */
822
+ name: string;
823
+ /** MCP tool name. */
824
+ tool: string;
825
+ /** Argument spelling for CLI usage output. */
826
+ usage: string;
827
+ /** Shown to an agent choosing a tool, so it carries the judgment too. */
828
+ description: string;
829
+ input: z.ZodObject<Shape>;
830
+ /** Positional argv → the same object MCP receives. */
831
+ fromArgv(argv: string[], bundlePath: string, stdin: () => Promise<string>): Promise<unknown> | unknown;
832
+ run(ctx: KbCommandContext, input: z.infer<z.ZodObject<Shape>>): Promise<unknown>;
833
+ /**
834
+ * Turns a result into a non-zero exit for the CLI. A check that reports a
835
+ * problem has succeeded as a command and failed as a check, and a shell
836
+ * caller can only see the difference through the exit code.
837
+ */
838
+ failsWhen?(result: unknown): boolean;
839
+ };
840
+ declare const KB_COMMANDS: KbCommand<z.ZodRawShape>[];
841
+ declare const KB_COMMANDS_BY_NAME: Map<string, KbCommand<Readonly<{
842
+ [k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
843
+ }>>>;
844
+
845
+ /**
846
+ * A knowledge base's own MCP server, over stdio.
847
+ *
848
+ * Standalone because a base is self-contained: a directory of markdown that
849
+ * needs no database, no HTTP surface, and no running service to read. Folding
850
+ * these tools into a larger server would make every consumer start that server
851
+ * to open files it could open itself.
852
+ *
853
+ * Every tool is a projection of `KB_COMMANDS`, which the CLI also projects, so
854
+ * the two cannot drift.
855
+ */
856
+ declare function createKbMcpServer(): McpServer;
857
+ declare function runKbMcpServer(): Promise<void>;
858
+
859
+ declare function runKbCli(argv: string[]): Promise<void>;
860
+
861
+ declare const SEARCH_INDEX_FILE = ".index.sqlite";
862
+ /**
863
+ * BM25 over one knowledge base, via qmd's SDK.
864
+ *
865
+ * Reached only when a base is too large to hand over whole — see `load()`,
866
+ * which is the first thing a reader should try.
867
+ *
868
+ * Lexical only. `searchLex` is BM25 and needs no model. Measured against the
869
+ * substring scan it replaces it wins on word forms and little else: `pages`
870
+ * finds a record saying only `page`, and eight of nine probe queries returned
871
+ * exactly what substring returned.
872
+ *
873
+ * The vector tier does close the semantic gap — "why not just use a mutex"
874
+ * finds the record about compare-and-swap, which no lexical match can. It stays
875
+ * off because its scores do not separate right from wrong: a wrong hit scored
876
+ * 0.318 against a correct one at 0.295, and a query about a subject absent from
877
+ * the base still returns its nearest record rather than nothing.
878
+ *
879
+ * The index is derived and disposable — one file per base, gitignored, deleted
880
+ * and rebuilt at will. That is only true because a base is self-contained: an
881
+ * index covering exactly one directory can always be rebuilt from it.
882
+ *
883
+ * qmd is used as a **library**, and is an optional peer dependency: with it
884
+ * absent every path here still answers, `searchBase` returns null, and the
885
+ * store falls back to a substring scan. Its own MCP server would let an agent
886
+ * reach a base without passing the store, and its default markdown glob would
887
+ * return `INDEX.md` as a search hit — the case `list()` already excludes,
888
+ * reintroduced by a reader this package does not control. Hence the explicit
889
+ * `ignore` below.
890
+ */
891
+ /**
892
+ * A hit as qmd reports it — by its own normalised path, not by our identity.
893
+ *
894
+ * qmd rewrites `decision.cursor-keyset.md` to `decision-cursor-keyset.md`,
895
+ * which destroys the separator between a record's type and its slug and cannot
896
+ * be undone from the string alone: `decision-cursor-keyset` could be the type
897
+ * `decision` or a type `decision-cursor`. The caller maps these back through
898
+ * the records it already holds rather than parsing them.
899
+ */
900
+ type SearchHit = {
901
+ displayPath: string;
902
+ score: number;
903
+ };
904
+ type KbSearchLogger = {
905
+ warn?(entry: Record<string, unknown>): void;
906
+ };
907
+ /** What this module needs of qmd, which is all it is allowed to assume. */
908
+ type QmdModule = {
909
+ createStore(options: unknown): Promise<unknown>;
910
+ };
911
+ type SearchOptions = {
912
+ limit?: number;
913
+ logger?: KbSearchLogger;
914
+ /**
915
+ * The backend, supplied rather than imported. Production passes nothing and
916
+ * gets the dynamic import below; a caller that already holds qmd — or a test
917
+ * covering the present-backend branch on a machine where the optional peer is
918
+ * not installed — passes it here.
919
+ */
920
+ qmd?: QmdModule;
921
+ };
922
+ /**
923
+ * Opens (or creates) the base's index and answers a query against it.
924
+ *
925
+ * Re-indexes when the index is older than the newest record rather than on a
926
+ * schedule or a write hook: the same repair-on-read rule `INDEX.md` follows,
927
+ * and for the same reason — a derived artifact that can rebuild itself does not
928
+ * need anyone to remember to rebuild it.
929
+ */
930
+ declare function searchBase(bundlePath: string, query: string, options?: SearchOptions): Promise<SearchHit[] | null>;
931
+ /**
932
+ * Maps qmd's normalised paths back onto real records.
933
+ *
934
+ * Comparing on the same normalisation rather than trying to invert it: a dot in
935
+ * a concept id and a dash in a slug are indistinguishable once qmd has rewritten
936
+ * the name, so the only reliable direction is forwards, from ids we hold.
937
+ */
938
+ declare function resolveHits<T extends {
939
+ conceptId: string;
940
+ }>(hits: SearchHit[], records: T[]): T[];
941
+ /**
942
+ * Loaded on demand so a caller that never searches pays nothing for a
943
+ * dependency that pulls in native SQLite bindings and a llama runtime — and
944
+ * returns null rather than throwing when it is not installed at all, which is
945
+ * the normal case for an optional peer.
946
+ */
947
+ declare function loadQmd(logger?: KbSearchLogger): Promise<QmdModule | null>;
948
+
949
+ /**
950
+ * Frontmatter round-tripping, thin over gray-matter.
951
+ *
952
+ * Thin on purpose. A record is a YAML block and a markdown body, and every
953
+ * hand-rolled reader of that shape eventually meets a nested map — an OKF
954
+ * `generated`, a `sources[]`, a `verified[]` — and misreads it. The parser is
955
+ * therefore borrowed and the only thing added is the schema gate below.
956
+ */
957
+ declare function stringifyMarkdownWithFrontmatter(content: string, frontmatter: Record<string, unknown>): string;
958
+ declare function splitMarkdownFrontmatter(text: string): {
959
+ content: string;
960
+ prefix: string;
961
+ raw: Record<string, unknown>;
962
+ };
963
+ /**
964
+ * Splits, then validates the frontmatter against a schema.
965
+ *
966
+ * The result is a `safeParse` outcome rather than a throw: one malformed record
967
+ * must not make a whole directory unreadable, so the caller decides whether to
968
+ * skip it or fail.
969
+ */
970
+ declare function parseMarkdownWithFrontmatter<S extends z.ZodType>(text: string, schema: S): {
971
+ content: string;
972
+ prefix: string;
973
+ raw: Record<string, unknown>;
974
+ frontmatter: ReturnType<S["safeParse"]>;
975
+ };
976
+
977
+ export { BaseError, 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, type KbCommand, type KbCommandContext, KbInvalidConceptIdError, type KbLoadResult, type KbLogEntry, type KbLogReadResult, type KbLogger, type KbRecord, KbRecordAlreadyExistsError, type KbRecordFrontmatter, KbRecordNotFoundError, type KbRecordStatus, type KbRecordType, type KbRecordTypeSpec, type KbSearchLogger, type KbSource, type KbStanding, KbStore, type KbSupersededStub, type KbTraceEdge, type KbTraceOptions, type KbTraceStep, type KbValidationProblem, type KbWarning, KbWriteConflictError, type KbWriteInput, LOG_FILE, type MatchOptions, NO_DECISION_SLUG, type QmdModule, RECORD_TYPES, SEARCH_INDEX_FILE, type SearchHit, type SearchOptions, type SymbolRange, TRACE_EDGES, adjudicate, composeDecisionRecord, composeInputSchema, composeNoDecisionRecord, composeRecord, createKbMcpServer, decisionInputSchema, indexIsStale, isKbRecordType, isNoDecisionRecord, kbActorStampSchema, kbAnchorSchema, kbConceptIdSchema, kbJsonSchemas, kbLogEntrySchema, kbRecordFrontmatterSchema, kbSourceSchema, loadQmd, matchToDiff, parseLog, parseMarkdownWithFrontmatter, renderIndex, renderLogEntry, resolveHeads, resolveHits, runKbCli, runKbMcpServer, searchBase, selectDecisions, splitMarkdownFrontmatter, stringifyMarkdownWithFrontmatter, trace, validateBundle };