@saasontools/strauss-kb 0.1.0

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.js ADDED
@@ -0,0 +1,202 @@
1
+ import {
2
+ runKbCli
3
+ } from "./chunk-KGM34MYU.js";
4
+ import {
5
+ createKbMcpServer,
6
+ runKbMcpServer
7
+ } from "./chunk-WFHYWZX5.js";
8
+ import {
9
+ BaseError,
10
+ DECISION_TYPE,
11
+ ErrorTypes,
12
+ Fault,
13
+ INDEX_FILE,
14
+ KB_COMMANDS,
15
+ KB_COMMANDS_BY_NAME,
16
+ KB_CONCEPT_ID_PATTERN,
17
+ KB_CONFIDENCES,
18
+ KB_DIR,
19
+ KB_MATERIALITIES,
20
+ KB_RECORD_STATUSES,
21
+ KB_RECORD_TYPES,
22
+ KB_SLUG_PATTERN,
23
+ KbInvalidConceptIdError,
24
+ KbRecordAlreadyExistsError,
25
+ KbRecordNotFoundError,
26
+ KbStore,
27
+ KbWriteConflictError,
28
+ LOG_FILE,
29
+ NO_DECISION_SLUG,
30
+ RECORD_TYPES,
31
+ SEARCH_INDEX_FILE,
32
+ TRACE_EDGES,
33
+ adjudicate,
34
+ composeDecisionRecord,
35
+ composeInputSchema,
36
+ composeNoDecisionRecord,
37
+ composeRecord,
38
+ decisionInputSchema,
39
+ indexIsStale,
40
+ isKbRecordType,
41
+ isNoDecisionRecord,
42
+ kbActorStampSchema,
43
+ kbAnchorSchema,
44
+ kbConceptIdSchema,
45
+ kbJsonSchemas,
46
+ kbLogEntrySchema,
47
+ kbRecordFrontmatterSchema,
48
+ kbSourceSchema,
49
+ loadQmd,
50
+ parseLog,
51
+ parseMarkdownWithFrontmatter,
52
+ renderIndex,
53
+ renderLogEntry,
54
+ resolveHeads,
55
+ resolveHits,
56
+ searchBase,
57
+ selectDecisions,
58
+ splitMarkdownFrontmatter,
59
+ stringifyMarkdownWithFrontmatter,
60
+ trace,
61
+ validateBundle
62
+ } from "./chunk-ZSYSHJVZ.js";
63
+
64
+ // src/match-diff.ts
65
+ function matchToDiff(files, records, options = {}) {
66
+ const ranges = indexRanges(options.symbolRanges ?? []);
67
+ const anchored = records.filter(
68
+ (record) => (record.frontmatter.strauss_anchors ?? []).length > 0
69
+ );
70
+ const matches = [];
71
+ for (const file of files) {
72
+ const candidates = anchored.map((record) => ({
73
+ record,
74
+ anchors: (record.frontmatter.strauss_anchors ?? []).filter(
75
+ (anchor) => normalize(anchor.file) === normalize(file.filePath)
76
+ )
77
+ })).filter(({ anchors }) => anchors.length > 0);
78
+ if (!candidates.length) continue;
79
+ for (const hunk of file.hunks) {
80
+ const hits = [];
81
+ let precision = "symbol";
82
+ for (const { record, anchors } of candidates) {
83
+ const placement = place(anchors, file.filePath, hunk, ranges);
84
+ if (placement === "miss") continue;
85
+ if (placement === "file") precision = "file";
86
+ hits.push(record);
87
+ }
88
+ if (!hits.length) continue;
89
+ matches.push({
90
+ filePath: file.filePath,
91
+ hunk,
92
+ records: order(adjudicate(hits, records, options.now)),
93
+ precision
94
+ });
95
+ }
96
+ }
97
+ return matches;
98
+ }
99
+ function place(anchors, filePath, hunk, ranges) {
100
+ let fallback = "miss";
101
+ for (const anchor of anchors) {
102
+ if (!anchor.symbol) return "file";
103
+ const resolved = ranges.get(key(filePath, anchor.symbol));
104
+ if (!resolved?.length) {
105
+ fallback = "file";
106
+ continue;
107
+ }
108
+ if (resolved.some((range) => overlaps(range, hunk))) return "symbol";
109
+ }
110
+ return fallback;
111
+ }
112
+ function overlaps(range, hunk) {
113
+ return range.startLine <= hunk.endLine && hunk.startLine <= range.endLine;
114
+ }
115
+ function order(records) {
116
+ const rank = {
117
+ current: 0,
118
+ unsettled: 1,
119
+ open: 2,
120
+ superseded: 3,
121
+ rejected: 4
122
+ };
123
+ return [...records].sort(
124
+ (left, right) => (rank[left.standing] ?? 9) - (rank[right.standing] ?? 9) || (left.record.frontmatter.generated?.at ?? "").localeCompare(
125
+ right.record.frontmatter.generated?.at ?? ""
126
+ )
127
+ );
128
+ }
129
+ function indexRanges(ranges) {
130
+ const byKey = /* @__PURE__ */ new Map();
131
+ for (const range of ranges) {
132
+ const id = key(range.file, range.symbol);
133
+ byKey.set(id, [...byKey.get(id) ?? [], range]);
134
+ }
135
+ return byKey;
136
+ }
137
+ function key(file, symbol) {
138
+ return `${normalize(file)}#${symbol}`;
139
+ }
140
+ function normalize(path) {
141
+ return path.replace(/^\.\//, "");
142
+ }
143
+ export {
144
+ BaseError,
145
+ DECISION_TYPE,
146
+ ErrorTypes,
147
+ Fault,
148
+ INDEX_FILE,
149
+ KB_COMMANDS,
150
+ KB_COMMANDS_BY_NAME,
151
+ KB_CONCEPT_ID_PATTERN,
152
+ KB_CONFIDENCES,
153
+ KB_DIR,
154
+ KB_MATERIALITIES,
155
+ KB_RECORD_STATUSES,
156
+ KB_RECORD_TYPES,
157
+ KB_SLUG_PATTERN,
158
+ KbInvalidConceptIdError,
159
+ KbRecordAlreadyExistsError,
160
+ KbRecordNotFoundError,
161
+ KbStore,
162
+ KbWriteConflictError,
163
+ LOG_FILE,
164
+ NO_DECISION_SLUG,
165
+ RECORD_TYPES,
166
+ SEARCH_INDEX_FILE,
167
+ TRACE_EDGES,
168
+ adjudicate,
169
+ composeDecisionRecord,
170
+ composeInputSchema,
171
+ composeNoDecisionRecord,
172
+ composeRecord,
173
+ createKbMcpServer,
174
+ decisionInputSchema,
175
+ indexIsStale,
176
+ isKbRecordType,
177
+ isNoDecisionRecord,
178
+ kbActorStampSchema,
179
+ kbAnchorSchema,
180
+ kbConceptIdSchema,
181
+ kbJsonSchemas,
182
+ kbLogEntrySchema,
183
+ kbRecordFrontmatterSchema,
184
+ kbSourceSchema,
185
+ loadQmd,
186
+ matchToDiff,
187
+ parseLog,
188
+ parseMarkdownWithFrontmatter,
189
+ renderIndex,
190
+ renderLogEntry,
191
+ resolveHeads,
192
+ resolveHits,
193
+ runKbCli,
194
+ runKbMcpServer,
195
+ searchBase,
196
+ selectDecisions,
197
+ splitMarkdownFrontmatter,
198
+ stringifyMarkdownWithFrontmatter,
199
+ trace,
200
+ validateBundle
201
+ };
202
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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":[]}