@saasontools/strauss-kb 0.1.12 → 0.1.13

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
@@ -71,6 +71,22 @@ declare const kbAnchorSchema: z.ZodObject<{
71
71
  resolved_at: z.ZodOptional<z.ZodString>;
72
72
  lines: z.ZodOptional<z.ZodNumber>;
73
73
  }, z.core.$strict>;
74
+ /**
75
+ * One typed causal edge, as the frontmatter stores it.
76
+ *
77
+ * Read-side, and therefore tolerant: `rel` is a plain string here even though
78
+ * the vocabulary is closed, for the same reason `type` is. Rejecting an unknown
79
+ * rel at parse time would make the file vanish from `list()`, and a bundle
80
+ * cannot report a defect in a record it refuses to load. `kb_validate` turns an
81
+ * unknown rel into an error; `composeRecord` stops one being written.
82
+ *
83
+ * `target` is likewise not required to resolve — a dangling target is a
84
+ * validation warning rather than a parse failure.
85
+ */
86
+ declare const kbLinkSchema: z.ZodObject<{
87
+ target: z.ZodString;
88
+ rel: z.ZodString;
89
+ }, z.core.$loose>;
74
90
  declare const KB_RECORD_TYPES: readonly ["fact", "requirement", "constraint", "decision", "assumption", "open-question", "risk", "contract", "flow", "affected-system", "test-obligation", "source-note"];
75
91
  type KbRecordType = (typeof KB_RECORD_TYPES)[number];
76
92
  /** Both halves of `<type>.<slug>` are kebab-case, and neither may be empty. */
@@ -126,6 +142,10 @@ declare const kbRecordFrontmatterSchema: z.ZodObject<{
126
142
  lines: z.ZodOptional<z.ZodNumber>;
127
143
  }, z.core.$strict>>>;
128
144
  strauss_verify: z.ZodOptional<z.ZodArray<z.ZodString>>;
145
+ strauss_links: z.ZodOptional<z.ZodArray<z.ZodObject<{
146
+ target: z.ZodString;
147
+ rel: z.ZodString;
148
+ }, z.core.$loose>>>;
129
149
  strauss_status: z.ZodDefault<z.ZodEnum<{
130
150
  draft: "draft";
131
151
  proposed: "proposed";
@@ -158,6 +178,7 @@ type KbSource = z.infer<typeof kbSourceSchema>;
158
178
  type KbActorStamp = z.infer<typeof kbActorStampSchema>;
159
179
  type KbVerifiedEvent = z.infer<typeof kbVerifiedEventSchema>;
160
180
  type KbAnchor = z.infer<typeof kbAnchorSchema>;
181
+ type KbLink = z.infer<typeof kbLinkSchema>;
161
182
  type KbRecordFrontmatter = z.infer<typeof kbRecordFrontmatterSchema>;
162
183
  type KbRecord = {
163
184
  /** Path minus `.md`, relative to the bundle root. OKF's concept identity. */
@@ -361,8 +382,19 @@ declare function resolveHeads(from: KbRecord, byId: Map<string, KbRecord>): {
361
382
  * suits a bounded pack but floods a timeline. Also absent by design:
362
383
  * `strauss_answered` carries no target id, so a question's resolution lives
363
384
  * in its own body rather than in another record.
385
+ *
386
+ * `typed-link` is in, where `body-link` is out, because the two differ in how
387
+ * cheaply they are made. A body link is any markdown a writer happened to
388
+ * type; a `strauss_links` entry is a deliberate claim from a closed vocabulary
389
+ * about what this record depends on. That is exactly the kind of edge a
390
+ * timeline should follow — "we chose this because of that" is the history.
391
+ *
392
+ * Only the causal rels, though. `related_to` asserts no dependence and reaches
393
+ * whatever a writer thought worth mentioning, which is the same flooding
394
+ * `body-link` is excluded for; a bibliography is a neighbourhood's business
395
+ * (`pack`), not a history's.
364
396
  */
365
- declare const TRACE_EDGES: readonly ["supersession", "anchor", "source"];
397
+ declare const TRACE_EDGES: readonly ["typed-link", "supersession", "anchor", "source"];
366
398
  type KbTraceEdge = (typeof TRACE_EDGES)[number];
367
399
  type KbTraceStep = {
368
400
  record: KbRecord;
@@ -515,6 +547,105 @@ declare function catalog(bundle: KbRecord[], options?: {
515
547
  */
516
548
  declare function renderCatalogLine(entry: KbCatalogEntry): string;
517
549
 
550
+ /**
551
+ * Types for the inbound half of the typed causal graph. `kb-edges.ts` answers
552
+ * "what does this record point at"; this module answers who is leaning on it,
553
+ * and what breaks if it moves.
554
+ */
555
+ /** One inbound typed edge: `from --rel--> the record asked about`. */
556
+ type KbInboundEdge = {
557
+ from: string;
558
+ /**
559
+ * A stored rel, not necessarily a known one, so a record carrying an
560
+ * unrecognised rel is reported rather than dropped from the answer.
561
+ */
562
+ rel: string;
563
+ };
564
+ /** One record pointing at the target, with the standing of what it claims. */
565
+ type KbBacklink = KbInboundEdge & {
566
+ title: string | null;
567
+ standing: KbStanding;
568
+ warnings: KbWarning[];
569
+ };
570
+ type KbBacklinksResult = {
571
+ target: string;
572
+ /** Every inbound edge, whatever its rel. Ordered by source id, then rel. */
573
+ backlinks: KbBacklink[];
574
+ };
575
+ /**
576
+ * One typed edge as the impact walk traversed it, both ends named. The walk
577
+ * follows `depends_on` against the edge and `informs` along it, so a reader of
578
+ * `via` needs the edge as written, not as walked.
579
+ */
580
+ type KbLinkEdge = {
581
+ source: string;
582
+ target: string;
583
+ rel: string;
584
+ };
585
+ type KbImpactOptions = {
586
+ /**
587
+ * Which rels the walk follows. Defaults to every rel that carries a
588
+ * direction of dependence. A rel outside that set — unknown, or the inert
589
+ * `related_to` — is refused rather than ignored.
590
+ */
591
+ rels?: readonly string[];
592
+ /**
593
+ * How many hops out to walk. Unbounded by default: a blast radius cut at a
594
+ * depth the caller did not choose looks like a small one.
595
+ */
596
+ depth?: number;
597
+ };
598
+ type KbImpactedRecord = {
599
+ conceptId: string;
600
+ title: string | null;
601
+ standing: KbStanding;
602
+ warnings: KbWarning[];
603
+ /** Hops from the record asked about. 1 is a direct dependant. */
604
+ depth: number;
605
+ /** Every edge that reached it, nearest first. */
606
+ via: KbLinkEdge[];
607
+ };
608
+ type KbImpactResult = {
609
+ root: string;
610
+ /** Ordered by depth, then concept id. Never includes the root itself. */
611
+ impacted: KbImpactedRecord[];
612
+ /**
613
+ * Reached, reported, and not walked through: records whose standing means
614
+ * their own declared edges no longer hold. Named rather than dropped, so the
615
+ * caller can see where the walk stopped.
616
+ */
617
+ stopped: string[];
618
+ /**
619
+ * Whether the depth cap ended the walk with dependants still to expand. A cut
620
+ * blast radius must say so, or it reads as a complete one that is small.
621
+ */
622
+ truncated: boolean;
623
+ /** The records the depth cap left unexpanded. Empty unless `truncated`. */
624
+ unexpanded: string[];
625
+ };
626
+
627
+ /**
628
+ * Every edge the bundle holds against this id: one hop, every rel including
629
+ * `related_to`, each row carrying the standing of the record that made it.
630
+ * Flat and factual, where `impact` answers the causal, transitive question.
631
+ */
632
+ declare function backlinks(targetId: string, bundle: KbRecord[]): KbBacklinksResult;
633
+
634
+ /**
635
+ * The transitive set of *dependants*, not the set of inbound edges: each rel is
636
+ * followed in its own direction of dependence. `related_to` and unknown rels
637
+ * propagate nothing. A superseded or rejected record is reported, then not
638
+ * walked through. A record expands once, so cycles terminate.
639
+ */
640
+ declare function impact(targetId: string, bundle: KbRecord[], options?: KbImpactOptions): KbImpactResult;
641
+
642
+ /**
643
+ * The whole bundle's typed edges, indexed by target — built once per call, so a
644
+ * walk stays linear in the bundle rather than quadratic. Order is `list()`'s
645
+ * own, then each record's declared link order, so repeated runs agree.
646
+ */
647
+ declare function inboundIndex(bundle: KbRecord[]): Map<string, KbInboundEdge[]>;
648
+
518
649
  declare const LOG_FILE = "log.jsonl";
519
650
  declare const kbLogEntrySchema: z.ZodObject<{
520
651
  at: z.ZodISODateTime;
@@ -785,6 +916,10 @@ declare class KbStore {
785
916
  }): Promise<KbCatalogResult>;
786
917
  /** A bounded neighbourhood around one record. See `pack.ts`. */
787
918
  pack(bundlePath: string, rootId: string, options?: KbPackOptions): Promise<KbPackResult>;
919
+ /** What breaks if this record changes. See `kb-links/impact.ts`. */
920
+ impact(bundlePath: string, targetId: string, options?: KbImpactOptions): Promise<KbImpactResult>;
921
+ /** Who points at this record, one hop. See `kb-links/backlinks.ts`. */
922
+ backlinks(bundlePath: string, targetId: string): Promise<KbBacklinksResult>;
788
923
  /**
789
924
  * The stored index, rebuilt if it disagrees with the records.
790
925
  *
@@ -908,6 +1043,7 @@ declare enum ErrorTypes {
908
1043
  KbPackBudgetExceeded = "KbPackBudgetExceeded",
909
1044
  KbRecordNotFound = "KbRecordNotFound",
910
1045
  KbSelfVerification = "KbSelfVerification",
1046
+ KbUnknownLinkRel = "KbUnknownLinkRel",
911
1047
  KbWriteConflict = "KbWriteConflict"
912
1048
  }
913
1049
  type ErrorDetails = Record<string, string | boolean | number | string[] | boolean[] | number[]>;
@@ -971,6 +1107,19 @@ declare class KbPackBudgetExceededError extends BaseError {
971
1107
  readonly excluded: string[];
972
1108
  constructor(recordCount: number, approxTokens: number, budgetTokens: number, excluded: string[]);
973
1109
  }
1110
+ /**
1111
+ * A `rels` option naming something the walk can never follow.
1112
+ *
1113
+ * Refused rather than ignored: dropping an unrecognised rel would return an
1114
+ * empty impact set — "nothing breaks" — indistinguishable from a genuine
1115
+ * result. `related_to` is refused here too, since it asserts no dependence and
1116
+ * following it can only produce that same empty set.
1117
+ */
1118
+ declare class KbUnknownLinkRelError extends BaseError {
1119
+ readonly rel: string;
1120
+ readonly expected: readonly string[];
1121
+ constructor(rel: string, expected: readonly string[]);
1122
+ }
974
1123
  /**
975
1124
  * A flag that takes a value, given none.
976
1125
  *
@@ -1009,7 +1158,68 @@ type KbRecordTypeSpec = {
1009
1158
  };
1010
1159
  declare const RECORD_TYPES: Readonly<Record<KbRecordType, KbRecordTypeSpec>>;
1011
1160
  declare function isKbRecordType(value: string): value is KbRecordType;
1161
+ /**
1162
+ * The closed vocabulary of typed causal edges — `strauss_links[].rel` — which a
1163
+ * producer may not extend. `related_to` is the non-dependence escape hatch and
1164
+ * the one rel `kb_impact` does not follow. Every edge lives on the source's
1165
+ * frontmatter, source → target; `phrase` is the template compose.ts renders.
1166
+ */
1167
+ type KbLinkRelSpec = {
1168
+ /** One line, for schema output and CLI help. */
1169
+ purpose: string;
1170
+ /** Sentence stem: `<phrase> [target](target.md).` */
1171
+ phrase: string;
1172
+ /**
1173
+ * Which end of the edge breaks when the other end changes.
1174
+ *
1175
+ * The rel's *direction of dependence*, which does not follow the edge's own
1176
+ * direction. `A depends_on B` puts the dependant at the source: A breaks when
1177
+ * B moves. `A informs B` puts it at the target. A walk that treated both as
1178
+ * "inbound" would report the blast radius of `informs`, `blocks`,
1179
+ * `invalidates` and `constrains` backwards.
1180
+ *
1181
+ * `null` means the rel asserts no dependence either way, so nothing
1182
+ * propagates along it. `related_to` is the only such rel.
1183
+ */
1184
+ dependant: "source" | "target" | null;
1185
+ };
1186
+ declare const KB_LINK_RELS: readonly ["depends_on", "constrains", "informs", "blocks", "invalidates", "verified_by", "satisfies", "related_to"];
1187
+ type KbLinkRel = (typeof KB_LINK_RELS)[number];
1188
+ declare const LINK_RELS: Readonly<Record<KbLinkRel, KbLinkRelSpec>>;
1189
+ /**
1190
+ * The rels that carry a direction of dependence, and therefore the only ones
1191
+ * anything can propagate along. `kb_impact`'s default edge set, and what
1192
+ * `trace` follows.
1193
+ *
1194
+ * Derived from the table rather than restated, so a rel cannot be causal in one
1195
+ * place and inert in another. The cast is a non-emptiness assertion for
1196
+ * `z.enum`.
1197
+ */
1198
+ declare const KB_CAUSAL_LINK_RELS: [KbLinkRel, ...KbLinkRel[]];
1199
+ declare function isKbLinkRel(value: string): value is KbLinkRel;
1012
1200
 
1201
+ /**
1202
+ * One typed causal edge, as a producer states it.
1203
+ *
1204
+ * Strict where `kbLinkSchema` is tolerant, the same split
1205
+ * `kbVerifiedEventSchema` makes against `kbActorStampSchema`: what this package
1206
+ * writes must be inside the closed vocabulary, what it reads may not be, since
1207
+ * a foreign record has to stay loadable for `kb_validate` to fault it.
1208
+ */
1209
+ declare const composeLinkSchema: z.ZodObject<{
1210
+ target: z.ZodString;
1211
+ rel: z.ZodEnum<{
1212
+ depends_on: "depends_on";
1213
+ constrains: "constrains";
1214
+ informs: "informs";
1215
+ blocks: "blocks";
1216
+ invalidates: "invalidates";
1217
+ verified_by: "verified_by";
1218
+ satisfies: "satisfies";
1219
+ related_to: "related_to";
1220
+ }>;
1221
+ }, z.core.$strict>;
1222
+ type ComposeLink = z.infer<typeof composeLinkSchema>;
1013
1223
  declare const composeInputSchema: z.ZodObject<{
1014
1224
  slug: z.ZodString;
1015
1225
  title: z.ZodString;
@@ -1036,6 +1246,19 @@ declare const composeInputSchema: z.ZodObject<{
1036
1246
  verify: z.ZodOptional<z.ZodArray<z.ZodString>>;
1037
1247
  tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
1038
1248
  relatedConceptIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
1249
+ links: z.ZodOptional<z.ZodArray<z.ZodObject<{
1250
+ target: z.ZodString;
1251
+ rel: z.ZodEnum<{
1252
+ depends_on: "depends_on";
1253
+ constrains: "constrains";
1254
+ informs: "informs";
1255
+ blocks: "blocks";
1256
+ invalidates: "invalidates";
1257
+ verified_by: "verified_by";
1258
+ satisfies: "satisfies";
1259
+ related_to: "related_to";
1260
+ }>;
1261
+ }, z.core.$strict>>>;
1039
1262
  supersedes: z.ZodOptional<z.ZodArray<z.ZodString>>;
1040
1263
  materiality: z.ZodOptional<z.ZodEnum<{
1041
1264
  blocking: "blocking";
@@ -1420,35 +1643,65 @@ declare function matchToDiff(files: DiffFile[], records: KbRecord[], options?: M
1420
1643
  * There is no separate `related` kind: compose.ts renders `relatedConceptIds`
1421
1644
  * as body links (`Relates to [id](id.md).`), so in stored form a related edge
1422
1645
  * IS a body link, and a distinct kind would count the same markdown twice.
1423
- */
1424
- declare const KB_EDGE_KINDS: readonly ["body-link", "supersession", "anchor", "source"];
1646
+ *
1647
+ * `typed-link` is not that case, despite compose.ts also rendering a sentence
1648
+ * per link. The edge is `strauss_links` in the frontmatter — the authoritative,
1649
+ * typed form — and the sentence is its rendering for a reader that only knows
1650
+ * OKF. A record can carry the frontmatter without the prose (hand-written, or
1651
+ * from a producer we did not write), so reading only the body would miss it.
1652
+ * A pair connected both ways comes back with both kinds in `via`, which is the
1653
+ * honest answer: it was declared, and it was written about.
1654
+ *
1655
+ * `body-link` and `typed-link` are DIRECTED — the edges a record itself makes,
1656
+ * read off its own body or frontmatter. `supersession`, `anchor` and `source`
1657
+ * are symmetric: they hold between two records because both name the same
1658
+ * thing, so either end sees the other. Callers wanting the inbound half of a
1659
+ * typed edge use `kb-links/` (`kb_backlinks`, `kb_impact`) rather than this
1660
+ * module, which answers "what does this record point at".
1661
+ */
1662
+ declare const KB_EDGE_KINDS: readonly ["body-link", "typed-link", "supersession", "anchor", "source"];
1425
1663
  type KbEdgeKind = (typeof KB_EDGE_KINDS)[number];
1426
1664
  type KbNeighbour = {
1427
1665
  record: KbRecord;
1428
1666
  /** Every edge kind that connects it to the record asked about. */
1429
1667
  via: KbEdgeKind[];
1430
1668
  };
1669
+ /**
1670
+ * Which rels a `typed-link` walk may follow.
1671
+ *
1672
+ * Defaults to the whole known vocabulary — including `related_to`, since a
1673
+ * neighbourhood is the one place a bibliography belongs. It never includes an
1674
+ * unknown rel: a rel outside the vocabulary is not a claim any walk can
1675
+ * interpret, so no walk traverses it anywhere, and `kb_validate` reports it as
1676
+ * an error rather than a walk quietly acting on it.
1677
+ */
1678
+ declare const DEFAULT_TYPED_LINK_RELS: readonly string[];
1431
1679
  /**
1432
1680
  * Every record `from` touches, each carrying the full set of edge kinds that
1433
1681
  * connect the pair. Order is deterministic: bundle order per kind, kinds in
1434
1682
  * the order given.
1435
1683
  */
1436
- declare function neighbours(from: KbRecord, bundle: KbRecord[], kinds?: readonly KbEdgeKind[]): KbNeighbour[];
1437
- /** The records one edge kind connects `from` to, in bundle order. */
1438
- declare function edgeNeighbours(from: KbRecord, bundle: KbRecord[], kind: KbEdgeKind): KbRecord[];
1684
+ declare function neighbours(from: KbRecord, bundle: KbRecord[], kinds?: readonly KbEdgeKind[], linkRels?: readonly string[]): KbNeighbour[];
1685
+ /**
1686
+ * The records one edge kind connects `from` to, in bundle order.
1687
+ *
1688
+ * `linkRels` narrows the `typed-link` kind and is ignored by the others —
1689
+ * `trace` passes the causal rels, `pack` takes the default.
1690
+ */
1691
+ declare function edgeNeighbours(from: KbRecord, bundle: KbRecord[], kind: KbEdgeKind, linkRels?: readonly string[]): KbRecord[];
1439
1692
 
1693
+ /** `error` fails the check; `warning` does not. */
1694
+ type KbValidationSeverity = "error" | "warning";
1440
1695
  type KbValidationProblem = {
1441
1696
  check: string;
1442
1697
  conceptId: string;
1443
1698
  note: string;
1699
+ severity: KbValidationSeverity;
1444
1700
  };
1445
1701
  /**
1446
- * Checks that only hold across the whole bundle.
1447
- *
1448
- * Per-record shape is the schema's job and is enforced on every read, so
1449
- * nothing here re-states it. What a schema cannot see is whether one record's
1450
- * pointers agree with another's — and since `supersede()` now writes both
1451
- * directions, a disagreement means someone edited a file by hand.
1702
+ * Checks that only hold across the whole bundle: whether one record's pointers
1703
+ * agree with another's. Per-record shape is the schema's job, enforced on every
1704
+ * read, so nothing here re-states it.
1452
1705
  */
1453
1706
  declare function validateBundle(records: KbRecord[]): KbValidationProblem[];
1454
1707
 
@@ -1574,6 +1827,19 @@ declare const decisionInputSchema: z.ZodObject<{
1574
1827
  }, z.core.$strict>>>;
1575
1828
  verify: z.ZodOptional<z.ZodArray<z.ZodString>>;
1576
1829
  relatedConceptIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
1830
+ links: z.ZodOptional<z.ZodArray<z.ZodObject<{
1831
+ target: z.ZodString;
1832
+ rel: z.ZodEnum<{
1833
+ depends_on: "depends_on";
1834
+ constrains: "constrains";
1835
+ informs: "informs";
1836
+ blocks: "blocks";
1837
+ invalidates: "invalidates";
1838
+ verified_by: "verified_by";
1839
+ satisfies: "satisfies";
1840
+ related_to: "related_to";
1841
+ }>;
1842
+ }, z.core.$strict>>>;
1577
1843
  supersedes: z.ZodOptional<z.ZodArray<z.ZodString>>;
1578
1844
  materiality: z.ZodOptional<z.ZodEnum<{
1579
1845
  blocking: "blocking";
@@ -1805,4 +2071,4 @@ declare function parseMarkdownWithFrontmatter<S extends z.ZodType>(text: string,
1805
2071
  frontmatter: ReturnType<S["safeParse"]>;
1806
2072
  };
1807
2073
 
1808
- 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 };
2074
+ export { type AnchorResolver, BaseError, CONTEXT_BEGIN, CONTEXT_END, CONTEXT_PROFILES, type ComposeInput, type ComposeLink, type ComposedRecord, DECISION_TYPE, DEFAULT_AGING_DAYS, DEFAULT_EXPIRING_DAYS, DEFAULT_LOAD_BUDGET, DEFAULT_PACK_HOPS, DEFAULT_PACK_MAX_NODES, DEFAULT_TYPED_LINK_RELS, DEFAULT_UNVERIFIED_DAYS, type DecisionInput, type DiffFile, type DiffHunk, type DiffMatch, type ErrorDetails, type ErrorProps, ErrorTypes, Fault, INDEX_FILE, KB_CAUSAL_LINK_RELS, KB_COMMANDS, KB_COMMANDS_BY_NAME, KB_CONCEPT_ID_PATTERN, KB_CONFIDENCES, KB_DIR, KB_DOCTOR_CHECKS, KB_EDGE_KINDS, KB_LINK_RELS, KB_MATERIALITIES, KB_RECORD_STATUSES, KB_RECORD_TYPES, KB_SLUG_PATTERN, type KbActorStamp, type KbAdjudicated, type KbAnchor, type KbAnchorDriftEntry, type KbBacklink, type KbBacklinksResult, 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, type KbImpactOptions, type KbImpactResult, type KbImpactedRecord, type KbInboundEdge, KbInvalidConceptIdError, type KbLink, type KbLinkEdge, type KbLinkRel, type KbLinkRelSpec, 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, KbUnknownLinkRelError, type KbValidationProblem, type KbValidationSeverity, type KbVerifiedEvent, type KbWarning, KbWriteConflictError, type KbWriteInput, LINK_RELS, 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, backlinks, buildContext, catalog, composeDecisionRecord, composeInputSchema, composeLinkSchema, composeNoDecisionRecord, composeRecord, contextProfileBudgets, createKbMcpServer, decisionInputSchema, detectAnchorDrift, doctor, edgeNeighbours, hashAnchorText, impact, inboundIndex, indexIsStale, isKbLinkRel, isKbRecordType, isNoDecisionRecord, kbActorStampSchema, kbAnchorSchema, kbConceptIdSchema, kbJsonSchemas, kbLinkSchema, 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-F2U2YLWV.js";
3
+ } from "./chunk-ZICKDZGY.js";
4
4
  import {
5
5
  createKbMcpServer,
6
6
  runKbMcpServer
7
- } from "./chunk-EXKK2KUN.js";
7
+ } from "./chunk-WZODZNR6.js";
8
8
  import {
9
9
  BaseError,
10
10
  CONTEXT_BEGIN,
@@ -16,10 +16,12 @@ import {
16
16
  DEFAULT_LOAD_BUDGET,
17
17
  DEFAULT_PACK_HOPS,
18
18
  DEFAULT_PACK_MAX_NODES,
19
+ DEFAULT_TYPED_LINK_RELS,
19
20
  DEFAULT_UNVERIFIED_DAYS,
20
21
  ErrorTypes,
21
22
  Fault,
22
23
  INDEX_FILE,
24
+ KB_CAUSAL_LINK_RELS,
23
25
  KB_COMMANDS,
24
26
  KB_COMMANDS_BY_NAME,
25
27
  KB_CONCEPT_ID_PATTERN,
@@ -27,6 +29,7 @@ import {
27
29
  KB_DIR,
28
30
  KB_DOCTOR_CHECKS,
29
31
  KB_EDGE_KINDS,
32
+ KB_LINK_RELS,
30
33
  KB_MATERIALITIES,
31
34
  KB_RECORD_STATUSES,
32
35
  KB_RECORD_TYPES,
@@ -40,7 +43,9 @@ import {
40
43
  KbRecordNotFoundError,
41
44
  KbSelfVerificationError,
42
45
  KbStore,
46
+ KbUnknownLinkRelError,
43
47
  KbWriteConflictError,
48
+ LINK_RELS,
44
49
  LOG_FILE,
45
50
  NO_DECISION_SLUG,
46
51
  PINS_FILE,
@@ -52,10 +57,12 @@ import {
52
57
  adjudicate,
53
58
  anchorFilePath,
54
59
  assertBaseNotFrozen,
60
+ backlinks,
55
61
  buildContext,
56
62
  catalog,
57
63
  composeDecisionRecord,
58
64
  composeInputSchema,
65
+ composeLinkSchema,
59
66
  composeNoDecisionRecord,
60
67
  composeRecord,
61
68
  contextProfileBudgets,
@@ -64,13 +71,17 @@ import {
64
71
  doctor,
65
72
  edgeNeighbours,
66
73
  hashAnchorText,
74
+ impact,
75
+ inboundIndex,
67
76
  indexIsStale,
77
+ isKbLinkRel,
68
78
  isKbRecordType,
69
79
  isNoDecisionRecord,
70
80
  kbActorStampSchema,
71
81
  kbAnchorSchema,
72
82
  kbConceptIdSchema,
73
83
  kbJsonSchemas,
84
+ kbLinkSchema,
74
85
  kbLogEntrySchema,
75
86
  kbRecordFrontmatterSchema,
76
87
  kbSourceSchema,
@@ -103,7 +114,7 @@ import {
103
114
  trace,
104
115
  unpinBase,
105
116
  validateBundle
106
- } from "./chunk-33ZCBEUV.js";
117
+ } from "./chunk-XALWG3EZ.js";
107
118
 
108
119
  // src/match-diff.ts
109
120
  function matchToDiff(files, records, options = {}) {
@@ -195,10 +206,12 @@ export {
195
206
  DEFAULT_LOAD_BUDGET,
196
207
  DEFAULT_PACK_HOPS,
197
208
  DEFAULT_PACK_MAX_NODES,
209
+ DEFAULT_TYPED_LINK_RELS,
198
210
  DEFAULT_UNVERIFIED_DAYS,
199
211
  ErrorTypes,
200
212
  Fault,
201
213
  INDEX_FILE,
214
+ KB_CAUSAL_LINK_RELS,
202
215
  KB_COMMANDS,
203
216
  KB_COMMANDS_BY_NAME,
204
217
  KB_CONCEPT_ID_PATTERN,
@@ -206,6 +219,7 @@ export {
206
219
  KB_DIR,
207
220
  KB_DOCTOR_CHECKS,
208
221
  KB_EDGE_KINDS,
222
+ KB_LINK_RELS,
209
223
  KB_MATERIALITIES,
210
224
  KB_RECORD_STATUSES,
211
225
  KB_RECORD_TYPES,
@@ -219,7 +233,9 @@ export {
219
233
  KbRecordNotFoundError,
220
234
  KbSelfVerificationError,
221
235
  KbStore,
236
+ KbUnknownLinkRelError,
222
237
  KbWriteConflictError,
238
+ LINK_RELS,
223
239
  LOG_FILE,
224
240
  NO_DECISION_SLUG,
225
241
  PINS_FILE,
@@ -231,10 +247,12 @@ export {
231
247
  adjudicate,
232
248
  anchorFilePath,
233
249
  assertBaseNotFrozen,
250
+ backlinks,
234
251
  buildContext,
235
252
  catalog,
236
253
  composeDecisionRecord,
237
254
  composeInputSchema,
255
+ composeLinkSchema,
238
256
  composeNoDecisionRecord,
239
257
  composeRecord,
240
258
  contextProfileBudgets,
@@ -244,13 +262,17 @@ export {
244
262
  doctor,
245
263
  edgeNeighbours,
246
264
  hashAnchorText,
265
+ impact,
266
+ inboundIndex,
247
267
  indexIsStale,
268
+ isKbLinkRel,
248
269
  isKbRecordType,
249
270
  isNoDecisionRecord,
250
271
  kbActorStampSchema,
251
272
  kbAnchorSchema,
252
273
  kbConceptIdSchema,
253
274
  kbJsonSchemas,
275
+ kbLinkSchema,
254
276
  kbLogEntrySchema,
255
277
  kbRecordFrontmatterSchema,
256
278
  kbSourceSchema,
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":"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","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":[]}