@saasontools/strauss-kb 0.1.21 → 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp-main.cjs CHANGED
@@ -32,569 +32,91 @@ 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 import_zod3 = require("zod");
35
+ var import_zod4 = require("zod");
36
36
 
37
37
  // src/compose.ts
38
- var import_zod2 = require("zod");
38
+ var import_zod3 = require("zod");
39
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
- });
40
+ // src/concurrency.ts
41
+ var DEFAULT_IO_CONCURRENCY = 16;
42
+ async function mapLimit(items, limit, fn) {
43
+ if (!Number.isInteger(limit) || limit < 1) {
44
+ throw new RangeError(
45
+ `mapLimit: "limit" must be a positive integer, got ${limit}`
46
+ );
123
47
  }
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
- });
48
+ const out = new Array(items.length);
49
+ let next = 0;
50
+ let failed = false;
51
+ const runners = Array.from(
52
+ { length: Math.min(limit, items.length) },
53
+ async () => {
54
+ while (!failed && next < items.length) {
55
+ const at2 = next++;
56
+ try {
57
+ out[at2] = await fn(items[at2], at2);
58
+ } catch (error) {
59
+ failed = true;
60
+ throw error;
61
+ }
62
+ }
63
+ }
64
+ );
65
+ await Promise.all(runners);
66
+ return out;
67
+ }
68
+
69
+ // src/drift/git.ts
70
+ var import_node_child_process2 = require("child_process");
71
+ var import_node_util2 = require("util");
72
+
73
+ // src/remote-repo/git.ts
74
+ var import_node_child_process = require("child_process");
75
+ var import_node_util = require("util");
76
+
77
+ // src/anchor-resolver/model.ts
78
+ var MAX_ANCHOR_FILE_BYTES = 1048576;
79
+
80
+ // src/remote-repo/git.ts
81
+ var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
82
+ function childEnv() {
83
+ const env = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
84
+ for (const name of ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE"]) {
85
+ delete env[name];
130
86
  }
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"
87
+ return env;
88
+ }
89
+ async function git(args, options = {}) {
90
+ try {
91
+ const { stdout, stderr } = await execFileAsync("git", args, {
92
+ ...options.cwd ? { cwd: options.cwd } : {},
93
+ timeout: options.timeoutMs ?? 3e4,
94
+ maxBuffer: options.maxBytes ?? MAX_ANCHOR_FILE_BYTES,
95
+ encoding: "utf8",
96
+ windowsHide: true,
97
+ env: childEnv()
136
98
  });
99
+ return { ok: true, stdout, stderr, overflowed: false };
100
+ } catch (error) {
101
+ const failure = error;
102
+ return {
103
+ ok: false,
104
+ stdout: failure.stdout ?? "",
105
+ stderr: failure.stderr ?? "",
106
+ overflowed: failure.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER"
107
+ };
137
108
  }
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
- });
109
+ }
110
+ function transportReason(stderr) {
111
+ const text = stderr.toLowerCase();
112
+ if (text.includes("authentication failed") || text.includes("permission denied") || text.includes("could not read username") || text.includes("403 forbidden") || text.includes("access denied")) {
113
+ return "repo-unauthorized";
144
114
  }
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
- }
514
-
515
- // src/commands/anchor-resolve.ts
516
- var import_zod7 = require("zod");
517
-
518
- // src/concurrency.ts
519
- var DEFAULT_IO_CONCURRENCY = 16;
520
- async function mapLimit(items, limit, fn) {
521
- if (!Number.isInteger(limit) || limit < 1) {
522
- throw new RangeError(
523
- `mapLimit: "limit" must be a positive integer, got ${limit}`
524
- );
525
- }
526
- const out = new Array(items.length);
527
- let next = 0;
528
- let failed = false;
529
- const runners = Array.from(
530
- { length: Math.min(limit, items.length) },
531
- async () => {
532
- while (!failed && next < items.length) {
533
- const at2 = next++;
534
- try {
535
- out[at2] = await fn(items[at2], at2);
536
- } catch (error) {
537
- failed = true;
538
- throw error;
539
- }
540
- }
541
- }
542
- );
543
- await Promise.all(runners);
544
- return out;
545
- }
546
-
547
- // src/drift/git.ts
548
- var import_node_child_process2 = require("child_process");
549
- var import_node_util2 = require("util");
550
-
551
- // src/remote-repo/git.ts
552
- var import_node_child_process = require("child_process");
553
- var import_node_util = require("util");
554
-
555
- // src/anchor-resolver/model.ts
556
- var MAX_ANCHOR_FILE_BYTES = 1048576;
557
-
558
- // src/remote-repo/git.ts
559
- var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
560
- function childEnv() {
561
- const env = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
562
- for (const name of ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE"]) {
563
- delete env[name];
564
- }
565
- return env;
566
- }
567
- async function git(args, options = {}) {
568
- try {
569
- const { stdout, stderr } = await execFileAsync("git", args, {
570
- ...options.cwd ? { cwd: options.cwd } : {},
571
- timeout: options.timeoutMs ?? 3e4,
572
- maxBuffer: options.maxBytes ?? MAX_ANCHOR_FILE_BYTES,
573
- encoding: "utf8",
574
- windowsHide: true,
575
- env: childEnv()
576
- });
577
- return { ok: true, stdout, stderr, overflowed: false };
578
- } catch (error) {
579
- const failure = error;
580
- return {
581
- ok: false,
582
- stdout: failure.stdout ?? "",
583
- stderr: failure.stderr ?? "",
584
- overflowed: failure.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER"
585
- };
586
- }
587
- }
588
- function transportReason(stderr) {
589
- const text = stderr.toLowerCase();
590
- if (text.includes("authentication failed") || text.includes("permission denied") || text.includes("could not read username") || text.includes("403 forbidden") || text.includes("access denied")) {
591
- return "repo-unauthorized";
592
- }
593
- if (text.includes("couldn't find remote ref") || text.includes("unadvertised object") || text.includes("not our ref")) {
594
- return "ref-not-found";
595
- }
596
- return "remote-unreachable";
597
- }
115
+ if (text.includes("couldn't find remote ref") || text.includes("unadvertised object") || text.includes("not our ref")) {
116
+ return "ref-not-found";
117
+ }
118
+ return "remote-unreachable";
119
+ }
598
120
 
599
121
  // src/remote-repo/validate.ts
600
122
  var MAX_REF_LENGTH = 200;
@@ -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 import_zod4 = require("zod");
1331
- var sha2562 = import_zod4.z.string().regex(/^[0-9a-f]{64}$/);
1332
- var grammarWasmSchema = import_zod4.z.object({
1333
- url: import_zod4.z.string().min(1),
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: import_zod4.z.number().int().positive()
857
+ bytes: import_zod.z.number().int().positive()
1336
858
  });
1337
- var grammarTagsSchema = import_zod4.z.object({ url: import_zod4.z.string().min(1), sha256: sha2562 });
1338
- var grammarPackSchema = import_zod4.z.object({
1339
- package: import_zod4.z.string().min(1),
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: import_zod4.z.array(grammarTagsSchema),
1342
- license: import_zod4.z.string().min(1),
1343
- extensions: import_zod4.z.array(import_zod4.z.string().min(1))
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 = import_zod4.z.object({
867
+ var grammarManifestSchema = import_zod.z.object({
1346
868
  /** The runtime the packs were proved against. */
1347
- webTreeSitter: import_zod4.z.string().min(1),
1348
- linguist: import_zod4.z.object({ tag: import_zod4.z.string().min(1), commit: import_zod4.z.string().min(1) }),
1349
- packs: import_zod4.z.record(import_zod4.z.string().min(1), grammarPackSchema)
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
@@ -1784,505 +1306,1092 @@ var TreeSitterResolver = class {
1784
1306
  tree.delete();
1785
1307
  }
1786
1308
  }
1787
- /** Drops cached trees. Grammars stay loaded — they are immutable. */
1788
- reset() {
1789
- for (const parsed of this.trees.values()) parsed.tree.delete();
1790
- this.trees.clear();
1791
- this.stats.parses = 0;
1792
- this.stats.cacheHits = 0;
1309
+ /** Drops cached trees. Grammars stay loaded — they are immutable. */
1310
+ reset() {
1311
+ for (const parsed of this.trees.values()) parsed.tree.delete();
1312
+ this.trees.clear();
1313
+ this.stats.parses = 0;
1314
+ this.stats.cacheHits = 0;
1315
+ }
1316
+ };
1317
+ function why(error) {
1318
+ const text = error instanceof Error ? error.message : String(error);
1319
+ return text || "no reason given";
1320
+ }
1321
+
1322
+ // src/anchor-resolver/resolver.ts
1323
+ var PARENT_SCOPE_LINES = 50;
1324
+ var CLEAN_STATE = { blockComment: false, template: false };
1325
+ function stripLine(line, state) {
1326
+ let out = "";
1327
+ let index2 = 0;
1328
+ let { blockComment, template } = state;
1329
+ while (index2 < line.length) {
1330
+ const char = line[index2];
1331
+ const next = line[index2 + 1];
1332
+ if (blockComment) {
1333
+ if (char === "*" && next === "/") {
1334
+ blockComment = false;
1335
+ index2 += 2;
1336
+ continue;
1337
+ }
1338
+ index2 += 1;
1339
+ continue;
1340
+ }
1341
+ if (template) {
1342
+ if (char === "\\") {
1343
+ index2 += 2;
1344
+ continue;
1345
+ }
1346
+ if (char === "`") template = false;
1347
+ index2 += 1;
1348
+ continue;
1349
+ }
1350
+ if (char === "/" && next === "*") {
1351
+ blockComment = true;
1352
+ index2 += 2;
1353
+ continue;
1354
+ }
1355
+ if (char === "/" && next === "/") break;
1356
+ if (char === "`") {
1357
+ template = true;
1358
+ index2 += 1;
1359
+ continue;
1360
+ }
1361
+ if (char === "'" || char === '"') {
1362
+ const quote = char;
1363
+ index2 += 1;
1364
+ while (index2 < line.length) {
1365
+ if (line[index2] === "\\") {
1366
+ index2 += 2;
1367
+ continue;
1368
+ }
1369
+ if (line[index2] === quote) {
1370
+ index2 += 1;
1371
+ break;
1372
+ }
1373
+ index2 += 1;
1374
+ }
1375
+ continue;
1376
+ }
1377
+ out += char;
1378
+ index2 += 1;
1379
+ }
1380
+ return { code: out, state: { blockComment, template } };
1381
+ }
1382
+ function span(lines, from, to) {
1383
+ return {
1384
+ text: lines.slice(from, to + 1).join("\n"),
1385
+ startLine: from + 1,
1386
+ endLine: to + 1
1387
+ };
1388
+ }
1389
+ function captureBraceBlock(lines, matchLine) {
1390
+ let depth = 0;
1391
+ let opened = false;
1392
+ let state = CLEAN_STATE;
1393
+ for (let index2 = matchLine; index2 < lines.length; index2++) {
1394
+ const stripped = stripLine(lines[index2] ?? "", state);
1395
+ state = stripped.state;
1396
+ for (const char of stripped.code) {
1397
+ if (char === "{") {
1398
+ depth += 1;
1399
+ opened = true;
1400
+ } else if (char === "}") {
1401
+ depth = Math.max(0, depth - 1);
1402
+ } else if (char === ";" && !opened) {
1403
+ return span(lines, matchLine, index2);
1404
+ }
1405
+ }
1406
+ if (opened && depth === 0) return span(lines, matchLine, index2);
1407
+ }
1408
+ return null;
1409
+ }
1410
+ var PYTHON_HEADER = /^\s*(?:async\s+)?(?:def|class)\s+[A-Za-z_]\w*\s*[(:]/;
1411
+ function captureIndentedBlock(lines, matchLine) {
1412
+ const header2 = lines[matchLine] ?? "";
1413
+ const indent = header2.length - header2.trimStart().length;
1414
+ let headerEnd = -1;
1415
+ for (let index2 = matchLine; index2 < lines.length && index2 <= matchLine + 20; index2++) {
1416
+ const code = stripLine(lines[index2] ?? "", CLEAN_STATE).code.trimEnd();
1417
+ if (code.endsWith(":")) {
1418
+ headerEnd = index2;
1419
+ break;
1420
+ }
1421
+ if (code.includes(":")) return span(lines, matchLine, index2);
1422
+ }
1423
+ if (headerEnd === -1) return null;
1424
+ let end = headerEnd;
1425
+ for (let index2 = headerEnd + 1; index2 < lines.length; index2++) {
1426
+ const line = lines[index2] ?? "";
1427
+ if (line.trim() === "") continue;
1428
+ const lineIndent = line.length - line.trimStart().length;
1429
+ if (lineIndent <= indent) break;
1430
+ end = index2;
1431
+ }
1432
+ return end === headerEnd ? null : span(lines, matchLine, end);
1433
+ }
1434
+ var declarationTier = (name) => new RegExp(
1435
+ `(?:function|class|interface|type|enum|const|let|var|def)\\s+${name}\\b`
1436
+ );
1437
+ var assignmentTier = (name) => new RegExp(`\\b${name}\\s*[:=]`);
1438
+ var anchoredAssignmentTier = (name) => new RegExp(
1439
+ `^\\s*(?:export\\s+|readonly\\s+|pub\\s+|static\\s+|private\\s+|public\\s+|protected\\s+)*${name}\\s*[:=]`
1440
+ );
1441
+ var DEFINITION_TIERS = [declarationTier, anchoredAssignmentTier];
1442
+ var MENTION_TIERS = [
1443
+ (name) => new RegExp(`\\b${name}\\s*\\(`),
1444
+ (name) => new RegExp(`\\b${name}\\b`)
1445
+ ];
1446
+ var TIERS = [declarationTier, assignmentTier, ...MENTION_TIERS];
1447
+ function resolveWith(tiers, source, symbol) {
1448
+ const segments = symbol.split(".");
1449
+ const name = segments[segments.length - 1];
1450
+ if (!name) return null;
1451
+ const parent = segments.length > 1 ? segments[segments.length - 2] : void 0;
1452
+ const escaped = escapeRegExp(name);
1453
+ const parentPattern = parent ? new RegExp(`\\b${escapeRegExp(parent)}\\b`) : null;
1454
+ const lines = source.split("\n");
1455
+ for (const tier of tiers) {
1456
+ const pattern = tier(escaped);
1457
+ let candidates = lines.map((line, index2) => ({ line, index: index2 })).filter((entry) => pattern.test(entry.line)).map((entry) => entry.index);
1458
+ if (!candidates.length) continue;
1459
+ if (parentPattern && candidates.length > 1) {
1460
+ const distances = candidates.map(
1461
+ (index2) => distanceToParent(lines, index2, parentPattern)
1462
+ );
1463
+ const nearest = Math.min(...distances);
1464
+ if (Number.isFinite(nearest)) {
1465
+ candidates = candidates.filter((_, at2) => distances[at2] === nearest);
1466
+ }
1467
+ }
1468
+ if (candidates.length !== 1) return null;
1469
+ const matchLine = candidates[0];
1470
+ return PYTHON_HEADER.test(lines[matchLine] ?? "") ? captureIndentedBlock(lines, matchLine) : captureBraceBlock(lines, matchLine);
1471
+ }
1472
+ return null;
1473
+ }
1474
+ var regexResolver = {
1475
+ name: "regex",
1476
+ resolve(source, symbol) {
1477
+ return resolveWith(TIERS, source, symbol);
1478
+ },
1479
+ attempt(source, symbol, _file, options) {
1480
+ const tiers = options?.afterParsedMiss ? DEFINITION_TIERS : TIERS;
1481
+ const span2 = resolveWith(tiers, source, symbol);
1482
+ return span2 ? { kind: "resolved", span: span2 } : { kind: "unresolved", reason: "symbol-not-found" };
1483
+ }
1484
+ };
1485
+ function escapeRegExp(value) {
1486
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1487
+ }
1488
+ function distanceToParent(lines, index2, parent) {
1489
+ const floor = Math.max(0, index2 - PARENT_SCOPE_LINES);
1490
+ for (let at2 = index2; at2 >= floor; at2--) {
1491
+ if (parent.test(lines[at2] ?? "")) return index2 - at2;
1492
+ }
1493
+ return Number.POSITIVE_INFINITY;
1494
+ }
1495
+ function hashAnchorText(text) {
1496
+ return `sha256:${(0, import_node_crypto3.createHash)("sha256").update(text.replace(/\r\n/g, "\n")).digest("hex")}`;
1497
+ }
1498
+ function resolveAnchorSpan(source, anchor, resolvers = [regexResolver]) {
1499
+ const normalized = source.replace(/\r\n/g, "\n");
1500
+ if (anchor.span) return sliceSpan(normalized, anchor.span);
1501
+ if (!anchor.symbol) {
1502
+ const lines = normalized.split("\n");
1503
+ if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
1504
+ return {
1505
+ ok: true,
1506
+ span: {
1507
+ text: normalized,
1508
+ startLine: 1,
1509
+ endLine: Math.max(1, lines.length)
1510
+ }
1511
+ };
1512
+ }
1513
+ let afterParsedMiss = false;
1514
+ for (const resolver of resolvers) {
1515
+ const attempt = resolver.attempt ? resolver.attempt(normalized, anchor.symbol, anchor.file, {
1516
+ afterParsedMiss
1517
+ }) : fromResolve(resolver, normalized, anchor.symbol, anchor.file);
1518
+ if (attempt.kind === "abstain") continue;
1519
+ if (attempt.kind === "unresolved") {
1520
+ if (attempt.reason === "symbol-not-found") {
1521
+ if (resolver.attempt) afterParsedMiss = true;
1522
+ continue;
1523
+ }
1524
+ return { ok: false, reason: attempt.reason };
1525
+ }
1526
+ const tokens2 = resolver.normalize?.(attempt.span.text, anchor.file);
1527
+ return {
1528
+ ok: true,
1529
+ span: attempt.span,
1530
+ ...isResolverName(resolver.name) ? { resolver: resolver.name } : {},
1531
+ ...tokens2 ? { normalized: tokens2 } : {}
1532
+ };
1533
+ }
1534
+ return { ok: false, reason: "symbol-not-found" };
1535
+ }
1536
+ function sliceSpan(source, range) {
1537
+ const lines = source.split("\n");
1538
+ if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
1539
+ if (range.end > lines.length) {
1540
+ return { ok: false, reason: "span-out-of-range" };
1793
1541
  }
1794
- };
1795
- function why(error) {
1796
- const text = error instanceof Error ? error.message : String(error);
1797
- return text || "no reason given";
1542
+ return {
1543
+ ok: true,
1544
+ span: {
1545
+ text: lines.slice(range.start - 1, range.end).join("\n"),
1546
+ startLine: range.start,
1547
+ endLine: range.end
1548
+ },
1549
+ resolver: "span"
1550
+ };
1551
+ }
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" };
1798
1582
  }
1799
1583
 
1800
- // src/anchor-resolver/resolver.ts
1801
- var PARENT_SCOPE_LINES = 50;
1802
- var CLEAN_STATE = { blockComment: false, template: false };
1803
- function stripLine(line, state) {
1804
- let out = "";
1805
- let index2 = 0;
1806
- let { blockComment, template } = state;
1807
- while (index2 < line.length) {
1808
- const char = line[index2];
1809
- const next = line[index2 + 1];
1810
- if (blockComment) {
1811
- if (char === "*" && next === "/") {
1812
- blockComment = false;
1813
- index2 += 2;
1814
- continue;
1815
- }
1816
- index2 += 1;
1817
- continue;
1818
- }
1819
- if (template) {
1820
- if (char === "\\") {
1821
- index2 += 2;
1822
- continue;
1823
- }
1824
- if (char === "`") template = false;
1825
- index2 += 1;
1826
- continue;
1827
- }
1828
- if (char === "/" && next === "*") {
1829
- blockComment = true;
1830
- index2 += 2;
1831
- continue;
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);
1832
1609
  }
1833
- if (char === "/" && next === "/") break;
1834
- if (char === "`") {
1835
- template = true;
1836
- index2 += 1;
1837
- continue;
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);
1838
1619
  }
1839
- if (char === "'" || char === '"') {
1840
- const quote = char;
1841
- index2 += 1;
1842
- while (index2 < line.length) {
1843
- if (line[index2] === "\\") {
1844
- index2 += 2;
1845
- continue;
1846
- }
1847
- if (line[index2] === quote) {
1848
- index2 += 1;
1849
- break;
1850
- }
1851
- index2 += 1;
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;
1852
1642
  }
1853
- continue;
1643
+ const read = anchor.side === "old" ? committed.get(atRefKey(anchor)) : reads.get(anchor.file);
1644
+ entries.push(localEntry(anchor, read, resolvers));
1854
1645
  }
1855
- out += char;
1856
- index2 += 1;
1646
+ if (entries.length) drift.set(record.conceptId, entries);
1857
1647
  }
1858
- return { code: out, state: { blockComment, template } };
1648
+ return drift;
1859
1649
  }
1860
- function span(lines, from, to) {
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
+ };
1725
+ }
1726
+ function compared(anchor, current, extra = {}) {
1727
+ const matched = current.hash === anchor.hash;
1861
1728
  return {
1862
- text: lines.slice(from, to + 1).join("\n"),
1863
- startLine: from + 1,
1864
- endLine: to + 1
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
1865
1736
  };
1866
1737
  }
1867
- function captureBraceBlock(lines, matchLine) {
1868
- let depth = 0;
1869
- let opened = false;
1870
- let state = CLEAN_STATE;
1871
- for (let index2 = matchLine; index2 < lines.length; index2++) {
1872
- const stripped = stripLine(lines[index2] ?? "", state);
1873
- state = stripped.state;
1874
- for (const char of stripped.code) {
1875
- if (char === "{") {
1876
- depth += 1;
1877
- opened = true;
1878
- } else if (char === "}") {
1879
- depth = Math.max(0, depth - 1);
1880
- } else if (char === ";" && !opened) {
1881
- return span(lines, matchLine, index2);
1882
- }
1883
- }
1884
- if (opened && depth === 0) return span(lines, matchLine, index2);
1885
- }
1886
- return null;
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)
1746
+ );
1887
1747
  }
1888
- var PYTHON_HEADER = /^\s*(?:async\s+)?(?:def|class)\s+[A-Za-z_]\w*\s*[(:]/;
1889
- function captureIndentedBlock(lines, matchLine) {
1890
- const header2 = lines[matchLine] ?? "";
1891
- const indent = header2.length - header2.trimStart().length;
1892
- let headerEnd = -1;
1893
- for (let index2 = matchLine; index2 < lines.length && index2 <= matchLine + 20; index2++) {
1894
- const code = stripLine(lines[index2] ?? "", CLEAN_STATE).code.trimEnd();
1895
- if (code.endsWith(":")) {
1896
- headerEnd = index2;
1897
- break;
1898
- }
1899
- if (code.includes(":")) return span(lines, matchLine, index2);
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
+ });
1900
1766
  }
1901
- if (headerEnd === -1) return null;
1902
- let end = headerEnd;
1903
- for (let index2 = headerEnd + 1; index2 < lines.length; index2++) {
1904
- const line = lines[index2] ?? "";
1905
- if (line.trim() === "") continue;
1906
- const lineIndent = line.length - line.trimStart().length;
1907
- if (lineIndent <= indent) break;
1908
- end = index2;
1767
+ if (anchor.side === "old") {
1768
+ return compared(anchor, current, {
1769
+ repo,
1770
+ ...extras,
1771
+ remoteState: "matches-ref"
1772
+ });
1909
1773
  }
1910
- return end === headerEnd ? null : span(lines, matchLine, end);
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
+ });
1911
1787
  }
1912
- var declarationTier = (name) => new RegExp(
1913
- `(?:function|class|interface|type|enum|const|let|var|def)\\s+${name}\\b`
1914
- );
1915
- var assignmentTier = (name) => new RegExp(`\\b${name}\\s*[:=]`);
1916
- var anchoredAssignmentTier = (name) => new RegExp(
1917
- `^\\s*(?:export\\s+|readonly\\s+|pub\\s+|static\\s+|private\\s+|public\\s+|protected\\s+)*${name}\\s*[:=]`
1918
- );
1919
- var DEFINITION_TIERS = [declarationTier, anchoredAssignmentTier];
1920
- var MENTION_TIERS = [
1921
- (name) => new RegExp(`\\b${name}\\s*\\(`),
1922
- (name) => new RegExp(`\\b${name}\\b`)
1923
- ];
1924
- var TIERS = [declarationTier, assignmentTier, ...MENTION_TIERS];
1925
- function resolveWith(tiers, source, symbol) {
1926
- const segments = symbol.split(".");
1927
- const name = segments[segments.length - 1];
1928
- if (!name) return null;
1929
- const parent = segments.length > 1 ? segments[segments.length - 2] : void 0;
1930
- const escaped = escapeRegExp(name);
1931
- const parentPattern = parent ? new RegExp(`\\b${escapeRegExp(parent)}\\b`) : null;
1932
- const lines = source.split("\n");
1933
- for (const tier of tiers) {
1934
- const pattern = tier(escaped);
1935
- let candidates = lines.map((line, index2) => ({ line, index: index2 })).filter((entry) => pattern.test(entry.line)).map((entry) => entry.index);
1936
- if (!candidates.length) continue;
1937
- if (parentPattern && candidates.length > 1) {
1938
- const distances = candidates.map(
1939
- (index2) => distanceToParent(lines, index2, parentPattern)
1940
- );
1941
- const nearest = Math.min(...distances);
1942
- if (Number.isFinite(nearest)) {
1943
- candidates = candidates.filter((_, at2) => distances[at2] === nearest);
1944
- }
1945
- }
1946
- if (candidates.length !== 1) return null;
1947
- const matchLine = candidates[0];
1948
- return PYTHON_HEADER.test(lines[matchLine] ?? "") ? captureIndentedBlock(lines, matchLine) : captureBraceBlock(lines, matchLine);
1788
+
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
+ });
1949
1872
  }
1950
- return null;
1951
- }
1952
- var regexResolver = {
1953
- name: "regex",
1954
- resolve(source, symbol) {
1955
- return resolveWith(TIERS, source, symbol);
1956
- },
1957
- attempt(source, symbol, _file, options) {
1958
- const tiers = options?.afterParsedMiss ? DEFINITION_TIERS : TIERS;
1959
- const span2 = resolveWith(tiers, source, symbol);
1960
- return span2 ? { kind: "resolved", span: span2 } : { kind: "unresolved", reason: "symbol-not-found" };
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;
1961
1997
  }
1962
1998
  };
1963
- function escapeRegExp(value) {
1964
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1965
- }
1966
- function distanceToParent(lines, index2, parent) {
1967
- const floor = Math.max(0, index2 - PARENT_SCOPE_LINES);
1968
- for (let at2 = index2; at2 >= floor; at2--) {
1969
- if (parent.test(lines[at2] ?? "")) return index2 - at2;
1970
- }
1971
- return Number.POSITIVE_INFINITY;
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}`;
1972
2007
  }
1973
- function hashAnchorText(text) {
1974
- return `sha256:${(0, import_node_crypto3.createHash)("sha256").update(text.replace(/\r\n/g, "\n")).digest("hex")}`;
2008
+ var FIELD_CAP = 120;
2009
+ function cap(value) {
2010
+ return value.length > FIELD_CAP ? `${value.slice(0, FIELD_CAP - 1)}\u2026` : value;
1975
2011
  }
1976
- function resolveAnchorSpan(source, anchor, resolvers = [regexResolver]) {
1977
- const normalized = source.replace(/\r\n/g, "\n");
1978
- if (anchor.span) return sliceSpan(normalized, anchor.span);
1979
- if (!anchor.symbol) {
1980
- const lines = normalized.split("\n");
1981
- if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
1982
- return {
1983
- ok: true,
1984
- span: {
1985
- text: normalized,
1986
- startLine: 1,
1987
- endLine: Math.max(1, lines.length)
1988
- }
1989
- };
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;
1990
2024
  }
1991
- let afterParsedMiss = false;
1992
- for (const resolver of resolvers) {
1993
- const attempt = resolver.attempt ? resolver.attempt(normalized, anchor.symbol, anchor.file, {
1994
- afterParsedMiss
1995
- }) : fromResolve(resolver, normalized, anchor.symbol, anchor.file);
1996
- if (attempt.kind === "abstain") continue;
1997
- if (attempt.kind === "unresolved") {
1998
- if (attempt.reason === "symbol-not-found") {
1999
- if (resolver.attempt) afterParsedMiss = true;
2000
- continue;
2001
- }
2002
- return { ok: false, reason: attempt.reason };
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)));
2003
2044
  }
2004
- const tokens2 = resolver.normalize?.(attempt.span.text, anchor.file);
2005
- return {
2006
- ok: true,
2007
- span: attempt.span,
2008
- ...isResolverName(resolver.name) ? { resolver: resolver.name } : {},
2009
- ...tokens2 ? { normalized: tokens2 } : {}
2010
- };
2045
+ seen.add(key2);
2011
2046
  }
2012
- return { ok: false, reason: "symbol-not-found" };
2047
+ return { anchors, changes: diff(current, anchors) };
2013
2048
  }
2014
- function sliceSpan(source, range) {
2015
- const lines = source.split("\n");
2016
- if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
2017
- if (range.end > lines.length) {
2018
- return { ok: false, reason: "span-out-of-range" };
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;
2068
+ }
2069
+ if (beforeLocators.has(locatorKey(anchor))) continue;
2070
+ changes.push({ op: "add", to: locatorOf(anchor) });
2019
2071
  }
2020
- return {
2021
- ok: true,
2022
- span: {
2023
- text: lines.slice(range.start - 1, range.end).join("\n"),
2024
- startLine: range.start,
2025
- endLine: range.end
2026
- },
2027
- resolver: "span"
2028
- };
2029
- }
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";
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;
2036
2078
  }
2037
- async function prepareResolvers(resolvers, files) {
2038
- for (const resolver of resolvers) await resolver.prepare?.(files);
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
+ )
2086
+ );
2039
2087
  }
2040
- function defaultAnchorResolvers(grammars = {}) {
2041
- return [new TreeSitterResolver(grammars), regexResolver];
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
+ ]);
2042
2097
  }
2043
- function resolverChanged(source, anchor, produced) {
2044
- const previous = anchor.resolver ?? "regex";
2045
- if (!produced || !anchor.symbol || previous === produced) return false;
2046
- if (previous !== "regex") return false;
2047
- const before = regexResolver.resolve(
2048
- source.replace(/\r\n/g, "\n"),
2049
- anchor.symbol
2050
- );
2051
- return before !== null && hashAnchorText(before.text) === anchor.hash;
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);
2052
2164
  }
2053
- function anchorHashOf(anchor, outcome) {
2054
- if (outcome.resolver === "span") {
2055
- return { hash: hashAnchorText(outcome.span.text), kind: "raw" };
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
2056
2215
  }
2057
- const stored = anchor.hash ? anchor.hash_kind ?? "raw" : void 0;
2058
- const wanted = stored ?? (outcome.normalized ? "ast" : "raw");
2059
- return wanted === "ast" && outcome.normalized ? { hash: hashAnchorText(outcome.normalized), kind: "ast" } : { hash: hashAnchorText(outcome.span.text), kind: "raw" };
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);
2060
2222
  }
2061
2223
 
2062
- // src/anchor-resolver/drift.ts
2063
- async function detectAnchorDrift(records, options = {}) {
2064
- const repoRoot = options.repoRoot ?? process.cwd();
2065
- const resolvers = options.resolvers ?? (options.resolver ? [options.resolver] : defaultAnchorResolvers({
2066
- offline: options.remote?.offline === true
2067
- }));
2068
- const origin = new LazyOrigin(repoRoot);
2069
- const planned = /* @__PURE__ */ new Map();
2070
- let declaresRepo = false;
2071
- for (const record of records) {
2072
- const anchors = (record.frontmatter.strauss_anchors ?? []).filter(
2073
- (anchor) => anchor.hash
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(", ")}`
2074
2283
  );
2075
- if (!anchors.length) continue;
2076
- if (anchors.some((anchor) => anchor.repo)) declaresRepo = true;
2077
- planned.set(
2078
- record.conceptId,
2079
- anchors.map((anchor) => ({ anchor, foreign: false }))
2284
+ }
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
2294
+ };
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`
2080
2314
  );
2081
2315
  }
2082
- if (declaresRepo) {
2083
- await origin.prime();
2084
- for (const entries of planned.values()) {
2085
- for (const entry of entries)
2086
- entry.foreign = origin.isForeign(entry.anchor);
2087
- }
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}`);
2088
2323
  }
2089
- const files = [];
2090
- const committedWants = [];
2091
- const wants = [];
2092
- for (const entries of planned.values()) {
2093
- for (const { anchor, foreign } of entries) {
2094
- if (foreign) wants.push(...remoteWants(anchor));
2095
- else if (anchor.side === "old") committedWants.push(anchor);
2096
- else files.push(anchor.file);
2097
- }
2324
+ if (!blocks.length) blocks.push(parsed.why);
2325
+ for (const related of parsed.relatedConceptIds ?? []) {
2326
+ blocks.push(`Relates to [${related}](${related}.md).`);
2098
2327
  }
2099
- const [reads, committed, remote] = await Promise.all([
2100
- readAnchorFiles(
2101
- files,
2102
- options.reader ?? anchorFileReader(repoRoot),
2103
- options.concurrency ?? DEFAULT_IO_CONCURRENCY
2104
- ),
2105
- readCommitted(repoRoot, committedWants, options),
2106
- (options.readRemote ?? readRemoteAnchors)(wants, options.remote ?? {})
2107
- ]);
2108
- await prepareResolvers(resolvers, [
2109
- ...files,
2110
- ...committedWants.map((anchor) => anchor.file),
2111
- ...wants.map((want) => want.file)
2112
- ]);
2113
- const drift = /* @__PURE__ */ new Map();
2114
- for (const record of records) {
2115
- const entries = [];
2116
- for (const { anchor, foreign } of planned.get(record.conceptId) ?? []) {
2117
- if (foreign) {
2118
- entries.push(remoteEntry(anchor, remote, resolvers));
2119
- continue;
2120
- }
2121
- const read = anchor.side === "old" ? committed.get(atRefKey(anchor)) : reads.get(anchor.file);
2122
- entries.push(localEntry(anchor, read, resolvers));
2123
- }
2124
- if (entries.length) drift.set(record.conceptId, entries);
2328
+ for (const link2 of parsed.links ?? []) {
2329
+ blocks.push(
2330
+ `${LINK_RELS[link2.rel].phrase} [${link2.target}](${link2.target}.md).`
2331
+ );
2125
2332
  }
2126
- return drift;
2127
- }
2128
- function atRefKey(anchor) {
2129
- return `${anchor.ref ?? ""}\0${anchor.file}`;
2130
- }
2131
- async function readCommitted(repoRoot, anchors, options = {}) {
2132
- if (!anchors.length) return /* @__PURE__ */ new Map();
2133
- const read = options.readAtRef ?? readFileAtRef;
2134
- const byKey = /* @__PURE__ */ new Map();
2135
- for (const anchor of anchors) byKey.set(atRefKey(anchor), anchor);
2136
- const keys = [...byKey.keys()];
2137
- const results = await mapLimit(
2138
- keys,
2139
- options.concurrency ?? DEFAULT_IO_CONCURRENCY,
2140
- (key2) => read(repoRoot, byKey.get(key2))
2141
- );
2142
- return new Map(keys.map((key2, at2) => [key2, results[at2]]));
2143
- }
2144
- function remoteWants(anchor) {
2145
- const repo = anchor.repo;
2146
- const wants = [{ repo, file: anchor.file }];
2147
- if (anchor.ref) wants.unshift({ repo, ref: anchor.ref, file: anchor.file });
2148
- return wants;
2149
- }
2150
- function base(anchor) {
2151
- return {
2152
- file: anchor.file,
2153
- ...anchor.symbol ? { symbol: anchor.symbol } : {},
2154
- ...anchor.side === "old" ? { side: "old" } : {},
2155
- storedHash: anchor.hash
2156
- };
2157
- }
2158
- function unresolved(anchor, reason, repo) {
2159
- return {
2160
- ...base(anchor),
2161
- state: "unresolved",
2162
- diffSize: null,
2163
- ...reason ? { reason } : {},
2164
- ...repo ? { repo } : {},
2165
- ...classOf(reason)
2166
- };
2167
- }
2168
- var GONE_REASONS = /* @__PURE__ */ new Set([
2169
- "file-missing",
2170
- "symbol-not-found",
2171
- "span-out-of-range",
2172
- "ref-unreadable"
2173
- ]);
2174
- function provisionalDriftClass(entry) {
2175
- if (entry.state === "unresolved") {
2176
- return GONE_REASONS.has(entry.reason) ? "gone" : void 0;
2333
+ if (parsed.sources?.length) {
2334
+ blocks.push(
2335
+ parsed.sources.map((source) => `[^${source.id}]: ${source.title ?? source.resource}`).join("\n")
2336
+ );
2177
2337
  }
2178
- return entry.state === "drifted" ? "changed" : void 0;
2179
- }
2180
- function classOf(reason) {
2181
- const settled = provisionalDriftClass({ state: "unresolved", reason });
2182
- return settled ? { class: settled } : {};
2183
- }
2184
- function hashIn(source, anchor, resolvers) {
2185
- const outcome = resolveAnchorSpan(source, anchor, resolvers);
2186
- if (!outcome.ok) return { ok: false, reason: outcome.reason };
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
- }
2196
- };
2197
- }
2198
- function resolverExtras(source, anchor, current) {
2199
2338
  return {
2200
- ...current.resolver ? { resolver: current.resolver } : {},
2201
- ...current.hash !== anchor.hash && resolverChanged(source, anchor, current.resolver) ? { reason: "resolver-changed" } : {}
2339
+ type,
2340
+ slug: parsed.slug,
2341
+ frontmatter,
2342
+ body: `${blocks.join("\n\n")}
2343
+ `
2202
2344
  };
2203
2345
  }
2204
- function compared(anchor, current, extra = {}) {
2205
- const matched = current.hash === anchor.hash;
2206
- return {
2207
- ...base(anchor),
2208
- state: matched ? "match" : "drifted",
2209
- currentHash: current.hash,
2210
- hashKind: current.kind,
2211
- diffSize: anchor.lines === void 0 ? null : Math.abs(current.lines - anchor.lines),
2212
- ...matched ? {} : { class: "changed" },
2213
- ...extra
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 localEntry(anchor, read, resolvers) {
2217
- if (!read.ok) return unresolved(anchor, read.reason);
2218
- const found = hashIn(read.source, anchor, resolvers);
2219
- if (!found.ok) return unresolved(anchor, found.reason);
2220
- return compared(
2221
- anchor,
2222
- found.current,
2223
- resolverExtras(read.source, anchor, found.current)
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 remoteEntry(anchor, remote, resolvers) {
2227
- const repo = anchor.repo;
2228
- const key2 = normalizeRepoUrl(repo);
2229
- const atDefault = remote.get(wantKey(key2, void 0, anchor.file));
2230
- const primary = anchor.ref ? remote.get(wantKey(key2, anchor.ref, anchor.file)) : atDefault;
2231
- if (!primary) return unresolved(anchor, "remote-unreachable", repo);
2232
- if (!primary.ok) return unresolved(anchor, primary.reason, repo);
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/errors.ts
2268
- var BaseError = class extends Error {
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.ts
2394
+ var import_zod7 = require("zod");
2286
2395
 
2287
2396
  // src/kb-errors.ts
2288
2397
  var KbRecordAlreadyExistsError = class extends BaseError {
@@ -3193,14 +3302,117 @@ async function readSources(anchors, root, offline) {
3193
3302
  return sources;
3194
3303
  }
3195
3304
 
3196
- // src/commands/answer.ts
3305
+ // src/commands/anchor-set/model.ts
3197
3306
  var import_zod8 = require("zod");
3307
+ var anchorSetInputSchema = import_zod8.z.object({
3308
+ reason: import_zod8.z.string().refine((text) => text.trim().length > 0, {
3309
+ message: "reason must say what was reviewed"
3310
+ }).describe(
3311
+ "What the reviewer read that makes these the right pointers. Recorded in the log."
3312
+ ),
3313
+ anchors: import_zod8.z.array(kbAnchorWriteSchema).min(1).describe(
3314
+ "The complete new anchor set. Carry an anchor's hash forward to keep drift visible until the new code is read."
3315
+ )
3316
+ }).strict();
3317
+ var anchorSetCommandInput = import_zod8.z.object({
3318
+ bundlePath,
3319
+ conceptId,
3320
+ input: anchorSetInputSchema,
3321
+ resolve: import_zod8.z.boolean().optional().describe(
3322
+ "Also resolve and stamp every anchor against the current code, as anchor-resolve --rebaseline does."
3323
+ ),
3324
+ repoRoot: import_zod8.z.string().min(1).optional().describe(
3325
+ "Where the anchored source lives, for resolve. Defaults to the working directory."
3326
+ ),
3327
+ offline: import_zod8.z.boolean().optional().describe("With resolve, read foreign anchors from the repo cache only.")
3328
+ });
3329
+
3330
+ // src/commands/anchor-set/command.ts
3331
+ 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.";
3332
+ var STAMPED_NOTE = "pointers set and stamped against the current code. Not verification: run verify separately if someone reviewed it.";
3333
+ var anchorSetCommand = define({
3334
+ name: "anchor-set",
3335
+ tool: "kb_anchor_set",
3336
+ usage: "anchor-set <concept-id> [--resolve] [--repo-root <path>] [--offline] < anchors.json",
3337
+ 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.",
3338
+ input: anchorSetCommandInput,
3339
+ fromArgv: async (argv, path, stdin) => ({
3340
+ bundlePath: path,
3341
+ conceptId: argv[1],
3342
+ input: JSON.parse(await stdin()),
3343
+ resolve: argv.includes("--resolve"),
3344
+ repoRoot: argvFlag(argv, "--repo-root"),
3345
+ offline: argv.includes("--offline")
3346
+ }),
3347
+ run: async (ctx, { bundlePath: path, conceptId: id, input, resolve: resolve7, repoRoot, offline }) => {
3348
+ const { store, actor } = ctx;
3349
+ await assertBaseNotFrozen(process.cwd(), path);
3350
+ let applied;
3351
+ const record = await store.updateAnchors(
3352
+ path,
3353
+ id,
3354
+ (current) => {
3355
+ applied = applyAnchorSet(current, input.anchors);
3356
+ return {
3357
+ anchors: applied.anchors,
3358
+ log: {
3359
+ operation: "anchor-set",
3360
+ reason: input.reason,
3361
+ anchors: applied.changes
3362
+ }
3363
+ };
3364
+ },
3365
+ actor
3366
+ );
3367
+ const changes = applied?.changes ?? [];
3368
+ if (!resolve7) {
3369
+ return {
3370
+ conceptId: id,
3371
+ reason: input.reason,
3372
+ changes,
3373
+ anchors: record.frontmatter.strauss_anchors ?? [],
3374
+ baseline: "unchanged",
3375
+ note: NOTE
3376
+ };
3377
+ }
3378
+ const resolved = await anchorResolveCommand.run(
3379
+ ctx,
3380
+ anchorResolveCommand.input.parse({
3381
+ bundlePath: path,
3382
+ conceptId: id,
3383
+ rebaseline: true,
3384
+ ...repoRoot ? { repoRoot } : {},
3385
+ ...offline ? { offline } : {}
3386
+ })
3387
+ );
3388
+ const after = await store.read(path, id);
3389
+ return {
3390
+ conceptId: id,
3391
+ reason: input.reason,
3392
+ changes,
3393
+ anchors: after?.frontmatter.strauss_anchors ?? [],
3394
+ baseline: "stamped",
3395
+ resolved: resolved.results,
3396
+ note: STAMPED_NOTE
3397
+ };
3398
+ },
3399
+ // With `resolve`, a pointer that names nothing is a failed set, not a
3400
+ // finding to read later. A remote nothing could reach was never checked, so
3401
+ // it does not fail — the same line anchor-resolve draws.
3402
+ failsWhen: (result) => (result.resolved ?? []).some((entry) => {
3403
+ const { state, reason } = entry;
3404
+ return state === "unresolved" && !isUncheckedReason(reason);
3405
+ })
3406
+ });
3407
+
3408
+ // src/commands/answer.ts
3409
+ var import_zod9 = require("zod");
3198
3410
  var answerCommand = define({
3199
3411
  name: "answer",
3200
3412
  tool: "kb_answer",
3201
3413
  usage: "answer <concept-id> <answer...>",
3202
3414
  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: import_zod8.z.object({ bundlePath, conceptId, answer: import_zod8.z.string().min(1) }),
3415
+ input: import_zod9.z.object({ bundlePath, conceptId, answer: import_zod9.z.string().min(1) }),
3204
3416
  fromArgv: (argv, path) => ({
3205
3417
  bundlePath: path,
3206
3418
  conceptId: argv[1],
@@ -3214,19 +3426,19 @@ var answerCommand = define({
3214
3426
  });
3215
3427
 
3216
3428
  // src/commands/backlinks.ts
3217
- var import_zod9 = require("zod");
3429
+ var import_zod10 = require("zod");
3218
3430
  var backlinksCommand = define({
3219
3431
  name: "backlinks",
3220
3432
  tool: "kb_backlinks",
3221
3433
  usage: "backlinks <concept-id>",
3222
3434
  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: import_zod9.z.object({ bundlePath, conceptId }),
3435
+ input: import_zod10.z.object({ bundlePath, conceptId }),
3224
3436
  fromArgv: (argv, path) => ({ bundlePath: path, conceptId: argv[1] }),
3225
3437
  run: async ({ store }, { bundlePath: path, conceptId: id }) => store.backlinks(path, id)
3226
3438
  });
3227
3439
 
3228
3440
  // src/commands/catalog.ts
3229
- var import_zod10 = require("zod");
3441
+ var import_zod11 = require("zod");
3230
3442
 
3231
3443
  // src/adjudicate.ts
3232
3444
  var STANDING = {
@@ -3405,9 +3617,9 @@ var catalogCommand = define({
3405
3617
  tool: "kb_catalog",
3406
3618
  usage: "catalog [type] [--tag T]...",
3407
3619
  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: import_zod10.z.object({
3620
+ input: import_zod11.z.object({
3409
3621
  bundlePath,
3410
- type: import_zod10.z.enum(KB_RECORD_TYPES).optional(),
3622
+ type: import_zod11.z.enum(KB_RECORD_TYPES).optional(),
3411
3623
  tags: TAGS
3412
3624
  }),
3413
3625
  fromArgv: (argv, path) => {
@@ -3479,7 +3691,7 @@ function count(value, noun) {
3479
3691
  var import_node_buffer = require("buffer");
3480
3692
  var import_promises7 = require("fs/promises");
3481
3693
  var import_node_path10 = require("path");
3482
- var import_zod13 = require("zod");
3694
+ var import_zod14 = require("zod");
3483
3695
 
3484
3696
  // src/match-diff.ts
3485
3697
  function matchToDiff(files, records, options = {}) {
@@ -4083,7 +4295,7 @@ function claimOf(record) {
4083
4295
  }
4084
4296
 
4085
4297
  // src/commands/match/command.ts
4086
- var import_zod12 = require("zod");
4298
+ var import_zod13 = require("zod");
4087
4299
 
4088
4300
  // src/commands/match/errors.ts
4089
4301
  var KbMatchInputError = class extends BaseError {
@@ -4103,21 +4315,21 @@ var KbMatchInputError = class extends BaseError {
4103
4315
  };
4104
4316
 
4105
4317
  // src/commands/match/model.ts
4106
- var import_zod11 = require("zod");
4107
- var diffHunkSchema = import_zod11.z.object({
4108
- startLine: import_zod11.z.number().int().positive(),
4109
- endLine: import_zod11.z.number().int().positive(),
4110
- side: import_zod11.z.enum(["old", "new"]).optional()
4318
+ var import_zod12 = require("zod");
4319
+ var diffHunkSchema = import_zod12.z.object({
4320
+ startLine: import_zod12.z.number().int().positive(),
4321
+ endLine: import_zod12.z.number().int().positive(),
4322
+ side: import_zod12.z.enum(["old", "new"]).optional()
4111
4323
  }).passthrough();
4112
- var diffFileSchema = import_zod11.z.object({
4113
- filePath: import_zod11.z.string().min(1).describe("Repo-relative, spelled the way anchors are."),
4114
- hunks: import_zod11.z.array(diffHunkSchema)
4324
+ var diffFileSchema = import_zod12.z.object({
4325
+ filePath: import_zod12.z.string().min(1).describe("Repo-relative, spelled the way anchors are."),
4326
+ hunks: import_zod12.z.array(diffHunkSchema)
4115
4327
  });
4116
- var symbolRangeSchema = import_zod11.z.object({
4117
- file: import_zod11.z.string().min(1),
4118
- symbol: import_zod11.z.string().min(1),
4119
- startLine: import_zod11.z.number().int().positive(),
4120
- endLine: import_zod11.z.number().int().positive()
4328
+ var symbolRangeSchema = import_zod12.z.object({
4329
+ file: import_zod12.z.string().min(1),
4330
+ symbol: import_zod12.z.string().min(1),
4331
+ startLine: import_zod12.z.number().int().positive(),
4332
+ endLine: import_zod12.z.number().int().positive()
4121
4333
  });
4122
4334
 
4123
4335
  // src/commands/match/parse-unified-diff.ts
@@ -4366,17 +4578,17 @@ var matchCommand = define({
4366
4578
  tool: "kb_match",
4367
4579
  usage: "match --git <base>..<head> | --stdin [--repo-root <path>] [--offline] [--include-non-current]",
4368
4580
  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: import_zod12.z.object({
4581
+ input: import_zod13.z.object({
4370
4582
  bundlePath,
4371
- files: import_zod12.z.array(diffFileSchema).describe("The changed files, each with its post-change line ranges."),
4372
- symbolRanges: import_zod12.z.array(symbolRangeSchema).optional().describe(
4583
+ files: import_zod13.z.array(diffFileSchema).describe("The changed files, each with its post-change line ranges."),
4584
+ symbolRanges: import_zod13.z.array(symbolRangeSchema).optional().describe(
4373
4585
  "Symbol spans the caller already has. Resolved from repoRoot when omitted."
4374
4586
  ),
4375
4587
  repoRoot: REPO_ROOT,
4376
- offline: import_zod12.z.boolean().optional().describe(
4588
+ offline: import_zod13.z.boolean().optional().describe(
4377
4589
  "Resolve symbol ranges from what is already on disk, never fetching a grammar."
4378
4590
  ),
4379
- includeNonCurrent: import_zod12.z.boolean().optional().describe(
4591
+ includeNonCurrent: import_zod13.z.boolean().optional().describe(
4380
4592
  "Return superseded, rejected and unsettled records too, each carrying its standing."
4381
4593
  )
4382
4594
  }),
@@ -4390,11 +4602,11 @@ var matchCommand = define({
4390
4602
  ...argv.includes("--include-non-current") ? { includeNonCurrent: true } : {}
4391
4603
  };
4392
4604
  if (range !== void 0) {
4393
- const diff = await readRangeDiff(repoRoot ?? process.cwd(), range);
4394
- if (!diff.ok) {
4395
- throw new KbMatchInputError(`--git ${range} ${REFUSED[diff.reason]}`);
4605
+ const diff2 = await readRangeDiff(repoRoot ?? process.cwd(), range);
4606
+ if (!diff2.ok) {
4607
+ throw new KbMatchInputError(`--git ${range} ${REFUSED[diff2.reason]}`);
4396
4608
  }
4397
- return { ...base2, files: parseUnifiedDiff(diff.text) };
4609
+ return { ...base2, files: parseUnifiedDiff(diff2.text) };
4398
4610
  }
4399
4611
  if (!argv.includes("--stdin")) {
4400
4612
  throw new KbMatchInputError(
@@ -4480,22 +4692,22 @@ function project(match, ranges, all) {
4480
4692
 
4481
4693
  // src/commands/classify.ts
4482
4694
  var classifyFileSchema = diffFileSchema.extend({
4483
- hunks: import_zod13.z.array(
4484
- diffHunkSchema.extend({ lines: import_zod13.z.array(import_zod13.z.string()).optional() })
4695
+ hunks: import_zod14.z.array(
4696
+ diffHunkSchema.extend({ lines: import_zod14.z.array(import_zod14.z.string()).optional() })
4485
4697
  ),
4486
- renamedFrom: import_zod13.z.string().min(1).optional().describe("Where `git diff -M` says the path came from."),
4487
- similarity: import_zod13.z.number().min(0).max(100).optional()
4698
+ renamedFrom: import_zod14.z.string().min(1).optional().describe("Where `git diff -M` says the path came from."),
4699
+ similarity: import_zod14.z.number().min(0).max(100).optional()
4488
4700
  });
4489
4701
  var classifyCommand = define({
4490
4702
  name: "classify",
4491
4703
  tool: "kb_classify",
4492
4704
  usage: "classify --git <base>..<head> | --stdin [--repo-root <path>] [--offline]",
4493
4705
  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: import_zod13.z.object({
4706
+ input: import_zod14.z.object({
4495
4707
  bundlePath,
4496
- files: import_zod13.z.array(classifyFileSchema).describe("The changed files, each with its line ranges."),
4708
+ files: import_zod14.z.array(classifyFileSchema).describe("The changed files, each with its line ranges."),
4497
4709
  repoRoot: REPO_ROOT,
4498
- offline: import_zod13.z.boolean().optional().describe(
4710
+ offline: import_zod14.z.boolean().optional().describe(
4499
4711
  "Resolve symbol ranges from what is already on disk, never fetching a grammar."
4500
4712
  )
4501
4713
  }),
@@ -4508,15 +4720,15 @@ var classifyCommand = define({
4508
4720
  ...argv.includes("--offline") ? { offline: true } : {}
4509
4721
  };
4510
4722
  if (range !== void 0) {
4511
- const diff = await readRangeDiff(repoRoot ?? process.cwd(), range);
4512
- if (!diff.ok) {
4723
+ const diff2 = await readRangeDiff(repoRoot ?? process.cwd(), range);
4724
+ if (!diff2.ok) {
4513
4725
  throw new KbClassifyInputError(
4514
- `--git ${range} ${REFUSED2[diff.reason]}`
4726
+ `--git ${range} ${REFUSED2[diff2.reason]}`
4515
4727
  );
4516
4728
  }
4517
4729
  return {
4518
4730
  ...base2,
4519
- files: parseUnifiedDiff(diff.text, {
4731
+ files: parseUnifiedDiff(diff2.text, {
4520
4732
  keepEmpty: true,
4521
4733
  withLines: true
4522
4734
  })
@@ -4606,7 +4818,7 @@ function renderClassify(result) {
4606
4818
  }
4607
4819
 
4608
4820
  // src/commands/context.ts
4609
- var import_zod14 = require("zod");
4821
+ var import_zod15 = require("zod");
4610
4822
 
4611
4823
  // src/kb-context.ts
4612
4824
  var import_promises8 = require("fs/promises");
@@ -4877,23 +5089,23 @@ var contextCommand = define({
4877
5089
  tool: "kb_context",
4878
5090
  usage: "context [--profile NAME] [--budget N] [--full-under N] [--exclude-tag T]... [--format json] [--event NAME]",
4879
5091
  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: import_zod14.z.object({
4881
- budgetTokens: import_zod14.z.number().int().positive().optional().describe(
5092
+ input: import_zod15.z.object({
5093
+ budgetTokens: import_zod15.z.number().int().positive().optional().describe(
4882
5094
  "Ceiling on the whole emitted block; past it the command refuses with a list of bases rather than truncating. Defaults to 4000."
4883
5095
  ),
4884
- fullUnderTokens: import_zod14.z.number().int().positive().optional().describe(
5096
+ fullUnderTokens: import_zod15.z.number().int().positive().optional().describe(
4885
5097
  "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
5098
  ),
4887
- profile: import_zod14.z.string().optional().describe(
5099
+ profile: import_zod15.z.string().optional().describe(
4888
5100
  "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
5101
  ),
4890
- excludeTags: import_zod14.z.array(import_zod14.z.string().min(1)).optional().describe(
5102
+ excludeTags: import_zod15.z.array(import_zod15.z.string().min(1)).optional().describe(
4891
5103
  "Frontmatter tags whose records stay out of the block. The base stays pinned and stays readable by tool; resolved like the budgets."
4892
5104
  ),
4893
- format: import_zod14.z.enum(["markdown", "json"]).optional().describe(
5105
+ format: import_zod15.z.enum(["markdown", "json"]).optional().describe(
4894
5106
  "CLI envelope for hook protocols that require strict JSON on stdout. MCP callers omit this \u2014 the block itself is identical."
4895
5107
  ),
4896
- event: import_zod14.z.string().optional().describe(
5108
+ event: import_zod15.z.string().optional().describe(
4897
5109
  "hookEventName stamped into the JSON envelope. Only meaningful with format=json."
4898
5110
  )
4899
5111
  }),
@@ -4932,7 +5144,7 @@ var contextCommand = define({
4932
5144
  });
4933
5145
 
4934
5146
  // src/commands/doctor.ts
4935
- var import_zod16 = require("zod");
5147
+ var import_zod17 = require("zod");
4936
5148
 
4937
5149
  // src/kb-edges.ts
4938
5150
  var KB_EDGE_KINDS = [
@@ -5448,17 +5660,17 @@ function ageInDays(record, now) {
5448
5660
  }
5449
5661
 
5450
5662
  // src/commands/reassess.ts
5451
- var import_zod15 = require("zod");
5663
+ var import_zod16 = require("zod");
5452
5664
  var reassessCommand = define({
5453
5665
  name: "reassess",
5454
5666
  tool: "kb_reassess",
5455
5667
  usage: "reassess <concept-id> [--repo-root <path>] [--with-diff]",
5456
5668
  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: import_zod15.z.object({
5669
+ input: import_zod16.z.object({
5458
5670
  bundlePath,
5459
5671
  conceptId,
5460
5672
  repoRoot: REPO_ROOT,
5461
- withDiff: import_zod15.z.boolean().optional().describe(
5673
+ withDiff: import_zod16.z.boolean().optional().describe(
5462
5674
  "Recover each anchor's committed span and render the diff. Reads git history."
5463
5675
  )
5464
5676
  }),
@@ -5603,13 +5815,13 @@ function at(file, symbol) {
5603
5815
  }
5604
5816
 
5605
5817
  // src/commands/doctor.ts
5606
- var days = (what, fallback) => import_zod16.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
5818
+ var days = (what, fallback) => import_zod17.z.number().int().positive().optional().describe(`${what} Defaults to ${fallback}.`);
5607
5819
  var doctorCommand = define({
5608
5820
  name: "doctor",
5609
5821
  tool: "kb_doctor",
5610
5822
  usage: "doctor [--expiring-days N] [--unverified-days N] [--aging-days N] [--repo-root PATH] [--offline] [--strict] [--drifted [--with-diff]]",
5611
5823
  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: import_zod16.z.object({
5824
+ input: import_zod17.z.object({
5613
5825
  bundlePath,
5614
5826
  repoRoot: REPO_ROOT,
5615
5827
  expiringDays: days(
@@ -5624,16 +5836,16 @@ var doctorCommand = define({
5624
5836
  "How long a record may stay `open` or `proposed` before `aging` reports it, in days.",
5625
5837
  DEFAULT_AGING_DAYS
5626
5838
  ),
5627
- offline: import_zod16.z.boolean().optional().describe(
5839
+ offline: import_zod17.z.boolean().optional().describe(
5628
5840
  "Read foreign anchors from the local repo cache only, never fetching."
5629
5841
  ),
5630
- strict: import_zod16.z.boolean().optional().describe(
5842
+ strict: import_zod17.z.boolean().optional().describe(
5631
5843
  "Turn an expired record into a non-zero exit for the CLI. No effect on the report itself."
5632
5844
  ),
5633
- drifted: import_zod16.z.boolean().optional().describe(
5845
+ drifted: import_zod17.z.boolean().optional().describe(
5634
5846
  "Report only drift, as a reassessment packet per record: claim, per-anchor class, and what depends on it."
5635
5847
  ),
5636
- withDiff: import_zod16.z.boolean().optional().describe(
5848
+ withDiff: import_zod17.z.boolean().optional().describe(
5637
5849
  "With `drifted`: recover each anchor's committed span and render the old-vs-new diff. Reads git history."
5638
5850
  )
5639
5851
  }),
@@ -5810,7 +6022,7 @@ function renderPackets(result) {
5810
6022
  // src/commands/export.ts
5811
6023
  var import_promises9 = require("fs/promises");
5812
6024
  var import_node_path11 = require("path");
5813
- var import_zod17 = require("zod");
6025
+ var import_zod18 = require("zod");
5814
6026
  var NUMBERED = /^(\d{4})-(.+)\.md$/;
5815
6027
  var MARKER = "<!-- strauss-kb export: ";
5816
6028
  var exportCommand = define({
@@ -5818,10 +6030,10 @@ var exportCommand = define({
5818
6030
  tool: "kb_export",
5819
6031
  usage: "export --format madr --to <dir>",
5820
6032
  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: import_zod17.z.object({
6033
+ input: import_zod18.z.object({
5822
6034
  bundlePath,
5823
- format: import_zod17.z.enum(["madr"]).describe("Output layout. `madr` is the only one so far."),
5824
- to: import_zod17.z.string().min(1).describe("Directory the ADR files are written into.")
6035
+ format: import_zod18.z.enum(["madr"]).describe("Output layout. `madr` is the only one so far."),
6036
+ to: import_zod18.z.string().min(1).describe("Directory the ADR files are written into.")
5825
6037
  }),
5826
6038
  fromArgv: (argv, path) => ({
5827
6039
  bundlePath: path,
@@ -5943,19 +6155,19 @@ function bodySections(body) {
5943
6155
  }
5944
6156
 
5945
6157
  // src/commands/impact.ts
5946
- var import_zod18 = require("zod");
6158
+ var import_zod19 = require("zod");
5947
6159
  var impactCommand = define({
5948
6160
  name: "impact",
5949
6161
  tool: "kb_impact",
5950
6162
  usage: "impact <concept-id> [--depth N] [--rels a,b]",
5951
6163
  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: import_zod18.z.object({
6164
+ input: import_zod19.z.object({
5953
6165
  bundlePath,
5954
6166
  conceptId,
5955
- depth: import_zod18.z.number().int().positive().optional().describe(
6167
+ depth: import_zod19.z.number().int().positive().optional().describe(
5956
6168
  "Hops out from the record. Unbounded when omitted; a walk this cuts reports truncated: true."
5957
6169
  ),
5958
- rels: import_zod18.z.array(import_zod18.z.enum(KB_CAUSAL_LINK_RELS)).optional().describe(
6170
+ rels: import_zod19.z.array(import_zod19.z.enum(KB_CAUSAL_LINK_RELS)).optional().describe(
5959
6171
  "Narrow which rels the walk follows. Defaults to every rel that carries a dependence \u2014 all but related_to."
5960
6172
  )
5961
6173
  }),
@@ -5976,15 +6188,15 @@ var impactCommand = define({
5976
6188
  });
5977
6189
 
5978
6190
  // src/commands/list.ts
5979
- var import_zod19 = require("zod");
6191
+ var import_zod20 = require("zod");
5980
6192
  var listCommand = define({
5981
6193
  name: "list",
5982
6194
  tool: "kb_list",
5983
6195
  usage: "list [type] [--tag T]...",
5984
6196
  description: "Every record, optionally one type or tag. For enumerating; use kb_query for a question.",
5985
- input: import_zod19.z.object({
6197
+ input: import_zod20.z.object({
5986
6198
  bundlePath,
5987
- type: import_zod19.z.enum(KB_RECORD_TYPES).optional(),
6199
+ type: import_zod20.z.enum(KB_RECORD_TYPES).optional(),
5988
6200
  tags: TAGS
5989
6201
  }),
5990
6202
  fromArgv: (argv, path) => {
@@ -6008,17 +6220,17 @@ var listCommand = define({
6008
6220
  });
6009
6221
 
6010
6222
  // src/commands/load.ts
6011
- var import_zod20 = require("zod");
6223
+ var import_zod21 = require("zod");
6012
6224
  var loadCommand = define({
6013
6225
  name: "load",
6014
6226
  tool: "kb_load",
6015
6227
  usage: "load [type] [--budget N | --all] [--repo-root PATH]",
6016
6228
  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: import_zod20.z.object({
6229
+ input: import_zod21.z.object({
6018
6230
  bundlePath,
6019
- type: import_zod20.z.enum(KB_RECORD_TYPES).optional(),
6020
- budgetTokens: import_zod20.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
6021
- all: import_zod20.z.boolean().optional().describe(
6231
+ type: import_zod21.z.enum(KB_RECORD_TYPES).optional(),
6232
+ budgetTokens: import_zod21.z.number().int().positive().optional().describe("Approximate token ceiling. Defaults to 25000."),
6233
+ all: import_zod21.z.boolean().optional().describe(
6022
6234
  "Loads the entire base regardless of size, bypassing the token budget; mutually exclusive with budgetTokens."
6023
6235
  ),
6024
6236
  repoRoot: REPO_ROOT
@@ -6060,25 +6272,25 @@ var loadCommand = define({
6060
6272
  });
6061
6273
 
6062
6274
  // src/commands/log.ts
6063
- var import_zod21 = require("zod");
6275
+ var import_zod22 = require("zod");
6064
6276
  var logCommand = define({
6065
6277
  name: "log",
6066
6278
  tool: "kb_log",
6067
6279
  usage: "log",
6068
6280
  description: "Who touched what, and when. Append-only; malformed lines are reported, never repaired.",
6069
- input: import_zod21.z.object({ bundlePath }),
6281
+ input: import_zod22.z.object({ bundlePath }),
6070
6282
  fromArgv: (_argv, path) => ({ bundlePath: path }),
6071
6283
  run: ({ store }, { bundlePath: path }) => store.readLog(path)
6072
6284
  });
6073
6285
 
6074
6286
  // src/commands/no-decision.ts
6075
- var import_zod22 = require("zod");
6287
+ var import_zod23 = require("zod");
6076
6288
  var noDecisionCommand = define({
6077
6289
  name: "no-decision",
6078
6290
  tool: "kb_no_decision",
6079
6291
  usage: "no-decision <reason...>",
6080
6292
  description: "Record in one sentence that a piece of work had nothing to decide. Idempotent.",
6081
- input: import_zod22.z.object({ bundlePath, reason: import_zod22.z.string().min(1) }),
6293
+ input: import_zod23.z.object({ bundlePath, reason: import_zod23.z.string().min(1) }),
6082
6294
  fromArgv: (argv, path) => ({
6083
6295
  bundlePath: path,
6084
6296
  reason: argv.slice(1).join(" ").trim()
@@ -6095,20 +6307,20 @@ var noDecisionCommand = define({
6095
6307
  });
6096
6308
 
6097
6309
  // src/commands/pack.ts
6098
- var import_zod23 = require("zod");
6310
+ var import_zod24 = require("zod");
6099
6311
  var packCommand = define({
6100
6312
  name: "pack",
6101
6313
  tool: "kb_pack",
6102
6314
  usage: "pack <conceptId> [--hops N] [--max-nodes N] [--budget N]",
6103
6315
  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: import_zod23.z.object({
6316
+ input: import_zod24.z.object({
6105
6317
  bundlePath,
6106
6318
  conceptId,
6107
- hops: import_zod23.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
6108
- maxNodes: import_zod23.z.number().int().positive().optional().describe(
6319
+ hops: import_zod24.z.number().int().positive().optional().describe("How far from the root the walk may reach. Defaults to 2."),
6320
+ maxNodes: import_zod24.z.number().int().positive().optional().describe(
6109
6321
  "How many records the pack may hold, root included. Defaults to 20."
6110
6322
  ),
6111
- budgetTokens: import_zod23.z.number().int().positive().optional().describe(
6323
+ budgetTokens: import_zod24.z.number().int().positive().optional().describe(
6112
6324
  "Approximate token ceiling over what is actually emitted. Defaults to 25000."
6113
6325
  )
6114
6326
  }),
@@ -6195,22 +6407,22 @@ function warningLabel(warning) {
6195
6407
  }
6196
6408
 
6197
6409
  // src/commands/pin.ts
6198
- var import_zod24 = require("zod");
6410
+ var import_zod25 = require("zod");
6199
6411
  var pinCommand = define({
6200
6412
  name: "pin",
6201
6413
  tool: "kb_pin",
6202
6414
  usage: "pin [bundle-path] [--mode full|index] [--profiles a,b] [--local|--user] [--frozen|--unfreeze]",
6203
6415
  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: import_zod24.z.object({
6416
+ input: import_zod25.z.object({
6205
6417
  bundlePath,
6206
- mode: import_zod24.z.enum(["full", "index"]).optional().describe(
6418
+ mode: import_zod25.z.enum(["full", "index"]).optional().describe(
6207
6419
  "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
6420
  ),
6209
- profiles: import_zod24.z.array(import_zod24.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
6210
- layer: import_zod24.z.enum(["project", "local", "user"]).optional().describe(
6421
+ profiles: import_zod25.z.array(import_zod25.z.string()).optional().describe("Context profiles this pin surfaces in. Absent: all of them."),
6422
+ layer: import_zod25.z.enum(["project", "local", "user"]).optional().describe(
6211
6423
  "Which manifest to write: project (committed, default), local (personal, gitignored), user (~/.strauss, every workspace)."
6212
6424
  ),
6213
- frozen: import_zod24.z.boolean().optional().describe(
6425
+ frozen: import_zod25.z.boolean().optional().describe(
6214
6426
  "true: the base is concluded \u2014 writes against it refuse while pinned. false: lift a freeze."
6215
6427
  )
6216
6428
  }),
@@ -6239,13 +6451,13 @@ var pinCommand = define({
6239
6451
  });
6240
6452
 
6241
6453
  // src/commands/pins.ts
6242
- var import_zod25 = require("zod");
6454
+ var import_zod26 = require("zod");
6243
6455
  var pinsCommand = define({
6244
6456
  name: "pins",
6245
6457
  tool: "kb_pins",
6246
6458
  usage: "pins",
6247
6459
  description: "Every pinned base across the manifest layers, with its layer and whether it resolves to records. Takes no bundlePath.",
6248
- input: import_zod25.z.object({}),
6460
+ input: import_zod26.z.object({}),
6249
6461
  fromArgv: () => ({}),
6250
6462
  run: ({ store }) => listPins(store, process.cwd())
6251
6463
  });
@@ -6516,16 +6728,16 @@ function recordType(conceptId2) {
6516
6728
  }
6517
6729
 
6518
6730
  // src/commands/promote/model.ts
6519
- var import_zod26 = require("zod");
6520
- var promoteInputSchema = import_zod26.z.object({
6731
+ var import_zod27 = require("zod");
6732
+ var promoteInputSchema = import_zod27.z.object({
6521
6733
  bundlePath,
6522
- conceptIds: import_zod26.z.array(conceptId).max(64).optional().describe("Records to copy into the target base. Omit with `list`."),
6523
- to: import_zod26.z.string().min(1).optional().describe("Absolute path to the base being promoted into."),
6524
- source: import_zod26.z.string().min(1).optional().describe(
6734
+ conceptIds: import_zod27.z.array(conceptId).max(64).optional().describe("Records to copy into the target base. Omit with `list`."),
6735
+ to: import_zod27.z.string().min(1).optional().describe("Absolute path to the base being promoted into."),
6736
+ source: import_zod27.z.string().min(1).optional().describe(
6525
6737
  "Where the promotion came from, usually the pull request URL. Recorded on each copy as a source."
6526
6738
  ),
6527
- force: import_zod26.z.boolean().optional().describe("Overwrite a record the target base already holds."),
6528
- list: import_zod26.z.boolean().optional().describe("List the source base's candidates instead of promoting.")
6739
+ force: import_zod27.z.boolean().optional().describe("Overwrite a record the target base already holds."),
6740
+ list: import_zod27.z.boolean().optional().describe("List the source base's candidates instead of promoting.")
6529
6741
  }).refine((input) => input.list === true || input.to !== void 0, {
6530
6742
  message: "promote needs a target base \u2014 pass --to <bundle>, or --list",
6531
6743
  path: ["to"]
@@ -6662,17 +6874,17 @@ function renderPromote(result) {
6662
6874
  }
6663
6875
 
6664
6876
  // src/commands/query.ts
6665
- var import_zod27 = require("zod");
6877
+ var import_zod28 = require("zod");
6666
6878
  var queryCommand = define({
6667
6879
  name: "query",
6668
6880
  tool: "kb_query",
6669
6881
  usage: "query <text...> [--tag T]... [--repo-root PATH]",
6670
6882
  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: import_zod27.z.object({
6883
+ input: import_zod28.z.object({
6672
6884
  bundlePath,
6673
- text: import_zod27.z.string().optional(),
6674
- type: import_zod27.z.enum(KB_RECORD_TYPES).optional(),
6675
- includeNonCurrent: import_zod27.z.boolean().optional(),
6885
+ text: import_zod28.z.string().optional(),
6886
+ type: import_zod28.z.enum(KB_RECORD_TYPES).optional(),
6887
+ includeNonCurrent: import_zod28.z.boolean().optional(),
6676
6888
  tags: TAGS,
6677
6889
  repoRoot: REPO_ROOT
6678
6890
  }),
@@ -6706,27 +6918,32 @@ var queryCommand = define({
6706
6918
  });
6707
6919
 
6708
6920
  // src/commands/read-index.ts
6709
- var import_zod28 = require("zod");
6921
+ var import_zod29 = require("zod");
6710
6922
  var readIndexCommand = define({
6711
6923
  name: "index",
6712
6924
  tool: "kb_index",
6713
6925
  usage: "index",
6714
6926
  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: import_zod28.z.object({ bundlePath }),
6927
+ input: import_zod29.z.object({ bundlePath }),
6716
6928
  fromArgv: (_argv, path) => ({ bundlePath: path }),
6717
6929
  run: ({ store }, { bundlePath: path }) => store.readIndex(path)
6718
6930
  });
6719
6931
 
6720
6932
  // src/commands/schema.ts
6721
- var import_zod31 = require("zod");
6933
+ var import_zod32 = require("zod");
6722
6934
 
6723
6935
  // src/json-schema.ts
6724
- var import_zod30 = require("zod");
6936
+ var import_zod31 = require("zod");
6725
6937
 
6726
6938
  // src/kb-log.ts
6727
- var import_zod29 = require("zod");
6939
+ var import_zod30 = require("zod");
6728
6940
  var LOG_FILE = "log.jsonl";
6729
- var kbLogEntrySchema = import_zod29.z.object({
6941
+ var kbLogAnchorChangeSchema = import_zod30.z.object({
6942
+ op: import_zod30.z.enum(["move", "add", "drop"]),
6943
+ from: kbAnchorLocatorSchema.optional(),
6944
+ to: kbAnchorLocatorSchema.optional()
6945
+ }).strict();
6946
+ var kbLogEntryFields = import_zod30.z.object({
6730
6947
  // Validated, not just `min(1)`: `at` is a sort key (see `parseLog`
6731
6948
  // below), and a value that isn't actually chronological — a Unix
6732
6949
  // timestamp, a human-typed date, garbage — would sort wrong without
@@ -6735,18 +6952,28 @@ var kbLogEntrySchema = import_zod29.z.object({
6735
6952
  // and rejects everything else, including a non-`Z` offset — so a
6736
6953
  // malformed `at` is reported the same way a malformed line already is,
6737
6954
  // rather than silently sorting into the wrong place.
6738
- at: import_zod29.z.iso.datetime(),
6739
- by: import_zod29.z.string().min(1),
6740
- operation: import_zod29.z.string().min(1),
6741
- conceptId: import_zod29.z.string().min(1),
6955
+ at: import_zod30.z.iso.datetime(),
6956
+ by: import_zod30.z.string().min(1),
6957
+ operation: import_zod30.z.string().min(1),
6958
+ conceptId: import_zod30.z.string().min(1),
6742
6959
  /**
6743
6960
  * The operation's other end, where it has one: a second concept id for
6744
6961
  * supersession, the other base's path for promotion.
6745
6962
  */
6746
- target: import_zod29.z.string().min(1).optional()
6747
- }).strict();
6963
+ target: import_zod30.z.string().min(1).optional(),
6964
+ /**
6965
+ * Why the operation was performed, where the operation demands one.
6966
+ * `anchor-set` does: a pointer moved by a reader is only auditable if
6967
+ * the reading is recorded beside it.
6968
+ */
6969
+ reason: import_zod30.z.string().min(1).optional(),
6970
+ /** What `anchor-set` changed, derived from the record before and after. */
6971
+ anchors: import_zod30.z.array(kbLogAnchorChangeSchema).optional()
6972
+ });
6973
+ var kbLogEntrySchema = kbLogEntryFields.passthrough();
6974
+ var kbLogEntryWriteSchema = kbLogEntryFields.strict();
6748
6975
  function renderLogEntry(entry) {
6749
- return `${JSON.stringify(kbLogEntrySchema.parse(entry))}
6976
+ return `${JSON.stringify(kbLogEntryWriteSchema.parse(entry))}
6750
6977
  `;
6751
6978
  }
6752
6979
  var CONFLICT_MARKER = /^(<{7}|\|{7}|={7}|>{7})/;
@@ -6787,11 +7014,11 @@ function parseLog(raw) {
6787
7014
  // src/json-schema.ts
6788
7015
  function kbJsonSchemas() {
6789
7016
  return {
6790
- recordFrontmatter: import_zod30.z.toJSONSchema(kbRecordFrontmatterSchema, {
7017
+ recordFrontmatter: import_zod31.z.toJSONSchema(kbRecordFrontmatterSchema, {
6791
7018
  io: "input"
6792
7019
  }),
6793
- composeInput: import_zod30.z.toJSONSchema(composeInputSchema, { io: "input" }),
6794
- logEntry: import_zod30.z.toJSONSchema(kbLogEntrySchema, { io: "input" })
7020
+ composeInput: import_zod31.z.toJSONSchema(composeInputSchema, { io: "input" }),
7021
+ logEntry: import_zod31.z.toJSONSchema(kbLogEntrySchema, { io: "input" })
6795
7022
  };
6796
7023
  }
6797
7024
 
@@ -6801,25 +7028,25 @@ var schemaCommand = define({
6801
7028
  tool: "kb_schema",
6802
7029
  usage: "schema",
6803
7030
  description: "JSON Schema for frontmatter, write input, and log entries, generated from the enforcing code.",
6804
- input: import_zod31.z.object({}),
7031
+ input: import_zod32.z.object({}),
6805
7032
  fromArgv: () => ({}),
6806
7033
  run: () => Promise.resolve(kbJsonSchemas())
6807
7034
  });
6808
7035
 
6809
7036
  // src/commands/stamp.ts
6810
7037
  var import_promises10 = require("fs/promises");
6811
- var import_zod32 = require("zod");
7038
+ var import_zod33 = require("zod");
6812
7039
  var DIGEST = /^[0-9a-f]{64}$/;
6813
7040
  var stampCommand = define({
6814
7041
  name: "stamp",
6815
7042
  tool: "kb_stamp",
6816
7043
  usage: "stamp [--bundle PATH] [--since DIGEST|FILE]",
6817
7044
  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: import_zod32.z.object({
6819
- bundlePath: import_zod32.z.string().min(1).optional().describe(
7045
+ input: import_zod33.z.object({
7046
+ bundlePath: import_zod33.z.string().min(1).optional().describe(
6820
7047
  "Absolute path to one knowledge base. Omit to stamp every pinned base."
6821
7048
  ),
6822
- since: import_zod32.z.string().min(1).optional().describe(
7049
+ since: import_zod33.z.string().min(1).optional().describe(
6823
7050
  "Prior digest, or path to a prior `stamp --json`; only moved bases return, with changed ids when the baseline is a file."
6824
7051
  )
6825
7052
  }),
@@ -6905,16 +7132,16 @@ async function readBaseline(since) {
6905
7132
  }
6906
7133
 
6907
7134
  // src/commands/status.ts
6908
- var import_zod33 = require("zod");
7135
+ var import_zod34 = require("zod");
6909
7136
  var statusCommand = define({
6910
7137
  name: "status",
6911
7138
  tool: "kb_status",
6912
7139
  usage: "status <concept-id> <status>",
6913
7140
  description: "Move a record's status. Compare-and-swap: a concurrent change fails instead of being overwritten.",
6914
- input: import_zod33.z.object({
7141
+ input: import_zod34.z.object({
6915
7142
  bundlePath,
6916
7143
  conceptId,
6917
- status: import_zod33.z.enum(KB_RECORD_STATUSES)
7144
+ status: import_zod34.z.enum(KB_RECORD_STATUSES)
6918
7145
  }),
6919
7146
  fromArgv: (argv, path) => ({
6920
7147
  bundlePath: path,
@@ -6929,13 +7156,13 @@ var statusCommand = define({
6929
7156
  });
6930
7157
 
6931
7158
  // src/commands/supersede.ts
6932
- var import_zod34 = require("zod");
7159
+ var import_zod35 = require("zod");
6933
7160
  var supersedeCommand = define({
6934
7161
  name: "supersede",
6935
7162
  tool: "kb_supersede",
6936
7163
  usage: "supersede <concept-id> <replacement-id>",
6937
7164
  description: "Mark a record superseded by another, linked in both directions. Use instead of editing a record whose meaning changed.",
6938
- input: import_zod34.z.object({ bundlePath, conceptId, replacementId: conceptId }),
7165
+ input: import_zod35.z.object({ bundlePath, conceptId, replacementId: conceptId }),
6939
7166
  fromArgv: (argv, path) => ({
6940
7167
  bundlePath: path,
6941
7168
  conceptId: argv[1],
@@ -6949,7 +7176,7 @@ var supersedeCommand = define({
6949
7176
  });
6950
7177
 
6951
7178
  // src/commands/sweep.ts
6952
- var import_zod35 = require("zod");
7179
+ var import_zod36 = require("zod");
6953
7180
  var TERMINAL = [
6954
7181
  "resolved",
6955
7182
  "rejected",
@@ -6960,15 +7187,15 @@ var sweepCommand = define({
6960
7187
  tool: "kb_sweep",
6961
7188
  usage: "sweep --tag <tag> --terminal [--dry-run]",
6962
7189
  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: import_zod35.z.object({
7190
+ input: import_zod36.z.object({
6964
7191
  bundlePath,
6965
- tag: import_zod35.z.string({ error: "sweep needs --tag: it never sweeps a whole base" }).min(1).describe("Only records carrying this tag are considered."),
6966
- terminal: import_zod35.z.literal(true, {
7192
+ tag: import_zod36.z.string({ error: "sweep needs --tag: it never sweeps a whole base" }).min(1).describe("Only records carrying this tag are considered."),
7193
+ terminal: import_zod36.z.literal(true, {
6967
7194
  error: "sweep needs --terminal: it deletes only settled records"
6968
7195
  }).describe(
6969
7196
  "Required. Names the only scope sweep deletes: resolved, rejected and superseded records."
6970
7197
  ),
6971
- dryRun: import_zod35.z.boolean().optional().describe("Report what would go, and delete nothing.")
7198
+ dryRun: import_zod36.z.boolean().optional().describe("Report what would go, and delete nothing.")
6972
7199
  }),
6973
7200
  fromArgv: (argv, path) => ({
6974
7201
  bundlePath: path,
@@ -7085,16 +7312,16 @@ function renderSweep(result) {
7085
7312
  }
7086
7313
 
7087
7314
  // src/commands/sync-instructions.ts
7088
- var import_zod36 = require("zod");
7315
+ var import_zod37 = require("zod");
7089
7316
  var syncInstructionsCommand = define({
7090
7317
  name: "sync-instructions",
7091
7318
  usage: "sync-instructions <file> [--profile NAME] [--budget N] [--full-under N]",
7092
7319
  description: "CLI-only: plant the kb_context block between sentinel comments in AGENTS.md or CLAUDE.md, idempotently.",
7093
- input: import_zod36.z.object({
7094
- file: import_zod36.z.string().min(1).describe("The instruction file to edit in place."),
7095
- budgetTokens: import_zod36.z.number().int().positive().optional(),
7096
- fullUnderTokens: import_zod36.z.number().int().positive().optional(),
7097
- profile: import_zod36.z.string().optional()
7320
+ input: import_zod37.z.object({
7321
+ file: import_zod37.z.string().min(1).describe("The instruction file to edit in place."),
7322
+ budgetTokens: import_zod37.z.number().int().positive().optional(),
7323
+ fullUnderTokens: import_zod37.z.number().int().positive().optional(),
7324
+ profile: import_zod37.z.string().optional()
7098
7325
  }),
7099
7326
  fromArgv: (argv) => {
7100
7327
  const budget = argvFlag(argv, "--budget");
@@ -7120,7 +7347,7 @@ var syncInstructionsCommand = define({
7120
7347
  });
7121
7348
 
7122
7349
  // src/commands/trace.ts
7123
- var import_zod37 = require("zod");
7350
+ var import_zod38 = require("zod");
7124
7351
 
7125
7352
  // src/trace.ts
7126
7353
  var TRACE_EDGES = [
@@ -7176,11 +7403,11 @@ var traceCommand = define({
7176
7403
  tool: "kb_trace",
7177
7404
  usage: "trace <concept-id> [edges...]",
7178
7405
  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: import_zod37.z.object({
7406
+ input: import_zod38.z.object({
7180
7407
  bundlePath,
7181
7408
  conceptId,
7182
- edges: import_zod37.z.array(import_zod37.z.enum(TRACE_EDGES)).optional(),
7183
- depth: import_zod37.z.number().int().positive().optional()
7409
+ edges: import_zod38.z.array(import_zod38.z.enum(TRACE_EDGES)).optional(),
7410
+ depth: import_zod38.z.number().int().positive().optional()
7184
7411
  }),
7185
7412
  fromArgv: (argv, path) => ({
7186
7413
  bundlePath: path,
@@ -7202,37 +7429,37 @@ var traceCommand = define({
7202
7429
  });
7203
7430
 
7204
7431
  // src/commands/types.ts
7205
- var import_zod38 = require("zod");
7432
+ var import_zod39 = require("zod");
7206
7433
  var typesCommand = define({
7207
7434
  name: "types",
7208
7435
  tool: "kb_types",
7209
7436
  usage: "types",
7210
7437
  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: import_zod38.z.object({}),
7438
+ input: import_zod39.z.object({}),
7212
7439
  fromArgv: () => ({}),
7213
7440
  run: () => Promise.resolve(RECORD_TYPES)
7214
7441
  });
7215
7442
 
7216
7443
  // src/commands/unpin.ts
7217
- var import_zod39 = require("zod");
7444
+ var import_zod40 = require("zod");
7218
7445
  var unpinCommand = define({
7219
7446
  name: "unpin",
7220
7447
  tool: "kb_unpin",
7221
7448
  usage: "unpin [bundle-path]",
7222
7449
  description: "Remove a base from every manifest layer that holds it. Reports the layers touched.",
7223
- input: import_zod39.z.object({ bundlePath }),
7450
+ input: import_zod40.z.object({ bundlePath }),
7224
7451
  fromArgv: (argv, path) => ({ bundlePath: argv[1] ?? path }),
7225
7452
  run: (_ctx, { bundlePath: path }) => unpinBase(process.cwd(), path)
7226
7453
  });
7227
7454
 
7228
7455
  // src/commands/validate.ts
7229
- var import_zod40 = require("zod");
7456
+ var import_zod41 = require("zod");
7230
7457
  var validateCommand = define({
7231
7458
  name: "validate",
7232
7459
  tool: "kb_validate",
7233
7460
  usage: "validate",
7234
7461
  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: import_zod40.z.object({ bundlePath }),
7462
+ input: import_zod41.z.object({ bundlePath }),
7236
7463
  fromArgv: (_argv, path) => ({ bundlePath: path }),
7237
7464
  run: async ({ store }, { bundlePath: path }) => validateBundle(await store.list(path)),
7238
7465
  // Warnings never fail the exit code; every other severity does.
@@ -7242,16 +7469,16 @@ var validateCommand = define({
7242
7469
  });
7243
7470
 
7244
7471
  // src/commands/verify.ts
7245
- var import_zod41 = require("zod");
7472
+ var import_zod42 = require("zod");
7246
7473
  var verifyCommand = define({
7247
7474
  name: "verify",
7248
7475
  tool: "kb_verify",
7249
7476
  usage: "verify <concept-id> --note <text>",
7250
7477
  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: import_zod41.z.object({
7478
+ input: import_zod42.z.object({
7252
7479
  bundlePath,
7253
7480
  conceptId,
7254
- note: import_zod41.z.string().refine((s) => s.trim().length > 0, {
7481
+ note: import_zod42.z.string().refine((s) => s.trim().length > 0, {
7255
7482
  message: "note must say what the check found"
7256
7483
  })
7257
7484
  }),
@@ -7271,15 +7498,15 @@ var verifyCommand = define({
7271
7498
  });
7272
7499
 
7273
7500
  // src/commands/write.ts
7274
- var import_zod42 = require("zod");
7501
+ var import_zod43 = require("zod");
7275
7502
  var writeCommand = define({
7276
7503
  name: "write",
7277
7504
  tool: "kb_write",
7278
7505
  usage: "write <type> < record.json",
7279
7506
  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: import_zod42.z.object({
7507
+ input: import_zod43.z.object({
7281
7508
  bundlePath,
7282
- type: import_zod42.z.enum(KB_RECORD_TYPES),
7509
+ type: import_zod43.z.enum(KB_RECORD_TYPES),
7283
7510
  input: composeInputSchema
7284
7511
  }),
7285
7512
  fromArgv: async (argv, path, stdin) => ({
@@ -7303,13 +7530,13 @@ var writeCommand = define({
7303
7530
  });
7304
7531
 
7305
7532
  // src/commands/write-decision.ts
7306
- var import_zod43 = require("zod");
7533
+ var import_zod44 = require("zod");
7307
7534
  var writeDecisionCommand = define({
7308
7535
  name: "write-decision",
7309
7536
  tool: "kb_write_decision",
7310
7537
  usage: "write-decision < decision.json",
7311
7538
  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: import_zod43.z.object({ bundlePath, input: decisionInputSchema }),
7539
+ input: import_zod44.z.object({ bundlePath, input: decisionInputSchema }),
7313
7540
  fromArgv: async (_argv, path, stdin) => ({
7314
7541
  bundlePath: path,
7315
7542
  input: JSON.parse(await stdin())
@@ -7339,6 +7566,7 @@ var KB_COMMANDS = [
7339
7566
  answerCommand,
7340
7567
  verifyCommand,
7341
7568
  anchorResolveCommand,
7569
+ anchorSetCommand,
7342
7570
  reassessCommand,
7343
7571
  promoteCommand,
7344
7572
  loadCommand,
@@ -7774,23 +8002,31 @@ var KbStore = class {
7774
8002
  );
7775
8003
  }
7776
8004
  /**
7777
- * Replaces a record's anchors wholesale, preserving everything else.
7778
- *
7779
- * Wholesale rather than merged: the caller just resolved the anchors it is
7780
- * writing, so it holds the complete current set, and a merge would keep
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.
8005
+ * Replaces a record's anchors, preserving everything else. An array is the
8006
+ * whole set; a function is a patch and runs inside the mutation, against
8007
+ * the anchors the record holds then see
8008
+ * `decision.anchor-update-patch-inside-mutation`.
7785
8009
  */
7786
8010
  async updateAnchors(bundlePath2, conceptId2, anchors, actor = "unknown") {
7787
8011
  assertActor(actor);
7788
- const checked = anchors.map((anchor) => kbAnchorWriteSchema.parse(anchor));
8012
+ let entry = {
8013
+ operation: "anchor-resolve",
8014
+ by: actor
8015
+ };
7789
8016
  return this.mutate(
7790
8017
  bundlePath2,
7791
8018
  conceptId2,
7792
- (frontmatter) => ({ ...frontmatter, strauss_anchors: checked }),
7793
- { operation: "anchor-resolve", by: actor }
8019
+ (frontmatter) => {
8020
+ const write = typeof anchors === "function" ? anchors(frontmatter.strauss_anchors ?? []) : { anchors };
8021
+ if (write.log) entry = { ...write.log, by: actor };
8022
+ return {
8023
+ ...frontmatter,
8024
+ strauss_anchors: write.anchors.map(
8025
+ (anchor) => kbAnchorWriteSchema.parse(anchor)
8026
+ )
8027
+ };
8028
+ },
8029
+ () => entry
7794
8030
  );
7795
8031
  }
7796
8032
  /**
@@ -8244,7 +8480,10 @@ ${answer}
8244
8480
  throw new KbWriteConflictError(conceptId2);
8245
8481
  }
8246
8482
  await this.publish(target, contents, true, conceptId2);
8247
- await this.record(this.root(bundlePath2), { ...entry, conceptId: conceptId2 });
8483
+ await this.record(this.root(bundlePath2), {
8484
+ ...typeof entry === "function" ? entry() : entry,
8485
+ conceptId: conceptId2
8486
+ });
8248
8487
  return { conceptId: conceptId2, frontmatter, body };
8249
8488
  }
8250
8489
  /**
@@ -8460,7 +8699,7 @@ function assertActor(actor, { named = false } = {}) {
8460
8699
  }
8461
8700
 
8462
8701
  // src/version.ts
8463
- var VERSION = true ? "0.1.21" : "0.0.0-dev";
8702
+ var VERSION = true ? "0.1.22" : "0.0.0-dev";
8464
8703
 
8465
8704
  // src/mcp.ts
8466
8705
  function createKbMcpServer() {