@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.
- package/LICENSE +21 -0
- package/README.md +340 -0
- package/dist/chunk-KGM34MYU.js +93 -0
- package/dist/chunk-KGM34MYU.js.map +1 -0
- package/dist/chunk-WFHYWZX5.js +47 -0
- package/dist/chunk-WFHYWZX5.js.map +1 -0
- package/dist/chunk-ZSYSHJVZ.js +1527 -0
- package/dist/chunk-ZSYSHJVZ.js.map +1 -0
- package/dist/cli-main.cjs +1567 -0
- package/dist/cli-main.cjs.map +1 -0
- package/dist/cli-main.d.cts +1 -0
- package/dist/cli-main.d.ts +1 -0
- package/dist/cli-main.js +15 -0
- package/dist/cli-main.js.map +1 -0
- package/dist/index.cjs +1817 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +977 -0
- package/dist/index.d.ts +977 -0
- package/dist/index.js +202 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-main.cjs +1522 -0
- package/dist/mcp-main.cjs.map +1 -0
- package/dist/mcp-main.d.cts +1 -0
- package/dist/mcp-main.d.ts +1 -0
- package/dist/mcp-main.js +15 -0
- package/dist/mcp-main.js.map +1 -0
- package/package.json +72 -0
package/dist/index.d.ts
ADDED
|
@@ -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 };
|