@saasontools/strauss-kb 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -47,13 +47,29 @@ declare const kbVerifiedEventSchema: z.ZodObject<{
47
47
  *
48
48
  * Symbolic on purpose. These are written while the code is still moving: a
49
49
  * `line: 379` recorded at minute five is wrong by minute forty, but
50
- * `OrderService.cancel` survives every edit that does not rename it. A later
51
- * pass resolves symbols to line ranges once the change has settled, and records
52
- * that resolution as a `verified[]` entry.
50
+ * `OrderService.cancel` survives every edit that does not rename it. Once the
51
+ * change settles, a resolution pass (`anchor-resolver.ts`) stamps `hash`,
52
+ * `resolved_at`, and `lines`; drift detection later re-resolves and compares.
53
+ *
54
+ * `hash` is prefixed with the algorithm so a future one can coexist with
55
+ * stored values. `lines` exists because the anchor keeps a hash, not the text:
56
+ * without the line count at hash time, a drift report could say "changed" but
57
+ * never how much.
58
+ *
59
+ * `repo` and `ref` say *which* code, for bases that describe more than one
60
+ * repository. They are author-owned identity: a resolver stamps `hash`,
61
+ * `lines`, and `resolved_at`, and never writes these two. Both are optional and
62
+ * independent of each other, so every anchor written before they existed stays
63
+ * valid.
53
64
  */
54
65
  declare const kbAnchorSchema: z.ZodObject<{
55
66
  file: z.ZodString;
56
67
  symbol: z.ZodOptional<z.ZodString>;
68
+ repo: z.ZodOptional<z.ZodString>;
69
+ ref: z.ZodOptional<z.ZodString>;
70
+ hash: z.ZodOptional<z.ZodString>;
71
+ resolved_at: z.ZodOptional<z.ZodString>;
72
+ lines: z.ZodOptional<z.ZodNumber>;
57
73
  }, z.core.$strict>;
58
74
  declare const KB_RECORD_TYPES: readonly ["fact", "requirement", "constraint", "decision", "assumption", "open-question", "risk", "contract", "flow", "affected-system", "test-obligation", "source-note"];
59
75
  type KbRecordType = (typeof KB_RECORD_TYPES)[number];
@@ -103,6 +119,11 @@ declare const kbRecordFrontmatterSchema: z.ZodObject<{
103
119
  strauss_anchors: z.ZodOptional<z.ZodArray<z.ZodObject<{
104
120
  file: z.ZodString;
105
121
  symbol: z.ZodOptional<z.ZodString>;
122
+ repo: z.ZodOptional<z.ZodString>;
123
+ ref: z.ZodOptional<z.ZodString>;
124
+ hash: z.ZodOptional<z.ZodString>;
125
+ resolved_at: z.ZodOptional<z.ZodString>;
126
+ lines: z.ZodOptional<z.ZodNumber>;
106
127
  }, z.core.$strict>>>;
107
128
  strauss_verify: z.ZodOptional<z.ZodArray<z.ZodString>>;
108
129
  strauss_status: z.ZodDefault<z.ZodEnum<{
@@ -145,6 +166,104 @@ type KbRecord = {
145
166
  body: string;
146
167
  };
147
168
 
169
+ /**
170
+ * Resolves symbolic anchors to text and detects drift against stored hashes.
171
+ *
172
+ * Resolvers are pure — source string in, range out; only the file readers touch
173
+ * disk. Any shape the lexer cannot handle confidently returns `null` and is
174
+ * reported `unresolved` rather than guessed at.
175
+ */
176
+ type ResolvedSymbol = {
177
+ text: string;
178
+ /** 1-based, inclusive. */
179
+ startLine: number;
180
+ endLine: number;
181
+ };
182
+ interface AnchorResolver {
183
+ name: string;
184
+ resolve(source: string, symbol: string): ResolvedSymbol | null;
185
+ }
186
+ /** Why an anchor could not be compared. Never an error — always a finding. */
187
+ type AnchorUnresolvedReason = "file-missing" | "symbol-not-found" | "outside-repo" | "file-too-large" | "file-unreadable"
188
+ /**
189
+ * The anchor names a repository this root is not. Expected rather than
190
+ * wrong — a base describing several repositories resolves against one tree
191
+ * at a time — so it is never a drift finding and never reaches a reader as
192
+ * a warning. Multi-root resolution is SAA-709.
193
+ */
194
+ | "foreign-repo";
195
+ /**
196
+ * v1 heuristic resolver. A dotted symbol like `OrderService.cancel` matches on
197
+ * its last segment, with the parent used to scope the search: a candidate
198
+ * counts only if the parent name appears in the fifty lines above it, when any
199
+ * candidate satisfies that at all.
200
+ *
201
+ * Deterministic, and ambiguity is not resolved by guessing — two lines of
202
+ * equally good shape mean the resolver cannot tell which one the record meant,
203
+ * and it says so by returning `null`.
204
+ */
205
+ declare const regexResolver: AnchorResolver;
206
+ /** CRLF normalized to LF before hashing, so checkout style cannot read as drift. */
207
+ declare function hashAnchorText(text: string): string;
208
+ /**
209
+ * An anchor without a symbol is about the whole file; with one, the resolver
210
+ * decides. Source newlines are normalized first so line counts and hashes
211
+ * agree with `hashAnchorText`.
212
+ *
213
+ * A file's last line is the last line with content: a trailing newline is a
214
+ * terminator, not an empty line, and counting it would have made every
215
+ * whole-file anchor's `lines` one larger than the file.
216
+ */
217
+ declare function resolveAnchor(source: string, anchor: KbAnchor, resolver?: AnchorResolver): ResolvedSymbol | null;
218
+ type KbAnchorDriftEntry = {
219
+ file: string;
220
+ symbol?: string;
221
+ state: "match" | "drifted" | "unresolved";
222
+ storedHash: string;
223
+ currentHash?: string;
224
+ /** `null` when the anchor recorded no `lines` — size unknown, not zero. */
225
+ diffSize: number | null;
226
+ reason?: AnchorUnresolvedReason;
227
+ };
228
+ /**
229
+ * An anchor's `file` must stay inside the repository root — a record points
230
+ * at code, not at arbitrary files on the machine reading it. Bundles are
231
+ * data, so a traversal or absolute path here is untrusted input, not a bug
232
+ * in the caller. Returns the resolved path, or `null` when it escapes.
233
+ *
234
+ * Lexical only, and therefore not the whole containment check: see
235
+ * `readAnchorFile`, which re-tests the real path after following symlinks.
236
+ */
237
+ declare function anchorFilePath(repoRoot: string, file: string): string | null;
238
+ type AnchorRead = {
239
+ ok: true;
240
+ source: string;
241
+ } | {
242
+ ok: false;
243
+ reason: AnchorUnresolvedReason;
244
+ };
245
+ type AnchorFileReader = (file: string) => Promise<AnchorRead>;
246
+ /**
247
+ * Re-resolves every hash-carrying anchor and compares against the stored hash.
248
+ *
249
+ * Anchors without a `hash` are skipped; one naming another repository is
250
+ * reported `foreign-repo` and never read, and `origin` is asked for once per
251
+ * run, only when some anchor declares a `repo`. A missing file or unresolvable
252
+ * symbol is a finding (`unresolved`), never a throw. Each distinct file is read
253
+ * once per run; all checked entries are returned per record, callers filter.
254
+ *
255
+ * Three phases: collect the checkable anchors, read their distinct files with
256
+ * a bounded pool, then resolve and hash in record order — so the output does
257
+ * not depend on which read finished first.
258
+ */
259
+ declare function detectAnchorDrift(records: KbRecord[], options?: {
260
+ repoRoot?: string;
261
+ resolver?: AnchorResolver;
262
+ concurrency?: number;
263
+ /** Test seam: replaces the disk reader. */
264
+ reader?: AnchorFileReader;
265
+ }): Promise<Map<string, KbAnchorDriftEntry[]>>;
266
+
148
267
  /**
149
268
  * Why a matched record must not be read as a plain answer.
150
269
  *
@@ -189,6 +308,18 @@ type KbWarning =
189
308
  staleAfter: string;
190
309
  } | {
191
310
  kind: "unverified";
311
+ }
312
+ /** The code this record anchors to has changed since its hash was recorded —
313
+ * the record may describe code that no longer exists in that form. */
314
+ | {
315
+ kind: "drifted";
316
+ anchors: {
317
+ file: string;
318
+ symbol?: string;
319
+ /** `null` when the anchor recorded no line count — size unknown. */
320
+ diffSize: number | null;
321
+ reason?: string;
322
+ }[];
192
323
  };
193
324
  type KbStanding = "current" | "superseded" | "rejected" | "unsettled" | "open";
194
325
  type KbAdjudicated = {
@@ -205,7 +336,7 @@ type KbAdjudicated = {
205
336
  * invisible: the caller cannot tell it missed anything, so a dropped record is
206
337
  * worse than a flagged one — it turns a knowable gap into an unknowable one.
207
338
  */
208
- declare function adjudicate(hits: KbRecord[], bundle: KbRecord[], now?: Date): KbAdjudicated[];
339
+ declare function adjudicate(hits: KbRecord[], bundle: KbRecord[], now?: Date, anchorDrift?: Map<string, KbAnchorDriftEntry[]>): KbAdjudicated[];
209
340
  /**
210
341
  * Walks a supersession chain to whatever currently stands in its place.
211
342
  *
@@ -311,6 +442,79 @@ type KbPackResult = {
311
442
  */
312
443
  declare function pack(bundle: KbRecord[], rootId: string, options?: KbPackOptions): KbPackResult;
313
444
 
445
+ /** One record as the catalog names it — no body, no description, one line. */
446
+ type KbCatalogEntry = {
447
+ conceptId: string;
448
+ type: string;
449
+ title: string | null;
450
+ standing: KbStanding;
451
+ /** Where the supersession chain ends. Empty when broken, cyclic, or n/a. */
452
+ supersededBy: string[];
453
+ /** `stale_after` is in the past. The one freshness signal a line can carry. */
454
+ stale: boolean;
455
+ };
456
+ type KbCatalogResult = {
457
+ entries: KbCatalogEntry[];
458
+ /** Every record the catalog names, filter applied. */
459
+ recordCount: number;
460
+ /**
461
+ * How many records hold each standing. Sums to `recordCount` — every record
462
+ * has exactly one standing, so the reader can see that nothing went missing.
463
+ */
464
+ standings: Record<KbStanding, number>;
465
+ /** Shorthand for `standings.current` — records that simply hold. */
466
+ currentCount: number;
467
+ /** Shorthand for `standings.superseded`. */
468
+ supersededCount: number;
469
+ /**
470
+ * Records whose `stale_after` has passed. A flag over the standings rather
471
+ * than one of them — a current record can be stale — so this deliberately
472
+ * does not participate in the sum.
473
+ */
474
+ staleCount: number;
475
+ };
476
+ /**
477
+ * The tier-one listing: every record named, nothing spelled out.
478
+ *
479
+ * `load` hands over bodies and `pack` hands over a neighbourhood; both have to
480
+ * decide what the reader can afford. The catalog is the rung below either — one
481
+ * line per record at roughly thirty tokens, so a base far past `load`'s gate
482
+ * still fits in a single call. What it buys is the ability to choose: a reader
483
+ * that can see every id, type, title and standing knows which record to `pack`
484
+ * and knows when no record covers the question at all, which is the conclusion
485
+ * a truncated read can never support.
486
+ *
487
+ * Standing travels on the line for the same reason it travels with every other
488
+ * result here: a title is a claim, and a superseded claim reads exactly like a
489
+ * live one. A superseded entry names its replacement, so the line the reader
490
+ * should follow instead is already in front of them.
491
+ *
492
+ * Unbounded, alone among the read paths. `load` and `pack` refuse past a
493
+ * ceiling because a partial body set reads as a complete one; a catalog has no
494
+ * such failure — it is the rung a caller lands on *because* something else
495
+ * refused, and a second refusal there would leave nowhere to go. The cost is
496
+ * linear and cheap: roughly thirty tokens a record, so a thousand-record base
497
+ * is about 30k and five thousand about 150k. Past that the `type` filter
498
+ * narrows it, and no ceiling is needed to make that available.
499
+ *
500
+ * Deterministic given a fixed `now`: no timestamp is emitted, and the ordering
501
+ * is total down to the concept id, so two catalogs of an unchanged base within
502
+ * one `stale_after` window are byte-identical and diff to nothing. The default
503
+ * clock is the wall clock, so a line can still flip to stale as a date passes —
504
+ * pass `now` when byte-equality has to hold across that boundary.
505
+ */
506
+ declare function catalog(bundle: KbRecord[], options?: {
507
+ type?: string;
508
+ now?: Date;
509
+ }): KbCatalogResult;
510
+ /**
511
+ * One entry, as one line.
512
+ *
513
+ * ` · `-separated rather than a table: a table pays for column alignment on
514
+ * every row, and nothing downstream parses these.
515
+ */
516
+ declare function renderCatalogLine(entry: KbCatalogEntry): string;
517
+
314
518
  declare const LOG_FILE = "log.jsonl";
315
519
  declare const kbLogEntrySchema: z.ZodObject<{
316
520
  at: z.ZodISODateTime;
@@ -391,6 +595,8 @@ type KbLoadResult = {
391
595
  recordCount: number;
392
596
  approxTokens: number;
393
597
  budgetTokens: number;
598
+ /** The refusal in words, naming the budget and what to call next. */
599
+ message: string;
394
600
  };
395
601
  type KbWriteInput = {
396
602
  type: string;
@@ -451,6 +657,14 @@ declare class KbStore {
451
657
  * timeouts.
452
658
  */
453
659
  setStatus(bundlePath: string, conceptId: string, status: KbRecordStatus, actor?: string): Promise<KbRecord>;
660
+ /**
661
+ * Replaces a record's anchors wholesale, preserving everything else.
662
+ *
663
+ * Wholesale rather than merged: the caller just resolved the anchors it is
664
+ * writing, so it holds the complete current set, and a merge would keep
665
+ * stale entries the resolution pass deliberately dropped.
666
+ */
667
+ updateAnchors(bundlePath: string, conceptId: string, anchors: KbAnchor[], actor?: string): Promise<KbRecord>;
454
668
  /**
455
669
  * Appends one `verified[]` event: who checked the record, when, and what the
456
670
  * check found. Append-only — prior events are history, and are spread into
@@ -488,8 +702,31 @@ declare class KbStore {
488
702
  query(bundlePath: string, text: string, options?: {
489
703
  type?: string;
490
704
  includeNonCurrent?: boolean;
705
+ repoRoot?: string;
491
706
  }): Promise<KbAdjudicated[]>;
492
707
  private rank;
708
+ /**
709
+ * Anchor drift over the records about to be handed back. Like the search
710
+ * index, this is an enrichment: a filesystem failure degrades to "no drift
711
+ * reported" rather than failing the read. Anchors without a stored hash are
712
+ * skipped inside `detectAnchorDrift`, so a base nobody has stamped pays no
713
+ * fs cost here. `repoRoot` defaults to the working directory — the CLI runs
714
+ * at the repo root, and the MCP server's cwd is the workspace.
715
+ *
716
+ * Public because `doctor` needs the same map with the same degradation: a
717
+ * sweep that failed to read the tree should report no drift, not fail.
718
+ *
719
+ * When no root was given and not one anchored file was found, the finding is
720
+ * discarded. A base read from somewhere other than the tree it describes
721
+ * misses every file at once, and that shape is far likelier to be a wrong
722
+ * default root than a repository where every anchored file was deleted on
723
+ * the same day. Reporting it would put a drift warning on every record in
724
+ * the base, which teaches a reader to ignore the warning — the one outcome
725
+ * worse than not having it. One file found anywhere makes the root
726
+ * plausible, and the misses become findings again; an explicit `repoRoot` is
727
+ * taken at its word either way.
728
+ */
729
+ detectDrift(records: KbRecord[], repoRoot?: string): Promise<Map<string, KbAnchorDriftEntry[]> | undefined>;
493
730
  /**
494
731
  * The whole base, adjudicated, when it is small enough to hand over.
495
732
  *
@@ -509,17 +746,30 @@ declare class KbStore {
509
746
  * is indistinguishable from a complete one, so a caller would answer "that
510
747
  * was never decided" from a slice it did not know was a slice.
511
748
  *
512
- * That refusal is the default guardrail. `all` bypasses it outright and
513
- * always hands back the whole bundle: an explicit, never-accidental escape
514
- * hatch for an operator who has the budget to spend, not a wider default.
749
+ * A token budget decides that, measured over what is actually handed back.
750
+ * The refusal names the estimate and the budget, because a caller told only
751
+ * "too big" cannot tell whether to narrow the type filter, raise the budget,
752
+ * or stop loading the base whole altogether. Past the budget the answer is
753
+ * the catalog and then a pack, which is what the refusal says.
754
+ *
755
+ * That refusal is the default guardrail. `all` bypasses the budget outright
756
+ * and always hands back the whole bundle: an explicit, never-accidental
757
+ * escape hatch for an operator who has the budget to spend, not a wider
758
+ * default.
515
759
  */
516
760
  load(bundlePath: string, options?: {
517
761
  budgetTokens?: number;
518
762
  type?: string;
519
763
  all?: boolean;
764
+ repoRoot?: string;
520
765
  }): Promise<KbLoadResult>;
521
766
  /** How a position was arrived at, as a timeline. See `trace.ts`. */
522
767
  trace(bundlePath: string, seedId: string, options?: KbTraceOptions): Promise<KbTraceStep[]>;
768
+ /** Every record named in one line each. See `catalog.ts`. */
769
+ catalog(bundlePath: string, options?: {
770
+ type?: string;
771
+ now?: Date;
772
+ }): Promise<KbCatalogResult>;
523
773
  /** A bounded neighbourhood around one record. See `pack.ts`. */
524
774
  pack(bundlePath: string, rootId: string, options?: KbPackOptions): Promise<KbPackResult>;
525
775
  /**
@@ -641,6 +891,7 @@ declare enum Fault {
641
891
  declare enum ErrorTypes {
642
892
  KbRecordAlreadyExists = "KbRecordAlreadyExists",
643
893
  KbInvalidConceptId = "KbInvalidConceptId",
894
+ KbMissingFlagValue = "KbMissingFlagValue",
644
895
  KbPackBudgetExceeded = "KbPackBudgetExceeded",
645
896
  KbRecordNotFound = "KbRecordNotFound",
646
897
  KbSelfVerification = "KbSelfVerification",
@@ -707,6 +958,18 @@ declare class KbPackBudgetExceededError extends BaseError {
707
958
  readonly excluded: string[];
708
959
  constructor(recordCount: number, approxTokens: number, budgetTokens: number, excluded: string[]);
709
960
  }
961
+ /**
962
+ * A flag that takes a value, given none.
963
+ *
964
+ * `strauss-kb load --budget` used to read the next argv entry, find
965
+ * nothing, and quietly fall back to the default — so a caller who meant to
966
+ * raise a ceiling got the ceiling they were trying to move, and a typo looked
967
+ * exactly like success. Refusing is the only way that stays visible.
968
+ */
969
+ declare class KbMissingFlagValueError extends BaseError {
970
+ readonly flag: string;
971
+ constructor(flag: string);
972
+ }
710
973
  declare class KbInvalidConceptIdError extends BaseError {
711
974
  constructor(message: string, details: Record<string, string>);
712
975
  }
@@ -742,6 +1005,11 @@ declare const composeInputSchema: z.ZodObject<{
742
1005
  anchors: z.ZodOptional<z.ZodArray<z.ZodObject<{
743
1006
  file: z.ZodString;
744
1007
  symbol: z.ZodOptional<z.ZodString>;
1008
+ repo: z.ZodOptional<z.ZodString>;
1009
+ ref: z.ZodOptional<z.ZodString>;
1010
+ hash: z.ZodOptional<z.ZodString>;
1011
+ resolved_at: z.ZodOptional<z.ZodString>;
1012
+ lines: z.ZodOptional<z.ZodNumber>;
745
1013
  }, z.core.$strict>>>;
746
1014
  sources: z.ZodOptional<z.ZodArray<z.ZodObject<{
747
1015
  id: z.ZodString;
@@ -1181,9 +1449,10 @@ declare function validateBundle(records: KbRecord[]): KbValidationProblem[];
1181
1449
  * decay silently, because a stale record reads exactly like a live one and a
1182
1450
  * question nobody answered reads exactly like one nobody asked.
1183
1451
  *
1184
- * Grouped and counted rather than merged into one list: the seven checks are
1185
- * seven different repairs — re-verify, re-date, answer, link, or supersede
1186
- * and a flat list of "problems" would leave the reader sorting them again.
1452
+ * Grouped and counted rather than merged into one list: the eight checks are
1453
+ * eight different repairs — re-verify, re-date, answer, link, supersede, or
1454
+ * re-anchor — and a flat list of "problems" would leave the reader sorting
1455
+ * them again.
1187
1456
  *
1188
1457
  * Every group is emitted even when empty. A check that found nothing and a
1189
1458
  * check that never ran look identical in a report that only lists findings,
@@ -1192,7 +1461,7 @@ declare function validateBundle(records: KbRecord[]): KbValidationProblem[];
1192
1461
  declare const DEFAULT_EXPIRING_DAYS = 30;
1193
1462
  declare const DEFAULT_UNVERIFIED_DAYS = 90;
1194
1463
  declare const DEFAULT_AGING_DAYS = 90;
1195
- declare const KB_DOCTOR_CHECKS: readonly ["expired", "expiring", "unverified", "aging", "orphaned", "broken-supersession", "superseded-but-cited"];
1464
+ declare const KB_DOCTOR_CHECKS: readonly ["expired", "expiring", "unverified", "aging", "orphaned", "broken-supersession", "superseded-but-cited", "drifted"];
1196
1465
  type KbDoctorCheck = (typeof KB_DOCTOR_CHECKS)[number];
1197
1466
  type KbDoctorFinding = {
1198
1467
  conceptId: string;
@@ -1217,7 +1486,7 @@ type KbDoctorReport = {
1217
1486
  recordCount: number;
1218
1487
  thresholds: KbDoctorThresholds;
1219
1488
  counts: Record<KbDoctorCheck, number>;
1220
- /** All seven, in `KB_DOCTOR_CHECKS` order, empty ones included. */
1489
+ /** All eight, in `KB_DOCTOR_CHECKS` order, empty ones included. */
1221
1490
  groups: KbDoctorGroup[];
1222
1491
  findingCount: number;
1223
1492
  healthy: boolean;
@@ -1230,6 +1499,13 @@ type KbDoctorOptions = {
1230
1499
  /** How long `open` or `proposed` may stand before `aging` reports it. */
1231
1500
  agingDays?: number;
1232
1501
  now?: Date;
1502
+ /**
1503
+ * Anchor drift, precomputed by the caller. `doctor` stays pure and sync for
1504
+ * the same reason `adjudicate` does — the filesystem work of re-resolving
1505
+ * anchors belongs to `detectAnchorDrift`, and a sweep with no map simply
1506
+ * reports the `drifted` check as clean rather than half-running it.
1507
+ */
1508
+ anchorDrift?: Map<string, KbAnchorDriftEntry[]>;
1233
1509
  };
1234
1510
  declare function doctor(bundle: KbRecord[], options?: KbDoctorOptions): KbDoctorReport;
1235
1511
 
@@ -1277,6 +1553,11 @@ declare const decisionInputSchema: z.ZodObject<{
1277
1553
  anchors: z.ZodOptional<z.ZodArray<z.ZodObject<{
1278
1554
  file: z.ZodString;
1279
1555
  symbol: z.ZodOptional<z.ZodString>;
1556
+ repo: z.ZodOptional<z.ZodString>;
1557
+ ref: z.ZodOptional<z.ZodString>;
1558
+ hash: z.ZodOptional<z.ZodString>;
1559
+ resolved_at: z.ZodOptional<z.ZodString>;
1560
+ lines: z.ZodOptional<z.ZodNumber>;
1280
1561
  }, z.core.$strict>>>;
1281
1562
  verify: z.ZodOptional<z.ZodArray<z.ZodString>>;
1282
1563
  relatedConceptIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -1511,4 +1792,4 @@ declare function parseMarkdownWithFrontmatter<S extends z.ZodType>(text: string,
1511
1792
  frontmatter: ReturnType<S["safeParse"]>;
1512
1793
  };
1513
1794
 
1514
- export { BaseError, CONTEXT_BEGIN, CONTEXT_END, CONTEXT_PROFILES, type ComposeInput, type ComposedRecord, DECISION_TYPE, DEFAULT_AGING_DAYS, DEFAULT_EXPIRING_DAYS, DEFAULT_LOAD_BUDGET, DEFAULT_PACK_HOPS, DEFAULT_PACK_MAX_NODES, DEFAULT_UNVERIFIED_DAYS, type DecisionInput, type DiffFile, type DiffHunk, type DiffMatch, type ErrorDetails, type ErrorProps, ErrorTypes, Fault, INDEX_FILE, KB_COMMANDS, KB_COMMANDS_BY_NAME, KB_CONCEPT_ID_PATTERN, KB_CONFIDENCES, KB_DIR, KB_DOCTOR_CHECKS, KB_EDGE_KINDS, KB_MATERIALITIES, KB_RECORD_STATUSES, KB_RECORD_TYPES, KB_SLUG_PATTERN, type KbActorStamp, type KbAdjudicated, type KbAnchor, KbBaseFrozenError, type KbCommand, type KbCommandContext, type KbContextBudgets, type KbContextOptions, type KbContextResult, type KbDoctorCheck, type KbDoctorFinding, type KbDoctorGroup, type KbDoctorOptions, type KbDoctorReport, type KbDoctorThresholds, type KbEdgeKind, KbInvalidConceptIdError, type KbLoadResult, type KbLogEntry, type KbLogReadResult, type KbLogger, type KbMergedPin, type KbMergedPins, type KbNeighbour, KbPackBudgetExceededError, type KbPackOptions, type KbPackResult, type KbPackedRecord, type KbPin, type KbPinLayer, type KbPinOptions, type KbPinResult, type KbPinStatus, KbPinsMalformedError, type KbPinsManifest, type KbRecord, KbRecordAlreadyExistsError, type KbRecordFrontmatter, KbRecordNotFoundError, type KbRecordStatus, type KbRecordType, type KbRecordTypeSpec, type KbSearchLogger, KbSelfVerificationError, type KbSource, type KbStanding, KbStore, type KbSupersededStub, type KbSyncResult, type KbTraceEdge, type KbTraceOptions, type KbTraceStep, type KbValidationProblem, type KbVerifiedEvent, type KbWarning, KbWriteConflictError, type KbWriteInput, LOG_FILE, type MatchOptions, NO_DECISION_SLUG, PINS_FILE, PINS_LOCAL_FILE, PIN_LAYERS, type QmdModule, RECORD_TYPES, SEARCH_INDEX_FILE, type SearchHit, type SearchOptions, type SymbolRange, TRACE_EDGES, adjudicate, assertBaseNotFrozen, buildContext, composeDecisionRecord, composeInputSchema, composeNoDecisionRecord, composeRecord, contextProfileBudgets, createKbMcpServer, decisionInputSchema, doctor, edgeNeighbours, indexIsStale, isKbRecordType, isNoDecisionRecord, kbActorStampSchema, kbAnchorSchema, kbConceptIdSchema, kbJsonSchemas, kbLogEntrySchema, kbRecordFrontmatterSchema, kbSourceSchema, kbVerifiedEventSchema, listPins, loadQmd, matchToDiff, mergedContextBudgets, neighbours, pack, parseLog, parseMarkdownWithFrontmatter, pinBase, readMergedPins, readPinsLayer, renderIndex, renderIndexLine, renderLogEntry, resolveHeads, resolveHits, resolvePinPath, runKbCli, runKbMcpServer, searchBase, selectDecisions, splitMarkdownFrontmatter, stringifyMarkdownWithFrontmatter, syncInstructions, toHookJson, trace, unpinBase, validateBundle };
1795
+ export { type AnchorResolver, BaseError, CONTEXT_BEGIN, CONTEXT_END, CONTEXT_PROFILES, type ComposeInput, type ComposedRecord, DECISION_TYPE, DEFAULT_AGING_DAYS, DEFAULT_EXPIRING_DAYS, DEFAULT_LOAD_BUDGET, DEFAULT_PACK_HOPS, DEFAULT_PACK_MAX_NODES, DEFAULT_UNVERIFIED_DAYS, type DecisionInput, type DiffFile, type DiffHunk, type DiffMatch, type ErrorDetails, type ErrorProps, ErrorTypes, Fault, INDEX_FILE, KB_COMMANDS, KB_COMMANDS_BY_NAME, KB_CONCEPT_ID_PATTERN, KB_CONFIDENCES, KB_DIR, KB_DOCTOR_CHECKS, KB_EDGE_KINDS, KB_MATERIALITIES, KB_RECORD_STATUSES, KB_RECORD_TYPES, KB_SLUG_PATTERN, type KbActorStamp, type KbAdjudicated, type KbAnchor, type KbAnchorDriftEntry, KbBaseFrozenError, type KbCatalogEntry, type KbCatalogResult, type KbCommand, type KbCommandContext, type KbContextBudgets, type KbContextOptions, type KbContextResult, type KbDoctorCheck, type KbDoctorFinding, type KbDoctorGroup, type KbDoctorOptions, type KbDoctorReport, type KbDoctorThresholds, type KbEdgeKind, KbInvalidConceptIdError, type KbLoadResult, type KbLogEntry, type KbLogReadResult, type KbLogger, type KbMergedPin, type KbMergedPins, KbMissingFlagValueError, type KbNeighbour, KbPackBudgetExceededError, type KbPackOptions, type KbPackResult, type KbPackedRecord, type KbPin, type KbPinLayer, type KbPinOptions, type KbPinResult, type KbPinStatus, KbPinsMalformedError, type KbPinsManifest, type KbRecord, KbRecordAlreadyExistsError, type KbRecordFrontmatter, KbRecordNotFoundError, type KbRecordStatus, type KbRecordType, type KbRecordTypeSpec, type KbSearchLogger, KbSelfVerificationError, type KbSource, type KbStanding, KbStore, type KbSupersededStub, type KbSyncResult, type KbTraceEdge, type KbTraceOptions, type KbTraceStep, type KbValidationProblem, type KbVerifiedEvent, type KbWarning, KbWriteConflictError, type KbWriteInput, LOG_FILE, type MatchOptions, NO_DECISION_SLUG, PINS_FILE, PINS_LOCAL_FILE, PIN_LAYERS, type QmdModule, RECORD_TYPES, type ResolvedSymbol, SEARCH_INDEX_FILE, type SearchHit, type SearchOptions, type SymbolRange, TRACE_EDGES, adjudicate, anchorFilePath, assertBaseNotFrozen, buildContext, catalog, composeDecisionRecord, composeInputSchema, composeNoDecisionRecord, composeRecord, contextProfileBudgets, createKbMcpServer, decisionInputSchema, detectAnchorDrift, doctor, edgeNeighbours, hashAnchorText, indexIsStale, isKbRecordType, isNoDecisionRecord, kbActorStampSchema, kbAnchorSchema, kbConceptIdSchema, kbJsonSchemas, kbLogEntrySchema, kbRecordFrontmatterSchema, kbSourceSchema, kbVerifiedEventSchema, listPins, loadQmd, matchToDiff, mergedContextBudgets, neighbours, pack, parseLog, parseMarkdownWithFrontmatter, pinBase, readMergedPins, readPinsLayer, regexResolver, renderCatalogLine, renderIndex, renderIndexLine, renderLogEntry, resolveAnchor, resolveHeads, resolveHits, resolvePinPath, runKbCli, runKbMcpServer, searchBase, selectDecisions, splitMarkdownFrontmatter, stringifyMarkdownWithFrontmatter, syncInstructions, toHookJson, trace, unpinBase, validateBundle };
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  runKbCli
3
- } from "./chunk-KVEEISYQ.js";
3
+ } from "./chunk-I3WW4F6X.js";
4
4
  import {
5
5
  createKbMcpServer,
6
6
  runKbMcpServer
7
- } from "./chunk-MWWDD23L.js";
7
+ } from "./chunk-CWWXMD35.js";
8
8
  import {
9
9
  BaseError,
10
10
  CONTEXT_BEGIN,
@@ -33,6 +33,7 @@ import {
33
33
  KB_SLUG_PATTERN,
34
34
  KbBaseFrozenError,
35
35
  KbInvalidConceptIdError,
36
+ KbMissingFlagValueError,
36
37
  KbPackBudgetExceededError,
37
38
  KbPinsMalformedError,
38
39
  KbRecordAlreadyExistsError,
@@ -49,16 +50,20 @@ import {
49
50
  SEARCH_INDEX_FILE,
50
51
  TRACE_EDGES,
51
52
  adjudicate,
53
+ anchorFilePath,
52
54
  assertBaseNotFrozen,
53
55
  buildContext,
56
+ catalog,
54
57
  composeDecisionRecord,
55
58
  composeInputSchema,
56
59
  composeNoDecisionRecord,
57
60
  composeRecord,
58
61
  contextProfileBudgets,
59
62
  decisionInputSchema,
63
+ detectAnchorDrift,
60
64
  doctor,
61
65
  edgeNeighbours,
66
+ hashAnchorText,
62
67
  indexIsStale,
63
68
  isKbRecordType,
64
69
  isNoDecisionRecord,
@@ -80,9 +85,12 @@ import {
80
85
  pinBase,
81
86
  readMergedPins,
82
87
  readPinsLayer,
88
+ regexResolver,
89
+ renderCatalogLine,
83
90
  renderIndex,
84
91
  renderIndexLine,
85
92
  renderLogEntry,
93
+ resolveAnchor,
86
94
  resolveHeads,
87
95
  resolveHits,
88
96
  resolvePinPath,
@@ -95,7 +103,7 @@ import {
95
103
  trace,
96
104
  unpinBase,
97
105
  validateBundle
98
- } from "./chunk-OFDWRMY6.js";
106
+ } from "./chunk-OVRQCQ6P.js";
99
107
 
100
108
  // src/match-diff.ts
101
109
  function matchToDiff(files, records, options = {}) {
@@ -204,6 +212,7 @@ export {
204
212
  KB_SLUG_PATTERN,
205
213
  KbBaseFrozenError,
206
214
  KbInvalidConceptIdError,
215
+ KbMissingFlagValueError,
207
216
  KbPackBudgetExceededError,
208
217
  KbPinsMalformedError,
209
218
  KbRecordAlreadyExistsError,
@@ -220,8 +229,10 @@ export {
220
229
  SEARCH_INDEX_FILE,
221
230
  TRACE_EDGES,
222
231
  adjudicate,
232
+ anchorFilePath,
223
233
  assertBaseNotFrozen,
224
234
  buildContext,
235
+ catalog,
225
236
  composeDecisionRecord,
226
237
  composeInputSchema,
227
238
  composeNoDecisionRecord,
@@ -229,8 +240,10 @@ export {
229
240
  contextProfileBudgets,
230
241
  createKbMcpServer,
231
242
  decisionInputSchema,
243
+ detectAnchorDrift,
232
244
  doctor,
233
245
  edgeNeighbours,
246
+ hashAnchorText,
234
247
  indexIsStale,
235
248
  isKbRecordType,
236
249
  isNoDecisionRecord,
@@ -253,9 +266,12 @@ export {
253
266
  pinBase,
254
267
  readMergedPins,
255
268
  readPinsLayer,
269
+ regexResolver,
270
+ renderCatalogLine,
256
271
  renderIndex,
257
272
  renderIndexLine,
258
273
  renderLogEntry,
274
+ resolveAnchor,
259
275
  resolveHeads,
260
276
  resolveHits,
261
277
  resolvePinPath,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/match-diff.ts"],"sourcesContent":["import { adjudicate, type KbAdjudicated } from \"./adjudicate.js\";\nimport type { KbAnchor, KbRecord } from \"./kb-record.schema.js\";\n\n/**\n * Which records apply to which part of a change.\n *\n * Takes a structural description of a diff rather than a patch, so this package\n * carries no diff parser: callers already have one, and a knowledge base has no\n * business preferring a particular flavour of unified diff.\n *\n * Deterministic on purpose. Every step here is mechanical — the one judgment,\n * whether a matched record is worth showing a reviewer, is deliberately absent.\n * A model placed here would sit between the reviewer and their diff on every\n * review, to answer a question nobody has yet shown needs asking.\n *\n * Distinct from `load()`, which hands a reader the whole base. That answers\n * \"does anything address this question\"; this answers \"what is attached to this\n * code\", and an anchor is the author's own statement rather than an inference\n * from one. A reader guessing which record relates to a hunk would be guessing\n * at something already written down — and a diff has dozens of hunks, which is\n * dozens of reader calls against microseconds of matching. Where they compose:\n * this narrows a hunk to a few records, and a reader asked to explain them gets\n * those, not the base.\n */\nexport type DiffHunk = {\n /** 1-based, inclusive, in the file's post-change line numbering. */\n startLine: number;\n endLine: number;\n};\n\nexport type DiffFile = {\n /** Repo-relative, matching how anchors are written. */\n filePath: string;\n hunks: DiffHunk[];\n};\n\n/**\n * A symbol resolved to lines. Supplied by whatever the caller uses to index\n * symbols; absence is tolerated — see `place()`.\n */\nexport type SymbolRange = {\n file: string;\n symbol: string;\n startLine: number;\n endLine: number;\n};\n\nexport type DiffMatch = {\n filePath: string;\n hunk: DiffHunk;\n /** Current records first — what still holds should be read before what does not. */\n records: KbAdjudicated[];\n /**\n * `symbol` when every record here was placed by a resolved symbol range,\n * `file` when at least one fell back to the whole file. Reported rather than\n * hidden: a caller showing a file-level match as though it were pinned to\n * these lines is claiming a precision it does not have.\n */\n precision: \"symbol\" | \"file\";\n};\n\nexport type MatchOptions = {\n /** Without these, symbol anchors degrade to file level rather than vanishing. */\n symbolRanges?: SymbolRange[];\n now?: Date;\n};\n\nexport function matchToDiff(\n files: DiffFile[],\n records: KbRecord[],\n options: MatchOptions = {},\n): DiffMatch[] {\n const ranges = indexRanges(options.symbolRanges ?? []);\n const anchored = records.filter(\n (record) => (record.frontmatter.strauss_anchors ?? []).length > 0,\n );\n const matches: DiffMatch[] = [];\n\n for (const file of files) {\n const candidates = anchored\n .map((record) => ({\n record,\n anchors: (record.frontmatter.strauss_anchors ?? []).filter(\n (anchor) => normalize(anchor.file) === normalize(file.filePath),\n ),\n }))\n .filter(({ anchors }) => anchors.length > 0);\n if (!candidates.length) continue;\n\n for (const hunk of file.hunks) {\n const hits: KbRecord[] = [];\n let precision: DiffMatch[\"precision\"] = \"symbol\";\n\n for (const { record, anchors } of candidates) {\n const placement = place(anchors, file.filePath, hunk, ranges);\n if (placement === \"miss\") continue;\n if (placement === \"file\") precision = \"file\";\n hits.push(record);\n }\n\n if (!hits.length) continue;\n matches.push({\n filePath: file.filePath,\n hunk,\n records: order(adjudicate(hits, records, options.now)),\n precision,\n });\n }\n }\n\n return matches;\n}\n\n/**\n * Whether any of a record's anchors puts it on this hunk.\n *\n * An anchor naming only a file is about the whole file, so it lands on every\n * hunk in it. One naming a symbol lands only where that symbol's lines overlap\n * — unless nothing resolved the symbol, in which case it falls back to the file\n * rather than disappearing. A record silently absent because a resolver was\n * unavailable is worse than one shown imprecisely and labelled as such.\n */\nfunction place(\n anchors: KbAnchor[],\n filePath: string,\n hunk: DiffHunk,\n ranges: Map<string, SymbolRange[]>,\n): \"symbol\" | \"file\" | \"miss\" {\n let fallback: \"file\" | \"miss\" = \"miss\";\n\n for (const anchor of anchors) {\n if (!anchor.symbol) return \"file\";\n\n const resolved = ranges.get(key(filePath, anchor.symbol));\n if (!resolved?.length) {\n fallback = \"file\";\n continue;\n }\n if (resolved.some((range) => overlaps(range, hunk))) return \"symbol\";\n }\n\n return fallback;\n}\n\nfunction overlaps(range: SymbolRange, hunk: DiffHunk): boolean {\n return range.startLine <= hunk.endLine && hunk.startLine <= range.endLine;\n}\n\n/** Current before superseded, then oldest first, so an arc reads in order. */\nfunction order(records: KbAdjudicated[]): KbAdjudicated[] {\n const rank: Record<string, number> = {\n current: 0,\n unsettled: 1,\n open: 2,\n superseded: 3,\n rejected: 4,\n };\n return [...records].sort(\n (left, right) =>\n (rank[left.standing] ?? 9) - (rank[right.standing] ?? 9) ||\n (left.record.frontmatter.generated?.at ?? \"\").localeCompare(\n right.record.frontmatter.generated?.at ?? \"\",\n ),\n );\n}\n\nfunction indexRanges(ranges: SymbolRange[]): Map<string, SymbolRange[]> {\n const byKey = new Map<string, SymbolRange[]>();\n for (const range of ranges) {\n const id = key(range.file, range.symbol);\n byKey.set(id, [...(byKey.get(id) ?? []), range]);\n }\n return byKey;\n}\n\nfunction key(file: string, symbol: string): string {\n return `${normalize(file)}#${symbol}`;\n}\n\n/** Anchors are written by hand often enough that `./` shows up. */\nfunction normalize(path: string): string {\n return path.replace(/^\\.\\//, \"\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmEO,SAAS,YACd,OACA,SACA,UAAwB,CAAC,GACZ;AACb,QAAM,SAAS,YAAY,QAAQ,gBAAgB,CAAC,CAAC;AACrD,QAAM,WAAW,QAAQ;AAAA,IACvB,CAAC,YAAY,OAAO,YAAY,mBAAmB,CAAC,GAAG,SAAS;AAAA,EAClE;AACA,QAAM,UAAuB,CAAC;AAE9B,aAAW,QAAQ,OAAO;AACxB,UAAM,aAAa,SAChB,IAAI,CAAC,YAAY;AAAA,MAChB;AAAA,MACA,UAAU,OAAO,YAAY,mBAAmB,CAAC,GAAG;AAAA,QAClD,CAAC,WAAW,UAAU,OAAO,IAAI,MAAM,UAAU,KAAK,QAAQ;AAAA,MAChE;AAAA,IACF,EAAE,EACD,OAAO,CAAC,EAAE,QAAQ,MAAM,QAAQ,SAAS,CAAC;AAC7C,QAAI,CAAC,WAAW,OAAQ;AAExB,eAAW,QAAQ,KAAK,OAAO;AAC7B,YAAM,OAAmB,CAAC;AAC1B,UAAI,YAAoC;AAExC,iBAAW,EAAE,QAAQ,QAAQ,KAAK,YAAY;AAC5C,cAAM,YAAY,MAAM,SAAS,KAAK,UAAU,MAAM,MAAM;AAC5D,YAAI,cAAc,OAAQ;AAC1B,YAAI,cAAc,OAAQ,aAAY;AACtC,aAAK,KAAK,MAAM;AAAA,MAClB;AAEA,UAAI,CAAC,KAAK,OAAQ;AAClB,cAAQ,KAAK;AAAA,QACX,UAAU,KAAK;AAAA,QACf;AAAA,QACA,SAAS,MAAM,WAAW,MAAM,SAAS,QAAQ,GAAG,CAAC;AAAA,QACrD;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AACT;AAWA,SAAS,MACP,SACA,UACA,MACA,QAC4B;AAC5B,MAAI,WAA4B;AAEhC,aAAW,UAAU,SAAS;AAC5B,QAAI,CAAC,OAAO,OAAQ,QAAO;AAE3B,UAAM,WAAW,OAAO,IAAI,IAAI,UAAU,OAAO,MAAM,CAAC;AACxD,QAAI,CAAC,UAAU,QAAQ;AACrB,iBAAW;AACX;AAAA,IACF;AACA,QAAI,SAAS,KAAK,CAAC,UAAU,SAAS,OAAO,IAAI,CAAC,EAAG,QAAO;AAAA,EAC9D;AAEA,SAAO;AACT;AAEA,SAAS,SAAS,OAAoB,MAAyB;AAC7D,SAAO,MAAM,aAAa,KAAK,WAAW,KAAK,aAAa,MAAM;AACpE;AAGA,SAAS,MAAM,SAA2C;AACxD,QAAM,OAA+B;AAAA,IACnC,SAAS;AAAA,IACT,WAAW;AAAA,IACX,MAAM;AAAA,IACN,YAAY;AAAA,IACZ,UAAU;AAAA,EACZ;AACA,SAAO,CAAC,GAAG,OAAO,EAAE;AAAA,IAClB,CAAC,MAAM,WACJ,KAAK,KAAK,QAAQ,KAAK,MAAM,KAAK,MAAM,QAAQ,KAAK,OACrD,KAAK,OAAO,YAAY,WAAW,MAAM,IAAI;AAAA,MAC5C,MAAM,OAAO,YAAY,WAAW,MAAM;AAAA,IAC5C;AAAA,EACJ;AACF;AAEA,SAAS,YAAY,QAAmD;AACtE,QAAM,QAAQ,oBAAI,IAA2B;AAC7C,aAAW,SAAS,QAAQ;AAC1B,UAAM,KAAK,IAAI,MAAM,MAAM,MAAM,MAAM;AACvC,UAAM,IAAI,IAAI,CAAC,GAAI,MAAM,IAAI,EAAE,KAAK,CAAC,GAAI,KAAK,CAAC;AAAA,EACjD;AACA,SAAO;AACT;AAEA,SAAS,IAAI,MAAc,QAAwB;AACjD,SAAO,GAAG,UAAU,IAAI,CAAC,IAAI,MAAM;AACrC;AAGA,SAAS,UAAU,MAAsB;AACvC,SAAO,KAAK,QAAQ,SAAS,EAAE;AACjC;","names":[]}
1
+ {"version":3,"sources":["../src/match-diff.ts"],"sourcesContent":["import { adjudicate, type KbAdjudicated } from \"./adjudicate.js\";\nimport type { KbAnchor, KbRecord } from \"./kb-record.schema.js\";\n\n/**\n * Which records apply to which part of a change.\n *\n * Takes a structural description of a diff rather than a patch, so this package\n * carries no diff parser: callers already have one, and a knowledge base has no\n * business preferring a particular flavour of unified diff.\n *\n * Deterministic on purpose. Every step here is mechanical — the one judgment,\n * whether a matched record is worth showing a reviewer, is deliberately absent.\n * A model placed here would sit between the reviewer and their diff on every\n * review, to answer a question nobody has yet shown needs asking.\n *\n * Distinct from `load()`, which hands a reader the whole base. That answers\n * \"does anything address this question\"; this answers \"what is attached to this\n * code\", and an anchor is the author's own statement rather than an inference\n * from one. A reader guessing which record relates to a hunk would be guessing\n * at something already written down — and a diff has dozens of hunks, which is\n * dozens of reader calls against microseconds of matching. Where they compose:\n * this narrows a hunk to a few records, and a reader asked to explain them gets\n * those, not the base.\n */\nexport type DiffHunk = {\n /** 1-based, inclusive, in the file's post-change line numbering. */\n startLine: number;\n endLine: number;\n};\n\nexport type DiffFile = {\n /** Repo-relative, matching how anchors are written. */\n filePath: string;\n hunks: DiffHunk[];\n};\n\n/**\n * A symbol resolved to lines. Supplied by whatever the caller uses to index\n * symbols; absence is tolerated — see `place()`.\n */\nexport type SymbolRange = {\n file: string;\n symbol: string;\n startLine: number;\n endLine: number;\n};\n\nexport type DiffMatch = {\n filePath: string;\n hunk: DiffHunk;\n /** Current records first — what still holds should be read before what does not. */\n records: KbAdjudicated[];\n /**\n * `symbol` when every record here was placed by a resolved symbol range,\n * `file` when at least one fell back to the whole file. Reported rather than\n * hidden: a caller showing a file-level match as though it were pinned to\n * these lines is claiming a precision it does not have.\n */\n precision: \"symbol\" | \"file\";\n};\n\nexport type MatchOptions = {\n /** Without these, symbol anchors degrade to file level rather than vanishing. */\n symbolRanges?: SymbolRange[];\n now?: Date;\n};\n\nexport function matchToDiff(\n files: DiffFile[],\n records: KbRecord[],\n options: MatchOptions = {},\n): DiffMatch[] {\n const ranges = indexRanges(options.symbolRanges ?? []);\n const anchored = records.filter(\n (record) => (record.frontmatter.strauss_anchors ?? []).length > 0,\n );\n const matches: DiffMatch[] = [];\n\n for (const file of files) {\n const candidates = anchored\n .map((record) => ({\n record,\n anchors: (record.frontmatter.strauss_anchors ?? []).filter(\n (anchor) => normalize(anchor.file) === normalize(file.filePath),\n ),\n }))\n .filter(({ anchors }) => anchors.length > 0);\n if (!candidates.length) continue;\n\n for (const hunk of file.hunks) {\n const hits: KbRecord[] = [];\n let precision: DiffMatch[\"precision\"] = \"symbol\";\n\n for (const { record, anchors } of candidates) {\n const placement = place(anchors, file.filePath, hunk, ranges);\n if (placement === \"miss\") continue;\n if (placement === \"file\") precision = \"file\";\n hits.push(record);\n }\n\n if (!hits.length) continue;\n matches.push({\n filePath: file.filePath,\n hunk,\n records: order(adjudicate(hits, records, options.now)),\n precision,\n });\n }\n }\n\n return matches;\n}\n\n/**\n * Whether any of a record's anchors puts it on this hunk.\n *\n * An anchor naming only a file is about the whole file, so it lands on every\n * hunk in it. One naming a symbol lands only where that symbol's lines overlap\n * — unless nothing resolved the symbol, in which case it falls back to the file\n * rather than disappearing. A record silently absent because a resolver was\n * unavailable is worse than one shown imprecisely and labelled as such.\n */\nfunction place(\n anchors: KbAnchor[],\n filePath: string,\n hunk: DiffHunk,\n ranges: Map<string, SymbolRange[]>,\n): \"symbol\" | \"file\" | \"miss\" {\n let fallback: \"file\" | \"miss\" = \"miss\";\n\n for (const anchor of anchors) {\n if (!anchor.symbol) return \"file\";\n\n const resolved = ranges.get(key(filePath, anchor.symbol));\n if (!resolved?.length) {\n fallback = \"file\";\n continue;\n }\n if (resolved.some((range) => overlaps(range, hunk))) return \"symbol\";\n }\n\n return fallback;\n}\n\nfunction overlaps(range: SymbolRange, hunk: DiffHunk): boolean {\n return range.startLine <= hunk.endLine && hunk.startLine <= range.endLine;\n}\n\n/** Current before superseded, then oldest first, so an arc reads in order. */\nfunction order(records: KbAdjudicated[]): KbAdjudicated[] {\n const rank: Record<string, number> = {\n current: 0,\n unsettled: 1,\n open: 2,\n superseded: 3,\n rejected: 4,\n };\n return [...records].sort(\n (left, right) =>\n (rank[left.standing] ?? 9) - (rank[right.standing] ?? 9) ||\n (left.record.frontmatter.generated?.at ?? \"\").localeCompare(\n right.record.frontmatter.generated?.at ?? \"\",\n ),\n );\n}\n\nfunction indexRanges(ranges: SymbolRange[]): Map<string, SymbolRange[]> {\n const byKey = new Map<string, SymbolRange[]>();\n for (const range of ranges) {\n const id = key(range.file, range.symbol);\n byKey.set(id, [...(byKey.get(id) ?? []), range]);\n }\n return byKey;\n}\n\nfunction key(file: string, symbol: string): string {\n return `${normalize(file)}#${symbol}`;\n}\n\n/** Anchors are written by hand often enough that `./` shows up. */\nfunction normalize(path: string): string {\n return path.replace(/^\\.\\//, \"\");\n}\n"],"mappings":"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","names":[]}