@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/ARCHITECTURE.md +15 -0
- package/README.md +41 -4
- package/dist/{chunk-EXKK2KUN.js → chunk-WZODZNR6.js} +2 -2
- package/dist/{chunk-33ZCBEUV.js → chunk-XALWG3EZ.js} +463 -88
- package/dist/chunk-XALWG3EZ.js.map +1 -0
- package/dist/{chunk-F2U2YLWV.js → chunk-ZICKDZGY.js} +2 -2
- package/dist/cli-main.cjs +461 -98
- package/dist/cli-main.cjs.map +1 -1
- package/dist/cli-main.js +2 -2
- package/dist/index.cjs +540 -154
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +279 -13
- package/dist/index.d.ts +279 -13
- package/dist/index.js +25 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp-main.cjs +461 -98
- package/dist/mcp-main.cjs.map +1 -1
- package/dist/mcp-main.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-33ZCBEUV.js.map +0 -1
- /package/dist/{chunk-EXKK2KUN.js.map → chunk-WZODZNR6.js.map} +0 -0
- /package/dist/{chunk-F2U2YLWV.js.map → chunk-ZICKDZGY.js.map} +0 -0
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
|
-
|
|
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
|
-
/**
|
|
1438
|
-
|
|
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
|
-
*
|
|
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-
|
|
3
|
+
} from "./chunk-ZICKDZGY.js";
|
|
4
4
|
import {
|
|
5
5
|
createKbMcpServer,
|
|
6
6
|
runKbMcpServer
|
|
7
|
-
} from "./chunk-
|
|
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-
|
|
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":"
|
|
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":[]}
|