@saasontools/strauss-kb 0.1.21 → 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -4
- package/dist/{chunk-WVJULVGY.js → chunk-3EZ3PAPA.js} +2 -2
- package/dist/{chunk-VXWN62HT.js → chunk-5IPQVXCM.js} +2 -2
- package/dist/{chunk-XBZ7G5R5.js → chunk-XENLCPL5.js} +2509 -2169
- package/dist/chunk-XENLCPL5.js.map +1 -0
- package/dist/cli-main.cjs +1516 -1184
- package/dist/cli-main.cjs.map +1 -1
- package/dist/cli-main.js +2 -2
- package/dist/index.cjs +714 -367
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +328 -17
- package/dist/index.d.ts +328 -17
- package/dist/index.js +17 -3
- package/dist/mcp-main.cjs +1516 -1184
- package/dist/mcp-main.cjs.map +1 -1
- package/dist/mcp-main.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-XBZ7G5R5.js.map +0 -1
- /package/dist/{chunk-WVJULVGY.js.map → chunk-3EZ3PAPA.js.map} +0 -0
- /package/dist/{chunk-VXWN62HT.js.map → chunk-5IPQVXCM.js.map} +0 -0
package/dist/mcp-main.cjs
CHANGED
|
@@ -32,488 +32,10 @@ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
|
32
32
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
33
33
|
|
|
34
34
|
// src/decision-record.ts
|
|
35
|
-
var
|
|
36
|
-
|
|
37
|
-
// src/compose.ts
|
|
38
|
-
var import_zod2 = require("zod");
|
|
39
|
-
|
|
40
|
-
// src/kb-record.schema.ts
|
|
41
|
-
var import_zod = require("zod");
|
|
42
|
-
var kbSourceSchema = import_zod.z.object({
|
|
43
|
-
id: import_zod.z.string().min(1),
|
|
44
|
-
resource: import_zod.z.string().min(1),
|
|
45
|
-
title: import_zod.z.string().min(1).optional(),
|
|
46
|
-
author: import_zod.z.string().min(1).optional(),
|
|
47
|
-
last_modified: import_zod.z.string().min(1).optional()
|
|
48
|
-
}).passthrough();
|
|
49
|
-
var kbActorStampSchema = import_zod.z.object({
|
|
50
|
-
by: import_zod.z.string().min(1),
|
|
51
|
-
at: import_zod.z.string().min(1)
|
|
52
|
-
}).passthrough();
|
|
53
|
-
var kbVerifiedEventSchema = kbActorStampSchema.extend({
|
|
54
|
-
note: import_zod.z.string().refine((s) => s.trim().length > 0, {
|
|
55
|
-
message: "note must say what the check found"
|
|
56
|
-
})
|
|
57
|
-
});
|
|
58
|
-
var kbAnchorSpanSchema = import_zod.z.object({
|
|
59
|
-
start: import_zod.z.number().int().positive(),
|
|
60
|
-
end: import_zod.z.number().int().positive()
|
|
61
|
-
}).strict();
|
|
62
|
-
var kbAnchorSchema = import_zod.z.object({
|
|
63
|
-
file: import_zod.z.string().min(1),
|
|
64
|
-
symbol: import_zod.z.string().min(1).optional(),
|
|
65
|
-
/**
|
|
66
|
-
* The lines the concept names, when no symbol covers them — deleted code,
|
|
67
|
-
* YAML, SQL, Markdown. Alternative to `symbol`, never a refinement of it.
|
|
68
|
-
*/
|
|
69
|
-
span: kbAnchorSpanSchema.optional(),
|
|
70
|
-
/**
|
|
71
|
-
* Which side of the change the anchor describes. `old` is code as it was
|
|
72
|
-
* committed at `ref`, which is the only way to anchor something deleted;
|
|
73
|
-
* absent means the working tree.
|
|
74
|
-
*/
|
|
75
|
-
side: import_zod.z.enum(["old", "new"]).optional(),
|
|
76
|
-
/**
|
|
77
|
-
* Which repository the file lives in — a remote URL
|
|
78
|
-
* (`https://github.com/org/name`) or a short name. Absent means the base's
|
|
79
|
-
* own repository, which is what nearly every anchor means.
|
|
80
|
-
*
|
|
81
|
-
* Unvalidated beyond not-blank: one repository has many spellings, matched
|
|
82
|
-
* after normalisation. Only a full URL can be fetched from, so `validate`
|
|
83
|
-
* warns on a short one; see ARCHITECTURE.
|
|
84
|
-
*/
|
|
85
|
-
repo: import_zod.z.string().trim().min(1).optional(),
|
|
86
|
-
/**
|
|
87
|
-
* The git rev the evidence was taken at. Prefer a commit SHA: a branch
|
|
88
|
-
* name is a moving pointer, so an anchor pinned to one says the evidence
|
|
89
|
-
* came from wherever that branch happens to be now, which is not a
|
|
90
|
-
* baseline. A foreign anchor is checked at this rev, and compared against
|
|
91
|
-
* the remote's default branch on top of it.
|
|
92
|
-
*/
|
|
93
|
-
ref: import_zod.z.string().trim().min(1).optional(),
|
|
94
|
-
hash: import_zod.z.string().regex(/^sha256:[0-9a-f]{64}$/, {
|
|
95
|
-
message: "hash must be sha256:<64 hex chars>"
|
|
96
|
-
}).optional(),
|
|
97
|
-
/**
|
|
98
|
-
* What `hash` was taken over: the span's raw text, or the normalised token
|
|
99
|
-
* stream a parser sees (`ast`). Absent means `raw`, which is what every
|
|
100
|
-
* anchor stamped before this field carries, so old hashes keep comparing
|
|
101
|
-
* the way they were written. An `ast` hash is blind to whitespace and
|
|
102
|
-
* comments, so reformatting the anchored code is not drift.
|
|
103
|
-
*/
|
|
104
|
-
hash_kind: import_zod.z.enum(["raw", "ast"]).optional(),
|
|
105
|
-
/** ISO 8601 timestamp of the last successful resolution. */
|
|
106
|
-
resolved_at: import_zod.z.string().min(1).optional(),
|
|
107
|
-
/** Line count of the text the hash was taken over. */
|
|
108
|
-
lines: import_zod.z.number().int().positive().optional(),
|
|
109
|
-
/**
|
|
110
|
-
* Which resolver produced the hashed span. Absent means an anchor stamped
|
|
111
|
-
* before resolvers were named, which is read as `regex` — the only one
|
|
112
|
-
* there was. A hash from a different resolver is drift, not a match.
|
|
113
|
-
*/
|
|
114
|
-
resolver: import_zod.z.enum(["tree-sitter", "regex", "span"]).optional()
|
|
115
|
-
}).strict();
|
|
116
|
-
var kbAnchorWriteSchema = kbAnchorSchema.superRefine((anchor, ctx) => {
|
|
117
|
-
if (anchor.span && anchor.symbol) {
|
|
118
|
-
ctx.addIssue({
|
|
119
|
-
code: import_zod.z.ZodIssueCode.custom,
|
|
120
|
-
path: ["span"],
|
|
121
|
-
message: "an anchor names a symbol or a span, not both"
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
if (anchor.span && anchor.span.end < anchor.span.start) {
|
|
125
|
-
ctx.addIssue({
|
|
126
|
-
code: import_zod.z.ZodIssueCode.custom,
|
|
127
|
-
path: ["span", "end"],
|
|
128
|
-
message: "span end must not precede start"
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
if (anchor.span && anchor.hash_kind === "ast") {
|
|
132
|
-
ctx.addIssue({
|
|
133
|
-
code: import_zod.z.ZodIssueCode.custom,
|
|
134
|
-
path: ["hash_kind"],
|
|
135
|
-
message: "a span is hashed raw, never ast"
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
if (anchor.side === "old" && !anchor.ref) {
|
|
139
|
-
ctx.addIssue({
|
|
140
|
-
code: import_zod.z.ZodIssueCode.custom,
|
|
141
|
-
path: ["ref"],
|
|
142
|
-
message: 'side: "old" needs a ref \u2014 committed code has no other address'
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
|
-
var kbLinkSchema = import_zod.z.object({
|
|
147
|
-
target: import_zod.z.string().min(1),
|
|
148
|
-
rel: import_zod.z.string().min(1)
|
|
149
|
-
}).passthrough();
|
|
150
|
-
var KB_RECORD_TYPES = [
|
|
151
|
-
"fact",
|
|
152
|
-
"requirement",
|
|
153
|
-
"constraint",
|
|
154
|
-
"decision",
|
|
155
|
-
"assumption",
|
|
156
|
-
"open-question",
|
|
157
|
-
"risk",
|
|
158
|
-
"contract",
|
|
159
|
-
"flow",
|
|
160
|
-
"affected-system",
|
|
161
|
-
"test-obligation",
|
|
162
|
-
"source-note"
|
|
163
|
-
];
|
|
164
|
-
var KB_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
165
|
-
var KB_CONCEPT_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*\.[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
166
|
-
var kbConceptIdSchema = import_zod.z.string().regex(KB_CONCEPT_ID_PATTERN, {
|
|
167
|
-
message: "concept id must be <type>.<slug>, both kebab-case"
|
|
168
|
-
});
|
|
169
|
-
var KB_RECORD_STATUSES = [
|
|
170
|
-
"draft",
|
|
171
|
-
"proposed",
|
|
172
|
-
"accepted",
|
|
173
|
-
"open",
|
|
174
|
-
"resolved",
|
|
175
|
-
"rejected",
|
|
176
|
-
"superseded"
|
|
177
|
-
];
|
|
178
|
-
var KB_MATERIALITIES = [
|
|
179
|
-
"blocking",
|
|
180
|
-
"important",
|
|
181
|
-
"non-blocking"
|
|
182
|
-
];
|
|
183
|
-
var KB_CONFIDENCES = ["low", "medium", "high"];
|
|
184
|
-
var kbRecordFrontmatterSchema = import_zod.z.object({
|
|
185
|
-
// OKF: the only always-required key. A concept carrying just `type` is
|
|
186
|
-
// fully conformant, so everything below stays optional.
|
|
187
|
-
type: import_zod.z.string().min(1),
|
|
188
|
-
// OKF recommended.
|
|
189
|
-
title: import_zod.z.string().min(1).optional(),
|
|
190
|
-
description: import_zod.z.string().min(1).optional(),
|
|
191
|
-
resource: import_zod.z.string().min(1).optional(),
|
|
192
|
-
tags: import_zod.z.array(import_zod.z.string()).optional(),
|
|
193
|
-
// OKF optional: provenance and freshness.
|
|
194
|
-
sources: import_zod.z.array(kbSourceSchema).optional(),
|
|
195
|
-
generated: kbActorStampSchema.optional(),
|
|
196
|
-
verified: import_zod.z.array(kbActorStampSchema).optional(),
|
|
197
|
-
stale_after: import_zod.z.string().min(1).optional(),
|
|
198
|
-
// strauss extensions — see the module comment.
|
|
199
|
-
strauss_anchors: import_zod.z.array(kbAnchorSchema).optional(),
|
|
200
|
-
strauss_verify: import_zod.z.array(import_zod.z.string().min(1)).optional(),
|
|
201
|
-
// Typed causal edges, source → target, living on the source. `A depends_on
|
|
202
|
-
// B` means A needs B, so `kb_impact` walks these inbound: what breaks if B
|
|
203
|
-
// changes is whatever declared a dependence on it.
|
|
204
|
-
strauss_links: import_zod.z.array(kbLinkSchema).optional(),
|
|
205
|
-
// Total after parsing, tolerant before it. Our producers must supply a
|
|
206
|
-
// status — an absent one would leave every reader inventing its own default
|
|
207
|
-
// — but OKF calls a concept carrying only `type` fully conformant, so
|
|
208
|
-
// rejecting a foreign record for the lack of one would put us outside the
|
|
209
|
-
// spec. The default resolves it in the single place that can: here.
|
|
210
|
-
strauss_status: import_zod.z.enum(KB_RECORD_STATUSES).default("draft"),
|
|
211
|
-
strauss_supersedes: import_zod.z.array(import_zod.z.string().min(1)).optional(),
|
|
212
|
-
strauss_superseded_by: import_zod.z.string().min(1).optional(),
|
|
213
|
-
strauss_answered: kbActorStampSchema.optional(),
|
|
214
|
-
strauss_materiality: import_zod.z.enum(KB_MATERIALITIES).optional(),
|
|
215
|
-
strauss_confidence: import_zod.z.enum(KB_CONFIDENCES).optional(),
|
|
216
|
-
strauss_owner: import_zod.z.string().min(1).optional(),
|
|
217
|
-
// "No source exists" as a field rather than a sentinel entry inside
|
|
218
|
-
// `sources`. A sentinel in a reference list is a value doing work a field
|
|
219
|
-
// should do; as a field, `sources` may be legitimately empty.
|
|
220
|
-
strauss_assumption: import_zod.z.boolean().optional()
|
|
221
|
-
}).passthrough();
|
|
222
|
-
|
|
223
|
-
// src/record-types.ts
|
|
224
|
-
var RECORD_TYPES = {
|
|
225
|
-
fact: {
|
|
226
|
-
purpose: "Observed or sourced fact",
|
|
227
|
-
sections: ["Claim", "Evidence", "Implication"],
|
|
228
|
-
initialStatus: "accepted"
|
|
229
|
-
},
|
|
230
|
-
requirement: {
|
|
231
|
-
purpose: "Required behavior or outcome",
|
|
232
|
-
sections: ["Claim", "Evidence", "Implication"],
|
|
233
|
-
initialStatus: "proposed"
|
|
234
|
-
},
|
|
235
|
-
constraint: {
|
|
236
|
-
purpose: "Limitation, compatibility boundary, policy, or restriction",
|
|
237
|
-
sections: ["Claim", "Evidence", "Implication"],
|
|
238
|
-
initialStatus: "accepted"
|
|
239
|
-
},
|
|
240
|
-
decision: {
|
|
241
|
-
purpose: "Chosen or proposed direction",
|
|
242
|
-
sections: ["Decision", "Rationale", "Rejected", "Impact"],
|
|
243
|
-
initialStatus: "accepted"
|
|
244
|
-
},
|
|
245
|
-
assumption: {
|
|
246
|
-
purpose: "Unsourced or not-yet-confirmed working assumption",
|
|
247
|
-
sections: ["Claim", "Why we think so", "What would falsify it"],
|
|
248
|
-
initialStatus: "draft"
|
|
249
|
-
},
|
|
250
|
-
"open-question": {
|
|
251
|
-
purpose: "Question needing resolution",
|
|
252
|
-
sections: ["Question", "Why it matters", "Default assumption"],
|
|
253
|
-
initialStatus: "open"
|
|
254
|
-
},
|
|
255
|
-
risk: {
|
|
256
|
-
purpose: "Something that can go wrong",
|
|
257
|
-
sections: ["Risk", "Why it matters", "Mitigation", "Verification"],
|
|
258
|
-
initialStatus: "open"
|
|
259
|
-
},
|
|
260
|
-
contract: {
|
|
261
|
-
purpose: "API, data, event, schema, or permission contract",
|
|
262
|
-
sections: ["Contract", "Producer", "Consumer", "Compatibility"],
|
|
263
|
-
initialStatus: "proposed"
|
|
264
|
-
},
|
|
265
|
-
flow: {
|
|
266
|
-
purpose: "Sequence, lifecycle, or state behavior",
|
|
267
|
-
sections: ["Flow", "Trigger", "Steps", "Failure modes"],
|
|
268
|
-
initialStatus: "accepted"
|
|
269
|
-
},
|
|
270
|
-
"affected-system": {
|
|
271
|
-
purpose: "Component, service, package, integration, or external system",
|
|
272
|
-
sections: ["System", "How it is affected", "Blast radius"],
|
|
273
|
-
initialStatus: "accepted"
|
|
274
|
-
},
|
|
275
|
-
"test-obligation": {
|
|
276
|
-
purpose: "Behavior or contract that must be verified",
|
|
277
|
-
sections: ["Obligation", "Why it matters", "How to verify"],
|
|
278
|
-
initialStatus: "open"
|
|
279
|
-
},
|
|
280
|
-
"source-note": {
|
|
281
|
-
purpose: "Extracted note from source material",
|
|
282
|
-
sections: ["Note", "Where it came from"],
|
|
283
|
-
initialStatus: "accepted"
|
|
284
|
-
}
|
|
285
|
-
};
|
|
286
|
-
function isKbRecordType(value) {
|
|
287
|
-
return Object.prototype.hasOwnProperty.call(RECORD_TYPES, value);
|
|
288
|
-
}
|
|
289
|
-
var KB_LINK_RELS = [
|
|
290
|
-
"depends_on",
|
|
291
|
-
"constrains",
|
|
292
|
-
"informs",
|
|
293
|
-
"blocks",
|
|
294
|
-
"invalidates",
|
|
295
|
-
"verified_by",
|
|
296
|
-
"satisfies",
|
|
297
|
-
"related_to"
|
|
298
|
-
];
|
|
299
|
-
var LINK_RELS = {
|
|
300
|
-
depends_on: {
|
|
301
|
-
purpose: "The source needs the target to hold; the source breaks if the target changes",
|
|
302
|
-
phrase: "Depends on",
|
|
303
|
-
dependant: "source"
|
|
304
|
-
},
|
|
305
|
-
constrains: {
|
|
306
|
-
purpose: "The source bounds what the target may do; the target breaks if the constraint changes",
|
|
307
|
-
phrase: "Constrains",
|
|
308
|
-
dependant: "target"
|
|
309
|
-
},
|
|
310
|
-
informs: {
|
|
311
|
-
purpose: "The source shaped the target without binding it; the target is what needs revisiting",
|
|
312
|
-
phrase: "Informs",
|
|
313
|
-
dependant: "target"
|
|
314
|
-
},
|
|
315
|
-
blocks: {
|
|
316
|
-
purpose: "The target cannot proceed until the source is settled; the target is what waits",
|
|
317
|
-
phrase: "Blocks",
|
|
318
|
-
dependant: "target"
|
|
319
|
-
},
|
|
320
|
-
invalidates: {
|
|
321
|
-
purpose: "The source makes the target no longer hold; the target is what stops holding",
|
|
322
|
-
phrase: "Invalidates",
|
|
323
|
-
dependant: "target"
|
|
324
|
-
},
|
|
325
|
-
verified_by: {
|
|
326
|
-
purpose: "The target is the check that confirms the source; the source's confirmation moves with it",
|
|
327
|
-
phrase: "Verified by",
|
|
328
|
-
dependant: "source"
|
|
329
|
-
},
|
|
330
|
-
satisfies: {
|
|
331
|
-
purpose: "The source discharges the target's requirement; the source must change if the requirement does",
|
|
332
|
-
phrase: "Satisfies",
|
|
333
|
-
dependant: "source"
|
|
334
|
-
},
|
|
335
|
-
related_to: {
|
|
336
|
-
purpose: "A pointer worth following, with no claim of dependence",
|
|
337
|
-
phrase: "Relates to",
|
|
338
|
-
dependant: null
|
|
339
|
-
}
|
|
340
|
-
};
|
|
341
|
-
var KB_CAUSAL_LINK_RELS = KB_LINK_RELS.filter(
|
|
342
|
-
(rel) => LINK_RELS[rel].dependant !== null
|
|
343
|
-
);
|
|
344
|
-
function isKbLinkRel(value) {
|
|
345
|
-
return Object.prototype.hasOwnProperty.call(LINK_RELS, value);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// src/compose.ts
|
|
349
|
-
var composeLinkSchema = import_zod2.z.object({
|
|
350
|
-
target: kbConceptIdSchema,
|
|
351
|
-
rel: import_zod2.z.enum(KB_LINK_RELS)
|
|
352
|
-
}).strict();
|
|
353
|
-
var composeInputSchema = import_zod2.z.object({
|
|
354
|
-
slug: import_zod2.z.string().min(1),
|
|
355
|
-
/** One line, in the reader's terms. Becomes OKF `title`. */
|
|
356
|
-
title: import_zod2.z.string().min(1),
|
|
357
|
-
/** The consequence — what breaks if this is wrong. Becomes `description`. */
|
|
358
|
-
why: import_zod2.z.string().min(1),
|
|
359
|
-
/** Keyed by section heading from the type's spec. Unknown keys rejected. */
|
|
360
|
-
sections: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.string().min(1)).optional(),
|
|
361
|
-
anchors: import_zod2.z.array(kbAnchorWriteSchema).optional(),
|
|
362
|
-
sources: import_zod2.z.array(kbSourceSchema).optional(),
|
|
363
|
-
/** No source exists, as a claim rather than a sentinel in `sources`. */
|
|
364
|
-
assumption: import_zod2.z.boolean().optional(),
|
|
365
|
-
/**
|
|
366
|
-
* OKF `stale_after`: the absolute date this record stops being trusted.
|
|
367
|
-
* Anything the outside world can change — pricing, quotas, versions,
|
|
368
|
-
* reception counts — should carry one.
|
|
369
|
-
*/
|
|
370
|
-
stale_after: import_zod2.z.string().regex(/^\d{4}-\d{2}-\d{2}$/, {
|
|
371
|
-
message: "stale_after must be YYYY-MM-DD"
|
|
372
|
-
}).refine((date) => !Number.isNaN(Date.parse(date)), {
|
|
373
|
-
message: "stale_after must be a real date"
|
|
374
|
-
}).optional(),
|
|
375
|
-
verify: import_zod2.z.array(import_zod2.z.string().min(1)).optional(),
|
|
376
|
-
tags: import_zod2.z.array(import_zod2.z.string().min(1)).optional(),
|
|
377
|
-
/** Concept ids this record relates to; rendered as body links. */
|
|
378
|
-
relatedConceptIds: import_zod2.z.array(kbConceptIdSchema).optional(),
|
|
379
|
-
/**
|
|
380
|
-
* Typed causal edges, source → target: `{ target: "fact.b", rel:
|
|
381
|
-
* "depends_on" }` on record A says A needs B. Stored in frontmatter and
|
|
382
|
-
* also rendered as one prose sentence each, so the meaning survives a
|
|
383
|
-
* reader that knows only OKF. The vocabulary goes into the description from
|
|
384
|
-
* the same table the walk uses, so `kb_schema` emits it.
|
|
385
|
-
*/
|
|
386
|
-
links: import_zod2.z.array(composeLinkSchema).max(64).optional().describe(
|
|
387
|
-
`Typed causal edges, source \u2192 target \u2014 a link on this record says this record <rel> the target. ${KB_LINK_RELS.map(
|
|
388
|
-
(rel) => `${rel}: ${LINK_RELS[rel].purpose}`
|
|
389
|
-
).join("; ")}.`
|
|
390
|
-
),
|
|
391
|
-
/** Concept ids this record replaces. The store settles the backlinks. */
|
|
392
|
-
supersedes: import_zod2.z.array(kbConceptIdSchema).max(32).optional(),
|
|
393
|
-
materiality: import_zod2.z.enum(KB_MATERIALITIES).optional(),
|
|
394
|
-
confidence: import_zod2.z.enum(KB_CONFIDENCES).optional(),
|
|
395
|
-
owner: import_zod2.z.string().min(1).optional()
|
|
396
|
-
}).strict();
|
|
397
|
-
function composeRecord(type, input, writtenBy, writtenAt) {
|
|
398
|
-
const parsed = composeInputSchema.parse(input);
|
|
399
|
-
const spec = RECORD_TYPES[type];
|
|
400
|
-
const sections = parsed.sections ?? {};
|
|
401
|
-
const unknown = Object.keys(sections).filter(
|
|
402
|
-
(heading) => !spec.sections.includes(heading)
|
|
403
|
-
);
|
|
404
|
-
if (unknown.length) {
|
|
405
|
-
throw new Error(
|
|
406
|
-
`kb: ${type} has no section ${unknown.join(", ")} \u2014 expected one of ${spec.sections.join(", ")}`
|
|
407
|
-
);
|
|
408
|
-
}
|
|
409
|
-
const frontmatter = {
|
|
410
|
-
title: parsed.title,
|
|
411
|
-
description: parsed.why,
|
|
412
|
-
generated: { by: writtenBy, at: writtenAt },
|
|
413
|
-
// Empty rather than absent: a later verification pass appends here, and an
|
|
414
|
-
// empty list says "not yet verified" where a missing key would only say
|
|
415
|
-
// "this producer didn't think about it".
|
|
416
|
-
verified: [],
|
|
417
|
-
strauss_status: spec.initialStatus
|
|
418
|
-
};
|
|
419
|
-
if (parsed.stale_after) frontmatter.stale_after = parsed.stale_after;
|
|
420
|
-
if (parsed.anchors?.length) frontmatter.strauss_anchors = parsed.anchors;
|
|
421
|
-
if (parsed.verify?.length) frontmatter.strauss_verify = parsed.verify;
|
|
422
|
-
if (parsed.tags?.length) frontmatter.tags = parsed.tags;
|
|
423
|
-
if (parsed.sources?.length) frontmatter.sources = parsed.sources;
|
|
424
|
-
if (parsed.assumption) frontmatter.strauss_assumption = true;
|
|
425
|
-
if (parsed.materiality) frontmatter.strauss_materiality = parsed.materiality;
|
|
426
|
-
if (parsed.confidence) frontmatter.strauss_confidence = parsed.confidence;
|
|
427
|
-
if (parsed.owner) frontmatter.strauss_owner = parsed.owner;
|
|
428
|
-
if (parsed.supersedes?.length)
|
|
429
|
-
frontmatter.strauss_supersedes = parsed.supersedes;
|
|
430
|
-
const selfLink = parsed.links?.find(
|
|
431
|
-
(link2) => link2.target === `${type}.${parsed.slug}`
|
|
432
|
-
);
|
|
433
|
-
if (selfLink) {
|
|
434
|
-
throw new Error(
|
|
435
|
-
`kb: ${type}.${parsed.slug} cannot ${selfLink.rel} itself \u2014 a link must name another record`
|
|
436
|
-
);
|
|
437
|
-
}
|
|
438
|
-
if (parsed.links?.length) frontmatter.strauss_links = parsed.links;
|
|
439
|
-
const blocks = [];
|
|
440
|
-
for (const heading of spec.sections) {
|
|
441
|
-
const text = sections[heading];
|
|
442
|
-
if (text) blocks.push(`## ${heading}
|
|
443
|
-
|
|
444
|
-
${text}`);
|
|
445
|
-
}
|
|
446
|
-
if (!blocks.length) blocks.push(parsed.why);
|
|
447
|
-
for (const related of parsed.relatedConceptIds ?? []) {
|
|
448
|
-
blocks.push(`Relates to [${related}](${related}.md).`);
|
|
449
|
-
}
|
|
450
|
-
for (const link2 of parsed.links ?? []) {
|
|
451
|
-
blocks.push(
|
|
452
|
-
`${LINK_RELS[link2.rel].phrase} [${link2.target}](${link2.target}.md).`
|
|
453
|
-
);
|
|
454
|
-
}
|
|
455
|
-
if (parsed.sources?.length) {
|
|
456
|
-
blocks.push(
|
|
457
|
-
parsed.sources.map((source) => `[^${source.id}]: ${source.title ?? source.resource}`).join("\n")
|
|
458
|
-
);
|
|
459
|
-
}
|
|
460
|
-
return {
|
|
461
|
-
type,
|
|
462
|
-
slug: parsed.slug,
|
|
463
|
-
frontmatter,
|
|
464
|
-
body: `${blocks.join("\n\n")}
|
|
465
|
-
`
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
// src/decision-record.ts
|
|
470
|
-
var DECISION_TYPE = "decision";
|
|
471
|
-
var NO_DECISION_SLUG = "none";
|
|
472
|
-
var decisionInputSchema = composeInputSchema.omit({ sections: true }).extend({
|
|
473
|
-
alternative: import_zod3.z.string().min(1).optional(),
|
|
474
|
-
impact: import_zod3.z.string().min(1).optional()
|
|
475
|
-
}).strict();
|
|
476
|
-
function composeDecisionRecord(input, writtenBy, writtenAt) {
|
|
477
|
-
const { alternative, impact: impact2, ...rest } = input;
|
|
478
|
-
return composeRecord(
|
|
479
|
-
DECISION_TYPE,
|
|
480
|
-
{
|
|
481
|
-
...rest,
|
|
482
|
-
sections: {
|
|
483
|
-
Decision: input.title,
|
|
484
|
-
Rationale: input.why,
|
|
485
|
-
...alternative ? { Rejected: alternative } : {},
|
|
486
|
-
...impact2 ? { Impact: impact2 } : {}
|
|
487
|
-
}
|
|
488
|
-
},
|
|
489
|
-
writtenBy,
|
|
490
|
-
writtenAt
|
|
491
|
-
);
|
|
492
|
-
}
|
|
493
|
-
function composeNoDecisionRecord(reason, writtenBy, writtenAt) {
|
|
494
|
-
return composeRecord(
|
|
495
|
-
DECISION_TYPE,
|
|
496
|
-
{
|
|
497
|
-
slug: NO_DECISION_SLUG,
|
|
498
|
-
title: "No decision to record",
|
|
499
|
-
why: reason,
|
|
500
|
-
sections: { Decision: reason }
|
|
501
|
-
},
|
|
502
|
-
writtenBy,
|
|
503
|
-
writtenAt
|
|
504
|
-
);
|
|
505
|
-
}
|
|
506
|
-
function isNoDecisionRecord(record) {
|
|
507
|
-
return record.conceptId === `${DECISION_TYPE}.${NO_DECISION_SLUG}`;
|
|
508
|
-
}
|
|
509
|
-
function selectDecisions(records) {
|
|
510
|
-
return records.filter(
|
|
511
|
-
(record) => record.conceptId.startsWith(`${DECISION_TYPE}.`) && !isNoDecisionRecord(record)
|
|
512
|
-
);
|
|
513
|
-
}
|
|
35
|
+
var import_zod4 = require("zod");
|
|
514
36
|
|
|
515
|
-
// src/
|
|
516
|
-
var
|
|
37
|
+
// src/compose.ts
|
|
38
|
+
var import_zod3 = require("zod");
|
|
517
39
|
|
|
518
40
|
// src/concurrency.ts
|
|
519
41
|
var DEFAULT_IO_CONCURRENCY = 16;
|
|
@@ -1327,26 +849,26 @@ var import_node_path4 = require("path");
|
|
|
1327
849
|
var import_node_url = require("url");
|
|
1328
850
|
|
|
1329
851
|
// src/grammars/model.ts
|
|
1330
|
-
var
|
|
1331
|
-
var sha2562 =
|
|
1332
|
-
var grammarWasmSchema =
|
|
1333
|
-
url:
|
|
852
|
+
var import_zod = require("zod");
|
|
853
|
+
var sha2562 = import_zod.z.string().regex(/^[0-9a-f]{64}$/);
|
|
854
|
+
var grammarWasmSchema = import_zod.z.object({
|
|
855
|
+
url: import_zod.z.string().min(1),
|
|
1334
856
|
sha256: sha2562,
|
|
1335
|
-
bytes:
|
|
857
|
+
bytes: import_zod.z.number().int().positive()
|
|
1336
858
|
});
|
|
1337
|
-
var grammarTagsSchema =
|
|
1338
|
-
var grammarPackSchema =
|
|
1339
|
-
package:
|
|
859
|
+
var grammarTagsSchema = import_zod.z.object({ url: import_zod.z.string().min(1), sha256: sha2562 });
|
|
860
|
+
var grammarPackSchema = import_zod.z.object({
|
|
861
|
+
package: import_zod.z.string().min(1),
|
|
1340
862
|
wasm: grammarWasmSchema,
|
|
1341
|
-
tags:
|
|
1342
|
-
license:
|
|
1343
|
-
extensions:
|
|
863
|
+
tags: import_zod.z.array(grammarTagsSchema),
|
|
864
|
+
license: import_zod.z.string().min(1),
|
|
865
|
+
extensions: import_zod.z.array(import_zod.z.string().min(1))
|
|
1344
866
|
});
|
|
1345
|
-
var grammarManifestSchema =
|
|
867
|
+
var grammarManifestSchema = import_zod.z.object({
|
|
1346
868
|
/** The runtime the packs were proved against. */
|
|
1347
|
-
webTreeSitter:
|
|
1348
|
-
linguist:
|
|
1349
|
-
packs:
|
|
869
|
+
webTreeSitter: import_zod.z.string().min(1),
|
|
870
|
+
linguist: import_zod.z.object({ tag: import_zod.z.string().min(1), commit: import_zod.z.string().min(1) }),
|
|
871
|
+
packs: import_zod.z.record(import_zod.z.string().min(1), grammarPackSchema)
|
|
1350
872
|
});
|
|
1351
873
|
|
|
1352
874
|
// src/grammars/manifest.ts
|
|
@@ -2027,262 +1549,849 @@ function sliceSpan(source, range) {
|
|
|
2027
1549
|
resolver: "span"
|
|
2028
1550
|
};
|
|
2029
1551
|
}
|
|
2030
|
-
function fromResolve(resolver, source, symbol, file) {
|
|
2031
|
-
const span2 = resolver.resolve(source, symbol, file);
|
|
2032
|
-
return span2 ? { kind: "resolved", span: span2 } : { kind: "unresolved", reason: "symbol-not-found" };
|
|
2033
|
-
}
|
|
2034
|
-
function isResolverName(name) {
|
|
2035
|
-
return name === "tree-sitter" || name === "regex" || name === "span";
|
|
2036
|
-
}
|
|
2037
|
-
async function prepareResolvers(resolvers, files) {
|
|
2038
|
-
for (const resolver of resolvers) await resolver.prepare?.(files);
|
|
1552
|
+
function fromResolve(resolver, source, symbol, file) {
|
|
1553
|
+
const span2 = resolver.resolve(source, symbol, file);
|
|
1554
|
+
return span2 ? { kind: "resolved", span: span2 } : { kind: "unresolved", reason: "symbol-not-found" };
|
|
1555
|
+
}
|
|
1556
|
+
function isResolverName(name) {
|
|
1557
|
+
return name === "tree-sitter" || name === "regex" || name === "span";
|
|
1558
|
+
}
|
|
1559
|
+
async function prepareResolvers(resolvers, files) {
|
|
1560
|
+
for (const resolver of resolvers) await resolver.prepare?.(files);
|
|
1561
|
+
}
|
|
1562
|
+
function defaultAnchorResolvers(grammars = {}) {
|
|
1563
|
+
return [new TreeSitterResolver(grammars), regexResolver];
|
|
1564
|
+
}
|
|
1565
|
+
function resolverChanged(source, anchor, produced) {
|
|
1566
|
+
const previous = anchor.resolver ?? "regex";
|
|
1567
|
+
if (!produced || !anchor.symbol || previous === produced) return false;
|
|
1568
|
+
if (previous !== "regex") return false;
|
|
1569
|
+
const before = regexResolver.resolve(
|
|
1570
|
+
source.replace(/\r\n/g, "\n"),
|
|
1571
|
+
anchor.symbol
|
|
1572
|
+
);
|
|
1573
|
+
return before !== null && hashAnchorText(before.text) === anchor.hash;
|
|
1574
|
+
}
|
|
1575
|
+
function anchorHashOf(anchor, outcome) {
|
|
1576
|
+
if (outcome.resolver === "span") {
|
|
1577
|
+
return { hash: hashAnchorText(outcome.span.text), kind: "raw" };
|
|
1578
|
+
}
|
|
1579
|
+
const stored = anchor.hash ? anchor.hash_kind ?? "raw" : void 0;
|
|
1580
|
+
const wanted = stored ?? (outcome.normalized ? "ast" : "raw");
|
|
1581
|
+
return wanted === "ast" && outcome.normalized ? { hash: hashAnchorText(outcome.normalized), kind: "ast" } : { hash: hashAnchorText(outcome.span.text), kind: "raw" };
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
// src/anchor-resolver/drift.ts
|
|
1585
|
+
async function detectAnchorDrift(records, options = {}) {
|
|
1586
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
1587
|
+
const resolvers = options.resolvers ?? (options.resolver ? [options.resolver] : defaultAnchorResolvers({
|
|
1588
|
+
offline: options.remote?.offline === true
|
|
1589
|
+
}));
|
|
1590
|
+
const origin = new LazyOrigin(repoRoot);
|
|
1591
|
+
const planned = /* @__PURE__ */ new Map();
|
|
1592
|
+
let declaresRepo = false;
|
|
1593
|
+
for (const record of records) {
|
|
1594
|
+
const anchors = (record.frontmatter.strauss_anchors ?? []).filter(
|
|
1595
|
+
(anchor) => anchor.hash
|
|
1596
|
+
);
|
|
1597
|
+
if (!anchors.length) continue;
|
|
1598
|
+
if (anchors.some((anchor) => anchor.repo)) declaresRepo = true;
|
|
1599
|
+
planned.set(
|
|
1600
|
+
record.conceptId,
|
|
1601
|
+
anchors.map((anchor) => ({ anchor, foreign: false }))
|
|
1602
|
+
);
|
|
1603
|
+
}
|
|
1604
|
+
if (declaresRepo) {
|
|
1605
|
+
await origin.prime();
|
|
1606
|
+
for (const entries of planned.values()) {
|
|
1607
|
+
for (const entry of entries)
|
|
1608
|
+
entry.foreign = origin.isForeign(entry.anchor);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
const files = [];
|
|
1612
|
+
const committedWants = [];
|
|
1613
|
+
const wants = [];
|
|
1614
|
+
for (const entries of planned.values()) {
|
|
1615
|
+
for (const { anchor, foreign } of entries) {
|
|
1616
|
+
if (foreign) wants.push(...remoteWants(anchor));
|
|
1617
|
+
else if (anchor.side === "old") committedWants.push(anchor);
|
|
1618
|
+
else files.push(anchor.file);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
const [reads, committed, remote] = await Promise.all([
|
|
1622
|
+
readAnchorFiles(
|
|
1623
|
+
files,
|
|
1624
|
+
options.reader ?? anchorFileReader(repoRoot),
|
|
1625
|
+
options.concurrency ?? DEFAULT_IO_CONCURRENCY
|
|
1626
|
+
),
|
|
1627
|
+
readCommitted(repoRoot, committedWants, options),
|
|
1628
|
+
(options.readRemote ?? readRemoteAnchors)(wants, options.remote ?? {})
|
|
1629
|
+
]);
|
|
1630
|
+
await prepareResolvers(resolvers, [
|
|
1631
|
+
...files,
|
|
1632
|
+
...committedWants.map((anchor) => anchor.file),
|
|
1633
|
+
...wants.map((want) => want.file)
|
|
1634
|
+
]);
|
|
1635
|
+
const drift = /* @__PURE__ */ new Map();
|
|
1636
|
+
for (const record of records) {
|
|
1637
|
+
const entries = [];
|
|
1638
|
+
for (const { anchor, foreign } of planned.get(record.conceptId) ?? []) {
|
|
1639
|
+
if (foreign) {
|
|
1640
|
+
entries.push(remoteEntry(anchor, remote, resolvers));
|
|
1641
|
+
continue;
|
|
1642
|
+
}
|
|
1643
|
+
const read = anchor.side === "old" ? committed.get(atRefKey(anchor)) : reads.get(anchor.file);
|
|
1644
|
+
entries.push(localEntry(anchor, read, resolvers));
|
|
1645
|
+
}
|
|
1646
|
+
if (entries.length) drift.set(record.conceptId, entries);
|
|
1647
|
+
}
|
|
1648
|
+
return drift;
|
|
1649
|
+
}
|
|
1650
|
+
function atRefKey(anchor) {
|
|
1651
|
+
return `${anchor.ref ?? ""}\0${anchor.file}`;
|
|
1652
|
+
}
|
|
1653
|
+
async function readCommitted(repoRoot, anchors, options = {}) {
|
|
1654
|
+
if (!anchors.length) return /* @__PURE__ */ new Map();
|
|
1655
|
+
const read = options.readAtRef ?? readFileAtRef;
|
|
1656
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1657
|
+
for (const anchor of anchors) byKey.set(atRefKey(anchor), anchor);
|
|
1658
|
+
const keys = [...byKey.keys()];
|
|
1659
|
+
const results = await mapLimit(
|
|
1660
|
+
keys,
|
|
1661
|
+
options.concurrency ?? DEFAULT_IO_CONCURRENCY,
|
|
1662
|
+
(key2) => read(repoRoot, byKey.get(key2))
|
|
1663
|
+
);
|
|
1664
|
+
return new Map(keys.map((key2, at2) => [key2, results[at2]]));
|
|
1665
|
+
}
|
|
1666
|
+
function remoteWants(anchor) {
|
|
1667
|
+
const repo = anchor.repo;
|
|
1668
|
+
const wants = [{ repo, file: anchor.file }];
|
|
1669
|
+
if (anchor.ref) wants.unshift({ repo, ref: anchor.ref, file: anchor.file });
|
|
1670
|
+
return wants;
|
|
1671
|
+
}
|
|
1672
|
+
function base(anchor) {
|
|
1673
|
+
return {
|
|
1674
|
+
file: anchor.file,
|
|
1675
|
+
...anchor.symbol ? { symbol: anchor.symbol } : {},
|
|
1676
|
+
...anchor.side === "old" ? { side: "old" } : {},
|
|
1677
|
+
storedHash: anchor.hash
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
function unresolved(anchor, reason, repo) {
|
|
1681
|
+
return {
|
|
1682
|
+
...base(anchor),
|
|
1683
|
+
state: "unresolved",
|
|
1684
|
+
diffSize: null,
|
|
1685
|
+
...reason ? { reason } : {},
|
|
1686
|
+
...repo ? { repo } : {},
|
|
1687
|
+
...classOf(reason)
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
var GONE_REASONS = /* @__PURE__ */ new Set([
|
|
1691
|
+
"file-missing",
|
|
1692
|
+
"symbol-not-found",
|
|
1693
|
+
"span-out-of-range",
|
|
1694
|
+
"ref-unreadable"
|
|
1695
|
+
]);
|
|
1696
|
+
function provisionalDriftClass(entry) {
|
|
1697
|
+
if (entry.state === "unresolved") {
|
|
1698
|
+
return GONE_REASONS.has(entry.reason) ? "gone" : void 0;
|
|
1699
|
+
}
|
|
1700
|
+
return entry.state === "drifted" ? "changed" : void 0;
|
|
1701
|
+
}
|
|
1702
|
+
function classOf(reason) {
|
|
1703
|
+
const settled = provisionalDriftClass({ state: "unresolved", reason });
|
|
1704
|
+
return settled ? { class: settled } : {};
|
|
1705
|
+
}
|
|
1706
|
+
function hashIn(source, anchor, resolvers) {
|
|
1707
|
+
const outcome = resolveAnchorSpan(source, anchor, resolvers);
|
|
1708
|
+
if (!outcome.ok) return { ok: false, reason: outcome.reason };
|
|
1709
|
+
const { hash, kind } = anchorHashOf(anchor, outcome);
|
|
1710
|
+
return {
|
|
1711
|
+
ok: true,
|
|
1712
|
+
current: {
|
|
1713
|
+
hash,
|
|
1714
|
+
kind,
|
|
1715
|
+
lines: outcome.span.endLine - outcome.span.startLine + 1,
|
|
1716
|
+
...outcome.resolver ? { resolver: outcome.resolver } : {}
|
|
1717
|
+
}
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
function resolverExtras(source, anchor, current) {
|
|
1721
|
+
return {
|
|
1722
|
+
...current.resolver ? { resolver: current.resolver } : {},
|
|
1723
|
+
...current.hash !== anchor.hash && resolverChanged(source, anchor, current.resolver) ? { reason: "resolver-changed" } : {}
|
|
1724
|
+
};
|
|
2039
1725
|
}
|
|
2040
|
-
function
|
|
2041
|
-
|
|
1726
|
+
function compared(anchor, current, extra = {}) {
|
|
1727
|
+
const matched = current.hash === anchor.hash;
|
|
1728
|
+
return {
|
|
1729
|
+
...base(anchor),
|
|
1730
|
+
state: matched ? "match" : "drifted",
|
|
1731
|
+
currentHash: current.hash,
|
|
1732
|
+
hashKind: current.kind,
|
|
1733
|
+
diffSize: anchor.lines === void 0 ? null : Math.abs(current.lines - anchor.lines),
|
|
1734
|
+
...matched ? {} : { class: "changed" },
|
|
1735
|
+
...extra
|
|
1736
|
+
};
|
|
2042
1737
|
}
|
|
2043
|
-
function
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
if (
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
1738
|
+
function localEntry(anchor, read, resolvers) {
|
|
1739
|
+
if (!read.ok) return unresolved(anchor, read.reason);
|
|
1740
|
+
const found = hashIn(read.source, anchor, resolvers);
|
|
1741
|
+
if (!found.ok) return unresolved(anchor, found.reason);
|
|
1742
|
+
return compared(
|
|
1743
|
+
anchor,
|
|
1744
|
+
found.current,
|
|
1745
|
+
resolverExtras(read.source, anchor, found.current)
|
|
2050
1746
|
);
|
|
2051
|
-
return before !== null && hashAnchorText(before.text) === anchor.hash;
|
|
2052
1747
|
}
|
|
2053
|
-
function
|
|
2054
|
-
|
|
2055
|
-
|
|
1748
|
+
function remoteEntry(anchor, remote, resolvers) {
|
|
1749
|
+
const repo = anchor.repo;
|
|
1750
|
+
const key2 = normalizeRepoUrl(repo);
|
|
1751
|
+
const atDefault = remote.get(wantKey(key2, void 0, anchor.file));
|
|
1752
|
+
const primary = anchor.ref ? remote.get(wantKey(key2, anchor.ref, anchor.file)) : atDefault;
|
|
1753
|
+
if (!primary) return unresolved(anchor, "remote-unreachable", repo);
|
|
1754
|
+
if (!primary.ok) return unresolved(anchor, primary.reason, repo);
|
|
1755
|
+
const found = hashIn(primary.source, anchor, resolvers);
|
|
1756
|
+
if (!found.ok) return unresolved(anchor, found.reason, repo);
|
|
1757
|
+
const current = found.current;
|
|
1758
|
+
const extras = resolverExtras(primary.source, anchor, current);
|
|
1759
|
+
if (!anchor.ref) return compared(anchor, current, { repo, ...extras });
|
|
1760
|
+
if (current.hash !== anchor.hash) {
|
|
1761
|
+
return compared(anchor, current, {
|
|
1762
|
+
repo,
|
|
1763
|
+
...extras,
|
|
1764
|
+
remoteState: "drifted-from-ref"
|
|
1765
|
+
});
|
|
2056
1766
|
}
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
1767
|
+
if (anchor.side === "old") {
|
|
1768
|
+
return compared(anchor, current, {
|
|
1769
|
+
repo,
|
|
1770
|
+
...extras,
|
|
1771
|
+
remoteState: "matches-ref"
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
const head = atDefault?.ok ? hashIn(atDefault.source, anchor, resolvers) : null;
|
|
1775
|
+
return head?.ok && head.current.hash !== anchor.hash ? {
|
|
1776
|
+
...compared(anchor, head.current, {
|
|
1777
|
+
repo,
|
|
1778
|
+
...head.current.resolver ? { resolver: head.current.resolver } : {}
|
|
1779
|
+
}),
|
|
1780
|
+
state: "drifted",
|
|
1781
|
+
remoteState: "drifted-on-default"
|
|
1782
|
+
} : compared(anchor, current, {
|
|
1783
|
+
repo,
|
|
1784
|
+
...extras,
|
|
1785
|
+
remoteState: "matches-ref"
|
|
1786
|
+
});
|
|
2060
1787
|
}
|
|
2061
1788
|
|
|
2062
|
-
// src/
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
1789
|
+
// src/kb-record.schema.ts
|
|
1790
|
+
var import_zod2 = require("zod");
|
|
1791
|
+
var kbSourceSchema = import_zod2.z.object({
|
|
1792
|
+
id: import_zod2.z.string().min(1),
|
|
1793
|
+
resource: import_zod2.z.string().min(1),
|
|
1794
|
+
title: import_zod2.z.string().min(1).optional(),
|
|
1795
|
+
author: import_zod2.z.string().min(1).optional(),
|
|
1796
|
+
last_modified: import_zod2.z.string().min(1).optional()
|
|
1797
|
+
}).passthrough();
|
|
1798
|
+
var kbActorStampSchema = import_zod2.z.object({
|
|
1799
|
+
by: import_zod2.z.string().min(1),
|
|
1800
|
+
at: import_zod2.z.string().min(1)
|
|
1801
|
+
}).passthrough();
|
|
1802
|
+
var kbVerifiedEventSchema = kbActorStampSchema.extend({
|
|
1803
|
+
note: import_zod2.z.string().refine((s) => s.trim().length > 0, {
|
|
1804
|
+
message: "note must say what the check found"
|
|
1805
|
+
})
|
|
1806
|
+
});
|
|
1807
|
+
var kbAnchorSpanSchema = import_zod2.z.object({
|
|
1808
|
+
start: import_zod2.z.number().int().positive(),
|
|
1809
|
+
end: import_zod2.z.number().int().positive()
|
|
1810
|
+
}).strict();
|
|
1811
|
+
var kbAnchorSchema = import_zod2.z.object({
|
|
1812
|
+
file: import_zod2.z.string().min(1),
|
|
1813
|
+
symbol: import_zod2.z.string().min(1).optional(),
|
|
1814
|
+
/**
|
|
1815
|
+
* The lines the concept names, when no symbol covers them — deleted code,
|
|
1816
|
+
* YAML, SQL, Markdown. Alternative to `symbol`, never a refinement of it.
|
|
1817
|
+
*/
|
|
1818
|
+
span: kbAnchorSpanSchema.optional(),
|
|
1819
|
+
/**
|
|
1820
|
+
* Which side of the change the anchor describes. `old` is code as it was
|
|
1821
|
+
* committed at `ref`, which is the only way to anchor something deleted;
|
|
1822
|
+
* absent means the working tree.
|
|
1823
|
+
*/
|
|
1824
|
+
side: import_zod2.z.enum(["old", "new"]).optional(),
|
|
1825
|
+
/**
|
|
1826
|
+
* Which repository the file lives in — a remote URL
|
|
1827
|
+
* (`https://github.com/org/name`) or a short name. Absent means the base's
|
|
1828
|
+
* own repository, which is what nearly every anchor means.
|
|
1829
|
+
*
|
|
1830
|
+
* Unvalidated beyond not-blank: one repository has many spellings, matched
|
|
1831
|
+
* after normalisation. Only a full URL can be fetched from, so `validate`
|
|
1832
|
+
* warns on a short one; see ARCHITECTURE.
|
|
1833
|
+
*/
|
|
1834
|
+
repo: import_zod2.z.string().trim().min(1).optional(),
|
|
1835
|
+
/**
|
|
1836
|
+
* The git rev the evidence was taken at. Prefer a commit SHA: a branch
|
|
1837
|
+
* name is a moving pointer, so an anchor pinned to one says the evidence
|
|
1838
|
+
* came from wherever that branch happens to be now, which is not a
|
|
1839
|
+
* baseline. A foreign anchor is checked at this rev, and compared against
|
|
1840
|
+
* the remote's default branch on top of it.
|
|
1841
|
+
*/
|
|
1842
|
+
ref: import_zod2.z.string().trim().min(1).optional(),
|
|
1843
|
+
hash: import_zod2.z.string().regex(/^sha256:[0-9a-f]{64}$/, {
|
|
1844
|
+
message: "hash must be sha256:<64 hex chars>"
|
|
1845
|
+
}).optional(),
|
|
1846
|
+
/**
|
|
1847
|
+
* What `hash` was taken over: the span's raw text, or the normalised token
|
|
1848
|
+
* stream a parser sees (`ast`). Absent means `raw`, which is what every
|
|
1849
|
+
* anchor stamped before this field carries, so old hashes keep comparing
|
|
1850
|
+
* the way they were written. An `ast` hash is blind to whitespace and
|
|
1851
|
+
* comments, so reformatting the anchored code is not drift.
|
|
1852
|
+
*/
|
|
1853
|
+
hash_kind: import_zod2.z.enum(["raw", "ast"]).optional(),
|
|
1854
|
+
/** ISO 8601 timestamp of the last successful resolution. */
|
|
1855
|
+
resolved_at: import_zod2.z.string().min(1).optional(),
|
|
1856
|
+
/** Line count of the text the hash was taken over. */
|
|
1857
|
+
lines: import_zod2.z.number().int().positive().optional(),
|
|
1858
|
+
/**
|
|
1859
|
+
* Which resolver produced the hashed span. Absent means an anchor stamped
|
|
1860
|
+
* before resolvers were named, which is read as `regex` — the only one
|
|
1861
|
+
* there was. A hash from a different resolver is drift, not a match.
|
|
1862
|
+
*/
|
|
1863
|
+
resolver: import_zod2.z.enum(["tree-sitter", "regex", "span"]).optional()
|
|
1864
|
+
}).strict();
|
|
1865
|
+
var kbAnchorWriteSchema = kbAnchorSchema.superRefine((anchor, ctx) => {
|
|
1866
|
+
if (anchor.span && anchor.symbol) {
|
|
1867
|
+
ctx.addIssue({
|
|
1868
|
+
code: import_zod2.z.ZodIssueCode.custom,
|
|
1869
|
+
path: ["span"],
|
|
1870
|
+
message: "an anchor names a symbol or a span, not both"
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
if (anchor.span && anchor.span.end < anchor.span.start) {
|
|
1874
|
+
ctx.addIssue({
|
|
1875
|
+
code: import_zod2.z.ZodIssueCode.custom,
|
|
1876
|
+
path: ["span", "end"],
|
|
1877
|
+
message: "span end must not precede start"
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
if (anchor.span && anchor.hash_kind === "ast") {
|
|
1881
|
+
ctx.addIssue({
|
|
1882
|
+
code: import_zod2.z.ZodIssueCode.custom,
|
|
1883
|
+
path: ["hash_kind"],
|
|
1884
|
+
message: "a span is hashed raw, never ast"
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
if (anchor.side === "old" && !anchor.ref) {
|
|
1888
|
+
ctx.addIssue({
|
|
1889
|
+
code: import_zod2.z.ZodIssueCode.custom,
|
|
1890
|
+
path: ["ref"],
|
|
1891
|
+
message: 'side: "old" needs a ref \u2014 committed code has no other address'
|
|
1892
|
+
});
|
|
1893
|
+
}
|
|
1894
|
+
});
|
|
1895
|
+
var kbAnchorLocatorSchema = kbAnchorSchema.pick({
|
|
1896
|
+
file: true,
|
|
1897
|
+
symbol: true,
|
|
1898
|
+
span: true,
|
|
1899
|
+
side: true,
|
|
1900
|
+
repo: true,
|
|
1901
|
+
ref: true
|
|
1902
|
+
});
|
|
1903
|
+
var kbLinkSchema = import_zod2.z.object({
|
|
1904
|
+
target: import_zod2.z.string().min(1),
|
|
1905
|
+
rel: import_zod2.z.string().min(1)
|
|
1906
|
+
}).passthrough();
|
|
1907
|
+
var KB_RECORD_TYPES = [
|
|
1908
|
+
"fact",
|
|
1909
|
+
"requirement",
|
|
1910
|
+
"constraint",
|
|
1911
|
+
"decision",
|
|
1912
|
+
"assumption",
|
|
1913
|
+
"open-question",
|
|
1914
|
+
"risk",
|
|
1915
|
+
"contract",
|
|
1916
|
+
"flow",
|
|
1917
|
+
"affected-system",
|
|
1918
|
+
"test-obligation",
|
|
1919
|
+
"source-note"
|
|
1920
|
+
];
|
|
1921
|
+
var KB_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1922
|
+
var KB_CONCEPT_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*\.[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1923
|
+
var kbConceptIdSchema = import_zod2.z.string().regex(KB_CONCEPT_ID_PATTERN, {
|
|
1924
|
+
message: "concept id must be <type>.<slug>, both kebab-case"
|
|
1925
|
+
});
|
|
1926
|
+
var KB_RECORD_STATUSES = [
|
|
1927
|
+
"draft",
|
|
1928
|
+
"proposed",
|
|
1929
|
+
"accepted",
|
|
1930
|
+
"open",
|
|
1931
|
+
"resolved",
|
|
1932
|
+
"rejected",
|
|
1933
|
+
"superseded"
|
|
1934
|
+
];
|
|
1935
|
+
var KB_MATERIALITIES = [
|
|
1936
|
+
"blocking",
|
|
1937
|
+
"important",
|
|
1938
|
+
"non-blocking"
|
|
1939
|
+
];
|
|
1940
|
+
var KB_CONFIDENCES = ["low", "medium", "high"];
|
|
1941
|
+
var kbRecordFrontmatterSchema = import_zod2.z.object({
|
|
1942
|
+
// OKF: the only always-required key. A concept carrying just `type` is
|
|
1943
|
+
// fully conformant, so everything below stays optional.
|
|
1944
|
+
type: import_zod2.z.string().min(1),
|
|
1945
|
+
// OKF recommended.
|
|
1946
|
+
title: import_zod2.z.string().min(1).optional(),
|
|
1947
|
+
description: import_zod2.z.string().min(1).optional(),
|
|
1948
|
+
resource: import_zod2.z.string().min(1).optional(),
|
|
1949
|
+
tags: import_zod2.z.array(import_zod2.z.string()).optional(),
|
|
1950
|
+
// OKF optional: provenance and freshness.
|
|
1951
|
+
sources: import_zod2.z.array(kbSourceSchema).optional(),
|
|
1952
|
+
generated: kbActorStampSchema.optional(),
|
|
1953
|
+
verified: import_zod2.z.array(kbActorStampSchema).optional(),
|
|
1954
|
+
stale_after: import_zod2.z.string().min(1).optional(),
|
|
1955
|
+
// strauss extensions — see the module comment.
|
|
1956
|
+
strauss_anchors: import_zod2.z.array(kbAnchorSchema).optional(),
|
|
1957
|
+
strauss_verify: import_zod2.z.array(import_zod2.z.string().min(1)).optional(),
|
|
1958
|
+
// Typed causal edges, source → target, living on the source. `A depends_on
|
|
1959
|
+
// B` means A needs B, so `kb_impact` walks these inbound: what breaks if B
|
|
1960
|
+
// changes is whatever declared a dependence on it.
|
|
1961
|
+
strauss_links: import_zod2.z.array(kbLinkSchema).optional(),
|
|
1962
|
+
// Total after parsing, tolerant before it. Our producers must supply a
|
|
1963
|
+
// status — an absent one would leave every reader inventing its own default
|
|
1964
|
+
// — but OKF calls a concept carrying only `type` fully conformant, so
|
|
1965
|
+
// rejecting a foreign record for the lack of one would put us outside the
|
|
1966
|
+
// spec. The default resolves it in the single place that can: here.
|
|
1967
|
+
strauss_status: import_zod2.z.enum(KB_RECORD_STATUSES).default("draft"),
|
|
1968
|
+
strauss_supersedes: import_zod2.z.array(import_zod2.z.string().min(1)).optional(),
|
|
1969
|
+
strauss_superseded_by: import_zod2.z.string().min(1).optional(),
|
|
1970
|
+
strauss_answered: kbActorStampSchema.optional(),
|
|
1971
|
+
strauss_materiality: import_zod2.z.enum(KB_MATERIALITIES).optional(),
|
|
1972
|
+
strauss_confidence: import_zod2.z.enum(KB_CONFIDENCES).optional(),
|
|
1973
|
+
strauss_owner: import_zod2.z.string().min(1).optional(),
|
|
1974
|
+
// "No source exists" as a field rather than a sentinel entry inside
|
|
1975
|
+
// `sources`. A sentinel in a reference list is a value doing work a field
|
|
1976
|
+
// should do; as a field, `sources` may be legitimately empty.
|
|
1977
|
+
strauss_assumption: import_zod2.z.boolean().optional()
|
|
1978
|
+
}).passthrough();
|
|
1979
|
+
|
|
1980
|
+
// src/errors.ts
|
|
1981
|
+
var BaseError = class extends Error {
|
|
1982
|
+
code;
|
|
1983
|
+
errorType;
|
|
1984
|
+
fault;
|
|
1985
|
+
retriable;
|
|
1986
|
+
reportToUser;
|
|
1987
|
+
details;
|
|
1988
|
+
constructor(props) {
|
|
1989
|
+
super(props.message);
|
|
1990
|
+
this.name = props.name ?? this.constructor.name;
|
|
1991
|
+
this.code = props.code ?? 500;
|
|
1992
|
+
this.errorType = props.errorType;
|
|
1993
|
+
this.fault = props.fault;
|
|
1994
|
+
this.retriable = props.retriable ?? true;
|
|
1995
|
+
this.reportToUser = props.reportToUser ?? false;
|
|
1996
|
+
this.details = props.details;
|
|
2081
1997
|
}
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
1998
|
+
};
|
|
1999
|
+
|
|
2000
|
+
// src/anchors/errors.ts
|
|
2001
|
+
function locatorText(locator) {
|
|
2002
|
+
const span2 = locator.span ? `:${locator.span.start}-${locator.span.end}` : "";
|
|
2003
|
+
const symbol = locator.symbol ? `:${cap(locator.symbol)}` : "";
|
|
2004
|
+
const repo = locator.repo ? `${cap(locator.repo)}@` : "";
|
|
2005
|
+
const ref = locator.ref ? `@${cap(locator.ref)}` : "";
|
|
2006
|
+
return `${repo}${cap(locator.file)}${symbol}${span2}${ref}`;
|
|
2007
|
+
}
|
|
2008
|
+
var FIELD_CAP = 120;
|
|
2009
|
+
function cap(value) {
|
|
2010
|
+
return value.length > FIELD_CAP ? `${value.slice(0, FIELD_CAP - 1)}\u2026` : value;
|
|
2011
|
+
}
|
|
2012
|
+
var KbAnchorSetDuplicateError = class extends BaseError {
|
|
2013
|
+
constructor(locator) {
|
|
2014
|
+
super({
|
|
2015
|
+
message: `kb: ${locator} appears twice in this set \u2014 a record holds each pointer once`,
|
|
2016
|
+
errorType: "KbAnchorSetDuplicate" /* KbAnchorSetDuplicate */,
|
|
2017
|
+
code: 400,
|
|
2018
|
+
fault: "User" /* User */,
|
|
2019
|
+
retriable: false,
|
|
2020
|
+
reportToUser: true,
|
|
2021
|
+
details: { locator, action: "refused" }
|
|
2022
|
+
});
|
|
2023
|
+
this.locator = locator;
|
|
2088
2024
|
}
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2025
|
+
locator;
|
|
2026
|
+
};
|
|
2027
|
+
|
|
2028
|
+
// src/anchors/apply.ts
|
|
2029
|
+
var LOCATOR_FIELDS = [
|
|
2030
|
+
"file",
|
|
2031
|
+
"symbol",
|
|
2032
|
+
"span",
|
|
2033
|
+
"side",
|
|
2034
|
+
"repo",
|
|
2035
|
+
"ref"
|
|
2036
|
+
];
|
|
2037
|
+
function applyAnchorSet(current, incoming) {
|
|
2038
|
+
const anchors = incoming.map((anchor) => ({ ...anchor }));
|
|
2039
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2040
|
+
for (const anchor of anchors) {
|
|
2041
|
+
const key2 = locatorKey(anchor);
|
|
2042
|
+
if (seen.has(key2)) {
|
|
2043
|
+
throw new KbAnchorSetDuplicateError(locatorText(locatorOf(anchor)));
|
|
2097
2044
|
}
|
|
2045
|
+
seen.add(key2);
|
|
2098
2046
|
}
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
}
|
|
2121
|
-
const read = anchor.side === "old" ? committed.get(atRefKey(anchor)) : reads.get(anchor.file);
|
|
2122
|
-
entries.push(localEntry(anchor, read, resolvers));
|
|
2047
|
+
return { anchors, changes: diff(current, anchors) };
|
|
2048
|
+
}
|
|
2049
|
+
function diff(current, next) {
|
|
2050
|
+
const before = new Map(
|
|
2051
|
+
current.filter((anchor) => anchor.hash).map((a) => [a.hash, a])
|
|
2052
|
+
);
|
|
2053
|
+
const beforeLocators = new Map(current.map((a) => [locatorKey(a), a]));
|
|
2054
|
+
const afterLocators = new Set(next.map((anchor) => locatorKey(anchor)));
|
|
2055
|
+
const moved = /* @__PURE__ */ new Set();
|
|
2056
|
+
const changes = [];
|
|
2057
|
+
for (const anchor of next) {
|
|
2058
|
+
const source = anchor.hash ? before.get(anchor.hash) : void 0;
|
|
2059
|
+
if (source) {
|
|
2060
|
+
if (locatorKey(source) === locatorKey(anchor)) continue;
|
|
2061
|
+
moved.add(locatorKey(source));
|
|
2062
|
+
changes.push({
|
|
2063
|
+
op: "move",
|
|
2064
|
+
from: locatorOf(source),
|
|
2065
|
+
to: locatorOf(anchor)
|
|
2066
|
+
});
|
|
2067
|
+
continue;
|
|
2123
2068
|
}
|
|
2124
|
-
if (
|
|
2069
|
+
if (beforeLocators.has(locatorKey(anchor))) continue;
|
|
2070
|
+
changes.push({ op: "add", to: locatorOf(anchor) });
|
|
2125
2071
|
}
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2072
|
+
for (const anchor of current) {
|
|
2073
|
+
const key2 = locatorKey(anchor);
|
|
2074
|
+
if (afterLocators.has(key2) || moved.has(key2)) continue;
|
|
2075
|
+
changes.push({ op: "drop", from: locatorOf(anchor) });
|
|
2076
|
+
}
|
|
2077
|
+
return changes;
|
|
2130
2078
|
}
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
keys,
|
|
2139
|
-
options.concurrency ?? DEFAULT_IO_CONCURRENCY,
|
|
2140
|
-
(key2) => read(repoRoot, byKey.get(key2))
|
|
2079
|
+
function locatorOf(anchor) {
|
|
2080
|
+
return kbAnchorLocatorSchema.parse(
|
|
2081
|
+
Object.fromEntries(
|
|
2082
|
+
LOCATOR_FIELDS.flatMap(
|
|
2083
|
+
(field) => anchor[field] === void 0 ? [] : [[field, anchor[field]]]
|
|
2084
|
+
)
|
|
2085
|
+
)
|
|
2141
2086
|
);
|
|
2142
|
-
return new Map(keys.map((key2, at2) => [key2, results[at2]]));
|
|
2143
2087
|
}
|
|
2144
|
-
function
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2088
|
+
function locatorKey(anchor) {
|
|
2089
|
+
return JSON.stringify([
|
|
2090
|
+
anchor.file,
|
|
2091
|
+
anchor.symbol ?? "",
|
|
2092
|
+
anchor.span ? `${anchor.span.start}-${anchor.span.end}` : "",
|
|
2093
|
+
anchor.side ?? "new",
|
|
2094
|
+
anchor.repo === void 0 ? "" : normalizeRepoUrl(anchor.repo),
|
|
2095
|
+
anchor.ref ?? ""
|
|
2096
|
+
]);
|
|
2149
2097
|
}
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2098
|
+
|
|
2099
|
+
// src/record-types.ts
|
|
2100
|
+
var RECORD_TYPES = {
|
|
2101
|
+
fact: {
|
|
2102
|
+
purpose: "Observed or sourced fact",
|
|
2103
|
+
sections: ["Claim", "Evidence", "Implication"],
|
|
2104
|
+
initialStatus: "accepted"
|
|
2105
|
+
},
|
|
2106
|
+
requirement: {
|
|
2107
|
+
purpose: "Required behavior or outcome",
|
|
2108
|
+
sections: ["Claim", "Evidence", "Implication"],
|
|
2109
|
+
initialStatus: "proposed"
|
|
2110
|
+
},
|
|
2111
|
+
constraint: {
|
|
2112
|
+
purpose: "Limitation, compatibility boundary, policy, or restriction",
|
|
2113
|
+
sections: ["Claim", "Evidence", "Implication"],
|
|
2114
|
+
initialStatus: "accepted"
|
|
2115
|
+
},
|
|
2116
|
+
decision: {
|
|
2117
|
+
purpose: "Chosen or proposed direction",
|
|
2118
|
+
sections: ["Decision", "Rationale", "Rejected", "Impact"],
|
|
2119
|
+
initialStatus: "accepted"
|
|
2120
|
+
},
|
|
2121
|
+
assumption: {
|
|
2122
|
+
purpose: "Unsourced or not-yet-confirmed working assumption",
|
|
2123
|
+
sections: ["Claim", "Why we think so", "What would falsify it"],
|
|
2124
|
+
initialStatus: "draft"
|
|
2125
|
+
},
|
|
2126
|
+
"open-question": {
|
|
2127
|
+
purpose: "Question needing resolution",
|
|
2128
|
+
sections: ["Question", "Why it matters", "Default assumption"],
|
|
2129
|
+
initialStatus: "open"
|
|
2130
|
+
},
|
|
2131
|
+
risk: {
|
|
2132
|
+
purpose: "Something that can go wrong",
|
|
2133
|
+
sections: ["Risk", "Why it matters", "Mitigation", "Verification"],
|
|
2134
|
+
initialStatus: "open"
|
|
2135
|
+
},
|
|
2136
|
+
contract: {
|
|
2137
|
+
purpose: "API, data, event, schema, or permission contract",
|
|
2138
|
+
sections: ["Contract", "Producer", "Consumer", "Compatibility"],
|
|
2139
|
+
initialStatus: "proposed"
|
|
2140
|
+
},
|
|
2141
|
+
flow: {
|
|
2142
|
+
purpose: "Sequence, lifecycle, or state behavior",
|
|
2143
|
+
sections: ["Flow", "Trigger", "Steps", "Failure modes"],
|
|
2144
|
+
initialStatus: "accepted"
|
|
2145
|
+
},
|
|
2146
|
+
"affected-system": {
|
|
2147
|
+
purpose: "Component, service, package, integration, or external system",
|
|
2148
|
+
sections: ["System", "How it is affected", "Blast radius"],
|
|
2149
|
+
initialStatus: "accepted"
|
|
2150
|
+
},
|
|
2151
|
+
"test-obligation": {
|
|
2152
|
+
purpose: "Behavior or contract that must be verified",
|
|
2153
|
+
sections: ["Obligation", "Why it matters", "How to verify"],
|
|
2154
|
+
initialStatus: "open"
|
|
2155
|
+
},
|
|
2156
|
+
"source-note": {
|
|
2157
|
+
purpose: "Extracted note from source material",
|
|
2158
|
+
sections: ["Note", "Where it came from"],
|
|
2159
|
+
initialStatus: "accepted"
|
|
2160
|
+
}
|
|
2161
|
+
};
|
|
2162
|
+
function isKbRecordType(value) {
|
|
2163
|
+
return Object.prototype.hasOwnProperty.call(RECORD_TYPES, value);
|
|
2157
2164
|
}
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2165
|
+
var KB_LINK_RELS = [
|
|
2166
|
+
"depends_on",
|
|
2167
|
+
"constrains",
|
|
2168
|
+
"informs",
|
|
2169
|
+
"blocks",
|
|
2170
|
+
"invalidates",
|
|
2171
|
+
"verified_by",
|
|
2172
|
+
"satisfies",
|
|
2173
|
+
"related_to"
|
|
2174
|
+
];
|
|
2175
|
+
var LINK_RELS = {
|
|
2176
|
+
depends_on: {
|
|
2177
|
+
purpose: "The source needs the target to hold; the source breaks if the target changes",
|
|
2178
|
+
phrase: "Depends on",
|
|
2179
|
+
dependant: "source"
|
|
2180
|
+
},
|
|
2181
|
+
constrains: {
|
|
2182
|
+
purpose: "The source bounds what the target may do; the target breaks if the constraint changes",
|
|
2183
|
+
phrase: "Constrains",
|
|
2184
|
+
dependant: "target"
|
|
2185
|
+
},
|
|
2186
|
+
informs: {
|
|
2187
|
+
purpose: "The source shaped the target without binding it; the target is what needs revisiting",
|
|
2188
|
+
phrase: "Informs",
|
|
2189
|
+
dependant: "target"
|
|
2190
|
+
},
|
|
2191
|
+
blocks: {
|
|
2192
|
+
purpose: "The target cannot proceed until the source is settled; the target is what waits",
|
|
2193
|
+
phrase: "Blocks",
|
|
2194
|
+
dependant: "target"
|
|
2195
|
+
},
|
|
2196
|
+
invalidates: {
|
|
2197
|
+
purpose: "The source makes the target no longer hold; the target is what stops holding",
|
|
2198
|
+
phrase: "Invalidates",
|
|
2199
|
+
dependant: "target"
|
|
2200
|
+
},
|
|
2201
|
+
verified_by: {
|
|
2202
|
+
purpose: "The target is the check that confirms the source; the source's confirmation moves with it",
|
|
2203
|
+
phrase: "Verified by",
|
|
2204
|
+
dependant: "source"
|
|
2205
|
+
},
|
|
2206
|
+
satisfies: {
|
|
2207
|
+
purpose: "The source discharges the target's requirement; the source must change if the requirement does",
|
|
2208
|
+
phrase: "Satisfies",
|
|
2209
|
+
dependant: "source"
|
|
2210
|
+
},
|
|
2211
|
+
related_to: {
|
|
2212
|
+
purpose: "A pointer worth following, with no claim of dependence",
|
|
2213
|
+
phrase: "Relates to",
|
|
2214
|
+
dependant: null
|
|
2215
|
+
}
|
|
2216
|
+
};
|
|
2217
|
+
var KB_CAUSAL_LINK_RELS = KB_LINK_RELS.filter(
|
|
2218
|
+
(rel) => LINK_RELS[rel].dependant !== null
|
|
2219
|
+
);
|
|
2220
|
+
function isKbLinkRel(value) {
|
|
2221
|
+
return Object.prototype.hasOwnProperty.call(LINK_RELS, value);
|
|
2167
2222
|
}
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2223
|
+
|
|
2224
|
+
// src/compose.ts
|
|
2225
|
+
var composeLinkSchema = import_zod3.z.object({
|
|
2226
|
+
target: kbConceptIdSchema,
|
|
2227
|
+
rel: import_zod3.z.enum(KB_LINK_RELS)
|
|
2228
|
+
}).strict();
|
|
2229
|
+
var composeInputSchema = import_zod3.z.object({
|
|
2230
|
+
slug: import_zod3.z.string().min(1),
|
|
2231
|
+
/** One line, in the reader's terms. Becomes OKF `title`. */
|
|
2232
|
+
title: import_zod3.z.string().min(1),
|
|
2233
|
+
/** The consequence — what breaks if this is wrong. Becomes `description`. */
|
|
2234
|
+
why: import_zod3.z.string().min(1),
|
|
2235
|
+
/** Keyed by section heading from the type's spec. Unknown keys rejected. */
|
|
2236
|
+
sections: import_zod3.z.record(import_zod3.z.string(), import_zod3.z.string().min(1)).optional(),
|
|
2237
|
+
anchors: import_zod3.z.array(kbAnchorWriteSchema).optional(),
|
|
2238
|
+
sources: import_zod3.z.array(kbSourceSchema).optional(),
|
|
2239
|
+
/** No source exists, as a claim rather than a sentinel in `sources`. */
|
|
2240
|
+
assumption: import_zod3.z.boolean().optional(),
|
|
2241
|
+
/**
|
|
2242
|
+
* OKF `stale_after`: the absolute date this record stops being trusted.
|
|
2243
|
+
* Anything the outside world can change — pricing, quotas, versions,
|
|
2244
|
+
* reception counts — should carry one.
|
|
2245
|
+
*/
|
|
2246
|
+
stale_after: import_zod3.z.string().regex(/^\d{4}-\d{2}-\d{2}$/, {
|
|
2247
|
+
message: "stale_after must be YYYY-MM-DD"
|
|
2248
|
+
}).refine((date) => !Number.isNaN(Date.parse(date)), {
|
|
2249
|
+
message: "stale_after must be a real date"
|
|
2250
|
+
}).optional(),
|
|
2251
|
+
verify: import_zod3.z.array(import_zod3.z.string().min(1)).optional(),
|
|
2252
|
+
tags: import_zod3.z.array(import_zod3.z.string().min(1)).optional(),
|
|
2253
|
+
/** Concept ids this record relates to; rendered as body links. */
|
|
2254
|
+
relatedConceptIds: import_zod3.z.array(kbConceptIdSchema).optional(),
|
|
2255
|
+
/**
|
|
2256
|
+
* Typed causal edges, source → target: `{ target: "fact.b", rel:
|
|
2257
|
+
* "depends_on" }` on record A says A needs B. Stored in frontmatter and
|
|
2258
|
+
* also rendered as one prose sentence each, so the meaning survives a
|
|
2259
|
+
* reader that knows only OKF. The vocabulary goes into the description from
|
|
2260
|
+
* the same table the walk uses, so `kb_schema` emits it.
|
|
2261
|
+
*/
|
|
2262
|
+
links: import_zod3.z.array(composeLinkSchema).max(64).optional().describe(
|
|
2263
|
+
`Typed causal edges, source \u2192 target \u2014 a link on this record says this record <rel> the target. ${KB_LINK_RELS.map(
|
|
2264
|
+
(rel) => `${rel}: ${LINK_RELS[rel].purpose}`
|
|
2265
|
+
).join("; ")}.`
|
|
2266
|
+
),
|
|
2267
|
+
/** Concept ids this record replaces. The store settles the backlinks. */
|
|
2268
|
+
supersedes: import_zod3.z.array(kbConceptIdSchema).max(32).optional(),
|
|
2269
|
+
materiality: import_zod3.z.enum(KB_MATERIALITIES).optional(),
|
|
2270
|
+
confidence: import_zod3.z.enum(KB_CONFIDENCES).optional(),
|
|
2271
|
+
owner: import_zod3.z.string().min(1).optional()
|
|
2272
|
+
}).strict();
|
|
2273
|
+
function composeRecord(type, input, writtenBy, writtenAt) {
|
|
2274
|
+
const parsed = composeInputSchema.parse(input);
|
|
2275
|
+
const spec = RECORD_TYPES[type];
|
|
2276
|
+
const sections = parsed.sections ?? {};
|
|
2277
|
+
const unknown = Object.keys(sections).filter(
|
|
2278
|
+
(heading) => !spec.sections.includes(heading)
|
|
2279
|
+
);
|
|
2280
|
+
if (unknown.length) {
|
|
2281
|
+
throw new Error(
|
|
2282
|
+
`kb: ${type} has no section ${unknown.join(", ")} \u2014 expected one of ${spec.sections.join(", ")}`
|
|
2283
|
+
);
|
|
2177
2284
|
}
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
const { hash, kind } = anchorHashOf(anchor, outcome);
|
|
2188
|
-
return {
|
|
2189
|
-
ok: true,
|
|
2190
|
-
current: {
|
|
2191
|
-
hash,
|
|
2192
|
-
kind,
|
|
2193
|
-
lines: outcome.span.endLine - outcome.span.startLine + 1,
|
|
2194
|
-
...outcome.resolver ? { resolver: outcome.resolver } : {}
|
|
2195
|
-
}
|
|
2285
|
+
const frontmatter = {
|
|
2286
|
+
title: parsed.title,
|
|
2287
|
+
description: parsed.why,
|
|
2288
|
+
generated: { by: writtenBy, at: writtenAt },
|
|
2289
|
+
// Empty rather than absent: a later verification pass appends here, and an
|
|
2290
|
+
// empty list says "not yet verified" where a missing key would only say
|
|
2291
|
+
// "this producer didn't think about it".
|
|
2292
|
+
verified: [],
|
|
2293
|
+
strauss_status: spec.initialStatus
|
|
2196
2294
|
};
|
|
2197
|
-
|
|
2198
|
-
|
|
2295
|
+
if (parsed.stale_after) frontmatter.stale_after = parsed.stale_after;
|
|
2296
|
+
if (parsed.anchors?.length) {
|
|
2297
|
+
frontmatter.strauss_anchors = applyAnchorSet([], parsed.anchors).anchors;
|
|
2298
|
+
}
|
|
2299
|
+
if (parsed.verify?.length) frontmatter.strauss_verify = parsed.verify;
|
|
2300
|
+
if (parsed.tags?.length) frontmatter.tags = parsed.tags;
|
|
2301
|
+
if (parsed.sources?.length) frontmatter.sources = parsed.sources;
|
|
2302
|
+
if (parsed.assumption) frontmatter.strauss_assumption = true;
|
|
2303
|
+
if (parsed.materiality) frontmatter.strauss_materiality = parsed.materiality;
|
|
2304
|
+
if (parsed.confidence) frontmatter.strauss_confidence = parsed.confidence;
|
|
2305
|
+
if (parsed.owner) frontmatter.strauss_owner = parsed.owner;
|
|
2306
|
+
if (parsed.supersedes?.length)
|
|
2307
|
+
frontmatter.strauss_supersedes = parsed.supersedes;
|
|
2308
|
+
const selfLink = parsed.links?.find(
|
|
2309
|
+
(link2) => link2.target === `${type}.${parsed.slug}`
|
|
2310
|
+
);
|
|
2311
|
+
if (selfLink) {
|
|
2312
|
+
throw new Error(
|
|
2313
|
+
`kb: ${type}.${parsed.slug} cannot ${selfLink.rel} itself \u2014 a link must name another record`
|
|
2314
|
+
);
|
|
2315
|
+
}
|
|
2316
|
+
if (parsed.links?.length) frontmatter.strauss_links = parsed.links;
|
|
2317
|
+
const blocks = [];
|
|
2318
|
+
for (const heading of spec.sections) {
|
|
2319
|
+
const text = sections[heading];
|
|
2320
|
+
if (text) blocks.push(`## ${heading}
|
|
2321
|
+
|
|
2322
|
+
${text}`);
|
|
2323
|
+
}
|
|
2324
|
+
if (!blocks.length) blocks.push(parsed.why);
|
|
2325
|
+
for (const related of parsed.relatedConceptIds ?? []) {
|
|
2326
|
+
blocks.push(`Relates to [${related}](${related}.md).`);
|
|
2327
|
+
}
|
|
2328
|
+
for (const link2 of parsed.links ?? []) {
|
|
2329
|
+
blocks.push(
|
|
2330
|
+
`${LINK_RELS[link2.rel].phrase} [${link2.target}](${link2.target}.md).`
|
|
2331
|
+
);
|
|
2332
|
+
}
|
|
2333
|
+
if (parsed.sources?.length) {
|
|
2334
|
+
blocks.push(
|
|
2335
|
+
parsed.sources.map((source) => `[^${source.id}]: ${source.title ?? source.resource}`).join("\n")
|
|
2336
|
+
);
|
|
2337
|
+
}
|
|
2199
2338
|
return {
|
|
2200
|
-
|
|
2201
|
-
|
|
2339
|
+
type,
|
|
2340
|
+
slug: parsed.slug,
|
|
2341
|
+
frontmatter,
|
|
2342
|
+
body: `${blocks.join("\n\n")}
|
|
2343
|
+
`
|
|
2202
2344
|
};
|
|
2203
2345
|
}
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2346
|
+
|
|
2347
|
+
// src/decision-record.ts
|
|
2348
|
+
var DECISION_TYPE = "decision";
|
|
2349
|
+
var NO_DECISION_SLUG = "none";
|
|
2350
|
+
var decisionInputSchema = composeInputSchema.omit({ sections: true }).extend({
|
|
2351
|
+
alternative: import_zod4.z.string().min(1).optional(),
|
|
2352
|
+
impact: import_zod4.z.string().min(1).optional()
|
|
2353
|
+
}).strict();
|
|
2354
|
+
function composeDecisionRecord(input, writtenBy, writtenAt) {
|
|
2355
|
+
const { alternative, impact: impact2, ...rest } = input;
|
|
2356
|
+
return composeRecord(
|
|
2357
|
+
DECISION_TYPE,
|
|
2358
|
+
{
|
|
2359
|
+
...rest,
|
|
2360
|
+
sections: {
|
|
2361
|
+
Decision: input.title,
|
|
2362
|
+
Rationale: input.why,
|
|
2363
|
+
...alternative ? { Rejected: alternative } : {},
|
|
2364
|
+
...impact2 ? { Impact: impact2 } : {}
|
|
2365
|
+
}
|
|
2366
|
+
},
|
|
2367
|
+
writtenBy,
|
|
2368
|
+
writtenAt
|
|
2369
|
+
);
|
|
2215
2370
|
}
|
|
2216
|
-
function
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2371
|
+
function composeNoDecisionRecord(reason, writtenBy, writtenAt) {
|
|
2372
|
+
return composeRecord(
|
|
2373
|
+
DECISION_TYPE,
|
|
2374
|
+
{
|
|
2375
|
+
slug: NO_DECISION_SLUG,
|
|
2376
|
+
title: "No decision to record",
|
|
2377
|
+
why: reason,
|
|
2378
|
+
sections: { Decision: reason }
|
|
2379
|
+
},
|
|
2380
|
+
writtenBy,
|
|
2381
|
+
writtenAt
|
|
2224
2382
|
);
|
|
2225
2383
|
}
|
|
2226
|
-
function
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
const found = hashIn(primary.source, anchor, resolvers);
|
|
2234
|
-
if (!found.ok) return unresolved(anchor, found.reason, repo);
|
|
2235
|
-
const current = found.current;
|
|
2236
|
-
const extras = resolverExtras(primary.source, anchor, current);
|
|
2237
|
-
if (!anchor.ref) return compared(anchor, current, { repo, ...extras });
|
|
2238
|
-
if (current.hash !== anchor.hash) {
|
|
2239
|
-
return compared(anchor, current, {
|
|
2240
|
-
repo,
|
|
2241
|
-
...extras,
|
|
2242
|
-
remoteState: "drifted-from-ref"
|
|
2243
|
-
});
|
|
2244
|
-
}
|
|
2245
|
-
if (anchor.side === "old") {
|
|
2246
|
-
return compared(anchor, current, {
|
|
2247
|
-
repo,
|
|
2248
|
-
...extras,
|
|
2249
|
-
remoteState: "matches-ref"
|
|
2250
|
-
});
|
|
2251
|
-
}
|
|
2252
|
-
const head = atDefault?.ok ? hashIn(atDefault.source, anchor, resolvers) : null;
|
|
2253
|
-
return head?.ok && head.current.hash !== anchor.hash ? {
|
|
2254
|
-
...compared(anchor, head.current, {
|
|
2255
|
-
repo,
|
|
2256
|
-
...head.current.resolver ? { resolver: head.current.resolver } : {}
|
|
2257
|
-
}),
|
|
2258
|
-
state: "drifted",
|
|
2259
|
-
remoteState: "drifted-on-default"
|
|
2260
|
-
} : compared(anchor, current, {
|
|
2261
|
-
repo,
|
|
2262
|
-
...extras,
|
|
2263
|
-
remoteState: "matches-ref"
|
|
2264
|
-
});
|
|
2384
|
+
function isNoDecisionRecord(record) {
|
|
2385
|
+
return record.conceptId === `${DECISION_TYPE}.${NO_DECISION_SLUG}`;
|
|
2386
|
+
}
|
|
2387
|
+
function selectDecisions(records) {
|
|
2388
|
+
return records.filter(
|
|
2389
|
+
(record) => record.conceptId.startsWith(`${DECISION_TYPE}.`) && !isNoDecisionRecord(record)
|
|
2390
|
+
);
|
|
2265
2391
|
}
|
|
2266
2392
|
|
|
2267
|
-
// src/
|
|
2268
|
-
var
|
|
2269
|
-
code;
|
|
2270
|
-
errorType;
|
|
2271
|
-
fault;
|
|
2272
|
-
retriable;
|
|
2273
|
-
reportToUser;
|
|
2274
|
-
details;
|
|
2275
|
-
constructor(props) {
|
|
2276
|
-
super(props.message);
|
|
2277
|
-
this.name = props.name ?? this.constructor.name;
|
|
2278
|
-
this.code = props.code ?? 500;
|
|
2279
|
-
this.errorType = props.errorType;
|
|
2280
|
-
this.fault = props.fault;
|
|
2281
|
-
this.retriable = props.retriable ?? true;
|
|
2282
|
-
this.reportToUser = props.reportToUser ?? false;
|
|
2283
|
-
this.details = props.details;
|
|
2284
|
-
}
|
|
2285
|
-
};
|
|
2393
|
+
// src/commands/anchor-resolve/command.ts
|
|
2394
|
+
var import_zod8 = require("zod");
|
|
2286
2395
|
|
|
2287
2396
|
// src/kb-errors.ts
|
|
2288
2397
|
var KbRecordAlreadyExistsError = class extends BaseError {
|
|
@@ -2558,8 +2667,75 @@ var KbStampDigestBaselineError = class extends BaseError {
|
|
|
2558
2667
|
});
|
|
2559
2668
|
this.since = since;
|
|
2560
2669
|
}
|
|
2561
|
-
since;
|
|
2562
|
-
};
|
|
2670
|
+
since;
|
|
2671
|
+
};
|
|
2672
|
+
|
|
2673
|
+
// src/commands/model.ts
|
|
2674
|
+
var import_zod5 = require("zod");
|
|
2675
|
+
var bundlePath = import_zod5.z.string().min(1).describe("Absolute path to the knowledge base directory.");
|
|
2676
|
+
var conceptId = import_zod5.z.string().min(1).describe("e.g. decision.cursor-v2");
|
|
2677
|
+
var TAGS = import_zod5.z.array(import_zod5.z.string().min(1)).optional().describe(
|
|
2678
|
+
"Keep only records carrying every one of these frontmatter tags. Matched exactly."
|
|
2679
|
+
);
|
|
2680
|
+
var REPO_ROOT = import_zod5.z.string().min(1).optional().describe(
|
|
2681
|
+
"Where the anchored source lives, for the drift check. Defaults to the working directory."
|
|
2682
|
+
);
|
|
2683
|
+
function define(command) {
|
|
2684
|
+
return command;
|
|
2685
|
+
}
|
|
2686
|
+
function argvFlag(argv, name) {
|
|
2687
|
+
const joined = argv.find((arg) => arg.startsWith(`${name}=`));
|
|
2688
|
+
if (joined !== void 0) {
|
|
2689
|
+
const value2 = joined.slice(name.length + 1);
|
|
2690
|
+
if (!value2) throw new KbMissingFlagValueError(name);
|
|
2691
|
+
return value2;
|
|
2692
|
+
}
|
|
2693
|
+
const at2 = argv.indexOf(name);
|
|
2694
|
+
if (at2 === -1) return void 0;
|
|
2695
|
+
const value = argv[at2 + 1];
|
|
2696
|
+
if (value === void 0 || value.startsWith("--")) {
|
|
2697
|
+
throw new KbMissingFlagValueError(name);
|
|
2698
|
+
}
|
|
2699
|
+
return value;
|
|
2700
|
+
}
|
|
2701
|
+
function argvFlags(argv, name) {
|
|
2702
|
+
const values = [];
|
|
2703
|
+
for (const [at2, arg] of argv.entries()) {
|
|
2704
|
+
if (arg.startsWith(`${name}=`)) {
|
|
2705
|
+
const value = arg.slice(name.length + 1);
|
|
2706
|
+
if (!value) throw new KbMissingFlagValueError(name);
|
|
2707
|
+
values.push(value);
|
|
2708
|
+
} else if (arg === name) {
|
|
2709
|
+
const value = argv[at2 + 1];
|
|
2710
|
+
if (value === void 0 || value.startsWith("--")) {
|
|
2711
|
+
throw new KbMissingFlagValueError(name);
|
|
2712
|
+
}
|
|
2713
|
+
values.push(value);
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
return values;
|
|
2717
|
+
}
|
|
2718
|
+
function argvWithout(argv, ...names) {
|
|
2719
|
+
const kept = [];
|
|
2720
|
+
for (let at2 = 0; at2 < argv.length; at2 += 1) {
|
|
2721
|
+
const arg = argv[at2];
|
|
2722
|
+
if (names.some((name) => arg.startsWith(`${name}=`))) continue;
|
|
2723
|
+
if (names.includes(arg)) {
|
|
2724
|
+
at2 += 1;
|
|
2725
|
+
continue;
|
|
2726
|
+
}
|
|
2727
|
+
kept.push(arg);
|
|
2728
|
+
}
|
|
2729
|
+
return kept;
|
|
2730
|
+
}
|
|
2731
|
+
function argvPositional(argv, ...names) {
|
|
2732
|
+
return argvWithout(argv.slice(1), ...names).find(
|
|
2733
|
+
(arg) => !arg.startsWith("--")
|
|
2734
|
+
);
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
// src/commands/anchor-resolve/apply.ts
|
|
2738
|
+
var import_zod7 = require("zod");
|
|
2563
2739
|
|
|
2564
2740
|
// src/kb-pins/budgets.ts
|
|
2565
2741
|
function asBudgets(value) {
|
|
@@ -2622,14 +2798,14 @@ var import_node_path7 = require("path");
|
|
|
2622
2798
|
|
|
2623
2799
|
// src/kb-pins/model.ts
|
|
2624
2800
|
var import_node_path6 = require("path");
|
|
2625
|
-
var
|
|
2801
|
+
var import_zod6 = require("zod");
|
|
2626
2802
|
var PINS_FILE = (0, import_node_path6.join)(".strauss", "kb-pins.json");
|
|
2627
2803
|
var PINS_LOCAL_FILE = (0, import_node_path6.join)(".strauss", "kb-pins.local.json");
|
|
2628
2804
|
var PIN_LAYERS = ["project", "local", "user"];
|
|
2629
|
-
var pinSchema =
|
|
2805
|
+
var pinSchema = import_zod6.z.object({
|
|
2630
2806
|
/** Relative to the manifest's root, so the file is committable. */
|
|
2631
|
-
path:
|
|
2632
|
-
pinnedAt:
|
|
2807
|
+
path: import_zod6.z.string().min(1),
|
|
2808
|
+
pinnedAt: import_zod6.z.string().min(1).optional(),
|
|
2633
2809
|
/**
|
|
2634
2810
|
* How `context` renders this base. `full` preloads the whole base into
|
|
2635
2811
|
* the block regardless of the full-under threshold — for a base whose
|
|
@@ -2639,7 +2815,7 @@ var pinSchema = import_zod5.z.object({
|
|
|
2639
2815
|
* Absent: the profile's full-under threshold decides. Invalid values
|
|
2640
2816
|
* degrade to absent rather than failing the manifest.
|
|
2641
2817
|
*/
|
|
2642
|
-
mode:
|
|
2818
|
+
mode: import_zod6.z.enum(["full", "index"]).optional().catch(void 0),
|
|
2643
2819
|
/**
|
|
2644
2820
|
* Context profiles this pin surfaces in (e.g. only at session-start,
|
|
2645
2821
|
* not per turn). Absent: every profile. A run without a profile sees
|
|
@@ -2647,17 +2823,17 @@ var pinSchema = import_zod5.z.object({
|
|
|
2647
2823
|
* that skill at point of use than pinned at all — pins are what every
|
|
2648
2824
|
* session should see.
|
|
2649
2825
|
*/
|
|
2650
|
-
profiles:
|
|
2826
|
+
profiles: import_zod6.z.array(import_zod6.z.string()).optional().catch(void 0),
|
|
2651
2827
|
/**
|
|
2652
2828
|
* The base is concluded — a finished piece of research, a frozen ADR
|
|
2653
2829
|
* set. Write commands against it refuse while this workspace holds the
|
|
2654
2830
|
* pin, and `context` labels it read-only. Workspace policy, not base
|
|
2655
2831
|
* state: the base itself stays copyable and writable elsewhere.
|
|
2656
2832
|
*/
|
|
2657
|
-
frozen:
|
|
2833
|
+
frozen: import_zod6.z.boolean().optional().catch(void 0)
|
|
2658
2834
|
}).passthrough();
|
|
2659
|
-
var pinsManifestSchema =
|
|
2660
|
-
pins:
|
|
2835
|
+
var pinsManifestSchema = import_zod6.z.object({
|
|
2836
|
+
pins: import_zod6.z.array(pinSchema).default([]),
|
|
2661
2837
|
/**
|
|
2662
2838
|
* Per-repo budgets for the `context` command, keyed by profile —
|
|
2663
2839
|
* `"session-start"`, `"compact"`, `"turn"`, or `"default"` for all of
|
|
@@ -2666,7 +2842,7 @@ var pinsManifestSchema = import_zod5.z.object({
|
|
|
2666
2842
|
* the index at every session start. `contextProfileBudgets` does the
|
|
2667
2843
|
* tolerant read.
|
|
2668
2844
|
*/
|
|
2669
|
-
context:
|
|
2845
|
+
context: import_zod6.z.unknown().optional()
|
|
2670
2846
|
}).passthrough();
|
|
2671
2847
|
|
|
2672
2848
|
// src/kb-pins/layers.ts
|
|
@@ -2859,193 +3035,185 @@ async function unpinBase(workspaceDir, bundlePath2) {
|
|
|
2859
3035
|
};
|
|
2860
3036
|
}
|
|
2861
3037
|
|
|
2862
|
-
// src/commands/
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
);
|
|
2869
|
-
|
|
2870
|
-
"Where the anchored source lives, for the drift check. Defaults to the working directory."
|
|
2871
|
-
);
|
|
2872
|
-
function define(command) {
|
|
2873
|
-
return command;
|
|
2874
|
-
}
|
|
2875
|
-
function argvFlag(argv, name) {
|
|
2876
|
-
const joined = argv.find((arg) => arg.startsWith(`${name}=`));
|
|
2877
|
-
if (joined !== void 0) {
|
|
2878
|
-
const value2 = joined.slice(name.length + 1);
|
|
2879
|
-
if (!value2) throw new KbMissingFlagValueError(name);
|
|
2880
|
-
return value2;
|
|
2881
|
-
}
|
|
2882
|
-
const at2 = argv.indexOf(name);
|
|
2883
|
-
if (at2 === -1) return void 0;
|
|
2884
|
-
const value = argv[at2 + 1];
|
|
2885
|
-
if (value === void 0 || value.startsWith("--")) {
|
|
2886
|
-
throw new KbMissingFlagValueError(name);
|
|
3038
|
+
// src/commands/anchor-resolve/apply.ts
|
|
3039
|
+
async function baseFrozen(cwd, bundlePath2) {
|
|
3040
|
+
try {
|
|
3041
|
+
await assertBaseNotFrozen(cwd, bundlePath2);
|
|
3042
|
+
return false;
|
|
3043
|
+
} catch (caught) {
|
|
3044
|
+
if (!(caught instanceof KbBaseFrozenError)) throw caught;
|
|
3045
|
+
return true;
|
|
2887
3046
|
}
|
|
2888
|
-
return value;
|
|
2889
3047
|
}
|
|
2890
|
-
function
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
3048
|
+
async function applyPlan(plans, target) {
|
|
3049
|
+
if (!plans.some((plan) => plan.write)) {
|
|
3050
|
+
return { results: plans.map((plan) => plan.finding) };
|
|
3051
|
+
}
|
|
3052
|
+
let failure = target.frozen ? "frozen" : void 0;
|
|
3053
|
+
let error;
|
|
3054
|
+
if (!failure) {
|
|
3055
|
+
try {
|
|
3056
|
+
await target.store.updateAnchors(
|
|
3057
|
+
target.bundlePath,
|
|
3058
|
+
target.conceptId,
|
|
3059
|
+
plans.map((plan) => plan.anchor),
|
|
3060
|
+
target.actor
|
|
3061
|
+
);
|
|
3062
|
+
} catch (caught) {
|
|
3063
|
+
if (caught instanceof BaseError || caught instanceof import_zod7.z.ZodError) {
|
|
3064
|
+
throw caught;
|
|
2901
3065
|
}
|
|
2902
|
-
|
|
3066
|
+
failure = "write-failed";
|
|
3067
|
+
error = clamp(caught instanceof Error ? caught.message : String(caught));
|
|
2903
3068
|
}
|
|
2904
3069
|
}
|
|
2905
|
-
return
|
|
3070
|
+
return {
|
|
3071
|
+
results: plans.map((plan) => settle(plan, failure)),
|
|
3072
|
+
...error ? { error } : {}
|
|
3073
|
+
};
|
|
2906
3074
|
}
|
|
2907
|
-
function
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
if (names.some((name) => arg.startsWith(`${name}=`))) continue;
|
|
2912
|
-
if (names.includes(arg)) {
|
|
2913
|
-
at2 += 1;
|
|
2914
|
-
continue;
|
|
2915
|
-
}
|
|
2916
|
-
kept.push(arg);
|
|
3075
|
+
function settle(plan, failure) {
|
|
3076
|
+
if (!plan.write) return plan.finding;
|
|
3077
|
+
if (failure) {
|
|
3078
|
+
return { ...plan.finding, outcome: "failed", outcomeReason: failure };
|
|
2917
3079
|
}
|
|
2918
|
-
return
|
|
3080
|
+
if (plan.write === "refresh") return plan.finding;
|
|
3081
|
+
return plan.write === "stamp" ? { ...plan.finding, state: "stamped", outcome: "applied" } : { ...plan.finding, outcome: "applied", rebaselined: true };
|
|
2919
3082
|
}
|
|
2920
|
-
function
|
|
2921
|
-
|
|
2922
|
-
|
|
3083
|
+
function clamp(message) {
|
|
3084
|
+
const line = message.split("\n")[0] ?? "";
|
|
3085
|
+
return line.length > 200 ? `${line.slice(0, 199)}\u2026` : line;
|
|
3086
|
+
}
|
|
3087
|
+
|
|
3088
|
+
// src/commands/anchor-resolve/sources.ts
|
|
3089
|
+
async function readSources(anchors, root, offline) {
|
|
3090
|
+
const origin = new LazyOrigin(root);
|
|
3091
|
+
if (anchors.some((anchor) => anchor.repo)) await origin.prime();
|
|
3092
|
+
const foreign = new Map(
|
|
3093
|
+
anchors.map((anchor) => [anchor, origin.isForeign(anchor)])
|
|
3094
|
+
);
|
|
3095
|
+
const local = anchors.filter(
|
|
3096
|
+
(anchor) => !foreign.get(anchor) && anchor.side !== "old"
|
|
3097
|
+
);
|
|
3098
|
+
const committed = anchors.filter(
|
|
3099
|
+
(anchor) => !foreign.get(anchor) && anchor.side === "old"
|
|
3100
|
+
);
|
|
3101
|
+
const remote = anchors.filter((anchor) => foreign.get(anchor));
|
|
3102
|
+
const reads = await readAnchorFiles(
|
|
3103
|
+
local.map((anchor) => anchor.file),
|
|
3104
|
+
anchorFileReader(root)
|
|
2923
3105
|
);
|
|
3106
|
+
const atRef = await readCommitted(root, committed);
|
|
3107
|
+
const blobs = await readRemoteAnchors(remote.flatMap(remoteWants), {
|
|
3108
|
+
offline
|
|
3109
|
+
});
|
|
3110
|
+
const sources = /* @__PURE__ */ new Map();
|
|
3111
|
+
for (const anchor of local) {
|
|
3112
|
+
const read = reads.get(anchor.file);
|
|
3113
|
+
sources.set(
|
|
3114
|
+
anchor,
|
|
3115
|
+
read.ok ? { ok: true, source: read.source } : { ok: false, reason: read.reason }
|
|
3116
|
+
);
|
|
3117
|
+
}
|
|
3118
|
+
for (const anchor of committed) {
|
|
3119
|
+
const read = atRef.get(atRefKey(anchor));
|
|
3120
|
+
sources.set(
|
|
3121
|
+
anchor,
|
|
3122
|
+
read.ok ? { ok: true, source: read.source } : { ok: false, reason: read.reason }
|
|
3123
|
+
);
|
|
3124
|
+
}
|
|
3125
|
+
for (const anchor of remote) {
|
|
3126
|
+
const repo = anchor.repo;
|
|
3127
|
+
const key2 = normalizeRepoUrl(repo);
|
|
3128
|
+
const atDefault = blobs.get(wantKey(key2, void 0, anchor.file));
|
|
3129
|
+
const primary = anchor.ref ? blobs.get(wantKey(key2, anchor.ref, anchor.file)) : atDefault;
|
|
3130
|
+
if (!primary?.ok) {
|
|
3131
|
+
sources.set(anchor, {
|
|
3132
|
+
ok: false,
|
|
3133
|
+
reason: primary?.ok === false ? primary.reason : "remote-unreachable",
|
|
3134
|
+
repo
|
|
3135
|
+
});
|
|
3136
|
+
continue;
|
|
3137
|
+
}
|
|
3138
|
+
sources.set(anchor, {
|
|
3139
|
+
ok: true,
|
|
3140
|
+
source: primary.source,
|
|
3141
|
+
repo,
|
|
3142
|
+
...anchor.ref && atDefault?.ok ? { head: atDefault.source } : {}
|
|
3143
|
+
});
|
|
3144
|
+
}
|
|
3145
|
+
return sources;
|
|
2924
3146
|
}
|
|
2925
3147
|
|
|
2926
|
-
// src/commands/anchor-resolve.ts
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
)
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
rebaseline: argv.includes("--rebaseline"),
|
|
2955
|
-
restamp: argv.includes("--restamp"),
|
|
2956
|
-
check: argv.includes("--check")
|
|
2957
|
-
}),
|
|
2958
|
-
run: async ({ store, actor, now }, {
|
|
2959
|
-
bundlePath: path,
|
|
2960
|
-
conceptId: id,
|
|
2961
|
-
repoRoot,
|
|
2962
|
-
offline,
|
|
2963
|
-
rebaseline,
|
|
2964
|
-
restamp,
|
|
2965
|
-
check
|
|
2966
|
-
}) => {
|
|
2967
|
-
if (check && (rebaseline || restamp)) {
|
|
2968
|
-
throw new KbFlagConflictError([
|
|
2969
|
-
"check",
|
|
2970
|
-
rebaseline ? "rebaseline" : "restamp"
|
|
2971
|
-
]);
|
|
3148
|
+
// src/commands/anchor-resolve/plan.ts
|
|
3149
|
+
async function planAnchors(anchors, options) {
|
|
3150
|
+
const { root, offline, rebaseline, restamp, check, frozen, now } = options;
|
|
3151
|
+
const sources = await readSources(anchors, root, offline);
|
|
3152
|
+
const resolvers = defaultAnchorResolvers({ offline });
|
|
3153
|
+
await prepareResolvers(
|
|
3154
|
+
resolvers,
|
|
3155
|
+
anchors.map((anchor) => anchor.file)
|
|
3156
|
+
);
|
|
3157
|
+
const plans = [];
|
|
3158
|
+
for (const anchor of anchors) {
|
|
3159
|
+
const base2 = {
|
|
3160
|
+
file: anchor.file,
|
|
3161
|
+
...anchor.symbol ? { symbol: anchor.symbol } : {},
|
|
3162
|
+
...anchor.side === "old" ? { side: "old" } : {},
|
|
3163
|
+
// Carried onto unresolved findings too: an anchor that once hashed
|
|
3164
|
+
// and now resolves to nothing is a broken anchor, and the exit code
|
|
3165
|
+
// has to be able to tell it from one nobody ever stamped.
|
|
3166
|
+
...anchor.hash ? { storedHash: anchor.hash } : {}
|
|
3167
|
+
};
|
|
3168
|
+
const source = sources.get(anchor);
|
|
3169
|
+
if (source.repo) base2.repo = source.repo;
|
|
3170
|
+
if (!source.ok) {
|
|
3171
|
+
plans.push({
|
|
3172
|
+
finding: { ...base2, state: "unresolved", reason: source.reason },
|
|
3173
|
+
anchor
|
|
3174
|
+
});
|
|
3175
|
+
continue;
|
|
2972
3176
|
}
|
|
2973
|
-
const
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
results: [],
|
|
2981
|
-
note: "record has no anchors"
|
|
2982
|
-
};
|
|
3177
|
+
const outcome = resolveAnchorSpan(source.source, anchor, resolvers);
|
|
3178
|
+
if (!outcome.ok) {
|
|
3179
|
+
plans.push({
|
|
3180
|
+
finding: { ...base2, state: "unresolved", reason: outcome.reason },
|
|
3181
|
+
anchor
|
|
3182
|
+
});
|
|
3183
|
+
continue;
|
|
2983
3184
|
}
|
|
2984
|
-
const
|
|
2985
|
-
const
|
|
2986
|
-
|
|
2987
|
-
const
|
|
2988
|
-
const
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
};
|
|
3003
|
-
const source = sources.get(anchor);
|
|
3004
|
-
if (source.repo) base2.repo = source.repo;
|
|
3005
|
-
if (!source.ok) {
|
|
3006
|
-
results.push({ ...base2, state: "unresolved", reason: source.reason });
|
|
3007
|
-
updated.push(anchor);
|
|
3008
|
-
continue;
|
|
3009
|
-
}
|
|
3010
|
-
const outcome = resolveAnchorSpan(source.source, anchor, resolvers);
|
|
3011
|
-
if (!outcome.ok) {
|
|
3012
|
-
results.push({
|
|
3013
|
-
...base2,
|
|
3014
|
-
state: "unresolved",
|
|
3015
|
-
reason: outcome.reason
|
|
3016
|
-
});
|
|
3017
|
-
updated.push(anchor);
|
|
3018
|
-
continue;
|
|
3019
|
-
}
|
|
3020
|
-
const resolved = outcome.span;
|
|
3021
|
-
const producedBy = outcome.resolver;
|
|
3022
|
-
const { hash: currentHash, kind } = anchorHashOf(anchor, outcome);
|
|
3023
|
-
const currentLines = resolved.endLine - resolved.startLine + 1;
|
|
3024
|
-
const stampedKind = outcome.normalized ? "ast" : "raw";
|
|
3025
|
-
const stampedHash = outcome.normalized ? anchorHashOf({ ...anchor, hash: void 0 }, outcome).hash : currentHash;
|
|
3026
|
-
const stamped = {
|
|
3027
|
-
...anchor,
|
|
3028
|
-
hash: stampedHash,
|
|
3029
|
-
hash_kind: stampedKind,
|
|
3030
|
-
lines: currentLines,
|
|
3031
|
-
resolved_at: now(),
|
|
3032
|
-
...producedBy ? { resolver: producedBy } : {}
|
|
3033
|
-
};
|
|
3034
|
-
const pinned = anchor.ref !== void 0 && source.repo !== void 0;
|
|
3035
|
-
if (!anchor.hash) {
|
|
3036
|
-
results.push({
|
|
3185
|
+
const resolved = outcome.span;
|
|
3186
|
+
const producedBy = outcome.resolver;
|
|
3187
|
+
const { hash: currentHash, kind } = anchorHashOf(anchor, outcome);
|
|
3188
|
+
const currentLines = resolved.endLine - resolved.startLine + 1;
|
|
3189
|
+
const stampedKind = outcome.normalized ? "ast" : "raw";
|
|
3190
|
+
const stampedHash = outcome.normalized ? anchorHashOf({ ...anchor, hash: void 0 }, outcome).hash : currentHash;
|
|
3191
|
+
const stamped = {
|
|
3192
|
+
...anchor,
|
|
3193
|
+
hash: stampedHash,
|
|
3194
|
+
hash_kind: stampedKind,
|
|
3195
|
+
lines: currentLines,
|
|
3196
|
+
resolved_at: now(),
|
|
3197
|
+
...producedBy ? { resolver: producedBy } : {}
|
|
3198
|
+
};
|
|
3199
|
+
const pinned = anchor.ref !== void 0 && source.repo !== void 0;
|
|
3200
|
+
if (!anchor.hash) {
|
|
3201
|
+
plans.push({
|
|
3202
|
+
finding: {
|
|
3037
3203
|
...base2,
|
|
3038
|
-
state:
|
|
3204
|
+
state: "unstamped",
|
|
3039
3205
|
currentHash: stampedHash,
|
|
3040
3206
|
hashKind: stampedKind,
|
|
3041
3207
|
...producedBy ? { resolver: producedBy } : {}
|
|
3042
|
-
}
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3208
|
+
},
|
|
3209
|
+
anchor: check ? anchor : stamped,
|
|
3210
|
+
...check ? {} : { write: "stamp" }
|
|
3211
|
+
});
|
|
3212
|
+
continue;
|
|
3213
|
+
}
|
|
3214
|
+
if (anchor.hash !== currentHash) {
|
|
3215
|
+
plans.push({
|
|
3216
|
+
finding: {
|
|
3049
3217
|
...base2,
|
|
3050
3218
|
state: "drifted",
|
|
3051
3219
|
currentHash,
|
|
@@ -3055,152 +3223,289 @@ var anchorResolveCommand = define({
|
|
|
3055
3223
|
// A regex-stamped anchor re-read by tree-sitter drifts because the
|
|
3056
3224
|
// resolver changed, not because the code did.
|
|
3057
3225
|
...resolverChanged(source.source, anchor, producedBy) ? { reason: "resolver-changed" } : {},
|
|
3058
|
-
...pinned ? { remoteState: "drifted-from-ref" } : {}
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3226
|
+
...pinned ? { remoteState: "drifted-from-ref" } : {}
|
|
3227
|
+
},
|
|
3228
|
+
anchor: rebaseline && !check ? stamped : anchor,
|
|
3229
|
+
...rebaseline && !check ? { write: "rebaseline" } : {}
|
|
3230
|
+
});
|
|
3231
|
+
continue;
|
|
3232
|
+
}
|
|
3233
|
+
const onDefault = pinned ? headHash(source, anchor, resolvers) : void 0;
|
|
3234
|
+
if (onDefault && onDefault.hash !== anchor.hash) {
|
|
3235
|
+
plans.push({
|
|
3236
|
+
finding: {
|
|
3068
3237
|
...base2,
|
|
3069
3238
|
state: "drifted",
|
|
3070
3239
|
currentHash: onDefault.hash,
|
|
3071
3240
|
diffSize: lineDelta(anchor, onDefault.lines),
|
|
3072
|
-
remoteState: "drifted-on-default"
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3241
|
+
remoteState: "drifted-on-default",
|
|
3242
|
+
...rebaseline ? {
|
|
3243
|
+
outcome: "skipped",
|
|
3244
|
+
outcomeReason: "pinned-ref"
|
|
3245
|
+
} : {}
|
|
3246
|
+
},
|
|
3247
|
+
anchor
|
|
3248
|
+
});
|
|
3249
|
+
continue;
|
|
3250
|
+
}
|
|
3251
|
+
const backfill = anchor.resolved_at === void 0 && !frozen;
|
|
3252
|
+
const refresh = !check && (restamp || backfill);
|
|
3253
|
+
plans.push({
|
|
3254
|
+
finding: {
|
|
3078
3255
|
...base2,
|
|
3079
3256
|
state: "match",
|
|
3080
3257
|
currentHash,
|
|
3081
3258
|
hashKind: kind,
|
|
3082
3259
|
...producedBy ? { resolver: producedBy } : {},
|
|
3083
3260
|
...pinned ? { remoteState: "matches-ref" } : {}
|
|
3084
|
-
}
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3261
|
+
},
|
|
3262
|
+
anchor: refresh ? { ...anchor, resolved_at: now() } : anchor,
|
|
3263
|
+
...refresh ? { write: "refresh" } : {}
|
|
3264
|
+
});
|
|
3265
|
+
}
|
|
3266
|
+
return plans;
|
|
3267
|
+
}
|
|
3268
|
+
function lineDelta(anchor, current) {
|
|
3269
|
+
return anchor.lines === void 0 ? null : Math.abs(current - anchor.lines);
|
|
3270
|
+
}
|
|
3271
|
+
function headHash(source, anchor, resolvers) {
|
|
3272
|
+
if (source.head === void 0) return void 0;
|
|
3273
|
+
const outcome = resolveAnchorSpan(source.head, anchor, resolvers);
|
|
3274
|
+
if (!outcome.ok) return void 0;
|
|
3275
|
+
return {
|
|
3276
|
+
hash: hashAnchorText(outcome.span.text),
|
|
3277
|
+
lines: outcome.span.endLine - outcome.span.startLine + 1
|
|
3278
|
+
};
|
|
3279
|
+
}
|
|
3280
|
+
|
|
3281
|
+
// src/commands/anchor-resolve/command.ts
|
|
3282
|
+
var anchorResolveCommand = define({
|
|
3283
|
+
name: "anchor-resolve",
|
|
3284
|
+
tool: "kb_anchor_resolve",
|
|
3285
|
+
usage: "anchor-resolve <concept-id> [--repo-root <path>] [--offline] [--rebaseline] [--restamp] [--check]",
|
|
3286
|
+
description: "Resolve a record's anchors: stamp a hash onto anchors that lack one, report drift where the code moved. An anchor naming another repository is read from that remote through a bare cache; --offline uses the cache only. Never writes verified[]; a judgment is kb_verify. Each result says what it compared and whether the write applied.",
|
|
3287
|
+
input: import_zod8.z.object({
|
|
3288
|
+
bundlePath,
|
|
3289
|
+
conceptId,
|
|
3290
|
+
repoRoot: import_zod8.z.string().min(1).optional(),
|
|
3291
|
+
offline: import_zod8.z.boolean().optional().describe(
|
|
3292
|
+
"Resolve foreign anchors from the local repo cache only, never fetching."
|
|
3293
|
+
),
|
|
3294
|
+
rebaseline: import_zod8.z.boolean().optional().describe(
|
|
3295
|
+
"Accept the current code as the new baseline for anchors that drifted."
|
|
3296
|
+
),
|
|
3297
|
+
restamp: import_zod8.z.boolean().optional().describe(
|
|
3298
|
+
"Refresh `resolved_at` on anchors that already match. Off by default, so a green run writes nothing."
|
|
3299
|
+
),
|
|
3300
|
+
check: import_zod8.z.boolean().optional().describe(
|
|
3301
|
+
"Resolve and report only: no hash, no `resolved_at`, no log entry."
|
|
3302
|
+
)
|
|
3303
|
+
}),
|
|
3304
|
+
fromArgv: (argv, path) => ({
|
|
3305
|
+
bundlePath: path,
|
|
3306
|
+
conceptId: argv[1],
|
|
3307
|
+
repoRoot: argvFlag(argv, "--repo-root"),
|
|
3308
|
+
offline: argv.includes("--offline"),
|
|
3309
|
+
rebaseline: argv.includes("--rebaseline"),
|
|
3310
|
+
restamp: argv.includes("--restamp"),
|
|
3311
|
+
check: argv.includes("--check")
|
|
3312
|
+
}),
|
|
3313
|
+
run: async ({ store, actor, now }, {
|
|
3314
|
+
bundlePath: path,
|
|
3315
|
+
conceptId: id,
|
|
3316
|
+
repoRoot,
|
|
3317
|
+
offline,
|
|
3318
|
+
rebaseline,
|
|
3319
|
+
restamp,
|
|
3320
|
+
check
|
|
3321
|
+
}) => {
|
|
3322
|
+
if (check && (rebaseline || restamp)) {
|
|
3323
|
+
throw new KbFlagConflictError([
|
|
3324
|
+
"check",
|
|
3325
|
+
rebaseline ? "rebaseline" : "restamp"
|
|
3326
|
+
]);
|
|
3088
3327
|
}
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3328
|
+
const root = repoRoot ?? process.cwd();
|
|
3329
|
+
const record = await store.read(path, id);
|
|
3330
|
+
if (!record) throw new KbRecordNotFoundError(id);
|
|
3331
|
+
const anchors = record.frontmatter.strauss_anchors ?? [];
|
|
3332
|
+
if (!anchors.length) {
|
|
3333
|
+
return {
|
|
3334
|
+
conceptId: id,
|
|
3335
|
+
results: [],
|
|
3336
|
+
note: "record has no anchors"
|
|
3337
|
+
};
|
|
3098
3338
|
}
|
|
3099
|
-
const
|
|
3339
|
+
const frozen = check ? false : await baseFrozen(process.cwd(), path);
|
|
3340
|
+
const plans = await planAnchors(anchors, {
|
|
3341
|
+
root,
|
|
3342
|
+
offline: offline === true,
|
|
3343
|
+
rebaseline: rebaseline === true,
|
|
3344
|
+
restamp: restamp === true,
|
|
3345
|
+
check: check === true,
|
|
3346
|
+
frozen,
|
|
3347
|
+
now
|
|
3348
|
+
});
|
|
3349
|
+
const applied = await applyPlan(plans, {
|
|
3350
|
+
store,
|
|
3351
|
+
actor,
|
|
3352
|
+
bundlePath: path,
|
|
3353
|
+
conceptId: id,
|
|
3354
|
+
frozen
|
|
3355
|
+
});
|
|
3356
|
+
const results = applied.results;
|
|
3357
|
+
const refused = frozen && plans.some((plan) => plan.write);
|
|
3100
3358
|
const hints = grammarHints();
|
|
3101
3359
|
const hintNote = hints.length ? { hints } : {};
|
|
3102
3360
|
const unreachable = results.filter(
|
|
3103
3361
|
(entry) => isUncheckedReason(entry.reason)
|
|
3104
3362
|
).length;
|
|
3105
3363
|
const matches3 = results.filter((entry) => entry.state === "match").length;
|
|
3106
|
-
const note =
|
|
3364
|
+
const note = [
|
|
3365
|
+
unreachable ? `${matches3}/${results.length - unreachable} anchors match, ${unreachable} unreachable` : "",
|
|
3366
|
+
refused ? "base is frozen: nothing was stamped" : "",
|
|
3367
|
+
applied.error ? `nothing was written: ${applied.error}` : ""
|
|
3368
|
+
].filter(Boolean).join("; ");
|
|
3107
3369
|
return {
|
|
3108
3370
|
conceptId: id,
|
|
3109
3371
|
results,
|
|
3110
|
-
...
|
|
3111
|
-
...
|
|
3372
|
+
...note ? { note } : {},
|
|
3373
|
+
...refused ? { frozen: true } : {},
|
|
3112
3374
|
...hintNote
|
|
3113
3375
|
};
|
|
3114
3376
|
},
|
|
3377
|
+
// Drift is a finding until a write settles it: a rebaseline the base took is
|
|
3378
|
+
// the answer to the drift it reports, while one refused, skipped, or never
|
|
3379
|
+
// asked for leaves the gate exactly what it was meant to catch.
|
|
3380
|
+
//
|
|
3115
3381
|
// A stored hash that no longer resolves is a broken anchor, not an absence:
|
|
3116
|
-
// the file was deleted or the symbol renamed
|
|
3117
|
-
//
|
|
3118
|
-
//
|
|
3119
|
-
//
|
|
3120
|
-
// would gate on work this command did not do.
|
|
3382
|
+
// the file was deleted or the symbol renamed. An anchor nobody ever stamped
|
|
3383
|
+
// is still just unstamped, and one whose remote nothing could reach was
|
|
3384
|
+
// never checked — failing CI on either would gate on work this command did
|
|
3385
|
+
// not do.
|
|
3121
3386
|
failsWhen: (result) => result.results.some(
|
|
3122
|
-
(entry) => entry.state === "drifted" || entry.state === "unresolved" && entry.storedHash !== void 0 && !isUncheckedReason(entry.reason)
|
|
3387
|
+
(entry) => entry.outcome === "failed" || entry.outcome === "skipped" || entry.state === "drifted" && entry.outcome !== "applied" || entry.state === "unresolved" && entry.storedHash !== void 0 && !isUncheckedReason(entry.reason)
|
|
3123
3388
|
)
|
|
3124
3389
|
});
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
)
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
)
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
)
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
)
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3390
|
+
|
|
3391
|
+
// src/commands/anchor-set/model.ts
|
|
3392
|
+
var import_zod9 = require("zod");
|
|
3393
|
+
var anchorSetInputSchema = import_zod9.z.object({
|
|
3394
|
+
reason: import_zod9.z.string().refine((text) => text.trim().length > 0, {
|
|
3395
|
+
message: "reason must say what was reviewed"
|
|
3396
|
+
}).describe(
|
|
3397
|
+
"What the reviewer read that makes these the right pointers. Recorded in the log."
|
|
3398
|
+
),
|
|
3399
|
+
anchors: import_zod9.z.array(kbAnchorWriteSchema).min(1).describe(
|
|
3400
|
+
"The complete new anchor set. Carry an anchor's hash forward to keep drift visible until the new code is read."
|
|
3401
|
+
)
|
|
3402
|
+
}).strict();
|
|
3403
|
+
var anchorSetCommandInput = import_zod9.z.object({
|
|
3404
|
+
bundlePath,
|
|
3405
|
+
conceptId,
|
|
3406
|
+
input: anchorSetInputSchema,
|
|
3407
|
+
resolve: import_zod9.z.boolean().optional().describe(
|
|
3408
|
+
"Also resolve and stamp every anchor against the current code, as anchor-resolve --rebaseline does."
|
|
3409
|
+
),
|
|
3410
|
+
repoRoot: import_zod9.z.string().min(1).optional().describe(
|
|
3411
|
+
"Where the anchored source lives, for resolve. Defaults to the working directory."
|
|
3412
|
+
),
|
|
3413
|
+
offline: import_zod9.z.boolean().optional().describe("With resolve, read foreign anchors from the repo cache only.")
|
|
3414
|
+
});
|
|
3415
|
+
|
|
3416
|
+
// src/commands/anchor-set/command.ts
|
|
3417
|
+
var NOTE = "pointers only: nothing was resolved or verified. Run anchor-resolve to check the new pointers, --rebaseline to accept the code, or pass resolve to do both here.";
|
|
3418
|
+
var STAMPED_NOTE = "pointers set and stamped against the current code. Not verification: run verify separately if someone reviewed it.";
|
|
3419
|
+
var INCOMPLETE_NOTE = "pointers set, but not every anchor was stamped: see each resolved entry's state and outcome.";
|
|
3420
|
+
var anchorSetCommand = define({
|
|
3421
|
+
name: "anchor-set",
|
|
3422
|
+
tool: "kb_anchor_set",
|
|
3423
|
+
usage: "anchor-set <concept-id> [--resolve] [--repo-root <path>] [--offline] < anchors.json",
|
|
3424
|
+
description: "Set a record's code anchors after a reviewed refactor, with a reason. The array is the whole set. With resolve, every anchor is stamped against the current code in the same call; without it, run kb_anchor_resolve next. Recorded in the log, never verification.",
|
|
3425
|
+
input: anchorSetCommandInput,
|
|
3426
|
+
fromArgv: async (argv, path, stdin) => ({
|
|
3427
|
+
bundlePath: path,
|
|
3428
|
+
conceptId: argv[1],
|
|
3429
|
+
input: JSON.parse(await stdin()),
|
|
3430
|
+
resolve: argv.includes("--resolve"),
|
|
3431
|
+
repoRoot: argvFlag(argv, "--repo-root"),
|
|
3432
|
+
offline: argv.includes("--offline")
|
|
3433
|
+
}),
|
|
3434
|
+
run: async (ctx, { bundlePath: path, conceptId: id, input, resolve: resolve7, repoRoot, offline }) => {
|
|
3435
|
+
const { store, actor } = ctx;
|
|
3436
|
+
await assertBaseNotFrozen(process.cwd(), path);
|
|
3437
|
+
let applied;
|
|
3438
|
+
const record = await store.updateAnchors(
|
|
3439
|
+
path,
|
|
3440
|
+
id,
|
|
3441
|
+
(current) => {
|
|
3442
|
+
applied = applyAnchorSet(current, input.anchors);
|
|
3443
|
+
return {
|
|
3444
|
+
anchors: applied.anchors,
|
|
3445
|
+
log: {
|
|
3446
|
+
operation: "anchor-set",
|
|
3447
|
+
reason: input.reason,
|
|
3448
|
+
anchors: applied.changes
|
|
3449
|
+
}
|
|
3450
|
+
};
|
|
3451
|
+
},
|
|
3452
|
+
actor
|
|
3171
3453
|
);
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
repo
|
|
3183
|
-
});
|
|
3184
|
-
continue;
|
|
3454
|
+
const changes = applied?.changes ?? [];
|
|
3455
|
+
if (!resolve7) {
|
|
3456
|
+
return {
|
|
3457
|
+
conceptId: id,
|
|
3458
|
+
reason: input.reason,
|
|
3459
|
+
changes,
|
|
3460
|
+
anchors: record.frontmatter.strauss_anchors ?? [],
|
|
3461
|
+
baseline: "unchanged",
|
|
3462
|
+
note: NOTE
|
|
3463
|
+
};
|
|
3185
3464
|
}
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3465
|
+
const resolved = await anchorResolveCommand.run(
|
|
3466
|
+
ctx,
|
|
3467
|
+
anchorResolveCommand.input.parse({
|
|
3468
|
+
bundlePath: path,
|
|
3469
|
+
conceptId: id,
|
|
3470
|
+
rebaseline: true,
|
|
3471
|
+
...repoRoot ? { repoRoot } : {},
|
|
3472
|
+
...offline ? { offline } : {}
|
|
3473
|
+
})
|
|
3474
|
+
);
|
|
3475
|
+
const after = await store.read(path, id);
|
|
3476
|
+
const stamped = resolved.results.every(
|
|
3477
|
+
(entry) => entry.state === "match" || entry.outcome === "applied"
|
|
3478
|
+
);
|
|
3479
|
+
return {
|
|
3480
|
+
conceptId: id,
|
|
3481
|
+
reason: input.reason,
|
|
3482
|
+
changes,
|
|
3483
|
+
anchors: after?.frontmatter.strauss_anchors ?? [],
|
|
3484
|
+
baseline: stamped ? "stamped" : "incomplete",
|
|
3485
|
+
resolved: resolved.results,
|
|
3486
|
+
note: stamped ? STAMPED_NOTE : INCOMPLETE_NOTE
|
|
3487
|
+
};
|
|
3488
|
+
},
|
|
3489
|
+
// With `resolve`, a pointer that names nothing is a failed set, not a
|
|
3490
|
+
// finding to read later, and a stamp that did not land fails as it does in
|
|
3491
|
+
// anchor-resolve. A remote nothing could reach was never checked, so it does
|
|
3492
|
+
// not fail — the same line anchor-resolve draws.
|
|
3493
|
+
failsWhen: (result, input) => {
|
|
3494
|
+
const resolved = result.resolved ?? [];
|
|
3495
|
+
return resolved.some(
|
|
3496
|
+
(entry) => entry.state === "unresolved" && !isUncheckedReason(entry.reason)
|
|
3497
|
+
) || anchorResolveCommand.failsWhen?.({ results: resolved }, input) === true;
|
|
3192
3498
|
}
|
|
3193
|
-
|
|
3194
|
-
}
|
|
3499
|
+
});
|
|
3195
3500
|
|
|
3196
3501
|
// src/commands/answer.ts
|
|
3197
|
-
var
|
|
3502
|
+
var import_zod10 = require("zod");
|
|
3198
3503
|
var answerCommand = define({
|
|
3199
3504
|
name: "answer",
|
|
3200
3505
|
tool: "kb_answer",
|
|
3201
3506
|
usage: "answer <concept-id> <answer...>",
|
|
3202
3507
|
description: "Resolve an open question: set status, stamp who and when, append an Answer section. If the answer overturns a decision or assumption, supersede that record explicitly.",
|
|
3203
|
-
input:
|
|
3508
|
+
input: import_zod10.z.object({ bundlePath, conceptId, answer: import_zod10.z.string().min(1) }),
|
|
3204
3509
|
fromArgv: (argv, path) => ({
|
|
3205
3510
|
bundlePath: path,
|
|
3206
3511
|
conceptId: argv[1],
|
|
@@ -3214,19 +3519,19 @@ var answerCommand = define({
|
|
|
3214
3519
|
});
|
|
3215
3520
|
|
|
3216
3521
|
// src/commands/backlinks.ts
|
|
3217
|
-
var
|
|
3522
|
+
var import_zod11 = require("zod");
|
|
3218
3523
|
var backlinksCommand = define({
|
|
3219
3524
|
name: "backlinks",
|
|
3220
3525
|
tool: "kb_backlinks",
|
|
3221
3526
|
usage: "backlinks <concept-id>",
|
|
3222
3527
|
description: "Who points at this record: every inbound typed causal link (`strauss_links`), one hop, every rel including `related_to`, each with its rel and the standing of the record that made it. Use it when you need the exact edges \u2014 reviewing or renaming a record.",
|
|
3223
|
-
input:
|
|
3528
|
+
input: import_zod11.z.object({ bundlePath, conceptId }),
|
|
3224
3529
|
fromArgv: (argv, path) => ({ bundlePath: path, conceptId: argv[1] }),
|
|
3225
3530
|
run: async ({ store }, { bundlePath: path, conceptId: id }) => store.backlinks(path, id)
|
|
3226
3531
|
});
|
|
3227
3532
|
|
|
3228
3533
|
// src/commands/catalog.ts
|
|
3229
|
-
var
|
|
3534
|
+
var import_zod12 = require("zod");
|
|
3230
3535
|
|
|
3231
3536
|
// src/adjudicate.ts
|
|
3232
3537
|
var STANDING = {
|
|
@@ -3405,9 +3710,9 @@ var catalogCommand = define({
|
|
|
3405
3710
|
tool: "kb_catalog",
|
|
3406
3711
|
usage: "catalog [type] [--tag T]...",
|
|
3407
3712
|
description: "Lists every record as one line \u2014 concept id, type, title, standing, and a stale flag \u2014 at roughly thirty tokens each. Pick this over kb_load once kb_load refuses: kb_catalog never refuses. Superseded records show only their replacement; fetch bodies with kb_load, kb_pack, kb_query, or kb_trace.",
|
|
3408
|
-
input:
|
|
3713
|
+
input: import_zod12.z.object({
|
|
3409
3714
|
bundlePath,
|
|
3410
|
-
type:
|
|
3715
|
+
type: import_zod12.z.enum(KB_RECORD_TYPES).optional(),
|
|
3411
3716
|
tags: TAGS
|
|
3412
3717
|
}),
|
|
3413
3718
|
fromArgv: (argv, path) => {
|
|
@@ -3479,7 +3784,7 @@ function count(value, noun) {
|
|
|
3479
3784
|
var import_node_buffer = require("buffer");
|
|
3480
3785
|
var import_promises7 = require("fs/promises");
|
|
3481
3786
|
var import_node_path10 = require("path");
|
|
3482
|
-
var
|
|
3787
|
+
var import_zod15 = require("zod");
|
|
3483
3788
|
|
|
3484
3789
|
// src/match-diff.ts
|
|
3485
3790
|
function matchToDiff(files, records, options = {}) {
|
|
@@ -4083,7 +4388,7 @@ function claimOf(record) {
|
|
|
4083
4388
|
}
|
|
4084
4389
|
|
|
4085
4390
|
// src/commands/match/command.ts
|
|
4086
|
-
var
|
|
4391
|
+
var import_zod14 = require("zod");
|
|
4087
4392
|
|
|
4088
4393
|
// src/commands/match/errors.ts
|
|
4089
4394
|
var KbMatchInputError = class extends BaseError {
|
|
@@ -4103,21 +4408,21 @@ var KbMatchInputError = class extends BaseError {
|
|
|
4103
4408
|
};
|
|
4104
4409
|
|
|
4105
4410
|
// src/commands/match/model.ts
|
|
4106
|
-
var
|
|
4107
|
-
var diffHunkSchema =
|
|
4108
|
-
startLine:
|
|
4109
|
-
endLine:
|
|
4110
|
-
side:
|
|
4411
|
+
var import_zod13 = require("zod");
|
|
4412
|
+
var diffHunkSchema = import_zod13.z.object({
|
|
4413
|
+
startLine: import_zod13.z.number().int().positive(),
|
|
4414
|
+
endLine: import_zod13.z.number().int().positive(),
|
|
4415
|
+
side: import_zod13.z.enum(["old", "new"]).optional()
|
|
4111
4416
|
}).passthrough();
|
|
4112
|
-
var diffFileSchema =
|
|
4113
|
-
filePath:
|
|
4114
|
-
hunks:
|
|
4417
|
+
var diffFileSchema = import_zod13.z.object({
|
|
4418
|
+
filePath: import_zod13.z.string().min(1).describe("Repo-relative, spelled the way anchors are."),
|
|
4419
|
+
hunks: import_zod13.z.array(diffHunkSchema)
|
|
4115
4420
|
});
|
|
4116
|
-
var symbolRangeSchema =
|
|
4117
|
-
file:
|
|
4118
|
-
symbol:
|
|
4119
|
-
startLine:
|
|
4120
|
-
endLine:
|
|
4421
|
+
var symbolRangeSchema = import_zod13.z.object({
|
|
4422
|
+
file: import_zod13.z.string().min(1),
|
|
4423
|
+
symbol: import_zod13.z.string().min(1),
|
|
4424
|
+
startLine: import_zod13.z.number().int().positive(),
|
|
4425
|
+
endLine: import_zod13.z.number().int().positive()
|
|
4121
4426
|
});
|
|
4122
4427
|
|
|
4123
4428
|
// src/commands/match/parse-unified-diff.ts
|
|
@@ -4366,17 +4671,17 @@ var matchCommand = define({
|
|
|
4366
4671
|
tool: "kb_match",
|
|
4367
4672
|
usage: "match --git <base>..<head> | --stdin [--repo-root <path>] [--offline] [--include-non-current]",
|
|
4368
4673
|
description: "Which records sit on each changed hunk: the anchored records per file range, current first, each with its standing and the anchor that matched. kb_load hands over a whole base; this narrows a diff. Symbol ranges resolve from repoRoot when omitted; non-current records need includeNonCurrent.",
|
|
4369
|
-
input:
|
|
4674
|
+
input: import_zod14.z.object({
|
|
4370
4675
|
bundlePath,
|
|
4371
|
-
files:
|
|
4372
|
-
symbolRanges:
|
|
4676
|
+
files: import_zod14.z.array(diffFileSchema).describe("The changed files, each with its post-change line ranges."),
|
|
4677
|
+
symbolRanges: import_zod14.z.array(symbolRangeSchema).optional().describe(
|
|
4373
4678
|
"Symbol spans the caller already has. Resolved from repoRoot when omitted."
|
|
4374
4679
|
),
|
|
4375
4680
|
repoRoot: REPO_ROOT,
|
|
4376
|
-
offline:
|
|
4681
|
+
offline: import_zod14.z.boolean().optional().describe(
|
|
4377
4682
|
"Resolve symbol ranges from what is already on disk, never fetching a grammar."
|
|
4378
4683
|
),
|
|
4379
|
-
includeNonCurrent:
|
|
4684
|
+
includeNonCurrent: import_zod14.z.boolean().optional().describe(
|
|
4380
4685
|
"Return superseded, rejected and unsettled records too, each carrying its standing."
|
|
4381
4686
|
)
|
|
4382
4687
|
}),
|
|
@@ -4390,11 +4695,11 @@ var matchCommand = define({
|
|
|
4390
4695
|
...argv.includes("--include-non-current") ? { includeNonCurrent: true } : {}
|
|
4391
4696
|
};
|
|
4392
4697
|
if (range !== void 0) {
|
|
4393
|
-
const
|
|
4394
|
-
if (!
|
|
4395
|
-
throw new KbMatchInputError(`--git ${range} ${REFUSED[
|
|
4698
|
+
const diff2 = await readRangeDiff(repoRoot ?? process.cwd(), range);
|
|
4699
|
+
if (!diff2.ok) {
|
|
4700
|
+
throw new KbMatchInputError(`--git ${range} ${REFUSED[diff2.reason]}`);
|
|
4396
4701
|
}
|
|
4397
|
-
return { ...base2, files: parseUnifiedDiff(
|
|
4702
|
+
return { ...base2, files: parseUnifiedDiff(diff2.text) };
|
|
4398
4703
|
}
|
|
4399
4704
|
if (!argv.includes("--stdin")) {
|
|
4400
4705
|
throw new KbMatchInputError(
|
|
@@ -4480,22 +4785,22 @@ function project(match, ranges, all) {
|
|
|
4480
4785
|
|
|
4481
4786
|
// src/commands/classify.ts
|
|
4482
4787
|
var classifyFileSchema = diffFileSchema.extend({
|
|
4483
|
-
hunks:
|
|
4484
|
-
diffHunkSchema.extend({ lines:
|
|
4788
|
+
hunks: import_zod15.z.array(
|
|
4789
|
+
diffHunkSchema.extend({ lines: import_zod15.z.array(import_zod15.z.string()).optional() })
|
|
4485
4790
|
),
|
|
4486
|
-
renamedFrom:
|
|
4487
|
-
similarity:
|
|
4791
|
+
renamedFrom: import_zod15.z.string().min(1).optional().describe("Where `git diff -M` says the path came from."),
|
|
4792
|
+
similarity: import_zod15.z.number().min(0).max(100).optional()
|
|
4488
4793
|
});
|
|
4489
4794
|
var classifyCommand = define({
|
|
4490
4795
|
name: "classify",
|
|
4491
4796
|
tool: "kb_classify",
|
|
4492
4797
|
usage: "classify --git <base>..<head> | --stdin [--repo-root <path>] [--offline]",
|
|
4493
4798
|
description: "What kind of change each file carries: test, config, ci, docs, lockfile, generated, boilerplate, rename or source, with the rule that decided it. Derived from the diff and never stored; a `review:generated`, `review:boilerplate` or `review:move` fact anchored on a file overrides the heuristic. kb_match says what sits on a hunk; this says whether to read it.",
|
|
4494
|
-
input:
|
|
4799
|
+
input: import_zod15.z.object({
|
|
4495
4800
|
bundlePath,
|
|
4496
|
-
files:
|
|
4801
|
+
files: import_zod15.z.array(classifyFileSchema).describe("The changed files, each with its line ranges."),
|
|
4497
4802
|
repoRoot: REPO_ROOT,
|
|
4498
|
-
offline:
|
|
4803
|
+
offline: import_zod15.z.boolean().optional().describe(
|
|
4499
4804
|
"Resolve symbol ranges from what is already on disk, never fetching a grammar."
|
|
4500
4805
|
)
|
|
4501
4806
|
}),
|
|
@@ -4508,15 +4813,15 @@ var classifyCommand = define({
|
|
|
4508
4813
|
...argv.includes("--offline") ? { offline: true } : {}
|
|
4509
4814
|
};
|
|
4510
4815
|
if (range !== void 0) {
|
|
4511
|
-
const
|
|
4512
|
-
if (!
|
|
4816
|
+
const diff2 = await readRangeDiff(repoRoot ?? process.cwd(), range);
|
|
4817
|
+
if (!diff2.ok) {
|
|
4513
4818
|
throw new KbClassifyInputError(
|
|
4514
|
-
`--git ${range} ${REFUSED2[
|
|
4819
|
+
`--git ${range} ${REFUSED2[diff2.reason]}`
|
|
4515
4820
|
);
|
|
4516
4821
|
}
|
|
4517
4822
|
return {
|
|
4518
4823
|
...base2,
|
|
4519
|
-
files: parseUnifiedDiff(
|
|
4824
|
+
files: parseUnifiedDiff(diff2.text, {
|
|
4520
4825
|
keepEmpty: true,
|
|
4521
4826
|
withLines: true
|
|
4522
4827
|
})
|
|
@@ -4606,7 +4911,7 @@ function renderClassify(result) {
|
|
|
4606
4911
|
}
|
|
4607
4912
|
|
|
4608
4913
|
// src/commands/context.ts
|
|
4609
|
-
var
|
|
4914
|
+
var import_zod16 = require("zod");
|
|
4610
4915
|
|
|
4611
4916
|
// src/kb-context.ts
|
|
4612
4917
|
var import_promises8 = require("fs/promises");
|
|
@@ -4877,23 +5182,23 @@ var contextCommand = define({
|
|
|
4877
5182
|
tool: "kb_context",
|
|
4878
5183
|
usage: "context [--profile NAME] [--budget N] [--full-under N] [--exclude-tag T]... [--format json] [--event NAME]",
|
|
4879
5184
|
description: "Index block of pinned bases (ids, titles, standing) for injection at context birth. Takes no bundlePath \u2014 reads the workspace pin manifests. Empty when nothing is pinned; refuses over budget rather than truncating. Budget precedence: flags, then the manifest `context[profile]` over `context.default`, then the built-in profile, then package defaults.",
|
|
4880
|
-
input:
|
|
4881
|
-
budgetTokens:
|
|
5185
|
+
input: import_zod16.z.object({
|
|
5186
|
+
budgetTokens: import_zod16.z.number().int().positive().optional().describe(
|
|
4882
5187
|
"Ceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000."
|
|
4883
5188
|
),
|
|
4884
|
-
fullUnderTokens:
|
|
5189
|
+
fullUnderTokens: import_zod16.z.number().int().positive().optional().describe(
|
|
4885
5190
|
"Per-base rendering threshold, applied before the budget: a base whose complete load fits under this arrives as full records instead of index lines, and the whole block still answers to budgetTokens. Off by default \u2014 index-only is the safe default at a context birth, because injected bodies outlive the qualifiers on them; the session-start profile opts tiny bases in at 1500."
|
|
4886
5191
|
),
|
|
4887
|
-
profile:
|
|
5192
|
+
profile: import_zod16.z.string().optional().describe(
|
|
4888
5193
|
"Named budget set: built-ins are session-start (full-under 1500), compact and turn (budget 2500); the manifests' `context` tables override per repo. Unknown names fall through to defaults rather than failing."
|
|
4889
5194
|
),
|
|
4890
|
-
excludeTags:
|
|
5195
|
+
excludeTags: import_zod16.z.array(import_zod16.z.string().min(1)).optional().describe(
|
|
4891
5196
|
"Frontmatter tags whose records stay out of the block. The base stays pinned and stays readable by tool; resolved like the budgets."
|
|
4892
5197
|
),
|
|
4893
|
-
format:
|
|
5198
|
+
format: import_zod16.z.enum(["markdown", "json"]).optional().describe(
|
|
4894
5199
|
"CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
|
|
4895
5200
|
),
|
|
4896
|
-
event:
|
|
5201
|
+
event: import_zod16.z.string().optional().describe(
|
|
4897
5202
|
"hookEventName stamped into the JSON envelope. Only meaningful with format=json."
|
|
4898
5203
|
)
|
|
4899
5204
|
}),
|
|
@@ -4932,7 +5237,7 @@ var contextCommand = define({
|
|
|
4932
5237
|
});
|
|
4933
5238
|
|
|
4934
5239
|
// src/commands/doctor.ts
|
|
4935
|
-
var
|
|
5240
|
+
var import_zod18 = require("zod");
|
|
4936
5241
|
|
|
4937
5242
|
// src/kb-edges.ts
|
|
4938
5243
|
var KB_EDGE_KINDS = [
|
|
@@ -5448,17 +5753,17 @@ function ageInDays(record, now) {
|
|
|
5448
5753
|
}
|
|
5449
5754
|
|
|
5450
5755
|
// src/commands/reassess.ts
|
|
5451
|
-
var
|
|
5756
|
+
var import_zod17 = require("zod");
|
|
5452
5757
|
var reassessCommand = define({
|
|
5453
5758
|
name: "reassess",
|
|
5454
5759
|
tool: "kb_reassess",
|
|
5455
5760
|
usage: "reassess <concept-id> [--repo-root <path>] [--with-diff]",
|
|
5456
5761
|
description: "One drifted record, as something to judge: its claim, each anchor's drift class, the old-vs-new span diff, and the records that depend on it. Formatting-only drift is dropped. Empty when there is nothing to reassess. Writes: relocates moved anchors, keeping their hash; never verifies, supersedes, or changes standing.",
|
|
5457
|
-
input:
|
|
5762
|
+
input: import_zod17.z.object({
|
|
5458
5763
|
bundlePath,
|
|
5459
5764
|
conceptId,
|
|
5460
5765
|
repoRoot: REPO_ROOT,
|
|
5461
|
-
withDiff:
|
|
5766
|
+
withDiff: import_zod17.z.boolean().optional().describe(
|
|
5462
5767
|
"Recover each anchor's committed span and render the diff. Reads git history."
|
|
5463
5768
|
)
|
|
5464
5769
|
}),
|
|
@@ -5603,13 +5908,13 @@ function at(file, symbol) {
|
|
|
5603
5908
|
}
|
|
5604
5909
|
|
|
5605
5910
|
// src/commands/doctor.ts
|
|
5606
|
-
var days = (what, fallback) =>
|
|
5911
|
+
var days = (what, fallback) => import_zod18.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
|
|
5607
5912
|
var doctorCommand = define({
|
|
5608
5913
|
name: "doctor",
|
|
5609
5914
|
tool: "kb_doctor",
|
|
5610
5915
|
usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--offline] [--strict] [--drifted [--with-diff]]",
|
|
5611
5916
|
description: "Read-only health sweep: expired, expiring, unverified, aging, orphaned, broken-supersession, superseded-but-cited, drifted and unchecked anchors. Every group is reported even when empty; nothing is written or re-stamped. `drifted` narrows it to a reassessment packet per drifted record, `with_diff` adding each anchor's old-vs-new span.",
|
|
5612
|
-
input:
|
|
5917
|
+
input: import_zod18.z.object({
|
|
5613
5918
|
bundlePath,
|
|
5614
5919
|
repoRoot: REPO_ROOT,
|
|
5615
5920
|
expiringDays: days(
|
|
@@ -5624,16 +5929,16 @@ var doctorCommand = define({
|
|
|
5624
5929
|
"How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
|
|
5625
5930
|
DEFAULT_AGING_DAYS
|
|
5626
5931
|
),
|
|
5627
|
-
offline:
|
|
5932
|
+
offline: import_zod18.z.boolean().optional().describe(
|
|
5628
5933
|
"Read foreign anchors from the local repo cache only, never fetching."
|
|
5629
5934
|
),
|
|
5630
|
-
strict:
|
|
5935
|
+
strict: import_zod18.z.boolean().optional().describe(
|
|
5631
5936
|
"Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
|
|
5632
5937
|
),
|
|
5633
|
-
drifted:
|
|
5938
|
+
drifted: import_zod18.z.boolean().optional().describe(
|
|
5634
5939
|
"Report only drift, as a reassessment packet per record: claim, per-anchor class, and what depends on it."
|
|
5635
5940
|
),
|
|
5636
|
-
withDiff:
|
|
5941
|
+
withDiff: import_zod18.z.boolean().optional().describe(
|
|
5637
5942
|
"With `drifted`: recover each anchor's committed span and render the old-vs-new diff. Reads git history."
|
|
5638
5943
|
)
|
|
5639
5944
|
}),
|
|
@@ -5810,7 +6115,7 @@ function renderPackets(result) {
|
|
|
5810
6115
|
// src/commands/export.ts
|
|
5811
6116
|
var import_promises9 = require("fs/promises");
|
|
5812
6117
|
var import_node_path11 = require("path");
|
|
5813
|
-
var
|
|
6118
|
+
var import_zod19 = require("zod");
|
|
5814
6119
|
var NUMBERED = /^(\d{4})-(.+)\.md$/;
|
|
5815
6120
|
var MARKER = "<!-- strauss-kb export: ";
|
|
5816
6121
|
var exportCommand = define({
|
|
@@ -5818,10 +6123,10 @@ var exportCommand = define({
|
|
|
5818
6123
|
tool: "kb_export",
|
|
5819
6124
|
usage: "export --format madr --to <dir>",
|
|
5820
6125
|
description: "Write the base's decisions out as numbered MADR files, one per decision, for a repository that keeps ADRs of its own. Numbering is by slug, so a re-run rewrites its own files in place. A superseded decision is exported with what replaced it.",
|
|
5821
|
-
input:
|
|
6126
|
+
input: import_zod19.z.object({
|
|
5822
6127
|
bundlePath,
|
|
5823
|
-
format:
|
|
5824
|
-
to:
|
|
6128
|
+
format: import_zod19.z.enum(["madr"]).describe("Output layout. `madr` is the only one so far."),
|
|
6129
|
+
to: import_zod19.z.string().min(1).describe("Directory the ADR files are written into.")
|
|
5825
6130
|
}),
|
|
5826
6131
|
fromArgv: (argv, path) => ({
|
|
5827
6132
|
bundlePath: path,
|
|
@@ -5943,19 +6248,19 @@ function bodySections(body) {
|
|
|
5943
6248
|
}
|
|
5944
6249
|
|
|
5945
6250
|
// src/commands/impact.ts
|
|
5946
|
-
var
|
|
6251
|
+
var import_zod20 = require("zod");
|
|
5947
6252
|
var impactCommand = define({
|
|
5948
6253
|
name: "impact",
|
|
5949
6254
|
tool: "kb_impact",
|
|
5950
6255
|
usage: "impact <concept-id> [--depth N] [--rels a,b]",
|
|
5951
6256
|
description: "What breaks if this record changes: its transitive set of dependants, each with its standing. Each rel declares which of its ends depends on the other, and the walk follows each rel in its own direction. Naming `related_to` or an unknown rel in `rels` is an error. kb_backlinks gives one flat hop.",
|
|
5952
|
-
input:
|
|
6257
|
+
input: import_zod20.z.object({
|
|
5953
6258
|
bundlePath,
|
|
5954
6259
|
conceptId,
|
|
5955
|
-
depth:
|
|
6260
|
+
depth: import_zod20.z.number().int().positive().optional().describe(
|
|
5956
6261
|
"Hops out from the record. Unbounded when omitted; a walk this cuts reports truncated: true."
|
|
5957
6262
|
),
|
|
5958
|
-
rels:
|
|
6263
|
+
rels: import_zod20.z.array(import_zod20.z.enum(KB_CAUSAL_LINK_RELS)).optional().describe(
|
|
5959
6264
|
"Narrow which rels the walk follows. Defaults to every rel that carries a dependence \u2014 all but related_to."
|
|
5960
6265
|
)
|
|
5961
6266
|
}),
|
|
@@ -5976,15 +6281,15 @@ var impactCommand = define({
|
|
|
5976
6281
|
});
|
|
5977
6282
|
|
|
5978
6283
|
// src/commands/list.ts
|
|
5979
|
-
var
|
|
6284
|
+
var import_zod21 = require("zod");
|
|
5980
6285
|
var listCommand = define({
|
|
5981
6286
|
name: "list",
|
|
5982
6287
|
tool: "kb_list",
|
|
5983
6288
|
usage: "list [type] [--tag T]...",
|
|
5984
6289
|
description: "Every record, optionally one type or tag. For enumerating; use kb_query for a question.",
|
|
5985
|
-
input:
|
|
6290
|
+
input: import_zod21.z.object({
|
|
5986
6291
|
bundlePath,
|
|
5987
|
-
type:
|
|
6292
|
+
type: import_zod21.z.enum(KB_RECORD_TYPES).optional(),
|
|
5988
6293
|
tags: TAGS
|
|
5989
6294
|
}),
|
|
5990
6295
|
fromArgv: (argv, path) => {
|
|
@@ -6008,17 +6313,17 @@ var listCommand = define({
|
|
|
6008
6313
|
});
|
|
6009
6314
|
|
|
6010
6315
|
// src/commands/load.ts
|
|
6011
|
-
var
|
|
6316
|
+
var import_zod22 = require("zod");
|
|
6012
6317
|
var loadCommand = define({
|
|
6013
6318
|
name: "load",
|
|
6014
6319
|
tool: "kb_load",
|
|
6015
6320
|
usage: "load [type] [--budget N | --all] [--repo-root PATH]",
|
|
6016
6321
|
description: "Load the whole base, each record with its standing \u2014 call it first, at the point of use, since compaction drops it. Superseded records arrive as stubs; kb_trace has the history. Over budget it refuses: kb_catalog, then kb_pack, or narrow with `type`; `all` bypasses. Never read record files directly \u2014 only kb_* tools resolve supersession. `digest` stamps the base's content, so hooks know when to reload.",
|
|
6017
|
-
input:
|
|
6322
|
+
input: import_zod22.z.object({
|
|
6018
6323
|
bundlePath,
|
|
6019
|
-
type:
|
|
6020
|
-
budgetTokens:
|
|
6021
|
-
all:
|
|
6324
|
+
type: import_zod22.z.enum(KB_RECORD_TYPES).optional(),
|
|
6325
|
+
budgetTokens: import_zod22.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
|
|
6326
|
+
all: import_zod22.z.boolean().optional().describe(
|
|
6022
6327
|
"Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
|
|
6023
6328
|
),
|
|
6024
6329
|
repoRoot: REPO_ROOT
|
|
@@ -6060,25 +6365,25 @@ var loadCommand = define({
|
|
|
6060
6365
|
});
|
|
6061
6366
|
|
|
6062
6367
|
// src/commands/log.ts
|
|
6063
|
-
var
|
|
6368
|
+
var import_zod23 = require("zod");
|
|
6064
6369
|
var logCommand = define({
|
|
6065
6370
|
name: "log",
|
|
6066
6371
|
tool: "kb_log",
|
|
6067
6372
|
usage: "log",
|
|
6068
6373
|
description: "Who touched what, and when. Append-only; malformed lines are reported, never repaired.",
|
|
6069
|
-
input:
|
|
6374
|
+
input: import_zod23.z.object({ bundlePath }),
|
|
6070
6375
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
6071
6376
|
run: ({ store }, { bundlePath: path }) => store.readLog(path)
|
|
6072
6377
|
});
|
|
6073
6378
|
|
|
6074
6379
|
// src/commands/no-decision.ts
|
|
6075
|
-
var
|
|
6380
|
+
var import_zod24 = require("zod");
|
|
6076
6381
|
var noDecisionCommand = define({
|
|
6077
6382
|
name: "no-decision",
|
|
6078
6383
|
tool: "kb_no_decision",
|
|
6079
6384
|
usage: "no-decision <reason...>",
|
|
6080
6385
|
description: "Record in one sentence that a piece of work had nothing to decide. Idempotent.",
|
|
6081
|
-
input:
|
|
6386
|
+
input: import_zod24.z.object({ bundlePath, reason: import_zod24.z.string().min(1) }),
|
|
6082
6387
|
fromArgv: (argv, path) => ({
|
|
6083
6388
|
bundlePath: path,
|
|
6084
6389
|
reason: argv.slice(1).join(" ").trim()
|
|
@@ -6095,20 +6400,20 @@ var noDecisionCommand = define({
|
|
|
6095
6400
|
});
|
|
6096
6401
|
|
|
6097
6402
|
// src/commands/pack.ts
|
|
6098
|
-
var
|
|
6403
|
+
var import_zod25 = require("zod");
|
|
6099
6404
|
var packCommand = define({
|
|
6100
6405
|
name: "pack",
|
|
6101
6406
|
tool: "kb_pack",
|
|
6102
6407
|
usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
|
|
6103
6408
|
description: "Bounded neighbourhood around one record: within `hops`, ranked, cut to `maxNodes`, with every cut record named under Excluded. Use when the base is over kb_load's budget and the work centres on a record you can name. Refuses over budget rather than truncating. Everything below the header is byte-stable across runs. Resolves supersession like kb_load.",
|
|
6104
|
-
input:
|
|
6409
|
+
input: import_zod25.z.object({
|
|
6105
6410
|
bundlePath,
|
|
6106
6411
|
conceptId,
|
|
6107
|
-
hops:
|
|
6108
|
-
maxNodes:
|
|
6412
|
+
hops: import_zod25.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
|
|
6413
|
+
maxNodes: import_zod25.z.number().int().positive().optional().describe(
|
|
6109
6414
|
"How many records the pack may hold, root included. Defaults to 20."
|
|
6110
6415
|
),
|
|
6111
|
-
budgetTokens:
|
|
6416
|
+
budgetTokens: import_zod25.z.number().int().positive().optional().describe(
|
|
6112
6417
|
"Approximate token ceiling over what is actually emitted. Defaults to 25000."
|
|
6113
6418
|
)
|
|
6114
6419
|
}),
|
|
@@ -6195,22 +6500,22 @@ function warningLabel(warning) {
|
|
|
6195
6500
|
}
|
|
6196
6501
|
|
|
6197
6502
|
// src/commands/pin.ts
|
|
6198
|
-
var
|
|
6503
|
+
var import_zod26 = require("zod");
|
|
6199
6504
|
var pinCommand = define({
|
|
6200
6505
|
name: "pin",
|
|
6201
6506
|
tool: "kb_pin",
|
|
6202
6507
|
usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
|
|
6203
6508
|
description: "Pin a base into a workspace manifest so kb_context surfaces it. Layers, nearest wins: project `.strauss/kb-pins.json` (default), `--local` (personal, gitignored), `--user` (`~/.strauss`). Idempotent; `--mode full|index`, `--profiles`, `--frozen`/`--unfreeze` update only those fields. A path with no records pins with a warning. Never touches the base itself.",
|
|
6204
|
-
input:
|
|
6509
|
+
input: import_zod26.z.object({
|
|
6205
6510
|
bundlePath,
|
|
6206
|
-
mode:
|
|
6511
|
+
mode: import_zod26.z.enum(["full", "index"]).optional().describe(
|
|
6207
6512
|
"full: always emit this base's records whole (still under the block budget); index: never upgrade. Absent: the profile's full-under threshold decides."
|
|
6208
6513
|
),
|
|
6209
|
-
profiles:
|
|
6210
|
-
layer:
|
|
6514
|
+
profiles: import_zod26.z.array(import_zod26.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
|
|
6515
|
+
layer: import_zod26.z.enum(["project", "local", "user"]).optional().describe(
|
|
6211
6516
|
"Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
|
|
6212
6517
|
),
|
|
6213
|
-
frozen:
|
|
6518
|
+
frozen: import_zod26.z.boolean().optional().describe(
|
|
6214
6519
|
"true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
|
|
6215
6520
|
)
|
|
6216
6521
|
}),
|
|
@@ -6239,13 +6544,13 @@ var pinCommand = define({
|
|
|
6239
6544
|
});
|
|
6240
6545
|
|
|
6241
6546
|
// src/commands/pins.ts
|
|
6242
|
-
var
|
|
6547
|
+
var import_zod27 = require("zod");
|
|
6243
6548
|
var pinsCommand = define({
|
|
6244
6549
|
name: "pins",
|
|
6245
6550
|
tool: "kb_pins",
|
|
6246
6551
|
usage: "pins",
|
|
6247
6552
|
description: "Every pinned base across the manifest layers, with its layer and whether it resolves to records. Takes no bundlePath.",
|
|
6248
|
-
input:
|
|
6553
|
+
input: import_zod27.z.object({}),
|
|
6249
6554
|
fromArgv: () => ({}),
|
|
6250
6555
|
run: ({ store }) => listPins(store, process.cwd())
|
|
6251
6556
|
});
|
|
@@ -6516,16 +6821,16 @@ function recordType(conceptId2) {
|
|
|
6516
6821
|
}
|
|
6517
6822
|
|
|
6518
6823
|
// src/commands/promote/model.ts
|
|
6519
|
-
var
|
|
6520
|
-
var promoteInputSchema =
|
|
6824
|
+
var import_zod28 = require("zod");
|
|
6825
|
+
var promoteInputSchema = import_zod28.z.object({
|
|
6521
6826
|
bundlePath,
|
|
6522
|
-
conceptIds:
|
|
6523
|
-
to:
|
|
6524
|
-
source:
|
|
6827
|
+
conceptIds: import_zod28.z.array(conceptId).max(64).optional().describe("Records to copy into the target base. Omit with `list`."),
|
|
6828
|
+
to: import_zod28.z.string().min(1).optional().describe("Absolute path to the base being promoted into."),
|
|
6829
|
+
source: import_zod28.z.string().min(1).optional().describe(
|
|
6525
6830
|
"Where the promotion came from, usually the pull request URL. Recorded on each copy as a source."
|
|
6526
6831
|
),
|
|
6527
|
-
force:
|
|
6528
|
-
list:
|
|
6832
|
+
force: import_zod28.z.boolean().optional().describe("Overwrite a record the target base already holds."),
|
|
6833
|
+
list: import_zod28.z.boolean().optional().describe("List the source base's candidates instead of promoting.")
|
|
6529
6834
|
}).refine((input) => input.list === true || input.to !== void 0, {
|
|
6530
6835
|
message: "promote needs a target base \u2014 pass --to <bundle>, or --list",
|
|
6531
6836
|
path: ["to"]
|
|
@@ -6662,17 +6967,17 @@ function renderPromote(result) {
|
|
|
6662
6967
|
}
|
|
6663
6968
|
|
|
6664
6969
|
// src/commands/query.ts
|
|
6665
|
-
var
|
|
6970
|
+
var import_zod29 = require("zod");
|
|
6666
6971
|
var queryCommand = define({
|
|
6667
6972
|
name: "query",
|
|
6668
6973
|
tool: "kb_query",
|
|
6669
6974
|
usage: "query <text...> [--tag T]... [--repo-root PATH]",
|
|
6670
6975
|
description: "Search; every hit carries its standing. Flagged, never filtered: a superseded hit returns with its replacement, a rejected one is marked. Prefer kb_load when the base fits its budget \u2014 a full read beats search. Results are volatile: place them at the tail, not the cached prefix. Never read record files directly.",
|
|
6671
|
-
input:
|
|
6976
|
+
input: import_zod29.z.object({
|
|
6672
6977
|
bundlePath,
|
|
6673
|
-
text:
|
|
6674
|
-
type:
|
|
6675
|
-
includeNonCurrent:
|
|
6978
|
+
text: import_zod29.z.string().optional(),
|
|
6979
|
+
type: import_zod29.z.enum(KB_RECORD_TYPES).optional(),
|
|
6980
|
+
includeNonCurrent: import_zod29.z.boolean().optional(),
|
|
6676
6981
|
tags: TAGS,
|
|
6677
6982
|
repoRoot: REPO_ROOT
|
|
6678
6983
|
}),
|
|
@@ -6706,27 +7011,32 @@ var queryCommand = define({
|
|
|
6706
7011
|
});
|
|
6707
7012
|
|
|
6708
7013
|
// src/commands/read-index.ts
|
|
6709
|
-
var
|
|
7014
|
+
var import_zod30 = require("zod");
|
|
6710
7015
|
var readIndexCommand = define({
|
|
6711
7016
|
name: "index",
|
|
6712
7017
|
tool: "kb_index",
|
|
6713
7018
|
usage: "index",
|
|
6714
7019
|
description: "The index \u2014 title, type, status, description per record \u2014 rebuilt if stale. Cheapest re-orientation after compaction: call it (or kb_context) first, then kb_load or fetch by id.",
|
|
6715
|
-
input:
|
|
7020
|
+
input: import_zod30.z.object({ bundlePath }),
|
|
6716
7021
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
6717
7022
|
run: ({ store }, { bundlePath: path }) => store.readIndex(path)
|
|
6718
7023
|
});
|
|
6719
7024
|
|
|
6720
7025
|
// src/commands/schema.ts
|
|
6721
|
-
var
|
|
7026
|
+
var import_zod33 = require("zod");
|
|
6722
7027
|
|
|
6723
7028
|
// src/json-schema.ts
|
|
6724
|
-
var
|
|
7029
|
+
var import_zod32 = require("zod");
|
|
6725
7030
|
|
|
6726
7031
|
// src/kb-log.ts
|
|
6727
|
-
var
|
|
7032
|
+
var import_zod31 = require("zod");
|
|
6728
7033
|
var LOG_FILE = "log.jsonl";
|
|
6729
|
-
var
|
|
7034
|
+
var kbLogAnchorChangeSchema = import_zod31.z.object({
|
|
7035
|
+
op: import_zod31.z.enum(["move", "add", "drop"]),
|
|
7036
|
+
from: kbAnchorLocatorSchema.optional(),
|
|
7037
|
+
to: kbAnchorLocatorSchema.optional()
|
|
7038
|
+
}).strict();
|
|
7039
|
+
var kbLogEntryFields = import_zod31.z.object({
|
|
6730
7040
|
// Validated, not just `min(1)`: `at` is a sort key (see `parseLog`
|
|
6731
7041
|
// below), and a value that isn't actually chronological — a Unix
|
|
6732
7042
|
// timestamp, a human-typed date, garbage — would sort wrong without
|
|
@@ -6735,18 +7045,28 @@ var kbLogEntrySchema = import_zod29.z.object({
|
|
|
6735
7045
|
// and rejects everything else, including a non-`Z` offset — so a
|
|
6736
7046
|
// malformed `at` is reported the same way a malformed line already is,
|
|
6737
7047
|
// rather than silently sorting into the wrong place.
|
|
6738
|
-
at:
|
|
6739
|
-
by:
|
|
6740
|
-
operation:
|
|
6741
|
-
conceptId:
|
|
7048
|
+
at: import_zod31.z.iso.datetime(),
|
|
7049
|
+
by: import_zod31.z.string().min(1),
|
|
7050
|
+
operation: import_zod31.z.string().min(1),
|
|
7051
|
+
conceptId: import_zod31.z.string().min(1),
|
|
6742
7052
|
/**
|
|
6743
7053
|
* The operation's other end, where it has one: a second concept id for
|
|
6744
7054
|
* supersession, the other base's path for promotion.
|
|
6745
7055
|
*/
|
|
6746
|
-
target:
|
|
6747
|
-
|
|
7056
|
+
target: import_zod31.z.string().min(1).optional(),
|
|
7057
|
+
/**
|
|
7058
|
+
* Why the operation was performed, where the operation demands one.
|
|
7059
|
+
* `anchor-set` does: a pointer moved by a reader is only auditable if
|
|
7060
|
+
* the reading is recorded beside it.
|
|
7061
|
+
*/
|
|
7062
|
+
reason: import_zod31.z.string().min(1).optional(),
|
|
7063
|
+
/** What `anchor-set` changed, derived from the record before and after. */
|
|
7064
|
+
anchors: import_zod31.z.array(kbLogAnchorChangeSchema).optional()
|
|
7065
|
+
});
|
|
7066
|
+
var kbLogEntrySchema = kbLogEntryFields.passthrough();
|
|
7067
|
+
var kbLogEntryWriteSchema = kbLogEntryFields.strict();
|
|
6748
7068
|
function renderLogEntry(entry) {
|
|
6749
|
-
return `${JSON.stringify(
|
|
7069
|
+
return `${JSON.stringify(kbLogEntryWriteSchema.parse(entry))}
|
|
6750
7070
|
`;
|
|
6751
7071
|
}
|
|
6752
7072
|
var CONFLICT_MARKER = /^(<{7}|\|{7}|={7}|>{7})/;
|
|
@@ -6787,11 +7107,11 @@ function parseLog(raw) {
|
|
|
6787
7107
|
// src/json-schema.ts
|
|
6788
7108
|
function kbJsonSchemas() {
|
|
6789
7109
|
return {
|
|
6790
|
-
recordFrontmatter:
|
|
7110
|
+
recordFrontmatter: import_zod32.z.toJSONSchema(kbRecordFrontmatterSchema, {
|
|
6791
7111
|
io: "input"
|
|
6792
7112
|
}),
|
|
6793
|
-
composeInput:
|
|
6794
|
-
logEntry:
|
|
7113
|
+
composeInput: import_zod32.z.toJSONSchema(composeInputSchema, { io: "input" }),
|
|
7114
|
+
logEntry: import_zod32.z.toJSONSchema(kbLogEntrySchema, { io: "input" })
|
|
6795
7115
|
};
|
|
6796
7116
|
}
|
|
6797
7117
|
|
|
@@ -6801,25 +7121,25 @@ var schemaCommand = define({
|
|
|
6801
7121
|
tool: "kb_schema",
|
|
6802
7122
|
usage: "schema",
|
|
6803
7123
|
description: "JSON Schema for frontmatter, write input, and log entries, generated from the enforcing code.",
|
|
6804
|
-
input:
|
|
7124
|
+
input: import_zod33.z.object({}),
|
|
6805
7125
|
fromArgv: () => ({}),
|
|
6806
7126
|
run: () => Promise.resolve(kbJsonSchemas())
|
|
6807
7127
|
});
|
|
6808
7128
|
|
|
6809
7129
|
// src/commands/stamp.ts
|
|
6810
7130
|
var import_promises10 = require("fs/promises");
|
|
6811
|
-
var
|
|
7131
|
+
var import_zod34 = require("zod");
|
|
6812
7132
|
var DIGEST = /^[0-9a-f]{64}$/;
|
|
6813
7133
|
var stampCommand = define({
|
|
6814
7134
|
name: "stamp",
|
|
6815
7135
|
tool: "kb_stamp",
|
|
6816
7136
|
usage: "stamp [--bundle PATH] [--since DIGEST|FILE]",
|
|
6817
7137
|
description: "Content stamp of a base \u2014 `load`'s digest, record counts, per-record digests, how many records have drifted anchors \u2014 without any bodies. Takes no bundlePath to stamp every pinned base. With `since`, reports only the bases that moved, naming the changed ids. Reads, never writes.",
|
|
6818
|
-
input:
|
|
6819
|
-
bundlePath:
|
|
7138
|
+
input: import_zod34.z.object({
|
|
7139
|
+
bundlePath: import_zod34.z.string().min(1).optional().describe(
|
|
6820
7140
|
"Absolute path to one knowledge base. Omit to stamp every pinned base."
|
|
6821
7141
|
),
|
|
6822
|
-
since:
|
|
7142
|
+
since: import_zod34.z.string().min(1).optional().describe(
|
|
6823
7143
|
"Prior digest, or path to a prior `stamp --json`; only moved bases return, with changed ids when the baseline is a file."
|
|
6824
7144
|
)
|
|
6825
7145
|
}),
|
|
@@ -6905,16 +7225,16 @@ async function readBaseline(since) {
|
|
|
6905
7225
|
}
|
|
6906
7226
|
|
|
6907
7227
|
// src/commands/status.ts
|
|
6908
|
-
var
|
|
7228
|
+
var import_zod35 = require("zod");
|
|
6909
7229
|
var statusCommand = define({
|
|
6910
7230
|
name: "status",
|
|
6911
7231
|
tool: "kb_status",
|
|
6912
7232
|
usage: "status <concept-id> <status>",
|
|
6913
7233
|
description: "Move a record's status. Compare-and-swap: a concurrent change fails instead of being overwritten.",
|
|
6914
|
-
input:
|
|
7234
|
+
input: import_zod35.z.object({
|
|
6915
7235
|
bundlePath,
|
|
6916
7236
|
conceptId,
|
|
6917
|
-
status:
|
|
7237
|
+
status: import_zod35.z.enum(KB_RECORD_STATUSES)
|
|
6918
7238
|
}),
|
|
6919
7239
|
fromArgv: (argv, path) => ({
|
|
6920
7240
|
bundlePath: path,
|
|
@@ -6929,13 +7249,13 @@ var statusCommand = define({
|
|
|
6929
7249
|
});
|
|
6930
7250
|
|
|
6931
7251
|
// src/commands/supersede.ts
|
|
6932
|
-
var
|
|
7252
|
+
var import_zod36 = require("zod");
|
|
6933
7253
|
var supersedeCommand = define({
|
|
6934
7254
|
name: "supersede",
|
|
6935
7255
|
tool: "kb_supersede",
|
|
6936
7256
|
usage: "supersede <concept-id> <replacement-id>",
|
|
6937
7257
|
description: "Mark a record superseded by another, linked in both directions. Use instead of editing a record whose meaning changed.",
|
|
6938
|
-
input:
|
|
7258
|
+
input: import_zod36.z.object({ bundlePath, conceptId, replacementId: conceptId }),
|
|
6939
7259
|
fromArgv: (argv, path) => ({
|
|
6940
7260
|
bundlePath: path,
|
|
6941
7261
|
conceptId: argv[1],
|
|
@@ -6949,7 +7269,7 @@ var supersedeCommand = define({
|
|
|
6949
7269
|
});
|
|
6950
7270
|
|
|
6951
7271
|
// src/commands/sweep.ts
|
|
6952
|
-
var
|
|
7272
|
+
var import_zod37 = require("zod");
|
|
6953
7273
|
var TERMINAL = [
|
|
6954
7274
|
"resolved",
|
|
6955
7275
|
"rejected",
|
|
@@ -6960,15 +7280,15 @@ var sweepCommand = define({
|
|
|
6960
7280
|
tool: "kb_sweep",
|
|
6961
7281
|
usage: "sweep --tag <tag> --terminal [--dry-run]",
|
|
6962
7282
|
description: "Delete tagged records that are resolved, rejected or superseded. Refuses without --tag, keeps any record a surviving record still points at, and logs each deletion.",
|
|
6963
|
-
input:
|
|
7283
|
+
input: import_zod37.z.object({
|
|
6964
7284
|
bundlePath,
|
|
6965
|
-
tag:
|
|
6966
|
-
terminal:
|
|
7285
|
+
tag: import_zod37.z.string({ error: "sweep needs --tag: it never sweeps a whole base" }).min(1).describe("Only records carrying this tag are considered."),
|
|
7286
|
+
terminal: import_zod37.z.literal(true, {
|
|
6967
7287
|
error: "sweep needs --terminal: it deletes only settled records"
|
|
6968
7288
|
}).describe(
|
|
6969
7289
|
"Required. Names the only scope sweep deletes: resolved, rejected and superseded records."
|
|
6970
7290
|
),
|
|
6971
|
-
dryRun:
|
|
7291
|
+
dryRun: import_zod37.z.boolean().optional().describe("Report what would go, and delete nothing.")
|
|
6972
7292
|
}),
|
|
6973
7293
|
fromArgv: (argv, path) => ({
|
|
6974
7294
|
bundlePath: path,
|
|
@@ -7085,16 +7405,16 @@ function renderSweep(result) {
|
|
|
7085
7405
|
}
|
|
7086
7406
|
|
|
7087
7407
|
// src/commands/sync-instructions.ts
|
|
7088
|
-
var
|
|
7408
|
+
var import_zod38 = require("zod");
|
|
7089
7409
|
var syncInstructionsCommand = define({
|
|
7090
7410
|
name: "sync-instructions",
|
|
7091
7411
|
usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
|
|
7092
7412
|
description: "CLI-only: plant the kb_context block between sentinel comments in AGENTS.md or CLAUDE.md, idempotently.",
|
|
7093
|
-
input:
|
|
7094
|
-
file:
|
|
7095
|
-
budgetTokens:
|
|
7096
|
-
fullUnderTokens:
|
|
7097
|
-
profile:
|
|
7413
|
+
input: import_zod38.z.object({
|
|
7414
|
+
file: import_zod38.z.string().min(1).describe("The instruction file to edit in place."),
|
|
7415
|
+
budgetTokens: import_zod38.z.number().int().positive().optional(),
|
|
7416
|
+
fullUnderTokens: import_zod38.z.number().int().positive().optional(),
|
|
7417
|
+
profile: import_zod38.z.string().optional()
|
|
7098
7418
|
}),
|
|
7099
7419
|
fromArgv: (argv) => {
|
|
7100
7420
|
const budget = argvFlag(argv, "--budget");
|
|
@@ -7120,7 +7440,7 @@ var syncInstructionsCommand = define({
|
|
|
7120
7440
|
});
|
|
7121
7441
|
|
|
7122
7442
|
// src/commands/trace.ts
|
|
7123
|
-
var
|
|
7443
|
+
var import_zod39 = require("zod");
|
|
7124
7444
|
|
|
7125
7445
|
// src/trace.ts
|
|
7126
7446
|
var TRACE_EDGES = [
|
|
@@ -7176,11 +7496,11 @@ var traceCommand = define({
|
|
|
7176
7496
|
tool: "kb_trace",
|
|
7177
7497
|
usage: "trace <concept-id> [edges...]",
|
|
7178
7498
|
description: 'Timeline of how a position was reached, ordered by write time, following supersession, shared anchors and shared sources. Includes rejected, draft and superseded records \u2014 in a history they are the content. For "why is it like this"; kb_load answers "what holds now".',
|
|
7179
|
-
input:
|
|
7499
|
+
input: import_zod39.z.object({
|
|
7180
7500
|
bundlePath,
|
|
7181
7501
|
conceptId,
|
|
7182
|
-
edges:
|
|
7183
|
-
depth:
|
|
7502
|
+
edges: import_zod39.z.array(import_zod39.z.enum(TRACE_EDGES)).optional(),
|
|
7503
|
+
depth: import_zod39.z.number().int().positive().optional()
|
|
7184
7504
|
}),
|
|
7185
7505
|
fromArgv: (argv, path) => ({
|
|
7186
7506
|
bundlePath: path,
|
|
@@ -7202,37 +7522,37 @@ var traceCommand = define({
|
|
|
7202
7522
|
});
|
|
7203
7523
|
|
|
7204
7524
|
// src/commands/types.ts
|
|
7205
|
-
var
|
|
7525
|
+
var import_zod40 = require("zod");
|
|
7206
7526
|
var typesCommand = define({
|
|
7207
7527
|
name: "types",
|
|
7208
7528
|
tool: "kb_types",
|
|
7209
7529
|
usage: "types",
|
|
7210
7530
|
description: "The twelve record types with their purpose, body sections, and starting status. Read this before writing rather than guessing headings \u2014 a section the type does not define is rejected.",
|
|
7211
|
-
input:
|
|
7531
|
+
input: import_zod40.z.object({}),
|
|
7212
7532
|
fromArgv: () => ({}),
|
|
7213
7533
|
run: () => Promise.resolve(RECORD_TYPES)
|
|
7214
7534
|
});
|
|
7215
7535
|
|
|
7216
7536
|
// src/commands/unpin.ts
|
|
7217
|
-
var
|
|
7537
|
+
var import_zod41 = require("zod");
|
|
7218
7538
|
var unpinCommand = define({
|
|
7219
7539
|
name: "unpin",
|
|
7220
7540
|
tool: "kb_unpin",
|
|
7221
7541
|
usage: "unpin [bundle-path]",
|
|
7222
7542
|
description: "Remove a base from every manifest layer that holds it. Reports the layers touched.",
|
|
7223
|
-
input:
|
|
7543
|
+
input: import_zod41.z.object({ bundlePath }),
|
|
7224
7544
|
fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
|
|
7225
7545
|
run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
|
|
7226
7546
|
});
|
|
7227
7547
|
|
|
7228
7548
|
// src/commands/validate.ts
|
|
7229
|
-
var
|
|
7549
|
+
var import_zod42 = require("zod");
|
|
7230
7550
|
var validateCommand = define({
|
|
7231
7551
|
name: "validate",
|
|
7232
7552
|
tool: "kb_validate",
|
|
7233
7553
|
usage: "validate",
|
|
7234
7554
|
description: "Check pointers no single record can see: supersession links that disagree between the two records, typed causal links, and assumptions that cite sources. Each finding carries a severity: errors fail the exit code, warnings do not.",
|
|
7235
|
-
input:
|
|
7555
|
+
input: import_zod42.z.object({ bundlePath }),
|
|
7236
7556
|
fromArgv: (_argv, path) => ({ bundlePath: path }),
|
|
7237
7557
|
run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
|
|
7238
7558
|
// Warnings never fail the exit code; every other severity does.
|
|
@@ -7242,16 +7562,16 @@ var validateCommand = define({
|
|
|
7242
7562
|
});
|
|
7243
7563
|
|
|
7244
7564
|
// src/commands/verify.ts
|
|
7245
|
-
var
|
|
7565
|
+
var import_zod43 = require("zod");
|
|
7246
7566
|
var verifyCommand = define({
|
|
7247
7567
|
name: "verify",
|
|
7248
7568
|
tool: "kb_verify",
|
|
7249
7569
|
usage: "verify <concept-id> --note <text>",
|
|
7250
7570
|
description: "Append a verified[] event: who checked, when, and what was found. Append-only. A record's own generator is refused unless the actor is `human:`-prefixed.",
|
|
7251
|
-
input:
|
|
7571
|
+
input: import_zod43.z.object({
|
|
7252
7572
|
bundlePath,
|
|
7253
7573
|
conceptId,
|
|
7254
|
-
note:
|
|
7574
|
+
note: import_zod43.z.string().refine((s) => s.trim().length > 0, {
|
|
7255
7575
|
message: "note must say what the check found"
|
|
7256
7576
|
})
|
|
7257
7577
|
}),
|
|
@@ -7271,15 +7591,15 @@ var verifyCommand = define({
|
|
|
7271
7591
|
});
|
|
7272
7592
|
|
|
7273
7593
|
// src/commands/write.ts
|
|
7274
|
-
var
|
|
7594
|
+
var import_zod44 = require("zod");
|
|
7275
7595
|
var writeCommand = define({
|
|
7276
7596
|
name: "write",
|
|
7277
7597
|
tool: "kb_write",
|
|
7278
7598
|
usage: "write <type> < record.json",
|
|
7279
7599
|
description: "Write one record. Search first \u2014 a duplicate concept id is rejected, not overwritten; kb_types lists each type's sections. An unsourced claim is an `assumption` with assumption: true, never a vague `fact`. Conflicting records get a `risk`, `open-question`, or superseding `decision`. Prefer a new short record over overloading one. Never delete; supersede.",
|
|
7280
|
-
input:
|
|
7600
|
+
input: import_zod44.z.object({
|
|
7281
7601
|
bundlePath,
|
|
7282
|
-
type:
|
|
7602
|
+
type: import_zod44.z.enum(KB_RECORD_TYPES),
|
|
7283
7603
|
input: composeInputSchema
|
|
7284
7604
|
}),
|
|
7285
7605
|
fromArgv: async (argv, path, stdin) => ({
|
|
@@ -7303,13 +7623,13 @@ var writeCommand = define({
|
|
|
7303
7623
|
});
|
|
7304
7624
|
|
|
7305
7625
|
// src/commands/write-decision.ts
|
|
7306
|
-
var
|
|
7626
|
+
var import_zod45 = require("zod");
|
|
7307
7627
|
var writeDecisionCommand = define({
|
|
7308
7628
|
name: "write-decision",
|
|
7309
7629
|
tool: "kb_write_decision",
|
|
7310
7630
|
usage: "write-decision < decision.json",
|
|
7311
7631
|
description: "Write a decision, with `alternative` (what was rejected and why) and `impact` as fields. Record one when a later reader would otherwise simplify the constraint away; skip when the diff already answers it. `sources` for material read, `anchors` for code, `relatedConceptIds` for records.",
|
|
7312
|
-
input:
|
|
7632
|
+
input: import_zod45.z.object({ bundlePath, input: decisionInputSchema }),
|
|
7313
7633
|
fromArgv: async (_argv, path, stdin) => ({
|
|
7314
7634
|
bundlePath: path,
|
|
7315
7635
|
input: JSON.parse(await stdin())
|
|
@@ -7339,6 +7659,7 @@ var KB_COMMANDS = [
|
|
|
7339
7659
|
answerCommand,
|
|
7340
7660
|
verifyCommand,
|
|
7341
7661
|
anchorResolveCommand,
|
|
7662
|
+
anchorSetCommand,
|
|
7342
7663
|
reassessCommand,
|
|
7343
7664
|
promoteCommand,
|
|
7344
7665
|
loadCommand,
|
|
@@ -7774,23 +8095,31 @@ var KbStore = class {
|
|
|
7774
8095
|
);
|
|
7775
8096
|
}
|
|
7776
8097
|
/**
|
|
7777
|
-
* Replaces a record's anchors
|
|
7778
|
-
*
|
|
7779
|
-
*
|
|
7780
|
-
*
|
|
7781
|
-
* stale entries the resolution pass deliberately dropped.
|
|
7782
|
-
*
|
|
7783
|
-
* Through the write schema: this is a write, and a defect a hand-edit put in
|
|
7784
|
-
* the frontmatter must not be published back out under an actor stamp.
|
|
8098
|
+
* Replaces a record's anchors, preserving everything else. An array is the
|
|
8099
|
+
* whole set; a function is a patch and runs inside the mutation, against
|
|
8100
|
+
* the anchors the record holds then — see
|
|
8101
|
+
* `decision.anchor-update-patch-inside-mutation`.
|
|
7785
8102
|
*/
|
|
7786
8103
|
async updateAnchors(bundlePath2, conceptId2, anchors, actor = "unknown") {
|
|
7787
8104
|
assertActor(actor);
|
|
7788
|
-
|
|
8105
|
+
let entry = {
|
|
8106
|
+
operation: "anchor-resolve",
|
|
8107
|
+
by: actor
|
|
8108
|
+
};
|
|
7789
8109
|
return this.mutate(
|
|
7790
8110
|
bundlePath2,
|
|
7791
8111
|
conceptId2,
|
|
7792
|
-
(frontmatter) =>
|
|
7793
|
-
|
|
8112
|
+
(frontmatter) => {
|
|
8113
|
+
const write = typeof anchors === "function" ? anchors(frontmatter.strauss_anchors ?? []) : { anchors };
|
|
8114
|
+
if (write.log) entry = { ...write.log, by: actor };
|
|
8115
|
+
return {
|
|
8116
|
+
...frontmatter,
|
|
8117
|
+
strauss_anchors: write.anchors.map(
|
|
8118
|
+
(anchor) => kbAnchorWriteSchema.parse(anchor)
|
|
8119
|
+
)
|
|
8120
|
+
};
|
|
8121
|
+
},
|
|
8122
|
+
() => entry
|
|
7794
8123
|
);
|
|
7795
8124
|
}
|
|
7796
8125
|
/**
|
|
@@ -8244,7 +8573,10 @@ ${answer}
|
|
|
8244
8573
|
throw new KbWriteConflictError(conceptId2);
|
|
8245
8574
|
}
|
|
8246
8575
|
await this.publish(target, contents, true, conceptId2);
|
|
8247
|
-
await this.record(this.root(bundlePath2), {
|
|
8576
|
+
await this.record(this.root(bundlePath2), {
|
|
8577
|
+
...typeof entry === "function" ? entry() : entry,
|
|
8578
|
+
conceptId: conceptId2
|
|
8579
|
+
});
|
|
8248
8580
|
return { conceptId: conceptId2, frontmatter, body };
|
|
8249
8581
|
}
|
|
8250
8582
|
/**
|
|
@@ -8460,7 +8792,7 @@ function assertActor(actor, { named = false } = {}) {
|
|
|
8460
8792
|
}
|
|
8461
8793
|
|
|
8462
8794
|
// src/version.ts
|
|
8463
|
-
var VERSION = true ? "0.1.
|
|
8795
|
+
var VERSION = true ? "0.1.23" : "0.0.0-dev";
|
|
8464
8796
|
|
|
8465
8797
|
// src/mcp.ts
|
|
8466
8798
|
function createKbMcpServer() {
|