@titan-design/code-read 0.1.8 → 0.2.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/README.md +111 -3
- package/dist/chunk-7FNTXO7C.js +2344 -0
- package/dist/chunk-7FNTXO7C.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +133 -3
- package/dist/index.js.map +1 -1
- package/dist/query/index.d.ts +1283 -5
- package/dist/query/index.js +77 -1
- package/package.json +5 -5
- package/dist/chunk-VXQFCVGB.js +0 -1326
- package/dist/chunk-VXQFCVGB.js.map +0 -1
|
@@ -0,0 +1,2344 @@
|
|
|
1
|
+
// src/query/contract.ts
|
|
2
|
+
import { toJSONSchema, z as z10 } from "zod";
|
|
3
|
+
|
|
4
|
+
// src/query/schemas.ts
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
var NodeKind = z.string();
|
|
7
|
+
var Severity = z.string();
|
|
8
|
+
var ProvenanceKind = z.enum(["measured", "derived", "model"]);
|
|
9
|
+
var Provenance = z.object({
|
|
10
|
+
kind: ProvenanceKind,
|
|
11
|
+
source: z.string(),
|
|
12
|
+
at: z.string().optional()
|
|
13
|
+
});
|
|
14
|
+
var SnapshotInfo = z.object({
|
|
15
|
+
id: z.number().int(),
|
|
16
|
+
ref: z.string(),
|
|
17
|
+
commit: z.string().nullable(),
|
|
18
|
+
takenAt: z.string(),
|
|
19
|
+
indexVersion: z.string()
|
|
20
|
+
});
|
|
21
|
+
var MetricDescriptor = z.object({
|
|
22
|
+
name: z.string(),
|
|
23
|
+
unit: z.string().nullable(),
|
|
24
|
+
appliesTo: z.array(NodeKind),
|
|
25
|
+
rollup: z.enum(["sum", "max", "mean", "none"]),
|
|
26
|
+
direction: z.enum(["higher-worse", "lower-worse", "neutral"]),
|
|
27
|
+
absent: z.enum(["zero", "exclude"]),
|
|
28
|
+
window: z.string().optional(),
|
|
29
|
+
description: z.string(),
|
|
30
|
+
provenance: Provenance
|
|
31
|
+
});
|
|
32
|
+
var RuleSummary = z.object({ id: z.string(), type: z.string(), severity: Severity });
|
|
33
|
+
var SnapshotRef = z.union([z.number().int().positive(), z.string().min(1)]);
|
|
34
|
+
var Span = z.object({ startLine: z.number().int(), endLine: z.number().int() });
|
|
35
|
+
var NodeRef = z.object({
|
|
36
|
+
id: z.string(),
|
|
37
|
+
kind: NodeKind,
|
|
38
|
+
name: z.string(),
|
|
39
|
+
/** The declaring file for a symbol, the directory path for a directory, "" for the repo. */
|
|
40
|
+
path: z.string(),
|
|
41
|
+
span: Span.optional()
|
|
42
|
+
});
|
|
43
|
+
var MissingReason = z.string();
|
|
44
|
+
var Capabilities = z.object({
|
|
45
|
+
findings: z.enum(["none", "check-rules", "store"]),
|
|
46
|
+
verdicts: z.boolean(),
|
|
47
|
+
themes: z.boolean(),
|
|
48
|
+
feedback: z.boolean(),
|
|
49
|
+
embeddings: z.object({ model: z.string(), embedded: z.number(), symbols: z.number() }).nullable(),
|
|
50
|
+
cochange: z.boolean(),
|
|
51
|
+
sourceAtCommit: z.boolean(),
|
|
52
|
+
excerpts: z.enum(["any", "flagged", "none"])
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// src/query/contract-findings.ts
|
|
56
|
+
import { z as z2 } from "zod";
|
|
57
|
+
var FINDINGS_PAGE_MAX = 500;
|
|
58
|
+
var EXCERPT_LINE_CAP = 80;
|
|
59
|
+
var FindingStatus = z2.enum(["new", "carryover", "resolved", "worsened", "improved"]);
|
|
60
|
+
var BaselineFields = {
|
|
61
|
+
baselineSnapshotId: z2.number().int().optional(),
|
|
62
|
+
comparable: z2.boolean().optional()
|
|
63
|
+
};
|
|
64
|
+
var Finding = z2.object({
|
|
65
|
+
/** Opaque and stable across snapshots while the rule, node, and destination are unchanged. */
|
|
66
|
+
id: z2.string(),
|
|
67
|
+
snapshotId: z2.number().int(),
|
|
68
|
+
rule: z2.string(),
|
|
69
|
+
/** Open string: "check" for check-rule findings today. */
|
|
70
|
+
tool: z2.string(),
|
|
71
|
+
severity: Severity,
|
|
72
|
+
node: NodeRef,
|
|
73
|
+
destination: NodeRef.optional(),
|
|
74
|
+
/** The first flagged line range; absent when the finding is about the whole node. */
|
|
75
|
+
range: Span.optional(),
|
|
76
|
+
metric: z2.string().optional(),
|
|
77
|
+
value: z2.number().optional(),
|
|
78
|
+
threshold: z2.number().optional(),
|
|
79
|
+
/** How far past the threshold, as a ratio where larger is worse; null when the rule has no threshold. */
|
|
80
|
+
excess: z2.number().nullable(),
|
|
81
|
+
/** Present only when a baseline was given. */
|
|
82
|
+
status: FindingStatus.optional(),
|
|
83
|
+
message: z2.string(),
|
|
84
|
+
provenance: Provenance
|
|
85
|
+
});
|
|
86
|
+
var findingsListArgs = z2.object({
|
|
87
|
+
snapshot: SnapshotRef.optional(),
|
|
88
|
+
baseline: SnapshotRef.optional(),
|
|
89
|
+
scope: z2.string().optional(),
|
|
90
|
+
rule: z2.array(z2.string()).default([]),
|
|
91
|
+
severity: z2.array(Severity).default([]),
|
|
92
|
+
tool: z2.array(z2.string()).default([]),
|
|
93
|
+
provenance: z2.array(ProvenanceKind).default([]),
|
|
94
|
+
kind: z2.array(z2.string()).default([]),
|
|
95
|
+
status: z2.array(FindingStatus).default([]),
|
|
96
|
+
sort: z2.enum(["severity", "excess", "value", "path", "rule"]).default("severity"),
|
|
97
|
+
order: z2.enum(["asc", "desc"]).default("desc"),
|
|
98
|
+
offset: z2.number().int().min(0).default(0),
|
|
99
|
+
limit: z2.number().int().min(0).max(FINDINGS_PAGE_MAX).default(20),
|
|
100
|
+
facets: z2.boolean().default(false)
|
|
101
|
+
});
|
|
102
|
+
var findingsListResult = z2.object({
|
|
103
|
+
snapshotId: z2.number().int(),
|
|
104
|
+
...BaselineFields,
|
|
105
|
+
rows: z2.array(Finding),
|
|
106
|
+
total: z2.number().int(),
|
|
107
|
+
/** Facet name to value to count, over every row that passed the filters; each facet's counts sum to `total`. */
|
|
108
|
+
facets: z2.record(z2.string(), z2.record(z2.string(), z2.number().int())).optional()
|
|
109
|
+
});
|
|
110
|
+
var FINDINGS_LIST = {
|
|
111
|
+
description: "Findings in a snapshot, filtered, sorted, and offset-paginated, with optional facet counts. Check-rule findings are derived on read (provenance 'derived'), so one disappears when its rule or threshold changes. `scope` limits to a node and everything under it. With `baseline`, each row gets a status and resolved findings come back from the baseline.",
|
|
112
|
+
args: findingsListArgs,
|
|
113
|
+
result: findingsListResult
|
|
114
|
+
};
|
|
115
|
+
var SourceExcerpt = z2.object({
|
|
116
|
+
path: z2.string(),
|
|
117
|
+
startLine: z2.number().int(),
|
|
118
|
+
endLine: z2.number().int(),
|
|
119
|
+
text: z2.string(),
|
|
120
|
+
/** Of the whole file at the snapshot. */
|
|
121
|
+
contentHash: z2.string(),
|
|
122
|
+
origin: z2.enum(["commit", "worktree", "export"]),
|
|
123
|
+
highlights: z2.array(Span),
|
|
124
|
+
/** True when the cap cut the requested window short. */
|
|
125
|
+
truncated: z2.boolean()
|
|
126
|
+
});
|
|
127
|
+
var findingGetArgs = z2.object({
|
|
128
|
+
snapshot: SnapshotRef.optional(),
|
|
129
|
+
id: z2.string(),
|
|
130
|
+
baseline: SnapshotRef.optional(),
|
|
131
|
+
context_lines: z2.number().int().min(0).max(20).default(5)
|
|
132
|
+
});
|
|
133
|
+
var findingGetResult = z2.object({
|
|
134
|
+
snapshotId: z2.number().int(),
|
|
135
|
+
...BaselineFields,
|
|
136
|
+
finding: Finding,
|
|
137
|
+
rule: z2.object({ id: z2.string(), type: z2.string(), severity: Severity, text: z2.string() }),
|
|
138
|
+
measured: z2.object({
|
|
139
|
+
value: z2.number().nullable(),
|
|
140
|
+
threshold: z2.number().nullable(),
|
|
141
|
+
percentile: z2.number().nullable(),
|
|
142
|
+
siblingMedian: z2.number().nullable(),
|
|
143
|
+
baselineValue: z2.number().nullable().optional()
|
|
144
|
+
}),
|
|
145
|
+
why: z2.string(),
|
|
146
|
+
excerpt: SourceExcerpt.nullable(),
|
|
147
|
+
/** Open string: why `excerpt` is null, such as "no-source" or "changed-since-snapshot". */
|
|
148
|
+
excerptMissing: z2.string().optional(),
|
|
149
|
+
related: z2.array(Finding)
|
|
150
|
+
});
|
|
151
|
+
var FINDING_GET = {
|
|
152
|
+
description: "One finding by id with its rule, the measured value in context, a source excerpt of the flagged lines plus `context_lines` either side, and up to 10 related findings on the same node or on graph neighbours.",
|
|
153
|
+
args: findingGetArgs,
|
|
154
|
+
result: findingGetResult
|
|
155
|
+
};
|
|
156
|
+
var nodeNeighborsArgs = z2.object({
|
|
157
|
+
snapshot: SnapshotRef.optional(),
|
|
158
|
+
id: z2.string(),
|
|
159
|
+
direction: z2.enum(["in", "out", "both"]).default("both"),
|
|
160
|
+
edge_kinds: z2.array(z2.string()).default([]),
|
|
161
|
+
metrics: z2.array(z2.string().min(1)).max(10).default(["loc", "utilization"]),
|
|
162
|
+
offset: z2.number().int().min(0).default(0),
|
|
163
|
+
limit: z2.number().int().min(1).max(100).default(20)
|
|
164
|
+
});
|
|
165
|
+
var Neighbor = z2.object({
|
|
166
|
+
node: NodeRef,
|
|
167
|
+
kind: z2.string(),
|
|
168
|
+
weight: z2.number().nullable(),
|
|
169
|
+
specifier: z2.string().optional(),
|
|
170
|
+
values: z2.record(z2.string(), z2.number().nullable())
|
|
171
|
+
});
|
|
172
|
+
var nodeNeighborsResult = z2.object({
|
|
173
|
+
snapshotId: z2.number().int(),
|
|
174
|
+
node: NodeRef,
|
|
175
|
+
inbound: z2.array(Neighbor),
|
|
176
|
+
outbound: z2.array(Neighbor),
|
|
177
|
+
/** Edges in each direction before `offset` and `limit`; zero for a direction not asked for. */
|
|
178
|
+
total: z2.object({ inbound: z2.number().int(), outbound: z2.number().int() })
|
|
179
|
+
});
|
|
180
|
+
var NODE_NEIGHBORS = {
|
|
181
|
+
description: "Edges into and out of one stored node (file, symbol, module, or external), heaviest first, each with the neighbour's metric values. Empty `edge_kinds` means every kind except 'references' unless the node is a symbol.",
|
|
182
|
+
args: nodeNeighborsArgs,
|
|
183
|
+
result: nodeNeighborsResult
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// src/query/contract-hotspots.ts
|
|
187
|
+
import { z as z3 } from "zod";
|
|
188
|
+
var HOTSPOTS_PAGE_MAX = 500;
|
|
189
|
+
var ChurnWindow = z3.string().regex(/^(?:[1-9]\d*d|lifetime)$/);
|
|
190
|
+
var HotspotMark = z3.enum(["new", "worsened"]);
|
|
191
|
+
var Hotspot = z3.object({
|
|
192
|
+
node: NodeRef,
|
|
193
|
+
/** Commits touching the file in the window; a symbol reads its declaring file's churn. */
|
|
194
|
+
churn: z3.number(),
|
|
195
|
+
/** File: max cognitive complexity over its functions (cyclomatic when cognitive is absent). Symbol: its own, else its file's. */
|
|
196
|
+
complexity: z3.number(),
|
|
197
|
+
/** The age discount applied to the score, 1 for none; the symbol grain applies none. */
|
|
198
|
+
recency: z3.number(),
|
|
199
|
+
/** File: round(churn × complexity × recency). Symbol: utilization × complexity × churn (blast radius). */
|
|
200
|
+
score: z3.number(),
|
|
201
|
+
/** Symbol grain only. */
|
|
202
|
+
utilization: z3.number().optional(),
|
|
203
|
+
/** Present only with a baseline; null when the node was no hotspot there. */
|
|
204
|
+
baselineScore: z3.number().nullable().optional(),
|
|
205
|
+
/** Present only with a baseline, and only for a row that is new or scores higher than at the baseline. */
|
|
206
|
+
mark: HotspotMark.optional()
|
|
207
|
+
});
|
|
208
|
+
var hotspotsListArgs = z3.object({
|
|
209
|
+
snapshot: SnapshotRef.optional(),
|
|
210
|
+
baseline: SnapshotRef.optional(),
|
|
211
|
+
grain: z3.enum(["file", "symbol"]).default("file"),
|
|
212
|
+
window: ChurnWindow.default("30d"),
|
|
213
|
+
/** Keep rows scoring at least this; the threshold is the caller's policy. */
|
|
214
|
+
cutoff: z3.number().min(0).optional(),
|
|
215
|
+
offset: z3.number().int().min(0).default(0),
|
|
216
|
+
limit: z3.number().int().min(0).max(HOTSPOTS_PAGE_MAX).default(20)
|
|
217
|
+
});
|
|
218
|
+
var hotspotsListResult = z3.object({
|
|
219
|
+
snapshotId: z3.number().int(),
|
|
220
|
+
...BaselineFields,
|
|
221
|
+
rows: z3.array(Hotspot),
|
|
222
|
+
/** Rows at or above the cutoff, before `offset` and `limit`. */
|
|
223
|
+
total: z3.number().int()
|
|
224
|
+
});
|
|
225
|
+
var HOTSPOTS_LIST = {
|
|
226
|
+
description: "Files or symbols ranked by hotspot score, highest first, offset-paginated. File grain scores churn \xD7 complexity \xD7 recency over the churn `window`; symbol grain scores blast radius, utilization \xD7 complexity \xD7 file churn. Generated files and their symbols are left out. `cutoff` keeps rows scoring at least that. With `baseline`, each row gets its baseline score and is marked new or worsened.",
|
|
227
|
+
args: hotspotsListArgs,
|
|
228
|
+
result: hotspotsListResult
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// src/query/contract-nodes.ts
|
|
232
|
+
import { z as z5 } from "zod";
|
|
233
|
+
|
|
234
|
+
// src/query/contract-node-lenses.ts
|
|
235
|
+
import { z as z4 } from "zod";
|
|
236
|
+
var NodeLens = z4.enum(["exports", "score", "centrality", "coupling", "tests"]);
|
|
237
|
+
var ExportRow = z4.object({
|
|
238
|
+
name: z4.string(),
|
|
239
|
+
utilization: z4.number(),
|
|
240
|
+
/** The symbol's own cognitive complexity; null for a class, a type, or a re-export. */
|
|
241
|
+
cognitive: z4.number().nullable(),
|
|
242
|
+
/** Distinct files that reference the symbol. */
|
|
243
|
+
consumers: z4.number().int(),
|
|
244
|
+
exported: z4.boolean()
|
|
245
|
+
});
|
|
246
|
+
var ScoreBreakdown = z4.object({
|
|
247
|
+
grain: z4.enum(["file", "symbol"]),
|
|
248
|
+
window: z4.string(),
|
|
249
|
+
score: z4.number(),
|
|
250
|
+
churn: z4.number().optional(),
|
|
251
|
+
complexity: z4.number().optional(),
|
|
252
|
+
recency: z4.number().optional(),
|
|
253
|
+
/** Symbol grain only. */
|
|
254
|
+
utilization: z4.number().optional(),
|
|
255
|
+
/** 1 is the highest score; null at a score of 0. */
|
|
256
|
+
rank: z4.number().int().nullable(),
|
|
257
|
+
/** Nodes of the grain with a non-zero score. */
|
|
258
|
+
ranked: z4.number().int()
|
|
259
|
+
});
|
|
260
|
+
var Centrality = z4.object({
|
|
261
|
+
score: z4.number().nullable(),
|
|
262
|
+
rank: z4.number().int().nullable(),
|
|
263
|
+
/** Files in the reading order; every one of them has a rank. */
|
|
264
|
+
of: z4.number().int()
|
|
265
|
+
});
|
|
266
|
+
var CoupledPartners = z4.object({
|
|
267
|
+
/** False until co-change pairs are stored in the index; `partners` is then empty. */
|
|
268
|
+
measured: z4.boolean(),
|
|
269
|
+
partners: z4.array(z4.object({ node: NodeRef, count: z4.number().int() }))
|
|
270
|
+
});
|
|
271
|
+
var LinkedTests = z4.object({
|
|
272
|
+
/** Test files linked by path convention, such as `a.test.ts` beside `a.ts`. */
|
|
273
|
+
tests: z4.array(NodeRef),
|
|
274
|
+
/** The indexer's `linked_test_count`, which also counts tests linked by co-editing; null when the index holds no such metric. */
|
|
275
|
+
indexedCount: z4.number().int().nullable(),
|
|
276
|
+
/** False until co-change pairs are stored, so `tests` holds path-linked tests only. */
|
|
277
|
+
coEditMeasured: z4.boolean()
|
|
278
|
+
});
|
|
279
|
+
var NodeLenses = z4.object({
|
|
280
|
+
exports: z4.array(ExportRow).nullable().optional(),
|
|
281
|
+
score: ScoreBreakdown.nullable().optional(),
|
|
282
|
+
centrality: Centrality.nullable().optional(),
|
|
283
|
+
coupling: CoupledPartners.nullable().optional(),
|
|
284
|
+
tests: LinkedTests.nullable().optional()
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// src/query/contract-nodes.ts
|
|
288
|
+
var HIERARCHY_ROW_CAP = 5e3;
|
|
289
|
+
var MetricNames = z5.array(z5.string().min(1)).max(40);
|
|
290
|
+
var BaselineFields2 = {
|
|
291
|
+
baselineSnapshotId: z5.number().int().optional(),
|
|
292
|
+
comparable: z5.boolean().optional()
|
|
293
|
+
};
|
|
294
|
+
var hierarchyArgs = z5.object({
|
|
295
|
+
snapshot: SnapshotRef.optional(),
|
|
296
|
+
root: z5.string().optional(),
|
|
297
|
+
depth: z5.number().int().min(1).max(8).default(2),
|
|
298
|
+
metrics: MetricNames.default(["loc"]),
|
|
299
|
+
baseline: SnapshotRef.optional(),
|
|
300
|
+
include_symbols: z5.boolean().default(false),
|
|
301
|
+
exclude_roles: z5.array(z5.string()).default([])
|
|
302
|
+
});
|
|
303
|
+
var HierarchyRow = NodeRef.extend({
|
|
304
|
+
parentId: z5.string().nullable(),
|
|
305
|
+
/** Relative to the requested root, which is 0. */
|
|
306
|
+
depth: z5.number().int(),
|
|
307
|
+
/** Children in the full tree, symbols included, whatever `depth` and `include_symbols` returned. */
|
|
308
|
+
childCount: z5.number().int(),
|
|
309
|
+
role: z5.string().optional(),
|
|
310
|
+
values: z5.record(z5.string(), z5.number().nullable()),
|
|
311
|
+
missing: z5.record(z5.string(), MissingReason).optional(),
|
|
312
|
+
deltas: z5.record(z5.string(), z5.number().nullable()).optional()
|
|
313
|
+
});
|
|
314
|
+
var hierarchyResult = z5.object({
|
|
315
|
+
snapshotId: z5.number().int(),
|
|
316
|
+
...BaselineFields2,
|
|
317
|
+
nodes: z5.array(HierarchyRow),
|
|
318
|
+
truncated: z5.boolean()
|
|
319
|
+
});
|
|
320
|
+
var HIERARCHY_GET = {
|
|
321
|
+
description: "A subtree of the synthesized repo, directory, file, and symbol hierarchy as a flat list with parent ids, shallowest first. Directory values roll up per the metric catalogue; a null value says why in `missing`.",
|
|
322
|
+
args: hierarchyArgs,
|
|
323
|
+
result: hierarchyResult
|
|
324
|
+
};
|
|
325
|
+
var nodeGetArgs = z5.object({
|
|
326
|
+
snapshot: SnapshotRef.optional(),
|
|
327
|
+
id: z5.string(),
|
|
328
|
+
baseline: SnapshotRef.optional(),
|
|
329
|
+
metrics: MetricNames.default([]),
|
|
330
|
+
/** Extra readings of the node; with none the result carries no `lenses` field. */
|
|
331
|
+
lenses: z5.array(NodeLens).max(NodeLens.options.length).default([]),
|
|
332
|
+
/** The churn window the `score` lens reads. */
|
|
333
|
+
window: ChurnWindow.default("30d")
|
|
334
|
+
});
|
|
335
|
+
var NodeMetric = z5.object({
|
|
336
|
+
name: z5.string(),
|
|
337
|
+
unit: z5.string().nullable(),
|
|
338
|
+
direction: MetricDescriptor.shape.direction,
|
|
339
|
+
rollup: MetricDescriptor.shape.rollup,
|
|
340
|
+
value: z5.number().nullable(),
|
|
341
|
+
missing: MissingReason.optional(),
|
|
342
|
+
percentile: z5.number().nullable(),
|
|
343
|
+
siblingMedian: z5.number().nullable(),
|
|
344
|
+
siblingRank: z5.number().int().nullable(),
|
|
345
|
+
siblingCount: z5.number().int(),
|
|
346
|
+
baseline: z5.number().nullable().optional(),
|
|
347
|
+
delta: z5.number().nullable().optional()
|
|
348
|
+
});
|
|
349
|
+
var nodeGetResult = z5.object({
|
|
350
|
+
snapshotId: z5.number().int(),
|
|
351
|
+
...BaselineFields2,
|
|
352
|
+
node: NodeRef.extend({
|
|
353
|
+
language: z5.string().optional(),
|
|
354
|
+
role: z5.string().optional(),
|
|
355
|
+
exported: z5.boolean().optional(),
|
|
356
|
+
signature: z5.string().optional(),
|
|
357
|
+
purpose: z5.string().optional()
|
|
358
|
+
}),
|
|
359
|
+
ancestors: z5.array(NodeRef),
|
|
360
|
+
childCounts: z5.record(z5.string(), z5.number().int()),
|
|
361
|
+
metrics: z5.array(NodeMetric),
|
|
362
|
+
/** Present only when `lenses` asked for any. */
|
|
363
|
+
lenses: NodeLenses.optional()
|
|
364
|
+
});
|
|
365
|
+
var NODE_GET = {
|
|
366
|
+
description: "One node by id (a directory id ends in '/', the repo is ''): its containing chain, child counts, and each metric with direction, percentile among same-kind nodes, sibling median and rank, and a delta against an optional baseline. Optional `lenses` add a file's exports, its hotspot score breakdown, its centrality rank, co-changed partners (unmeasured until pairs are stored), and its linked tests.",
|
|
367
|
+
args: nodeGetArgs,
|
|
368
|
+
result: nodeGetResult
|
|
369
|
+
};
|
|
370
|
+
var nodeResolveArgs = z5.object({
|
|
371
|
+
snapshot: SnapshotRef.optional(),
|
|
372
|
+
query: z5.string().min(1).optional(),
|
|
373
|
+
path: z5.string().min(1).optional(),
|
|
374
|
+
line: z5.number().int().min(1).optional(),
|
|
375
|
+
limit: z5.number().int().min(1).max(50).default(10)
|
|
376
|
+
});
|
|
377
|
+
var nodeResolveResult = z5.object({
|
|
378
|
+
candidates: z5.array(
|
|
379
|
+
z5.object({
|
|
380
|
+
node: NodeRef,
|
|
381
|
+
score: z5.number(),
|
|
382
|
+
/** Open string: "exact" | "span" | "suffix" | "prefix" | "substring" today. */
|
|
383
|
+
match: z5.string()
|
|
384
|
+
})
|
|
385
|
+
)
|
|
386
|
+
});
|
|
387
|
+
var NODE_RESOLVE = {
|
|
388
|
+
description: "Turn a loose reference into ranked candidate node ids. Pass exactly one of `query` (a path, path:line, symbol name, or file#Scope.name) or `path` with an optional `line`; a line resolves to the innermost symbol whose span contains it.",
|
|
389
|
+
args: nodeResolveArgs,
|
|
390
|
+
result: nodeResolveResult
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
// src/query/contract-overview.ts
|
|
394
|
+
import { DEFAULT_HEALTH_WEIGHTS } from "@titan-design/code-graph/analysis";
|
|
395
|
+
import { z as z6 } from "zod";
|
|
396
|
+
var OVERVIEW_ROWS_MAX = 50;
|
|
397
|
+
var AttentionSignalKey = z6.enum(["hotspots", "findings", "complexity", "hidden-coupling"]);
|
|
398
|
+
var AttentionSignal = z6.object({
|
|
399
|
+
key: AttentionSignalKey,
|
|
400
|
+
label: z6.string(),
|
|
401
|
+
penalty: z6.number(),
|
|
402
|
+
cap: z6.number(),
|
|
403
|
+
/** False when the index does not hold the signal's input yet; its penalty is then 0. */
|
|
404
|
+
measured: z6.boolean(),
|
|
405
|
+
detail: z6.string()
|
|
406
|
+
});
|
|
407
|
+
var LookFirstReason = z6.enum(["over-cutoff", "findings", "churn-complexity"]);
|
|
408
|
+
var LookFirstRow = z6.object({
|
|
409
|
+
node: NodeRef,
|
|
410
|
+
score: z6.number(),
|
|
411
|
+
churn: z6.number(),
|
|
412
|
+
complexity: z6.number(),
|
|
413
|
+
recency: z6.number(),
|
|
414
|
+
/** Why the row is here; `churn-complexity` only when no other reason applies. */
|
|
415
|
+
reasons: z6.array(LookFirstReason)
|
|
416
|
+
});
|
|
417
|
+
var ReadingOrderRow = z6.object({ node: NodeRef, centrality: z6.number() });
|
|
418
|
+
var weightShape = (w) => ({ each: z6.number().min(0).default(w.each), cap: z6.number().min(0).default(w.cap) });
|
|
419
|
+
var weight = (w) => z6.object(weightShape(w)).prefault({});
|
|
420
|
+
var { findings, complexity } = DEFAULT_HEALTH_WEIGHTS;
|
|
421
|
+
var signalWeights = z6.object({
|
|
422
|
+
hotspots: weight(DEFAULT_HEALTH_WEIGHTS.hotspots),
|
|
423
|
+
findings: z6.object({
|
|
424
|
+
each_new: z6.number().min(0).default(findings.eachNew),
|
|
425
|
+
each_carry: z6.number().min(0).default(findings.eachCarry),
|
|
426
|
+
cap: z6.number().min(0).default(findings.cap)
|
|
427
|
+
}).prefault({}),
|
|
428
|
+
complexity: z6.object({ ...weightShape(complexity), budget: z6.number().min(0).default(complexity.budget) }).prefault({}),
|
|
429
|
+
hidden_coupling: weight(DEFAULT_HEALTH_WEIGHTS.hiddenCoupling)
|
|
430
|
+
}).prefault({});
|
|
431
|
+
var overviewGetArgs = z6.object({
|
|
432
|
+
snapshot: SnapshotRef.optional(),
|
|
433
|
+
baseline: SnapshotRef.optional(),
|
|
434
|
+
window: ChurnWindow.default("30d"),
|
|
435
|
+
/** A file scoring at least this is over the cutoff; the threshold is the caller's policy. */
|
|
436
|
+
cutoff: z6.number().min(0).default(3e3),
|
|
437
|
+
weights: signalWeights,
|
|
438
|
+
/** Rules left out of the findings signal, such as one already counted as a hotspot over the cutoff. */
|
|
439
|
+
exclude_rules: z6.array(z6.string()).default([]),
|
|
440
|
+
/** Also return `combined`, 100 minus every signal's penalty. */
|
|
441
|
+
combined: z6.boolean().default(false),
|
|
442
|
+
reading_limit: z6.number().int().min(0).max(OVERVIEW_ROWS_MAX).default(6),
|
|
443
|
+
look_limit: z6.number().int().min(0).max(OVERVIEW_ROWS_MAX).default(8)
|
|
444
|
+
});
|
|
445
|
+
var FindingCounts = z6.object({
|
|
446
|
+
open: z6.number().int(),
|
|
447
|
+
/** Status counts against the baseline; present only with one. */
|
|
448
|
+
new: z6.number().int().optional(),
|
|
449
|
+
carryover: z6.number().int().optional(),
|
|
450
|
+
resolved: z6.number().int().optional()
|
|
451
|
+
});
|
|
452
|
+
var overviewGetResult = z6.object({
|
|
453
|
+
snapshotId: z6.number().int(),
|
|
454
|
+
...BaselineFields,
|
|
455
|
+
kpis: z6.object({
|
|
456
|
+
hotspotsOverCutoff: z6.number().int(),
|
|
457
|
+
/** Highest file complexity among files with churn in the window. */
|
|
458
|
+
maxComplexity: z6.number(),
|
|
459
|
+
/** Files with a bus factor of 1 in the window. */
|
|
460
|
+
knowledgeSilos: z6.number().int(),
|
|
461
|
+
findings: FindingCounts
|
|
462
|
+
}),
|
|
463
|
+
signals: z6.array(AttentionSignal),
|
|
464
|
+
/** Present only when asked for: one number hides which signal moved, so read `signals` first. */
|
|
465
|
+
combined: z6.number().optional(),
|
|
466
|
+
readingOrder: z6.array(ReadingOrderRow),
|
|
467
|
+
lookFirst: z6.array(LookFirstRow)
|
|
468
|
+
});
|
|
469
|
+
var OVERVIEW_GET = {
|
|
470
|
+
description: "Where to look first in a snapshot: KPIs, named attention signals (hotspots over `cutoff`, findings, complexity over budget, hidden coupling) each weighted and capped by the caller's `weights`, a reading order of files by PageRank centrality, and the top hotspots with the reasons each is listed. With `baseline`, findings count as new, carried over, or resolved. `combined` optionally adds 100 minus the penalties; it is a secondary summary, not a verdict.",
|
|
471
|
+
args: overviewGetArgs,
|
|
472
|
+
result: overviewGetResult
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
// src/query/contract-changes.ts
|
|
476
|
+
import { z as z7 } from "zod";
|
|
477
|
+
var CHANGES_ROWS_MAX = 500;
|
|
478
|
+
var ScoreChange = z7.object({
|
|
479
|
+
node: NodeRef,
|
|
480
|
+
before: z7.number(),
|
|
481
|
+
after: z7.number(),
|
|
482
|
+
delta: z7.number()
|
|
483
|
+
});
|
|
484
|
+
var NewFile = z7.object({ node: NodeRef, score: z7.number() });
|
|
485
|
+
var FindingChange = z7.object({ finding: Finding, before: z7.number(), delta: z7.number() });
|
|
486
|
+
var Regression = ScoreChange.extend({ findings: z7.array(z7.string()) });
|
|
487
|
+
var CoChangePair = z7.object({ a: NodeRef, b: NodeRef, count: z7.number().int() });
|
|
488
|
+
var changesGetArgs = z7.object({
|
|
489
|
+
snapshot: SnapshotRef.optional(),
|
|
490
|
+
baseline: SnapshotRef,
|
|
491
|
+
window: ChurnWindow.default("30d"),
|
|
492
|
+
/** A file scoring at least this is over the cutoff; the threshold is the caller's policy. */
|
|
493
|
+
cutoff: z7.number().min(0).default(3e3),
|
|
494
|
+
limit: z7.number().int().min(0).max(CHANGES_ROWS_MAX).default(20)
|
|
495
|
+
});
|
|
496
|
+
var changesGetResult = z7.object({
|
|
497
|
+
snapshotId: z7.number().int(),
|
|
498
|
+
baselineSnapshotId: z7.number().int(),
|
|
499
|
+
/** False across index versions; every list is then empty, because the two indexes measure differently. */
|
|
500
|
+
comparable: z7.boolean(),
|
|
501
|
+
files: z7.object({
|
|
502
|
+
/** Below the cutoff at the baseline, at or above it now; highest delta first. */
|
|
503
|
+
crossedCutoff: z7.array(ScoreChange),
|
|
504
|
+
/** Files the baseline does not hold; highest score first. */
|
|
505
|
+
added: z7.array(NewFile)
|
|
506
|
+
}),
|
|
507
|
+
findings: z7.object({
|
|
508
|
+
new: z7.array(Finding),
|
|
509
|
+
worsened: z7.array(FindingChange),
|
|
510
|
+
improved: z7.array(FindingChange),
|
|
511
|
+
resolved: z7.array(Finding)
|
|
512
|
+
}),
|
|
513
|
+
coupling: z7.object({
|
|
514
|
+
/** False until co-change pairs are stored in the index; `added` is then empty. */
|
|
515
|
+
measured: z7.boolean(),
|
|
516
|
+
added: z7.array(CoChangePair)
|
|
517
|
+
}),
|
|
518
|
+
regressions: z7.array(Regression),
|
|
519
|
+
counts: z7.object({
|
|
520
|
+
crossedCutoff: z7.number().int(),
|
|
521
|
+
newFiles: z7.number().int(),
|
|
522
|
+
newFindings: z7.number().int(),
|
|
523
|
+
worsened: z7.number().int(),
|
|
524
|
+
improved: z7.number().int(),
|
|
525
|
+
resolved: z7.number().int(),
|
|
526
|
+
newCoupling: z7.number().int(),
|
|
527
|
+
regressions: z7.number().int()
|
|
528
|
+
})
|
|
529
|
+
});
|
|
530
|
+
var CHANGES_GET = {
|
|
531
|
+
description: "What changed between a `baseline` snapshot and this one: files that crossed the hotspot `cutoff`, files the baseline does not hold, findings new, worsened, improved, or resolved, new co-change coupling (unmeasured until pairs are stored), and regressions: files whose hotspot score rose and that carry an open finding. Each list holds at most `limit` rows; `counts` holds the full sizes. Across index versions `comparable` is false and every list is empty.",
|
|
532
|
+
args: changesGetArgs,
|
|
533
|
+
result: changesGetResult
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
// src/query/contract-paths-impact.ts
|
|
537
|
+
import { z as z8 } from "zod";
|
|
538
|
+
var PATHS_IMPACT_MAX = 500;
|
|
539
|
+
var FindingDelta = z8.object({
|
|
540
|
+
new: z8.number().int(),
|
|
541
|
+
worsened: z8.number().int(),
|
|
542
|
+
improved: z8.number().int(),
|
|
543
|
+
resolved: z8.number().int()
|
|
544
|
+
});
|
|
545
|
+
var PathDelta = z8.object({
|
|
546
|
+
inBaseline: z8.boolean(),
|
|
547
|
+
/** A file with no score at the baseline counts as 0, as in `changes.get`; null when the baseline does not hold it. */
|
|
548
|
+
scoreBefore: z8.number().nullable(),
|
|
549
|
+
/** Current score minus `scoreBefore`; null when `scoreBefore` is. */
|
|
550
|
+
score: z8.number().nullable(),
|
|
551
|
+
complexityBefore: z8.number().nullable(),
|
|
552
|
+
/** Current complexity minus `complexityBefore`; null when either is unmeasured. */
|
|
553
|
+
complexity: z8.number().nullable(),
|
|
554
|
+
findings: FindingDelta
|
|
555
|
+
});
|
|
556
|
+
var PathHotspot = z8.object({
|
|
557
|
+
/** The file grain's score over the window, as `hotspots.list` scores it; 0 when the file has no churn or no complexity. */
|
|
558
|
+
score: z8.number(),
|
|
559
|
+
/** The file's position in `hotspots.list` at the same window, 1 the highest; null at a score of 0. */
|
|
560
|
+
rank: z8.number().int().nullable()
|
|
561
|
+
});
|
|
562
|
+
var IndexedPath = z8.object({
|
|
563
|
+
status: z8.literal("indexed"),
|
|
564
|
+
/** The path as given. */
|
|
565
|
+
input: z8.string(),
|
|
566
|
+
/** Repo-relative, the file's node id. */
|
|
567
|
+
path: z8.string(),
|
|
568
|
+
node: NodeRef,
|
|
569
|
+
/** The complexity factor of the hotspot score (max cognitive, else max cyclomatic); null when unmeasured. */
|
|
570
|
+
complexity: z8.number().nullable(),
|
|
571
|
+
hotspot: PathHotspot,
|
|
572
|
+
/** Open findings on the file or a symbol in it, worst first. With a baseline each carries a status. */
|
|
573
|
+
findings: z8.array(Finding),
|
|
574
|
+
/** Absent without a baseline; null when the baseline is not comparable (another index version). */
|
|
575
|
+
delta: PathDelta.nullable().optional()
|
|
576
|
+
});
|
|
577
|
+
var NotIndexedPath = z8.object({ status: z8.literal("not-indexed"), input: z8.string(), path: z8.string() });
|
|
578
|
+
var OutsideRepoPath = z8.object({ status: z8.literal("outside-repo"), input: z8.string() });
|
|
579
|
+
var PathImpact = z8.discriminatedUnion("status", [IndexedPath, NotIndexedPath, OutsideRepoPath]);
|
|
580
|
+
var ImpactRollup = z8.object({
|
|
581
|
+
indexed: z8.number().int(),
|
|
582
|
+
notIndexed: z8.number().int(),
|
|
583
|
+
outsideRepo: z8.number().int(),
|
|
584
|
+
/** Sum of the indexed files' hotspot scores. */
|
|
585
|
+
score: z8.number(),
|
|
586
|
+
/** Highest complexity over the indexed files; null when none is measured. */
|
|
587
|
+
maxComplexity: z8.number().nullable(),
|
|
588
|
+
/** Best (lowest) hotspot rank over the indexed files; null when none ranks. */
|
|
589
|
+
topRank: z8.number().int().nullable(),
|
|
590
|
+
openFindings: z8.number().int(),
|
|
591
|
+
/** The rows' deltas summed, plus the resolved findings of not-indexed (deleted) files that the baseline held; present and null exactly when every row's `delta` is. */
|
|
592
|
+
delta: z8.object({ score: z8.number(), findings: FindingDelta }).nullable().optional()
|
|
593
|
+
});
|
|
594
|
+
var pathsImpactArgs = z8.object({
|
|
595
|
+
snapshot: SnapshotRef.optional(),
|
|
596
|
+
baseline: SnapshotRef.optional(),
|
|
597
|
+
/** Repo-relative paths; a leading `./` is dropped. An absolute path counts only when it lies under `root`. */
|
|
598
|
+
paths: z8.array(z8.string()).max(PATHS_IMPACT_MAX),
|
|
599
|
+
/** The absolute directory the repo is checked out at, such as a worktree, so absolute paths under it can be read. A root above the index's repo root is rejected as invalid arguments. */
|
|
600
|
+
root: z8.string().optional(),
|
|
601
|
+
window: ChurnWindow.default("30d")
|
|
602
|
+
});
|
|
603
|
+
var pathsImpactResult = z8.object({
|
|
604
|
+
snapshotId: z8.number().int(),
|
|
605
|
+
...BaselineFields,
|
|
606
|
+
/** One row per distinct path, in the order first given. */
|
|
607
|
+
rows: z8.array(PathImpact),
|
|
608
|
+
/** Files with a non-zero hotspot score at the window; the denominator for `hotspot.rank`. */
|
|
609
|
+
ranked: z8.number().int(),
|
|
610
|
+
rollup: ImpactRollup
|
|
611
|
+
});
|
|
612
|
+
var PATHS_IMPACT = {
|
|
613
|
+
description: "What a set of files weighs in the code graph, such as the files a change touches: per file its complexity, its hotspot score and rank as `hotspots.list` ranks them at `window`, and its open findings, plus a rollup. A path the snapshot does not hold answers status 'not-indexed', and one outside the repo 'outside-repo'; neither is an error. With `baseline`, each indexed row gets a score, complexity, and findings delta, and the rollup delta also counts the resolved findings of a not-indexed path the baseline held (a deleted file); without one, `delta` is absent.",
|
|
614
|
+
args: pathsImpactArgs,
|
|
615
|
+
result: pathsImpactResult
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
// src/query/contract-packages.ts
|
|
619
|
+
import { z as z9 } from "zod";
|
|
620
|
+
var PACKAGES_MAX = 500;
|
|
621
|
+
var PackageLayer = z9.discriminatedUnion("status", [
|
|
622
|
+
z9.object({ status: z9.literal("declared"), tier: z9.number().int(), rule: z9.string() }),
|
|
623
|
+
z9.object({ status: z9.literal("undeclared") })
|
|
624
|
+
]);
|
|
625
|
+
var PackageStatsRow = z9.object({
|
|
626
|
+
/** The package root, a repo-relative path prefix such as `packages/cli`. */
|
|
627
|
+
id: z9.string(),
|
|
628
|
+
/** The root's last path segment; the index holds no package manifests. */
|
|
629
|
+
name: z9.string(),
|
|
630
|
+
fileCount: z9.number().int(),
|
|
631
|
+
internalEdges: z9.number().int(),
|
|
632
|
+
outgoingEdges: z9.number().int(),
|
|
633
|
+
incomingEdges: z9.number().int(),
|
|
634
|
+
/** internal / (internal + outgoing); 0 with neither. */
|
|
635
|
+
cohesion: z9.number(),
|
|
636
|
+
/** outgoing / (outgoing + incoming), Martin's I; 0 with neither. */
|
|
637
|
+
instability: z9.number(),
|
|
638
|
+
/** Share of the package's files with role "types", a stand-in for Martin's A. */
|
|
639
|
+
abstractness: z9.number(),
|
|
640
|
+
/** code-graph's instability band: at most 0.3 foundation, at least 0.9 top. Measured, unlike `layer`. */
|
|
641
|
+
band: z9.enum(["foundation", "middle", "top"]),
|
|
642
|
+
/** Open string: "weak-boundary" today. */
|
|
643
|
+
flags: z9.array(z9.string()),
|
|
644
|
+
layer: PackageLayer
|
|
645
|
+
});
|
|
646
|
+
var CrossEdge = z9.object({
|
|
647
|
+
from: z9.string(),
|
|
648
|
+
to: z9.string(),
|
|
649
|
+
edges: z9.number().int(),
|
|
650
|
+
/** edges / files(from). */
|
|
651
|
+
intensity: z9.number(),
|
|
652
|
+
/** At least 0.6 tight, at least 0.3 moderate. */
|
|
653
|
+
flag: z9.enum(["tight", "moderate", "none"])
|
|
654
|
+
});
|
|
655
|
+
var packagesStatsArgs = z9.object({
|
|
656
|
+
snapshot: SnapshotRef.optional(),
|
|
657
|
+
/** Package roots as repo-relative path prefixes; omitted means every package the tier config declares. */
|
|
658
|
+
packages: z9.array(z9.string().min(1)).max(PACKAGES_MAX).optional()
|
|
659
|
+
});
|
|
660
|
+
var packagesStatsResult = z9.object({
|
|
661
|
+
snapshotId: z9.number().int(),
|
|
662
|
+
/** Where the package roots came from: the `packages` argument, or the `layered-deps` rules; `none` when neither names one. */
|
|
663
|
+
packagesFrom: z9.enum(["args", "tiers", "none"]),
|
|
664
|
+
/** Newman-Girvan modularity Q of the package partition; 0 with no edges. */
|
|
665
|
+
modularity: z9.number(),
|
|
666
|
+
/** File-to-file edges with both ends in a package, internal and cross. */
|
|
667
|
+
totalEdges: z9.number().int(),
|
|
668
|
+
/** Files counted under no package root. */
|
|
669
|
+
unassignedFiles: z9.number().int(),
|
|
670
|
+
/** One row per package root, by id. */
|
|
671
|
+
packages: z9.array(PackageStatsRow),
|
|
672
|
+
/** By `from`, then `to`. */
|
|
673
|
+
crossEdges: z9.array(CrossEdge)
|
|
674
|
+
});
|
|
675
|
+
var PACKAGES_STATS = {
|
|
676
|
+
description: "Package structure for a snapshot: per package its file count, internal, outgoing, and incoming edges, cohesion, instability, abstractness, and its tier in the repo's layered-deps rules, plus the package-to-package edges and the snapshot's modularity. Test and fixture files are left out, as code-graph's arch view leaves them out. A package no tier names has layer status 'undeclared'.",
|
|
677
|
+
args: packagesStatsArgs,
|
|
678
|
+
result: packagesStatsResult
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
// src/query/contract.ts
|
|
682
|
+
var CODE_READ_API_VERSION = "0.1.8";
|
|
683
|
+
var describeArgs = z10.object({});
|
|
684
|
+
var describeResult = z10.object({
|
|
685
|
+
api: z10.string(),
|
|
686
|
+
dataset: z10.enum(["live", "static"]),
|
|
687
|
+
commands: z10.array(z10.string()),
|
|
688
|
+
newest: SnapshotInfo.nullable(),
|
|
689
|
+
indexVersions: z10.array(z10.string()),
|
|
690
|
+
capabilities: Capabilities,
|
|
691
|
+
metrics: z10.array(MetricDescriptor),
|
|
692
|
+
rules: z10.array(RuleSummary)
|
|
693
|
+
});
|
|
694
|
+
var snapshotListArgs = z10.object({
|
|
695
|
+
ref: z10.string().min(1).optional(),
|
|
696
|
+
limit: z10.number().int().min(1).max(500).default(50)
|
|
697
|
+
});
|
|
698
|
+
var snapshotListResult = z10.object({ snapshots: z10.array(SnapshotInfo) });
|
|
699
|
+
var CONTRACT = {
|
|
700
|
+
"api.describe": {
|
|
701
|
+
description: "Contract version, commands served, capabilities, the newest snapshot, and the metric and rule catalogues. Call first to pin compatibility.",
|
|
702
|
+
args: describeArgs,
|
|
703
|
+
result: describeResult
|
|
704
|
+
},
|
|
705
|
+
"snapshot.list": {
|
|
706
|
+
description: "Indexed snapshots, newest first, with ref, commit, time, and index version. Optionally one ref only.",
|
|
707
|
+
args: snapshotListArgs,
|
|
708
|
+
result: snapshotListResult
|
|
709
|
+
},
|
|
710
|
+
"hierarchy.get": HIERARCHY_GET,
|
|
711
|
+
"node.get": NODE_GET,
|
|
712
|
+
"node.resolve": NODE_RESOLVE,
|
|
713
|
+
"findings.list": FINDINGS_LIST,
|
|
714
|
+
"finding.get": FINDING_GET,
|
|
715
|
+
"node.neighbors": NODE_NEIGHBORS,
|
|
716
|
+
"hotspots.list": HOTSPOTS_LIST,
|
|
717
|
+
"overview.get": OVERVIEW_GET,
|
|
718
|
+
"changes.get": CHANGES_GET,
|
|
719
|
+
"paths.impact": PATHS_IMPACT,
|
|
720
|
+
"packages.stats": PACKAGES_STATS
|
|
721
|
+
};
|
|
722
|
+
var COMMAND_NAMES = Object.keys(CONTRACT).sort();
|
|
723
|
+
var AGENT_COMMANDS = [
|
|
724
|
+
"api.describe",
|
|
725
|
+
"finding.get",
|
|
726
|
+
"findings.list",
|
|
727
|
+
"node.get",
|
|
728
|
+
"node.neighbors",
|
|
729
|
+
"node.resolve"
|
|
730
|
+
];
|
|
731
|
+
function serializeContract() {
|
|
732
|
+
const commands = {};
|
|
733
|
+
for (const name of COMMAND_NAMES) {
|
|
734
|
+
const { args, result } = CONTRACT[name];
|
|
735
|
+
commands[name] = { args: toJSONSchema(args, { io: "input" }), result: toJSONSchema(result) };
|
|
736
|
+
}
|
|
737
|
+
return { api: CODE_READ_API_VERSION, commands };
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// src/query/model.ts
|
|
741
|
+
function buildReadModel(parts) {
|
|
742
|
+
const nodeById = new Map(parts.nodes.map((n) => [n.id, n]));
|
|
743
|
+
return {
|
|
744
|
+
snapshot: parts.snapshot,
|
|
745
|
+
nodes: parts.nodes,
|
|
746
|
+
nodeById,
|
|
747
|
+
edges: parts.edges,
|
|
748
|
+
aliases: parts.aliases,
|
|
749
|
+
metrics: indexMetrics(parts.metrics),
|
|
750
|
+
metricCatalogue: buildCatalogue(parts, nodeById),
|
|
751
|
+
findings: parts.findings ?? [],
|
|
752
|
+
rules: parts.rules ?? []
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
function indexMetrics(metrics) {
|
|
756
|
+
const byName = /* @__PURE__ */ new Map();
|
|
757
|
+
for (const m of metrics) {
|
|
758
|
+
let values = byName.get(m.name);
|
|
759
|
+
if (!values) byName.set(m.name, values = /* @__PURE__ */ new Map());
|
|
760
|
+
values.set(m.nodeId, m.value);
|
|
761
|
+
}
|
|
762
|
+
return byName;
|
|
763
|
+
}
|
|
764
|
+
function observeMetrics(metrics, nodeById) {
|
|
765
|
+
const observed = /* @__PURE__ */ new Map();
|
|
766
|
+
for (const m of metrics) {
|
|
767
|
+
let entry = observed.get(m.name);
|
|
768
|
+
if (!entry) observed.set(m.name, entry = { unit: m.unit ?? null, kinds: /* @__PURE__ */ new Set() });
|
|
769
|
+
const kind = nodeById.get(m.nodeId)?.kind;
|
|
770
|
+
if (kind) entry.kinds.add(kind);
|
|
771
|
+
}
|
|
772
|
+
return observed;
|
|
773
|
+
}
|
|
774
|
+
function buildCatalogue(parts, nodeById) {
|
|
775
|
+
const observed = observeMetrics(parts.metrics, nodeById);
|
|
776
|
+
const engine = `code-graph@${parts.snapshot.indexVersion}`;
|
|
777
|
+
return [...observed.keys()].sort().map((name) => {
|
|
778
|
+
const entry = parts.describe(name);
|
|
779
|
+
return entry ? fromCatalogue(entry, engine) : uncatalogued(name, observed.get(name), engine);
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
function fromCatalogue(entry, engine) {
|
|
783
|
+
const descriptor = {
|
|
784
|
+
name: entry.name,
|
|
785
|
+
unit: entry.unit,
|
|
786
|
+
appliesTo: [...entry.appliesTo],
|
|
787
|
+
rollup: entry.rollup,
|
|
788
|
+
direction: entry.direction,
|
|
789
|
+
absent: entry.absent,
|
|
790
|
+
description: entry.description,
|
|
791
|
+
provenance: { kind: "measured", source: `${engine}/${entry.source}` }
|
|
792
|
+
};
|
|
793
|
+
if (entry.window !== void 0) descriptor.window = entry.window;
|
|
794
|
+
return descriptor;
|
|
795
|
+
}
|
|
796
|
+
function uncatalogued(name, seen, engine) {
|
|
797
|
+
return {
|
|
798
|
+
name,
|
|
799
|
+
unit: seen.unit,
|
|
800
|
+
appliesTo: [...seen.kinds].sort(),
|
|
801
|
+
rollup: "none",
|
|
802
|
+
direction: "neutral",
|
|
803
|
+
absent: "exclude",
|
|
804
|
+
description: "Not in code-graph's metric catalogue.",
|
|
805
|
+
provenance: { kind: "measured", source: `${engine}/uncatalogued` }
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// src/query/source.ts
|
|
810
|
+
import { EXIT } from "@titan-design/rpc-protocol";
|
|
811
|
+
var ReadError = class extends Error {
|
|
812
|
+
constructor(message, code) {
|
|
813
|
+
super(message);
|
|
814
|
+
this.code = code;
|
|
815
|
+
this.name = "ReadError";
|
|
816
|
+
}
|
|
817
|
+
code;
|
|
818
|
+
};
|
|
819
|
+
function snapshotNotFound(snapshotId) {
|
|
820
|
+
return new ReadError(`No snapshot with id ${snapshotId}`, EXIT.NOINPUT);
|
|
821
|
+
}
|
|
822
|
+
function nodeNotFound(id, snapshotId) {
|
|
823
|
+
return new ReadError(`No node "${id}" in snapshot ${snapshotId}`, EXIT.NOINPUT);
|
|
824
|
+
}
|
|
825
|
+
function findingNotFound(id, snapshotId) {
|
|
826
|
+
return new ReadError(`No finding "${id}" in snapshot ${snapshotId}`, EXIT.NOINPUT);
|
|
827
|
+
}
|
|
828
|
+
function invalidArgs(message) {
|
|
829
|
+
return new ReadError(`Invalid arguments: ${message}`, EXIT.DATAERR);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// src/query/snapshot-ref.ts
|
|
833
|
+
import { EXIT as EXIT2 } from "@titan-design/rpc-protocol";
|
|
834
|
+
var DIGITS = /^\d+$/;
|
|
835
|
+
function resolveSnapshot(source, ref) {
|
|
836
|
+
const snapshots = source.snapshots();
|
|
837
|
+
if (ref === void 0) {
|
|
838
|
+
const newest = snapshots[0];
|
|
839
|
+
if (!newest) throw new ReadError("No snapshots have been indexed", EXIT2.NOINPUT);
|
|
840
|
+
return newest;
|
|
841
|
+
}
|
|
842
|
+
if (typeof ref === "number" || DIGITS.test(ref)) {
|
|
843
|
+
const id = Number(ref);
|
|
844
|
+
const found = snapshots.find((s) => s.id === id);
|
|
845
|
+
if (!found) throw snapshotNotFound(id);
|
|
846
|
+
return found;
|
|
847
|
+
}
|
|
848
|
+
const newestOfRef = snapshots.find((s) => s.ref === ref);
|
|
849
|
+
if (!newestOfRef) throw new ReadError(`No snapshot of ref "${ref}"`, EXIT2.NOINPUT);
|
|
850
|
+
return newestOfRef;
|
|
851
|
+
}
|
|
852
|
+
function modelFor(source, ref) {
|
|
853
|
+
return source.model(resolveSnapshot(source, ref).id);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// src/query/changes.ts
|
|
857
|
+
import {
|
|
858
|
+
bucketViolations,
|
|
859
|
+
computeReportDrift as computeReportDrift2,
|
|
860
|
+
hotspotScoreOf,
|
|
861
|
+
keepNode as keepNode2
|
|
862
|
+
} from "@titan-design/code-graph/analysis";
|
|
863
|
+
|
|
864
|
+
// src/query/tree.ts
|
|
865
|
+
var REPO_ID = "";
|
|
866
|
+
var SYMBOL_SEP = "#";
|
|
867
|
+
function directoryId(path) {
|
|
868
|
+
return path === "" ? REPO_ID : `${path}/`;
|
|
869
|
+
}
|
|
870
|
+
function spanOf(node) {
|
|
871
|
+
const { startLine, endLine } = node.attrs;
|
|
872
|
+
return typeof startLine === "number" && typeof endLine === "number" ? { startLine, endLine } : void 0;
|
|
873
|
+
}
|
|
874
|
+
function toRef(node) {
|
|
875
|
+
const sep = node.kind === "symbol" ? node.id.indexOf(SYMBOL_SEP) : -1;
|
|
876
|
+
const ref = { id: node.id, kind: node.kind, name: node.name, path: sep < 0 ? node.id : node.id.slice(0, sep) };
|
|
877
|
+
const span = spanOf(node);
|
|
878
|
+
if (span) ref.span = span;
|
|
879
|
+
return ref;
|
|
880
|
+
}
|
|
881
|
+
function treeNode(ref, parentId, stored) {
|
|
882
|
+
return stored ? { ref, parentId, depth: 0, stored, children: [] } : { ref, parentId, depth: 0, children: [] };
|
|
883
|
+
}
|
|
884
|
+
function dirPath(fileId) {
|
|
885
|
+
const slash = fileId.lastIndexOf("/");
|
|
886
|
+
return slash < 0 ? "" : fileId.slice(0, slash);
|
|
887
|
+
}
|
|
888
|
+
function ensureDirectory(byId, path) {
|
|
889
|
+
const id = directoryId(path);
|
|
890
|
+
const existing = byId.get(id);
|
|
891
|
+
if (existing) return existing;
|
|
892
|
+
const parent = ensureDirectory(byId, dirPath(path));
|
|
893
|
+
const name = path.slice(path.lastIndexOf("/") + 1);
|
|
894
|
+
const dir = treeNode({ id, kind: "directory", name, path }, parent.ref.id);
|
|
895
|
+
parent.children.push(dir);
|
|
896
|
+
byId.set(id, dir);
|
|
897
|
+
return dir;
|
|
898
|
+
}
|
|
899
|
+
function attach(byId, parent, stored) {
|
|
900
|
+
const node = treeNode(toRef(stored), parent.ref.id, stored);
|
|
901
|
+
parent.children.push(node);
|
|
902
|
+
byId.set(stored.id, node);
|
|
903
|
+
}
|
|
904
|
+
function containsStrictly(outer, inner) {
|
|
905
|
+
const wider = outer.endLine - outer.startLine > inner.endLine - inner.startLine;
|
|
906
|
+
return wider && outer.startLine <= inner.startLine && inner.endLine <= outer.endLine;
|
|
907
|
+
}
|
|
908
|
+
function innermostEnclosing(symbol, siblings) {
|
|
909
|
+
const span = spanOf(symbol);
|
|
910
|
+
if (!span) return void 0;
|
|
911
|
+
let best2;
|
|
912
|
+
let bestSpan;
|
|
913
|
+
for (const other of siblings) {
|
|
914
|
+
const outer = spanOf(other);
|
|
915
|
+
if (other === symbol || !outer || !containsStrictly(outer, span)) continue;
|
|
916
|
+
if (!bestSpan || outer.endLine - outer.startLine < bestSpan.endLine - bestSpan.startLine) [best2, bestSpan] = [other, outer];
|
|
917
|
+
}
|
|
918
|
+
return best2;
|
|
919
|
+
}
|
|
920
|
+
function symbolParentId(symbol, fileId, bySymbolId, siblings) {
|
|
921
|
+
const segments = symbol.name.split(".");
|
|
922
|
+
for (let n = segments.length - 1; n > 0; n--) {
|
|
923
|
+
const scopeId = `${fileId}${SYMBOL_SEP}${segments.slice(0, n).join(".")}`;
|
|
924
|
+
if (bySymbolId.has(scopeId)) return scopeId;
|
|
925
|
+
}
|
|
926
|
+
return innermostEnclosing(symbol, siblings)?.id ?? fileId;
|
|
927
|
+
}
|
|
928
|
+
function symbolsByFile(model, byId) {
|
|
929
|
+
const out = /* @__PURE__ */ new Map();
|
|
930
|
+
for (const node of model.nodes) {
|
|
931
|
+
if (node.kind !== "symbol" || node.parentId === null || !byId.has(node.parentId)) continue;
|
|
932
|
+
const list = out.get(node.parentId);
|
|
933
|
+
if (list) list.push(node);
|
|
934
|
+
else out.set(node.parentId, [node]);
|
|
935
|
+
}
|
|
936
|
+
return out;
|
|
937
|
+
}
|
|
938
|
+
function attachSymbols(model, byId) {
|
|
939
|
+
for (const [fileId, symbols] of symbolsByFile(model, byId)) {
|
|
940
|
+
const bySymbolId = new Map(symbols.map((s) => [s.id, s]));
|
|
941
|
+
const parentOf = new Map(symbols.map((s) => [s.id, symbolParentId(s, fileId, bySymbolId, symbols)]));
|
|
942
|
+
const pending = [...symbols];
|
|
943
|
+
while (pending.length > 0) {
|
|
944
|
+
const ready = pending.filter((s) => byId.has(parentOf.get(s.id)));
|
|
945
|
+
if (ready.length === 0) for (const s of pending) parentOf.set(s.id, fileId);
|
|
946
|
+
for (const s of ready) attach(byId, byId.get(parentOf.get(s.id)), s);
|
|
947
|
+
for (const s of ready) pending.splice(pending.indexOf(s), 1);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
var KIND_ORDER = { directory: 0, file: 1, symbol: 2 };
|
|
952
|
+
function compareSiblings(a, b) {
|
|
953
|
+
const byKind = (KIND_ORDER[a.ref.kind] ?? 3) - (KIND_ORDER[b.ref.kind] ?? 3);
|
|
954
|
+
if (byKind !== 0) return byKind;
|
|
955
|
+
const byLine = (a.ref.span?.startLine ?? Infinity) - (b.ref.span?.startLine ?? Infinity);
|
|
956
|
+
if (byLine !== 0 && !Number.isNaN(byLine)) return byLine;
|
|
957
|
+
return a.ref.id < b.ref.id ? -1 : a.ref.id > b.ref.id ? 1 : 0;
|
|
958
|
+
}
|
|
959
|
+
function finish(node, depth) {
|
|
960
|
+
node.depth = depth;
|
|
961
|
+
node.children.sort(compareSiblings);
|
|
962
|
+
for (const child of node.children) finish(child, depth + 1);
|
|
963
|
+
}
|
|
964
|
+
function buildTree(model, excludeRoles) {
|
|
965
|
+
const root = treeNode({ id: REPO_ID, kind: "repo", name: ".", path: "" }, null);
|
|
966
|
+
const byId = /* @__PURE__ */ new Map([[REPO_ID, root]]);
|
|
967
|
+
for (const node of model.nodes) {
|
|
968
|
+
if (node.kind !== "file" || node.role !== void 0 && excludeRoles.has(node.role)) continue;
|
|
969
|
+
attach(byId, ensureDirectory(byId, dirPath(node.id)), node);
|
|
970
|
+
}
|
|
971
|
+
attachSymbols(model, byId);
|
|
972
|
+
finish(root, 0);
|
|
973
|
+
return { root, byId };
|
|
974
|
+
}
|
|
975
|
+
var trees = /* @__PURE__ */ new WeakMap();
|
|
976
|
+
function treeFor(model, excludeRoles = []) {
|
|
977
|
+
const roles = [...new Set(excludeRoles)].sort();
|
|
978
|
+
const key = roles.join("\n");
|
|
979
|
+
let byKey = trees.get(model);
|
|
980
|
+
if (!byKey) trees.set(model, byKey = /* @__PURE__ */ new Map());
|
|
981
|
+
let tree = byKey.get(key);
|
|
982
|
+
if (!tree) byKey.set(key, tree = buildTree(model, new Set(roles)));
|
|
983
|
+
return tree;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// src/query/finding-rows.ts
|
|
987
|
+
var SEVERITY_RANK = { error: 2, warning: 1, info: 0 };
|
|
988
|
+
var CHECK_TOOL = "check";
|
|
989
|
+
function refFor(model, id) {
|
|
990
|
+
const node = model.nodeById.get(id);
|
|
991
|
+
return node ? toRef(node) : { id, kind: "unknown", name: id, path: id };
|
|
992
|
+
}
|
|
993
|
+
function excessOf(ruleType, value, threshold) {
|
|
994
|
+
if (value === void 0 || threshold === void 0) return null;
|
|
995
|
+
if (ruleType === "metric-min") return value > 0 ? threshold / value : null;
|
|
996
|
+
return threshold > 0 ? value / threshold : null;
|
|
997
|
+
}
|
|
998
|
+
function toFinding(model, f, rules) {
|
|
999
|
+
const out = {
|
|
1000
|
+
id: f.id,
|
|
1001
|
+
snapshotId: model.snapshot.id,
|
|
1002
|
+
rule: f.rule,
|
|
1003
|
+
tool: CHECK_TOOL,
|
|
1004
|
+
severity: f.severity,
|
|
1005
|
+
node: refFor(model, f.nodeId),
|
|
1006
|
+
excess: excessOf(rules.get(f.rule)?.type, f.value, f.threshold),
|
|
1007
|
+
message: f.message,
|
|
1008
|
+
provenance: { kind: "derived", source: `${CHECK_TOOL}/${f.rule}` }
|
|
1009
|
+
};
|
|
1010
|
+
if (f.destinationId !== void 0) out.destination = refFor(model, f.destinationId);
|
|
1011
|
+
if (f.ranges?.[0]) out.range = f.ranges[0];
|
|
1012
|
+
if (f.metric !== void 0) out.metric = f.metric;
|
|
1013
|
+
if (f.value !== void 0) out.value = f.value;
|
|
1014
|
+
if (f.threshold !== void 0) out.threshold = f.threshold;
|
|
1015
|
+
return out;
|
|
1016
|
+
}
|
|
1017
|
+
var rowsByModel = /* @__PURE__ */ new WeakMap();
|
|
1018
|
+
function findingsFor(model) {
|
|
1019
|
+
let rows = rowsByModel.get(model);
|
|
1020
|
+
if (!rows) {
|
|
1021
|
+
const rules = new Map(model.rules.map((r) => [r.id, r]));
|
|
1022
|
+
rowsByModel.set(model, rows = model.findings.map((f) => toFinding(model, f, rules)));
|
|
1023
|
+
}
|
|
1024
|
+
return rows;
|
|
1025
|
+
}
|
|
1026
|
+
function statusAgainst(current, before) {
|
|
1027
|
+
if (!before) return "new";
|
|
1028
|
+
if (current.excess === null || before.excess === null || current.excess === before.excess) return "carryover";
|
|
1029
|
+
return current.excess > before.excess ? "worsened" : "improved";
|
|
1030
|
+
}
|
|
1031
|
+
function withStatus(current, baseline) {
|
|
1032
|
+
const before = new Map(baseline.map((f) => [f.id, f]));
|
|
1033
|
+
const now = new Set(current.map((f) => f.id));
|
|
1034
|
+
const rows = current.map((f) => ({ ...f, status: statusAgainst(f, before.get(f.id)) }));
|
|
1035
|
+
for (const f of baseline) if (!now.has(f.id)) rows.push({ ...f, status: "resolved" });
|
|
1036
|
+
return rows;
|
|
1037
|
+
}
|
|
1038
|
+
var severityRank = (f) => SEVERITY_RANK[f.severity] ?? -1;
|
|
1039
|
+
var byText = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
1040
|
+
function byNullable(a, b, sign) {
|
|
1041
|
+
const [x, y] = [a ?? null, b ?? null];
|
|
1042
|
+
if (x === null || y === null) return x === y ? 0 : x === null ? 1 : -1;
|
|
1043
|
+
return sign * (x - y);
|
|
1044
|
+
}
|
|
1045
|
+
var PRIMARY = {
|
|
1046
|
+
severity: (sign) => (a, b) => sign * (severityRank(a) - severityRank(b)),
|
|
1047
|
+
excess: (sign) => (a, b) => byNullable(a.excess, b.excess, sign),
|
|
1048
|
+
value: (sign) => (a, b) => byNullable(a.value, b.value, sign),
|
|
1049
|
+
path: (sign) => (a, b) => sign * byText(a.node.path, b.node.path),
|
|
1050
|
+
rule: (sign) => (a, b) => sign * byText(a.rule, b.rule)
|
|
1051
|
+
};
|
|
1052
|
+
var TIES = [
|
|
1053
|
+
PRIMARY.severity(-1),
|
|
1054
|
+
PRIMARY.excess(-1),
|
|
1055
|
+
PRIMARY.path(1),
|
|
1056
|
+
PRIMARY.rule(1),
|
|
1057
|
+
(a, b) => byText(a.id, b.id)
|
|
1058
|
+
];
|
|
1059
|
+
function compareFindings(sort, order) {
|
|
1060
|
+
const chain = [PRIMARY[sort](order === "asc" ? 1 : -1), ...TIES];
|
|
1061
|
+
return (a, b) => {
|
|
1062
|
+
for (const compare2 of chain) {
|
|
1063
|
+
const c = compare2(a, b);
|
|
1064
|
+
if (c !== 0) return c;
|
|
1065
|
+
}
|
|
1066
|
+
return 0;
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// src/query/hotspots.ts
|
|
1071
|
+
import {
|
|
1072
|
+
buildBlastRadius,
|
|
1073
|
+
buildReportContext,
|
|
1074
|
+
collectNodeMetrics,
|
|
1075
|
+
collectSymbolUtil,
|
|
1076
|
+
computeReportDrift,
|
|
1077
|
+
keepNode,
|
|
1078
|
+
topHotspots
|
|
1079
|
+
} from "@titan-design/code-graph/analysis";
|
|
1080
|
+
|
|
1081
|
+
// src/query/rollup.ts
|
|
1082
|
+
var KIND_RANK = { repo: 0, directory: 1, file: 2, symbol: 3 };
|
|
1083
|
+
var EMPTY = { sum: 0, count: 0, max: -Infinity };
|
|
1084
|
+
function leafKind(descriptor) {
|
|
1085
|
+
if (descriptor.appliesTo.includes("file")) return "file";
|
|
1086
|
+
return descriptor.appliesTo.includes("symbol") ? "symbol" : null;
|
|
1087
|
+
}
|
|
1088
|
+
function rank(kind) {
|
|
1089
|
+
return KIND_RANK[kind] ?? Infinity;
|
|
1090
|
+
}
|
|
1091
|
+
function storedValue(descriptor, values, id) {
|
|
1092
|
+
const value = values?.get(id);
|
|
1093
|
+
if (value !== void 0 && value !== null) return { value };
|
|
1094
|
+
if (value === void 0 && descriptor.absent === "zero") return { value: 0 };
|
|
1095
|
+
return { value: null, missing: "not-measured" };
|
|
1096
|
+
}
|
|
1097
|
+
function merge(a, b) {
|
|
1098
|
+
return { sum: a.sum + b.sum, count: a.count + b.count, max: Math.max(a.max, b.max) };
|
|
1099
|
+
}
|
|
1100
|
+
function finalValue(rule, agg) {
|
|
1101
|
+
if (agg.count === 0) return { value: null, missing: "not-measured" };
|
|
1102
|
+
if (rule === "max") return { value: agg.max };
|
|
1103
|
+
if (rule === "mean") return { value: agg.sum / agg.count };
|
|
1104
|
+
return { value: agg.sum };
|
|
1105
|
+
}
|
|
1106
|
+
function buildColumn(model, name) {
|
|
1107
|
+
const descriptor = model.metricCatalogue.find((m) => m.name === name);
|
|
1108
|
+
if (!descriptor) return { descriptor, valueOf: () => ({ value: null, missing: "not-in-snapshot" }) };
|
|
1109
|
+
const values = model.metrics.get(name);
|
|
1110
|
+
const leaf = leafKind(descriptor);
|
|
1111
|
+
const aggregates = /* @__PURE__ */ new Map();
|
|
1112
|
+
const own = (node) => storedValue(descriptor, values, node.ref.id);
|
|
1113
|
+
const aggregate = (node) => {
|
|
1114
|
+
const cached = aggregates.get(node);
|
|
1115
|
+
if (cached) return cached;
|
|
1116
|
+
let agg = EMPTY;
|
|
1117
|
+
if (node.ref.kind === leaf) {
|
|
1118
|
+
const v = own(node).value;
|
|
1119
|
+
if (v !== null) agg = { sum: v, count: 1, max: v };
|
|
1120
|
+
}
|
|
1121
|
+
for (const child of node.children) if (rank(child.ref.kind) <= rank(leaf ?? "")) agg = merge(agg, aggregate(child));
|
|
1122
|
+
aggregates.set(node, agg);
|
|
1123
|
+
return agg;
|
|
1124
|
+
};
|
|
1125
|
+
const valueOf2 = (node) => {
|
|
1126
|
+
if (node.stored && descriptor.appliesTo.includes(node.ref.kind)) return own(node);
|
|
1127
|
+
if (leaf === null || rank(node.ref.kind) >= rank(leaf)) return { value: null, missing: "not-applicable" };
|
|
1128
|
+
if (descriptor.rollup === "none") return { value: null, missing: "no-rollup" };
|
|
1129
|
+
return finalValue(descriptor.rollup, aggregate(node));
|
|
1130
|
+
};
|
|
1131
|
+
return { descriptor, valueOf: valueOf2 };
|
|
1132
|
+
}
|
|
1133
|
+
var columns = /* @__PURE__ */ new WeakMap();
|
|
1134
|
+
function columnFor(model, tree, name) {
|
|
1135
|
+
let byName = columns.get(tree);
|
|
1136
|
+
if (!byName) columns.set(tree, byName = /* @__PURE__ */ new Map());
|
|
1137
|
+
let column = byName.get(name);
|
|
1138
|
+
if (!column) byName.set(name, column = buildColumn(model, name));
|
|
1139
|
+
return column;
|
|
1140
|
+
}
|
|
1141
|
+
function describesKind(descriptor, kind) {
|
|
1142
|
+
if (descriptor.appliesTo.includes(kind)) return true;
|
|
1143
|
+
const leaf = leafKind(descriptor);
|
|
1144
|
+
return leaf !== null && rank(kind) < rank(leaf);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// src/query/baseline.ts
|
|
1148
|
+
function openBaseline(source, ref, excludeRoles = []) {
|
|
1149
|
+
if (ref === void 0) return void 0;
|
|
1150
|
+
const model = modelFor(source, ref);
|
|
1151
|
+
return { model, tree: treeFor(model, excludeRoles) };
|
|
1152
|
+
}
|
|
1153
|
+
function baselineValue(baseline, id, name) {
|
|
1154
|
+
const node = baseline.tree.byId.get(id);
|
|
1155
|
+
return node ? columnFor(baseline.model, baseline.tree, name).valueOf(node).value : null;
|
|
1156
|
+
}
|
|
1157
|
+
function baselineFields(current, baseline) {
|
|
1158
|
+
if (!baseline) return {};
|
|
1159
|
+
return {
|
|
1160
|
+
baselineSnapshotId: baseline.model.snapshot.id,
|
|
1161
|
+
comparable: baseline.model.snapshot.indexVersion === current.snapshot.indexVersion
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
function delta(value, before) {
|
|
1165
|
+
return value !== null && before !== null ? value - before : null;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// src/query/hotspots.ts
|
|
1169
|
+
var NO_ROLES = /* @__PURE__ */ new Set();
|
|
1170
|
+
function toGraphNode(node) {
|
|
1171
|
+
const out = { id: node.id, kind: node.kind, name: node.name, attrs: node.attrs };
|
|
1172
|
+
if (node.parentId !== null) out.parentId = node.parentId;
|
|
1173
|
+
if (node.role !== void 0) out.role = node.role;
|
|
1174
|
+
return out;
|
|
1175
|
+
}
|
|
1176
|
+
function metricRows(model) {
|
|
1177
|
+
const rows = [];
|
|
1178
|
+
for (const [name, byNode] of model.metrics) for (const [nodeId, value] of byNode) rows.push({ nodeId, name, value });
|
|
1179
|
+
return rows;
|
|
1180
|
+
}
|
|
1181
|
+
var inputs = /* @__PURE__ */ new WeakMap();
|
|
1182
|
+
function inputsFor(model) {
|
|
1183
|
+
let found = inputs.get(model);
|
|
1184
|
+
if (!found) inputs.set(model, found = { nodes: model.nodes.map(toGraphNode), metrics: metricRows(model) });
|
|
1185
|
+
return found;
|
|
1186
|
+
}
|
|
1187
|
+
function symbolRows(rows, ctx) {
|
|
1188
|
+
const metrics = collectNodeMetrics([...rows.metrics]);
|
|
1189
|
+
const symbols = collectSymbolUtil(rows.nodes, metrics).filter((s) => keepNode(ctx, s.fileId));
|
|
1190
|
+
const churn = ctx.metricsByName.get(`churn_${ctx.windowSuffix}`) ?? /* @__PURE__ */ new Map();
|
|
1191
|
+
return buildBlastRadius(symbols, metrics, churn, Infinity).map((b) => ({
|
|
1192
|
+
nodeId: b.symbolId,
|
|
1193
|
+
churn: b.churn,
|
|
1194
|
+
complexity: b.complexity,
|
|
1195
|
+
recency: 1,
|
|
1196
|
+
score: b.score,
|
|
1197
|
+
utilization: b.utilization
|
|
1198
|
+
}));
|
|
1199
|
+
}
|
|
1200
|
+
function windowDays(window) {
|
|
1201
|
+
return window === "lifetime" ? "lifetime" : Number.parseInt(window, 10);
|
|
1202
|
+
}
|
|
1203
|
+
var byScoreThenId = (a, b) => b.score - a.score || (a.nodeId < b.nodeId ? -1 : a.nodeId > b.nodeId ? 1 : 0);
|
|
1204
|
+
function reportContext(model, window) {
|
|
1205
|
+
return buildReportContext({ ...inputsFor(model), excluders: [], excludedRoles: NO_ROLES, windowDays: windowDays(window) });
|
|
1206
|
+
}
|
|
1207
|
+
function scoredRows(model, args) {
|
|
1208
|
+
const ctx = reportContext(model, args.window);
|
|
1209
|
+
const scored = args.grain === "file" ? topHotspots(ctx, Infinity) : symbolRows(inputsFor(model), ctx);
|
|
1210
|
+
return scored.sort(byScoreThenId);
|
|
1211
|
+
}
|
|
1212
|
+
function driftBaseline(info) {
|
|
1213
|
+
const { commit, ...rest } = info;
|
|
1214
|
+
return { ...rest, commitHash: commit, attrs: {} };
|
|
1215
|
+
}
|
|
1216
|
+
function compare(current, before, baseline) {
|
|
1217
|
+
const drift = computeReportDrift({
|
|
1218
|
+
baselineSnapshot: driftBaseline(baseline),
|
|
1219
|
+
currentHotspots: current,
|
|
1220
|
+
baselineHotspots: before,
|
|
1221
|
+
currentHotspotScore: () => 0,
|
|
1222
|
+
currentSilos: [],
|
|
1223
|
+
baselineSilos: [],
|
|
1224
|
+
currentBusFactor: () => void 0,
|
|
1225
|
+
currentCoupling: [],
|
|
1226
|
+
baselineCoupling: []
|
|
1227
|
+
});
|
|
1228
|
+
const marks = /* @__PURE__ */ new Map();
|
|
1229
|
+
for (const r of drift.newHotspots) marks.set(r.nodeId, "new");
|
|
1230
|
+
for (const r of drift.worsenedHotspots) marks.set(r.nodeId, "worsened");
|
|
1231
|
+
return { scores: new Map(before.map((r) => [r.nodeId, r.score])), marks };
|
|
1232
|
+
}
|
|
1233
|
+
function toHotspot(model, r, comparison) {
|
|
1234
|
+
const row = { node: toRef(model.nodeById.get(r.nodeId)), churn: r.churn, complexity: r.complexity, recency: r.recency, score: r.score };
|
|
1235
|
+
if (r.utilization !== void 0) row.utilization = r.utilization;
|
|
1236
|
+
if (!comparison) return row;
|
|
1237
|
+
row.baselineScore = comparison.scores.get(r.nodeId) ?? null;
|
|
1238
|
+
const mark = comparison.marks.get(r.nodeId);
|
|
1239
|
+
if (mark) row.mark = mark;
|
|
1240
|
+
return row;
|
|
1241
|
+
}
|
|
1242
|
+
function listHotspots(source, args) {
|
|
1243
|
+
const model = modelFor(source, args.snapshot);
|
|
1244
|
+
const baseline = openBaseline(source, args.baseline);
|
|
1245
|
+
const rows = scoredRows(model, args);
|
|
1246
|
+
const comparison = baseline ? compare(rows, scoredRows(baseline.model, args), baseline.model.snapshot) : void 0;
|
|
1247
|
+
const { cutoff } = args;
|
|
1248
|
+
const kept = cutoff === void 0 ? rows : rows.filter((r) => r.score >= cutoff);
|
|
1249
|
+
return {
|
|
1250
|
+
snapshotId: model.snapshot.id,
|
|
1251
|
+
...baselineFields(model, baseline),
|
|
1252
|
+
rows: kept.slice(args.offset, args.offset + args.limit).map((r) => toHotspot(model, r, comparison)),
|
|
1253
|
+
total: kept.length
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
// src/query/changes.ts
|
|
1258
|
+
var byDeltaThenId = (a, b) => b.delta - a.delta || compareIds(a.node.id, b.node.id);
|
|
1259
|
+
var compareIds = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
1260
|
+
function risingFiles(current, baseline, args) {
|
|
1261
|
+
const scored = (model) => scoredRows(model, { grain: "file", window: args.window });
|
|
1262
|
+
const drift = computeReportDrift2({
|
|
1263
|
+
baselineSnapshot: driftBaseline(baseline.model.snapshot),
|
|
1264
|
+
currentHotspots: scored(current.model),
|
|
1265
|
+
baselineHotspots: scored(baseline.model),
|
|
1266
|
+
currentHotspotScore: (id) => hotspotScoreOf(current.ctx, id),
|
|
1267
|
+
baselineHotspotScore: (id) => baseline.model.nodeById.has(id) ? hotspotScoreOf(baseline.ctx, id) : void 0,
|
|
1268
|
+
currentSilos: [],
|
|
1269
|
+
baselineSilos: [],
|
|
1270
|
+
currentBusFactor: () => void 0,
|
|
1271
|
+
currentCoupling: [],
|
|
1272
|
+
baselineCoupling: []
|
|
1273
|
+
});
|
|
1274
|
+
const climbed = drift.newHotspots.flatMap((r) => r.before === void 0 ? [] : [{ ...r, after: r.score, before: r.before, delta: r.score - r.before }]);
|
|
1275
|
+
const node = (id) => toRef(current.model.nodeById.get(id));
|
|
1276
|
+
return [...drift.worsenedHotspots, ...climbed].map((r) => ({ node: node(r.nodeId), before: r.before, after: r.after, delta: r.delta }));
|
|
1277
|
+
}
|
|
1278
|
+
function addedFiles(current, baseline) {
|
|
1279
|
+
return current.model.nodes.filter((n) => n.kind === "file" && !baseline.model.nodeById.has(n.id) && keepNode2(current.ctx, n.id)).map((n) => ({ node: toRef(n), score: hotspotScoreOf(current.ctx, n.id) })).sort((a, b) => b.score - a.score || compareIds(a.node.id, b.node.id));
|
|
1280
|
+
}
|
|
1281
|
+
function keyed(model) {
|
|
1282
|
+
return findingsFor(model).map((row) => {
|
|
1283
|
+
const k = { ruleId: row.rule, nodeId: row.node.id, row };
|
|
1284
|
+
if (row.destination) k.destinationId = row.destination.id;
|
|
1285
|
+
if (row.value !== void 0) k.value = row.value;
|
|
1286
|
+
return k;
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
function bucketFindings(baseline, current) {
|
|
1290
|
+
return bucketViolations(keyed(baseline), keyed(current));
|
|
1291
|
+
}
|
|
1292
|
+
function findingChanges(current, baseline) {
|
|
1293
|
+
const buckets = bucketFindings(baseline, current);
|
|
1294
|
+
const change = (u) => ({ finding: u.to.row, before: u.from.value, delta: u.delta });
|
|
1295
|
+
return {
|
|
1296
|
+
new: buckets.newViolations.map((v) => v.row),
|
|
1297
|
+
worsened: buckets.worsened.map(change).sort((a, b) => b.delta - a.delta),
|
|
1298
|
+
improved: buckets.improved.map(change).sort((a, b) => a.delta - b.delta),
|
|
1299
|
+
resolved: buckets.resolvedViolations.map((v) => v.row)
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
function regressions(rising, current) {
|
|
1303
|
+
const open = /* @__PURE__ */ new Map();
|
|
1304
|
+
for (const f of findingsFor(current)) open.set(f.node.path, [...open.get(f.node.path) ?? [], f.id]);
|
|
1305
|
+
return rising.flatMap((r) => {
|
|
1306
|
+
const ids = open.get(r.node.id);
|
|
1307
|
+
return ids ? [{ ...r, findings: [...ids].sort(compareIds) }] : [];
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
function diff(current, baseline, args) {
|
|
1311
|
+
const rising = risingFiles(current, baseline, args).sort(byDeltaThenId);
|
|
1312
|
+
return {
|
|
1313
|
+
files: {
|
|
1314
|
+
crossedCutoff: rising.filter((r) => r.after >= args.cutoff && r.before < args.cutoff),
|
|
1315
|
+
added: addedFiles(current, baseline)
|
|
1316
|
+
},
|
|
1317
|
+
findings: findingChanges(current.model, baseline.model),
|
|
1318
|
+
// Co-change pairs are not stored in the index yet, so new coupling cannot be measured here.
|
|
1319
|
+
coupling: { measured: false, added: [] },
|
|
1320
|
+
regressions: regressions(rising, current.model)
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
var EMPTY2 = {
|
|
1324
|
+
files: { crossedCutoff: [], added: [] },
|
|
1325
|
+
findings: { new: [], worsened: [], improved: [], resolved: [] },
|
|
1326
|
+
coupling: { measured: false, added: [] },
|
|
1327
|
+
regressions: []
|
|
1328
|
+
};
|
|
1329
|
+
function counts(lists) {
|
|
1330
|
+
const { files, findings: findings2 } = lists;
|
|
1331
|
+
return {
|
|
1332
|
+
crossedCutoff: files.crossedCutoff.length,
|
|
1333
|
+
newFiles: files.added.length,
|
|
1334
|
+
newFindings: findings2.new.length,
|
|
1335
|
+
worsened: findings2.worsened.length,
|
|
1336
|
+
improved: findings2.improved.length,
|
|
1337
|
+
resolved: findings2.resolved.length,
|
|
1338
|
+
newCoupling: lists.coupling.added.length,
|
|
1339
|
+
regressions: lists.regressions.length
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
function capped(lists, limit) {
|
|
1343
|
+
const cap = (rows) => rows.slice(0, limit);
|
|
1344
|
+
const { files, findings: findings2, coupling } = lists;
|
|
1345
|
+
return {
|
|
1346
|
+
files: { crossedCutoff: cap(files.crossedCutoff), added: cap(files.added) },
|
|
1347
|
+
findings: { new: cap(findings2.new), worsened: cap(findings2.worsened), improved: cap(findings2.improved), resolved: cap(findings2.resolved) },
|
|
1348
|
+
coupling: { measured: coupling.measured, added: cap(coupling.added) },
|
|
1349
|
+
regressions: cap(lists.regressions)
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
function getChanges(source, args) {
|
|
1353
|
+
const side2 = (model) => ({ model, ctx: reportContext(model, args.window) });
|
|
1354
|
+
const current = side2(modelFor(source, args.snapshot));
|
|
1355
|
+
const baseline = side2(modelFor(source, args.baseline));
|
|
1356
|
+
const comparable = baseline.model.snapshot.indexVersion === current.model.snapshot.indexVersion;
|
|
1357
|
+
const lists = comparable ? diff(current, baseline, args) : EMPTY2;
|
|
1358
|
+
return {
|
|
1359
|
+
snapshotId: current.model.snapshot.id,
|
|
1360
|
+
baselineSnapshotId: baseline.model.snapshot.id,
|
|
1361
|
+
comparable,
|
|
1362
|
+
...capped(lists, args.limit),
|
|
1363
|
+
counts: counts(lists)
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// src/query/excerpt.ts
|
|
1368
|
+
function excerptWindow(ranges, context, lineCount) {
|
|
1369
|
+
const from = ranges.length === 0 ? 1 : Math.max(1, Math.min(...ranges.map((r) => r.startLine)) - context);
|
|
1370
|
+
const wanted = ranges.length === 0 ? lineCount : Math.min(lineCount, Math.max(...ranges.map((r) => r.endLine)) + context);
|
|
1371
|
+
const to = Math.min(wanted, from + EXCERPT_LINE_CAP - 1);
|
|
1372
|
+
return { from, to, truncated: to < wanted };
|
|
1373
|
+
}
|
|
1374
|
+
function clipped(ranges, from, to) {
|
|
1375
|
+
return ranges.filter((r) => r.endLine >= from && r.startLine <= to).map((r) => ({ startLine: Math.max(r.startLine, from), endLine: Math.min(r.endLine, to) }));
|
|
1376
|
+
}
|
|
1377
|
+
function holds(window, from, to) {
|
|
1378
|
+
return from >= window.startLine && to <= window.startLine + window.lines.length - 1;
|
|
1379
|
+
}
|
|
1380
|
+
function buildExcerpt(read, ranges, context) {
|
|
1381
|
+
if ("unavailable" in read) return { excerpt: null, missing: read.unavailable };
|
|
1382
|
+
const { from, to, truncated } = excerptWindow(ranges, context, read.lineCount);
|
|
1383
|
+
if (to >= from && !holds(read, from, to)) return { excerpt: null, missing: "not-in-export" };
|
|
1384
|
+
const text = read.lines.slice(from - read.startLine, to - read.startLine + 1).join("\n");
|
|
1385
|
+
const excerpt = {
|
|
1386
|
+
path: read.path,
|
|
1387
|
+
startLine: from,
|
|
1388
|
+
endLine: to,
|
|
1389
|
+
text,
|
|
1390
|
+
contentHash: read.contentHash,
|
|
1391
|
+
origin: read.origin,
|
|
1392
|
+
highlights: clipped(ranges, from, to),
|
|
1393
|
+
truncated
|
|
1394
|
+
};
|
|
1395
|
+
return { excerpt };
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// src/query/neighbors.ts
|
|
1399
|
+
var adjacencies = /* @__PURE__ */ new WeakMap();
|
|
1400
|
+
function push(map, key, edge) {
|
|
1401
|
+
const list = map.get(key);
|
|
1402
|
+
if (list) list.push(edge);
|
|
1403
|
+
else map.set(key, [edge]);
|
|
1404
|
+
}
|
|
1405
|
+
function adjacencyOf(model) {
|
|
1406
|
+
let adjacency = adjacencies.get(model);
|
|
1407
|
+
if (!adjacency) {
|
|
1408
|
+
const inbound = /* @__PURE__ */ new Map();
|
|
1409
|
+
const outbound = /* @__PURE__ */ new Map();
|
|
1410
|
+
for (const edge of model.edges) {
|
|
1411
|
+
push(inbound, edge.dstId, edge);
|
|
1412
|
+
push(outbound, edge.srcId, edge);
|
|
1413
|
+
}
|
|
1414
|
+
adjacencies.set(model, adjacency = { inbound, outbound });
|
|
1415
|
+
}
|
|
1416
|
+
return adjacency;
|
|
1417
|
+
}
|
|
1418
|
+
var weightOf = (edge) => typeof edge.attrs.weight === "number" ? edge.attrs.weight : null;
|
|
1419
|
+
function valueOf(model, tree, id, name) {
|
|
1420
|
+
const node = tree.byId.get(id);
|
|
1421
|
+
if (node) return columnFor(model, tree, name).valueOf(node).value;
|
|
1422
|
+
return model.metrics.get(name)?.get(id) ?? null;
|
|
1423
|
+
}
|
|
1424
|
+
function toNeighbor(model, tree, edge, otherId, metrics) {
|
|
1425
|
+
const neighbor = { node: refFor(model, otherId), kind: edge.kind, weight: weightOf(edge), values: {} };
|
|
1426
|
+
if (typeof edge.attrs.specifier === "string") neighbor.specifier = edge.attrs.specifier;
|
|
1427
|
+
for (const name of metrics) neighbor.values[name] = valueOf(model, tree, otherId, name);
|
|
1428
|
+
return neighbor;
|
|
1429
|
+
}
|
|
1430
|
+
function compareNeighbors(a, b) {
|
|
1431
|
+
const [x, y] = [a.weight ?? -Infinity, b.weight ?? -Infinity];
|
|
1432
|
+
if (x !== y) return y - x;
|
|
1433
|
+
if (a.node.id !== b.node.id) return a.node.id < b.node.id ? -1 : 1;
|
|
1434
|
+
return a.kind < b.kind ? -1 : a.kind > b.kind ? 1 : 0;
|
|
1435
|
+
}
|
|
1436
|
+
function edgeFilter(kinds, nodeKind) {
|
|
1437
|
+
if (kinds.length > 0) return (edge) => kinds.includes(edge.kind);
|
|
1438
|
+
return nodeKind === "symbol" ? () => true : (edge) => edge.kind !== "references";
|
|
1439
|
+
}
|
|
1440
|
+
function side(edges, other, p) {
|
|
1441
|
+
const all = (edges ?? []).filter(p.keep).map((e) => p.build(e, other(e))).sort(compareNeighbors);
|
|
1442
|
+
return { rows: all.slice(p.offset, p.offset + p.limit), total: all.length };
|
|
1443
|
+
}
|
|
1444
|
+
function getNeighbors(source, args) {
|
|
1445
|
+
const model = modelFor(source, args.snapshot);
|
|
1446
|
+
const tree = treeFor(model);
|
|
1447
|
+
const node = model.nodeById.get(args.id);
|
|
1448
|
+
if (!node && tree.byId.has(args.id)) throw invalidArgs(`${args.id} is synthesized; node.neighbors needs a stored node`);
|
|
1449
|
+
if (!node) throw nodeNotFound(args.id, model.snapshot.id);
|
|
1450
|
+
const { inbound, outbound } = adjacencyOf(model);
|
|
1451
|
+
const metrics = [...new Set(args.metrics)];
|
|
1452
|
+
const paging = {
|
|
1453
|
+
keep: edgeFilter(args.edge_kinds, node.kind),
|
|
1454
|
+
build: (e, id) => toNeighbor(model, tree, e, id, metrics),
|
|
1455
|
+
offset: args.offset,
|
|
1456
|
+
limit: args.limit
|
|
1457
|
+
};
|
|
1458
|
+
const none = { rows: [], total: 0 };
|
|
1459
|
+
const ins = args.direction === "out" ? none : side(inbound.get(node.id), (e) => e.srcId, paging);
|
|
1460
|
+
const outs = args.direction === "in" ? none : side(outbound.get(node.id), (e) => e.dstId, paging);
|
|
1461
|
+
return {
|
|
1462
|
+
snapshotId: model.snapshot.id,
|
|
1463
|
+
node: refFor(model, node.id),
|
|
1464
|
+
inbound: ins.rows,
|
|
1465
|
+
outbound: outs.rows,
|
|
1466
|
+
total: { inbound: ins.total, outbound: outs.total }
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
// src/query/stats.ts
|
|
1471
|
+
var sortedByKind = /* @__PURE__ */ new WeakMap();
|
|
1472
|
+
function valuesOf(column, nodes, kind) {
|
|
1473
|
+
const out = [];
|
|
1474
|
+
for (const node of nodes) {
|
|
1475
|
+
if (node.ref.kind !== kind) continue;
|
|
1476
|
+
const { value } = column.valueOf(node);
|
|
1477
|
+
if (value !== null) out.push(value);
|
|
1478
|
+
}
|
|
1479
|
+
return out;
|
|
1480
|
+
}
|
|
1481
|
+
function sortedPeers(column, tree, kind) {
|
|
1482
|
+
let byKind = sortedByKind.get(column);
|
|
1483
|
+
if (!byKind) sortedByKind.set(column, byKind = /* @__PURE__ */ new Map());
|
|
1484
|
+
let sorted = byKind.get(kind);
|
|
1485
|
+
if (!sorted) byKind.set(kind, sorted = valuesOf(column, tree.byId.values(), kind).sort((a, b) => a - b));
|
|
1486
|
+
return sorted;
|
|
1487
|
+
}
|
|
1488
|
+
function countAtMost(sorted, value) {
|
|
1489
|
+
let lo = 0;
|
|
1490
|
+
let hi = sorted.length;
|
|
1491
|
+
while (lo < hi) {
|
|
1492
|
+
const mid = lo + hi >>> 1;
|
|
1493
|
+
if (sorted[mid] <= value) lo = mid + 1;
|
|
1494
|
+
else hi = mid;
|
|
1495
|
+
}
|
|
1496
|
+
return lo;
|
|
1497
|
+
}
|
|
1498
|
+
function median(values) {
|
|
1499
|
+
if (values.length === 0) return null;
|
|
1500
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
1501
|
+
const mid = sorted.length >>> 1;
|
|
1502
|
+
return sorted.length % 2 === 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
1503
|
+
}
|
|
1504
|
+
var round1 = (n) => Math.round(n * 10) / 10;
|
|
1505
|
+
function peerStats(column, tree, node, value) {
|
|
1506
|
+
const parent = node.parentId === null ? void 0 : tree.byId.get(node.parentId);
|
|
1507
|
+
const siblings = valuesOf(column, parent ? parent.children : [node], node.ref.kind);
|
|
1508
|
+
if (value === null) return { percentile: null, siblingMedian: median(siblings), siblingRank: null, siblingCount: siblings.length };
|
|
1509
|
+
const peers = sortedPeers(column, tree, node.ref.kind);
|
|
1510
|
+
return {
|
|
1511
|
+
percentile: peers.length === 0 ? null : round1(100 * countAtMost(peers, value) / peers.length),
|
|
1512
|
+
siblingMedian: median(siblings),
|
|
1513
|
+
siblingRank: 1 + siblings.filter((v) => v > value).length,
|
|
1514
|
+
siblingCount: siblings.length
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// src/query/finding-get.ts
|
|
1519
|
+
var RELATED_CAP = 10;
|
|
1520
|
+
function measure(model, finding, baseline) {
|
|
1521
|
+
const measured = {
|
|
1522
|
+
value: finding.value ?? null,
|
|
1523
|
+
threshold: finding.threshold ?? null,
|
|
1524
|
+
percentile: null,
|
|
1525
|
+
siblingMedian: null
|
|
1526
|
+
};
|
|
1527
|
+
const tree = treeFor(model);
|
|
1528
|
+
const node = tree.byId.get(finding.node.id);
|
|
1529
|
+
const metric = finding.metric;
|
|
1530
|
+
if (node && metric !== void 0 && model.metrics.has(metric)) {
|
|
1531
|
+
const column = columnFor(model, tree, metric);
|
|
1532
|
+
const stats = peerStats(column, tree, node, column.valueOf(node).value);
|
|
1533
|
+
measured.percentile = stats.percentile;
|
|
1534
|
+
measured.siblingMedian = stats.siblingMedian;
|
|
1535
|
+
}
|
|
1536
|
+
if (baseline && metric !== void 0) measured.baselineValue = baselineValue(baseline, finding.node.id, metric);
|
|
1537
|
+
return measured;
|
|
1538
|
+
}
|
|
1539
|
+
function neighbourIds(model, id) {
|
|
1540
|
+
const { inbound, outbound } = adjacencyOf(model);
|
|
1541
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1542
|
+
for (const e of inbound.get(id) ?? []) if (e.kind !== "references") ids.add(e.srcId);
|
|
1543
|
+
for (const e of outbound.get(id) ?? []) if (e.kind !== "references") ids.add(e.dstId);
|
|
1544
|
+
return ids;
|
|
1545
|
+
}
|
|
1546
|
+
function relatedTo(model, rows, finding) {
|
|
1547
|
+
const near = neighbourIds(model, finding.node.id);
|
|
1548
|
+
const order = compareFindings("severity", "desc");
|
|
1549
|
+
const sameNode = rows.filter((f) => f.id !== finding.id && f.node.id === finding.node.id).sort(order);
|
|
1550
|
+
const sameRule = rows.filter((f) => f.rule === finding.rule && f.node.id !== finding.node.id && near.has(f.node.id)).sort(order);
|
|
1551
|
+
return [...sameNode, ...sameRule].slice(0, RELATED_CAP);
|
|
1552
|
+
}
|
|
1553
|
+
function ruleOf(model, finding) {
|
|
1554
|
+
const rule = model.rules.find((r) => r.id === finding.rule);
|
|
1555
|
+
if (!rule) return { id: finding.rule, type: "unknown", severity: finding.severity, text: "" };
|
|
1556
|
+
return { id: rule.id, type: rule.type, severity: rule.severity, text: rule.text };
|
|
1557
|
+
}
|
|
1558
|
+
function excerptOf(source, model, finding, context) {
|
|
1559
|
+
if (!source.readSource) return { excerpt: null, missing: "no-source" };
|
|
1560
|
+
const stored = model.findings.find((f) => f.id === finding.id);
|
|
1561
|
+
return buildExcerpt(source.readSource(model.snapshot.id, finding.node.path), stored?.ranges ?? [], context);
|
|
1562
|
+
}
|
|
1563
|
+
function getFinding(source, args) {
|
|
1564
|
+
const model = modelFor(source, args.snapshot);
|
|
1565
|
+
const baseline = openBaseline(source, args.baseline);
|
|
1566
|
+
const current = findingsFor(model);
|
|
1567
|
+
const rows = baseline ? withStatus(current, findingsFor(baseline.model)).filter((f) => f.status !== "resolved") : current;
|
|
1568
|
+
const finding = rows.find((f) => f.id === args.id);
|
|
1569
|
+
if (!finding) throw findingNotFound(args.id, model.snapshot.id);
|
|
1570
|
+
const rule = ruleOf(model, finding);
|
|
1571
|
+
const { excerpt, missing } = excerptOf(source, model, finding, args.context_lines);
|
|
1572
|
+
const result = {
|
|
1573
|
+
snapshotId: model.snapshot.id,
|
|
1574
|
+
...baselineFields(model, baseline),
|
|
1575
|
+
finding,
|
|
1576
|
+
rule,
|
|
1577
|
+
measured: measure(model, finding, baseline),
|
|
1578
|
+
why: rule.text ? `${rule.text} Here: ${finding.message}.` : finding.message,
|
|
1579
|
+
excerpt,
|
|
1580
|
+
related: relatedTo(model, rows, finding)
|
|
1581
|
+
};
|
|
1582
|
+
if (missing !== void 0) result.excerptMissing = missing;
|
|
1583
|
+
return result;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// src/query/findings-list.ts
|
|
1587
|
+
function childUnder(prefix, ref) {
|
|
1588
|
+
if (ref.kind === "external" || !ref.path.startsWith(prefix)) return ref.id;
|
|
1589
|
+
const rest = ref.path.slice(prefix.length);
|
|
1590
|
+
const slash = rest.indexOf("/");
|
|
1591
|
+
return slash < 0 ? ref.path : directoryId(prefix + rest.slice(0, slash));
|
|
1592
|
+
}
|
|
1593
|
+
function scopeOf(node) {
|
|
1594
|
+
const { kind, id, path } = node.ref;
|
|
1595
|
+
if (kind === "repo") return { contains: () => true, childOf: (ref) => childUnder("", ref) };
|
|
1596
|
+
if (kind === "directory") {
|
|
1597
|
+
const prefix = `${path}/`;
|
|
1598
|
+
return { contains: (ref) => ref.path.startsWith(prefix), childOf: (ref) => childUnder(prefix, ref) };
|
|
1599
|
+
}
|
|
1600
|
+
if (kind === "file") return { contains: (ref) => ref.path === id, childOf: (ref) => ref.id };
|
|
1601
|
+
return { contains: (ref) => ref.id === id || ref.id.startsWith(`${id}.`), childOf: (ref) => ref.id };
|
|
1602
|
+
}
|
|
1603
|
+
function oneOf(allowed, pick) {
|
|
1604
|
+
if (allowed.length === 0) return () => true;
|
|
1605
|
+
const set = new Set(allowed);
|
|
1606
|
+
return (f) => {
|
|
1607
|
+
const value = pick(f);
|
|
1608
|
+
return value !== void 0 && set.has(value);
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
function filtersFor(args, scope) {
|
|
1612
|
+
return [
|
|
1613
|
+
(f) => scope.contains(f.node),
|
|
1614
|
+
oneOf(args.rule, (f) => f.rule),
|
|
1615
|
+
oneOf(args.severity, (f) => f.severity),
|
|
1616
|
+
oneOf(args.tool, (f) => f.tool),
|
|
1617
|
+
oneOf(args.provenance, (f) => f.provenance.kind),
|
|
1618
|
+
oneOf(args.kind, (f) => f.node.kind),
|
|
1619
|
+
oneOf(args.status, (f) => f.status)
|
|
1620
|
+
];
|
|
1621
|
+
}
|
|
1622
|
+
var FACETS = {
|
|
1623
|
+
rule: (f) => f.rule,
|
|
1624
|
+
severity: (f) => f.severity,
|
|
1625
|
+
tool: (f) => f.tool,
|
|
1626
|
+
provenance: (f) => f.provenance.kind,
|
|
1627
|
+
kind: (f) => f.node.kind,
|
|
1628
|
+
child: (f, scope) => scope.childOf(f.node),
|
|
1629
|
+
status: (f) => f.status
|
|
1630
|
+
};
|
|
1631
|
+
function sortedCounts(counts2) {
|
|
1632
|
+
return Object.fromEntries([...counts2].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
|
|
1633
|
+
}
|
|
1634
|
+
function facetCounts(rows, scope, withBaseline) {
|
|
1635
|
+
const out = {};
|
|
1636
|
+
for (const [name, pick] of Object.entries(FACETS)) {
|
|
1637
|
+
if (name === "status" && !withBaseline) continue;
|
|
1638
|
+
const counts2 = /* @__PURE__ */ new Map();
|
|
1639
|
+
for (const f of rows) {
|
|
1640
|
+
const value = pick(f, scope) ?? "";
|
|
1641
|
+
counts2.set(value, (counts2.get(value) ?? 0) + 1);
|
|
1642
|
+
}
|
|
1643
|
+
out[name] = sortedCounts(counts2);
|
|
1644
|
+
}
|
|
1645
|
+
return out;
|
|
1646
|
+
}
|
|
1647
|
+
function listFindings(source, args) {
|
|
1648
|
+
const model = modelFor(source, args.snapshot);
|
|
1649
|
+
const baseline = openBaseline(source, args.baseline);
|
|
1650
|
+
if (args.status.length > 0 && !baseline) throw invalidArgs("status needs a baseline");
|
|
1651
|
+
const root = treeFor(model).byId.get(args.scope ?? REPO_ID);
|
|
1652
|
+
if (!root) throw nodeNotFound(args.scope ?? REPO_ID, model.snapshot.id);
|
|
1653
|
+
const scope = scopeOf(root);
|
|
1654
|
+
const all = baseline ? withStatus(findingsFor(model), findingsFor(baseline.model)) : findingsFor(model);
|
|
1655
|
+
const filters = filtersFor(args, scope);
|
|
1656
|
+
const kept = all.filter((f) => filters.every((keep) => keep(f))).sort(compareFindings(args.sort, args.order));
|
|
1657
|
+
const result = {
|
|
1658
|
+
snapshotId: model.snapshot.id,
|
|
1659
|
+
...baselineFields(model, baseline),
|
|
1660
|
+
rows: kept.slice(args.offset, args.offset + args.limit),
|
|
1661
|
+
total: kept.length
|
|
1662
|
+
};
|
|
1663
|
+
if (args.facets) result.facets = facetCounts(kept, scope, baseline !== void 0);
|
|
1664
|
+
return result;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
// src/query/hierarchy.ts
|
|
1668
|
+
function collectRows(root, depth, includeSymbols) {
|
|
1669
|
+
const rows = [];
|
|
1670
|
+
const queue = [root];
|
|
1671
|
+
for (let i = 0; i < queue.length; i++) {
|
|
1672
|
+
const node = queue[i];
|
|
1673
|
+
if (rows.length === HIERARCHY_ROW_CAP) return { rows, truncated: true };
|
|
1674
|
+
rows.push(node);
|
|
1675
|
+
if (node.depth - root.depth >= depth) continue;
|
|
1676
|
+
for (const child of node.children) if (includeSymbols || child.ref.kind !== "symbol") queue.push(child);
|
|
1677
|
+
}
|
|
1678
|
+
return { rows, truncated: false };
|
|
1679
|
+
}
|
|
1680
|
+
function toRow(node, rootDepth, columns2, names, baseline) {
|
|
1681
|
+
const row = { ...node.ref, parentId: node.parentId, depth: node.depth - rootDepth, childCount: node.children.length, values: {} };
|
|
1682
|
+
if (node.stored?.role !== void 0) row.role = node.stored.role;
|
|
1683
|
+
const missing = {};
|
|
1684
|
+
const deltas = {};
|
|
1685
|
+
names.forEach((name, i) => {
|
|
1686
|
+
const { value, missing: reason } = columns2[i].valueOf(node);
|
|
1687
|
+
row.values[name] = value;
|
|
1688
|
+
if (reason) missing[name] = reason;
|
|
1689
|
+
if (!baseline) return;
|
|
1690
|
+
deltas[name] = delta(value, baselineValue(baseline, node.ref.id, name));
|
|
1691
|
+
});
|
|
1692
|
+
if (Object.keys(missing).length > 0) row.missing = missing;
|
|
1693
|
+
if (baseline) row.deltas = deltas;
|
|
1694
|
+
return row;
|
|
1695
|
+
}
|
|
1696
|
+
function getHierarchy(source, args) {
|
|
1697
|
+
const model = modelFor(source, args.snapshot);
|
|
1698
|
+
const tree = treeFor(model, args.exclude_roles);
|
|
1699
|
+
const root = tree.byId.get(args.root ?? REPO_ID);
|
|
1700
|
+
if (!root) throw nodeNotFound(args.root ?? REPO_ID, model.snapshot.id);
|
|
1701
|
+
const baseline = openBaseline(source, args.baseline, args.exclude_roles);
|
|
1702
|
+
const names = [...new Set(args.metrics)];
|
|
1703
|
+
const columns2 = names.map((name) => columnFor(model, tree, name));
|
|
1704
|
+
const { rows, truncated } = collectRows(root, args.depth, args.include_symbols);
|
|
1705
|
+
const nodes = rows.map((node) => toRow(node, root.depth, columns2, names, baseline));
|
|
1706
|
+
return { snapshotId: model.snapshot.id, ...baselineFields(model, baseline), nodes, truncated };
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
// src/query/node-lenses.ts
|
|
1710
|
+
import {
|
|
1711
|
+
buildHotExports,
|
|
1712
|
+
collectNodeMetrics as collectNodeMetrics2,
|
|
1713
|
+
collectSymbolUtil as collectSymbolUtil2,
|
|
1714
|
+
computeSymbolConsumers,
|
|
1715
|
+
linkTestsToSources,
|
|
1716
|
+
topCentralFiles
|
|
1717
|
+
} from "@titan-design/code-graph/analysis";
|
|
1718
|
+
function perModel(derive) {
|
|
1719
|
+
const cache = /* @__PURE__ */ new WeakMap();
|
|
1720
|
+
return (model) => {
|
|
1721
|
+
if (!cache.has(model)) cache.set(model, derive(model));
|
|
1722
|
+
return cache.get(model);
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
var centralFiles = perModel((model) => {
|
|
1726
|
+
const nodes = inputsFor(model).nodes.filter((n) => n.kind !== "symbol");
|
|
1727
|
+
const edges = model.edges.filter((e) => e.kind !== "references" && e.kind !== "calls");
|
|
1728
|
+
return topCentralFiles(nodes, edges, reportContext(model, "lifetime"), Infinity);
|
|
1729
|
+
});
|
|
1730
|
+
var symbolFacts = perModel((model) => {
|
|
1731
|
+
const { nodes, metrics: rows } = inputsFor(model);
|
|
1732
|
+
const metrics = collectNodeMetrics2([...rows]);
|
|
1733
|
+
const references = model.edges.filter((e) => e.kind === "references");
|
|
1734
|
+
const consumers = new Map(computeSymbolConsumers(references).map((s) => [s.symbolId, s.consumers.length]));
|
|
1735
|
+
return { metrics, symbols: collectSymbolUtil2(nodes, metrics), consumers };
|
|
1736
|
+
});
|
|
1737
|
+
var testLinks = perModel((model) => linkTestsToSources(inputsFor(model).nodes, []));
|
|
1738
|
+
function exportsOf(model, fileId) {
|
|
1739
|
+
const { metrics, symbols, consumers } = symbolFacts(model);
|
|
1740
|
+
const declared = symbols.filter((s) => s.fileId === fileId);
|
|
1741
|
+
const rows = buildHotExports(declared, /* @__PURE__ */ new Set([fileId]), metrics, consumers)[fileId] ?? [];
|
|
1742
|
+
return rows.map((r) => ({ name: r.name, utilization: r.utilization, cognitive: r.cognitive ?? null, consumers: r.consumers, exported: r.exported }));
|
|
1743
|
+
}
|
|
1744
|
+
function scoreOf(model, node, window) {
|
|
1745
|
+
const grain = node.ref.kind;
|
|
1746
|
+
if (grain !== "file" && grain !== "symbol") return null;
|
|
1747
|
+
const rows = scoredRows(model, { grain, window });
|
|
1748
|
+
const at = rows.findIndex((r) => r.nodeId === node.ref.id);
|
|
1749
|
+
if (at < 0) return { grain, window, score: 0, rank: null, ranked: rows.length };
|
|
1750
|
+
const { score, churn, complexity: complexity2, recency, utilization } = rows[at];
|
|
1751
|
+
const factors = utilization === void 0 ? { churn, complexity: complexity2, recency } : { churn, complexity: complexity2, recency, utilization };
|
|
1752
|
+
return { grain, window, score, ...factors, rank: at + 1, ranked: rows.length };
|
|
1753
|
+
}
|
|
1754
|
+
function centralityOf(model, fileId) {
|
|
1755
|
+
const rows = centralFiles(model);
|
|
1756
|
+
const at = rows.findIndex((r) => r.nodeId === fileId);
|
|
1757
|
+
return { score: at < 0 ? null : rows[at].score, rank: at < 0 ? null : at + 1, of: rows.length };
|
|
1758
|
+
}
|
|
1759
|
+
function testsOf(model, fileId) {
|
|
1760
|
+
const tests = testLinks(model).filter((l) => l.sourceId === fileId).map((l) => toRef(model.nodeById.get(l.testId)));
|
|
1761
|
+
const counts2 = model.metrics.get("linked_test_count");
|
|
1762
|
+
const indexedCount = counts2 ? counts2.get(fileId) ?? 0 : null;
|
|
1763
|
+
return { tests, indexedCount, coEditMeasured: false };
|
|
1764
|
+
}
|
|
1765
|
+
function lensOf(model, node, lens, window) {
|
|
1766
|
+
if (lens === "score") return scoreOf(model, node, window);
|
|
1767
|
+
if (node.ref.kind !== "file") return null;
|
|
1768
|
+
const id = node.ref.id;
|
|
1769
|
+
if (lens === "exports") return exportsOf(model, id);
|
|
1770
|
+
if (lens === "centrality") return centralityOf(model, id);
|
|
1771
|
+
if (lens === "tests") return testsOf(model, id);
|
|
1772
|
+
return { measured: false, partners: [] };
|
|
1773
|
+
}
|
|
1774
|
+
function lensesFor(model, node, args) {
|
|
1775
|
+
return Object.fromEntries(args.lenses.map((lens) => [lens, lensOf(model, node, lens, args.window)]));
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
// src/query/node-get.ts
|
|
1779
|
+
function ancestorsOf(tree, node) {
|
|
1780
|
+
const chain = [];
|
|
1781
|
+
for (let id = node.parentId; id !== null; ) {
|
|
1782
|
+
const parent = tree.byId.get(id);
|
|
1783
|
+
chain.push(parent.ref);
|
|
1784
|
+
id = parent.parentId;
|
|
1785
|
+
}
|
|
1786
|
+
return chain.reverse();
|
|
1787
|
+
}
|
|
1788
|
+
function childCounts(node) {
|
|
1789
|
+
const counts2 = {};
|
|
1790
|
+
for (const child of node.children) counts2[child.ref.kind] = (counts2[child.ref.kind] ?? 0) + 1;
|
|
1791
|
+
return counts2;
|
|
1792
|
+
}
|
|
1793
|
+
function describeNode(node) {
|
|
1794
|
+
const out = { ...node.ref };
|
|
1795
|
+
const stored = node.stored;
|
|
1796
|
+
if (!stored) return out;
|
|
1797
|
+
if (stored.language !== void 0) out.language = stored.language;
|
|
1798
|
+
if (stored.role !== void 0) out.role = stored.role;
|
|
1799
|
+
const { exported, signature, purpose } = stored.attrs;
|
|
1800
|
+
if (typeof exported === "boolean") out.exported = exported;
|
|
1801
|
+
if (typeof signature === "string") out.signature = signature;
|
|
1802
|
+
if (typeof purpose === "string") out.purpose = purpose;
|
|
1803
|
+
return out;
|
|
1804
|
+
}
|
|
1805
|
+
function metricNames(model, kind, requested) {
|
|
1806
|
+
if (requested.length > 0) return [...new Set(requested)];
|
|
1807
|
+
return model.metricCatalogue.filter((d) => describesKind(d, kind)).map((d) => d.name);
|
|
1808
|
+
}
|
|
1809
|
+
function measure2(model, tree, node, name, baseline) {
|
|
1810
|
+
const column = columnFor(model, tree, name);
|
|
1811
|
+
const { value, missing } = column.valueOf(node);
|
|
1812
|
+
const d = column.descriptor;
|
|
1813
|
+
const metric = {
|
|
1814
|
+
name,
|
|
1815
|
+
unit: d?.unit ?? null,
|
|
1816
|
+
direction: d?.direction ?? "neutral",
|
|
1817
|
+
rollup: d?.rollup ?? "none",
|
|
1818
|
+
value,
|
|
1819
|
+
...missing ? { missing } : {},
|
|
1820
|
+
...peerStats(column, tree, node, value)
|
|
1821
|
+
};
|
|
1822
|
+
if (!baseline) return metric;
|
|
1823
|
+
metric.baseline = baselineValue(baseline, node.ref.id, name);
|
|
1824
|
+
metric.delta = delta(value, metric.baseline);
|
|
1825
|
+
return metric;
|
|
1826
|
+
}
|
|
1827
|
+
function getNode(source, args) {
|
|
1828
|
+
const model = modelFor(source, args.snapshot);
|
|
1829
|
+
const tree = treeFor(model);
|
|
1830
|
+
const node = tree.byId.get(args.id);
|
|
1831
|
+
if (!node) throw nodeNotFound(args.id, model.snapshot.id);
|
|
1832
|
+
const baseline = openBaseline(source, args.baseline);
|
|
1833
|
+
return {
|
|
1834
|
+
snapshotId: model.snapshot.id,
|
|
1835
|
+
...baselineFields(model, baseline),
|
|
1836
|
+
node: describeNode(node),
|
|
1837
|
+
ancestors: ancestorsOf(tree, node),
|
|
1838
|
+
childCounts: childCounts(node),
|
|
1839
|
+
metrics: metricNames(model, node.ref.kind, args.metrics).map((name) => measure2(model, tree, node, name, baseline)),
|
|
1840
|
+
...args.lenses.length > 0 ? { lenses: lensesFor(model, node, args) } : {}
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
// src/query/overview.ts
|
|
1845
|
+
import {
|
|
1846
|
+
computeHealth,
|
|
1847
|
+
topBusFactorRisks
|
|
1848
|
+
} from "@titan-design/code-graph/analysis";
|
|
1849
|
+
var UNMEASURED = /* @__PURE__ */ new Set(["hidden-coupling"]);
|
|
1850
|
+
function findingRows(model, baseline) {
|
|
1851
|
+
const current = findingsFor(model);
|
|
1852
|
+
return baseline ? withStatus(current, findingsFor(baseline.model)) : current.map((f) => ({ ...f, status: "carryover" }));
|
|
1853
|
+
}
|
|
1854
|
+
function findingCounts(rows, withBaseline) {
|
|
1855
|
+
const count = (status) => rows.filter((f) => f.status === status).length;
|
|
1856
|
+
const open = rows.length - count("resolved");
|
|
1857
|
+
if (!withBaseline) return { open };
|
|
1858
|
+
return { open, new: count("new"), carryover: open - count("new"), resolved: count("resolved") };
|
|
1859
|
+
}
|
|
1860
|
+
function toWeights(w) {
|
|
1861
|
+
return {
|
|
1862
|
+
hotspots: w.hotspots,
|
|
1863
|
+
findings: { eachNew: w.findings.each_new, eachCarry: w.findings.each_carry, cap: w.findings.cap },
|
|
1864
|
+
complexity: w.complexity,
|
|
1865
|
+
hiddenCoupling: w.hidden_coupling
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1868
|
+
function signals(args, kpis, findings2) {
|
|
1869
|
+
const excluded = new Set(args.exclude_rules);
|
|
1870
|
+
const counted = findings2.filter((f) => f.status !== "resolved" && !excluded.has(f.rule));
|
|
1871
|
+
const newCount = counted.filter((f) => f.status === "new").length;
|
|
1872
|
+
const input = {
|
|
1873
|
+
scary: kpis.hotspotsOverCutoff,
|
|
1874
|
+
newViolations: newCount,
|
|
1875
|
+
carryViolations: counted.length - newCount,
|
|
1876
|
+
maxComplexity: kpis.maxComplexity,
|
|
1877
|
+
hiddenCoupling: 0,
|
|
1878
|
+
scaryCutoff: args.cutoff
|
|
1879
|
+
};
|
|
1880
|
+
const { health, healthBreakdown } = computeHealth(input, toWeights(args.weights));
|
|
1881
|
+
return { signals: healthBreakdown.map((c) => ({ ...c, measured: !UNMEASURED.has(c.key) })), combined: health };
|
|
1882
|
+
}
|
|
1883
|
+
function readingOrder(model, limit) {
|
|
1884
|
+
return centralFiles(model).slice(0, limit).map((r) => ({ node: toRef(model.nodeById.get(r.nodeId)), centrality: r.score }));
|
|
1885
|
+
}
|
|
1886
|
+
function lookFirst(model, hotspots, findings2, args) {
|
|
1887
|
+
const flagged = new Set(findings2.filter((f) => f.status !== "resolved").map((f) => f.node.path));
|
|
1888
|
+
return hotspots.slice(0, args.look_limit).map((h) => {
|
|
1889
|
+
const reasons = [];
|
|
1890
|
+
if (h.score >= args.cutoff) reasons.push("over-cutoff");
|
|
1891
|
+
if (flagged.has(h.nodeId)) reasons.push("findings");
|
|
1892
|
+
const { score, churn, complexity: complexity2, recency } = h;
|
|
1893
|
+
const node = toRef(model.nodeById.get(h.nodeId));
|
|
1894
|
+
return { node, score, churn, complexity: complexity2, recency, reasons: reasons.length > 0 ? reasons : ["churn-complexity"] };
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
function getOverview(source, args) {
|
|
1898
|
+
const model = modelFor(source, args.snapshot);
|
|
1899
|
+
const baseline = openBaseline(source, args.baseline);
|
|
1900
|
+
const ctx = reportContext(model, args.window);
|
|
1901
|
+
const hotspots = scoredRows(model, { grain: "file", window: args.window });
|
|
1902
|
+
const findings2 = findingRows(model, baseline);
|
|
1903
|
+
const kpis = {
|
|
1904
|
+
hotspotsOverCutoff: hotspots.filter((h) => h.score >= args.cutoff).length,
|
|
1905
|
+
maxComplexity: hotspots.reduce((max, h) => Math.max(max, h.complexity), 0),
|
|
1906
|
+
knowledgeSilos: topBusFactorRisks(ctx, Infinity).length,
|
|
1907
|
+
findings: findingCounts(findings2, baseline !== void 0)
|
|
1908
|
+
};
|
|
1909
|
+
const { signals: attention, combined } = signals(args, kpis, findings2);
|
|
1910
|
+
return {
|
|
1911
|
+
snapshotId: model.snapshot.id,
|
|
1912
|
+
...baselineFields(model, baseline),
|
|
1913
|
+
kpis,
|
|
1914
|
+
signals: attention,
|
|
1915
|
+
...args.combined ? { combined } : {},
|
|
1916
|
+
readingOrder: readingOrder(model, args.reading_limit),
|
|
1917
|
+
lookFirst: lookFirst(model, hotspots, findings2, args)
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
// src/query/packages-stats.ts
|
|
1922
|
+
import {
|
|
1923
|
+
bucketFilesByPackage,
|
|
1924
|
+
computePartitionQuality,
|
|
1925
|
+
filteredFileIds
|
|
1926
|
+
} from "@titan-design/code-graph/analysis";
|
|
1927
|
+
var NON_STRUCTURAL = /* @__PURE__ */ new Set(["references", "calls"]);
|
|
1928
|
+
var UNDECLARED = { status: "undeclared" };
|
|
1929
|
+
var trimRoot = (id) => id.replace(/\/+$/, "");
|
|
1930
|
+
function declaredTiers(rules) {
|
|
1931
|
+
const out = /* @__PURE__ */ new Map();
|
|
1932
|
+
for (const rule of rules) {
|
|
1933
|
+
rule.layers?.forEach((tier, i) => {
|
|
1934
|
+
for (const id of tier.map(trimRoot)) if (!out.has(id)) out.set(id, { status: "declared", tier: i, rule: rule.id });
|
|
1935
|
+
});
|
|
1936
|
+
}
|
|
1937
|
+
return out;
|
|
1938
|
+
}
|
|
1939
|
+
function packageRoots(args, tiers) {
|
|
1940
|
+
const ids = [...new Set((args.packages ?? [...tiers.keys()]).map(trimRoot))].filter((id) => id !== "").sort();
|
|
1941
|
+
const packagesFrom = args.packages ? "args" : ids.length > 0 ? "tiers" : "none";
|
|
1942
|
+
return { packagesFrom, roots: ids.map((id) => ({ id, name: id.slice(id.lastIndexOf("/") + 1) })) };
|
|
1943
|
+
}
|
|
1944
|
+
function structuralEdges(model) {
|
|
1945
|
+
return model.edges.filter((e) => !NON_STRUCTURAL.has(e.kind)).map((e) => ({ srcId: e.srcId, dstId: e.dstId, kind: e.kind, attrs: e.attrs }));
|
|
1946
|
+
}
|
|
1947
|
+
function toRow2(stats, name, layer) {
|
|
1948
|
+
const { pkgId, layer: band, flags, ...measures } = stats;
|
|
1949
|
+
return { id: pkgId, name, ...measures, band, flags: [...flags], layer };
|
|
1950
|
+
}
|
|
1951
|
+
function packagesStats(source, args) {
|
|
1952
|
+
const model = modelFor(source, args.snapshot);
|
|
1953
|
+
const tiers = declaredTiers(model.rules);
|
|
1954
|
+
const { packagesFrom, roots } = packageRoots(args, tiers);
|
|
1955
|
+
const { nodes } = inputsFor(model);
|
|
1956
|
+
const fileByPackage = bucketFilesByPackage(filteredFileIds(nodes, {}), roots);
|
|
1957
|
+
const quality = computePartitionQuality({ packages: roots, fileByPackage, nodes, edges: structuralEdges(model) });
|
|
1958
|
+
const names = new Map(roots.map((r) => [r.id, r.name]));
|
|
1959
|
+
return {
|
|
1960
|
+
snapshotId: model.snapshot.id,
|
|
1961
|
+
packagesFrom,
|
|
1962
|
+
modularity: quality.modularityQ,
|
|
1963
|
+
totalEdges: quality.totalEdges,
|
|
1964
|
+
unassignedFiles: fileByPackage.get("")?.length ?? 0,
|
|
1965
|
+
packages: quality.perPackage.map((p) => toRow2(p, names.get(p.pkgId), tiers.get(p.pkgId) ?? UNDECLARED)),
|
|
1966
|
+
crossEdges: quality.pairCoupling.map((c) => ({ ...c }))
|
|
1967
|
+
};
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
// src/query/paths-impact.ts
|
|
1971
|
+
import { hotspotComplexityOf, hotspotScoreOf as hotspotScoreOf2 } from "@titan-design/code-graph/analysis";
|
|
1972
|
+
var WORST_FIRST = compareFindings("severity", "desc");
|
|
1973
|
+
function repoRelative(input, root) {
|
|
1974
|
+
const rel = underRoot(input, root);
|
|
1975
|
+
if (rel === null) return null;
|
|
1976
|
+
const parts = [];
|
|
1977
|
+
for (const segment of rel.split("/")) {
|
|
1978
|
+
if (segment === "" || segment === ".") continue;
|
|
1979
|
+
if (segment !== "..") parts.push(segment);
|
|
1980
|
+
else if (parts.pop() === void 0) return null;
|
|
1981
|
+
}
|
|
1982
|
+
return parts.join("/");
|
|
1983
|
+
}
|
|
1984
|
+
function underRoot(input, root) {
|
|
1985
|
+
if (!input.startsWith("/")) return input;
|
|
1986
|
+
if (root === void 0) return null;
|
|
1987
|
+
const base = root.replace(/\/+$/, "");
|
|
1988
|
+
if (input === base) return "";
|
|
1989
|
+
return input.startsWith(`${base}/`) ? input.slice(base.length + 1) : null;
|
|
1990
|
+
}
|
|
1991
|
+
var trimmed = (dir) => dir.replace(/\/+$/, "");
|
|
1992
|
+
function checkRoot(root, repoRoot) {
|
|
1993
|
+
if (root === void 0 || !repoRoot) return;
|
|
1994
|
+
const base = trimmed(repoRoot);
|
|
1995
|
+
const given = trimmed(root);
|
|
1996
|
+
if (given !== base && `${base}/`.startsWith(`${given}/`)) {
|
|
1997
|
+
throw invalidArgs(`root "${root}" is above the index's repo root; give the repo, or a checkout of it, as root`);
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
var isFile = (model, id) => model.nodeById.get(id)?.kind === "file";
|
|
2001
|
+
function sideOf(model, window) {
|
|
2002
|
+
const rows = scoredRows(model, { grain: "file", window });
|
|
2003
|
+
const hotspots = new Map(rows.map((r, i) => [r.nodeId, { score: r.score, rank: i + 1 }]));
|
|
2004
|
+
return { model, ctx: reportContext(model, window), hotspots };
|
|
2005
|
+
}
|
|
2006
|
+
function openByFile(model) {
|
|
2007
|
+
const out = /* @__PURE__ */ new Map();
|
|
2008
|
+
for (const f of findingsFor(model)) {
|
|
2009
|
+
const rows = out.get(f.node.path);
|
|
2010
|
+
if (rows) rows.push(f);
|
|
2011
|
+
else out.set(f.node.path, [f]);
|
|
2012
|
+
}
|
|
2013
|
+
for (const rows of out.values()) rows.sort(WORST_FIRST);
|
|
2014
|
+
return out;
|
|
2015
|
+
}
|
|
2016
|
+
function statuses(buckets) {
|
|
2017
|
+
const status = /* @__PURE__ */ new Map();
|
|
2018
|
+
for (const u of buckets.unchanged) status.set(u.to.row.id, "carryover");
|
|
2019
|
+
for (const u of buckets.worsened) status.set(u.to.row.id, "worsened");
|
|
2020
|
+
for (const u of buckets.improved) status.set(u.to.row.id, "improved");
|
|
2021
|
+
for (const v of buckets.newViolations) status.set(v.row.id, "new");
|
|
2022
|
+
return status;
|
|
2023
|
+
}
|
|
2024
|
+
function findingDeltas(buckets) {
|
|
2025
|
+
const out = /* @__PURE__ */ new Map();
|
|
2026
|
+
const count = (v, key) => {
|
|
2027
|
+
const d = out.get(v.row.node.path) ?? { new: 0, worsened: 0, improved: 0, resolved: 0 };
|
|
2028
|
+
d[key] += 1;
|
|
2029
|
+
out.set(v.row.node.path, d);
|
|
2030
|
+
};
|
|
2031
|
+
for (const v of buckets.newViolations) count(v, "new");
|
|
2032
|
+
for (const u of buckets.worsened) count(u.to, "worsened");
|
|
2033
|
+
for (const u of buckets.improved) count(u.to, "improved");
|
|
2034
|
+
for (const v of buckets.resolvedViolations) count(v, "resolved");
|
|
2035
|
+
return out;
|
|
2036
|
+
}
|
|
2037
|
+
function compareTo(current, baseline) {
|
|
2038
|
+
const buckets = bucketFindings(baseline.model, current.model);
|
|
2039
|
+
return { baseline, status: statuses(buckets), findings: findingDeltas(buckets) };
|
|
2040
|
+
}
|
|
2041
|
+
function deltaOf(row, comparison) {
|
|
2042
|
+
const { model, ctx } = comparison.baseline;
|
|
2043
|
+
const inBaseline = isFile(model, row.path);
|
|
2044
|
+
const scoreBefore = inBaseline ? hotspotScoreOf2(ctx, row.path) : null;
|
|
2045
|
+
const complexityBefore = inBaseline ? hotspotComplexityOf(ctx, row.path) ?? null : null;
|
|
2046
|
+
return {
|
|
2047
|
+
inBaseline,
|
|
2048
|
+
scoreBefore,
|
|
2049
|
+
score: delta(row.hotspot.score, scoreBefore),
|
|
2050
|
+
complexityBefore,
|
|
2051
|
+
complexity: delta(row.complexity, complexityBefore),
|
|
2052
|
+
findings: comparison.findings.get(row.path) ?? { new: 0, worsened: 0, improved: 0, resolved: 0 }
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
2055
|
+
function indexedRow(input, path, reading) {
|
|
2056
|
+
const { current, comparison } = reading;
|
|
2057
|
+
const open = reading.open.get(path) ?? [];
|
|
2058
|
+
const row = {
|
|
2059
|
+
status: "indexed",
|
|
2060
|
+
input,
|
|
2061
|
+
path,
|
|
2062
|
+
node: toRef(current.model.nodeById.get(path)),
|
|
2063
|
+
complexity: hotspotComplexityOf(current.ctx, path) ?? null,
|
|
2064
|
+
hotspot: current.hotspots.get(path) ?? { score: 0, rank: null },
|
|
2065
|
+
findings: comparison ? open.map((f) => ({ ...f, status: comparison.status.get(f.id) ?? "carryover" })) : open
|
|
2066
|
+
};
|
|
2067
|
+
if (comparison !== void 0) row.delta = comparison && deltaOf(row, comparison);
|
|
2068
|
+
return row;
|
|
2069
|
+
}
|
|
2070
|
+
function rowFor(input, path, reading) {
|
|
2071
|
+
if (path === null) return { status: "outside-repo", input };
|
|
2072
|
+
if (!isFile(reading.current.model, path)) return { status: "not-indexed", input, path };
|
|
2073
|
+
return indexedRow(input, path, reading);
|
|
2074
|
+
}
|
|
2075
|
+
function rowsFor(args, reading) {
|
|
2076
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2077
|
+
const rows = [];
|
|
2078
|
+
for (const input of args.paths) {
|
|
2079
|
+
const path = repoRelative(input, args.root);
|
|
2080
|
+
const key = path ?? `outside:${input}`;
|
|
2081
|
+
if (seen.has(key)) continue;
|
|
2082
|
+
seen.add(key);
|
|
2083
|
+
rows.push(rowFor(input, path, reading));
|
|
2084
|
+
}
|
|
2085
|
+
return rows;
|
|
2086
|
+
}
|
|
2087
|
+
var best = (values, pick) => {
|
|
2088
|
+
const measured = values.filter((v) => v !== null);
|
|
2089
|
+
return measured.length > 0 ? pick(...measured) : null;
|
|
2090
|
+
};
|
|
2091
|
+
function deletedResolved(rows, comparison) {
|
|
2092
|
+
return rows.reduce((sum, r) => sum + (r.status === "not-indexed" ? comparison.findings.get(r.path)?.resolved ?? 0 : 0), 0);
|
|
2093
|
+
}
|
|
2094
|
+
function rollupDelta(indexed, deleted) {
|
|
2095
|
+
const findings2 = { new: 0, worsened: 0, improved: 0, resolved: 0 };
|
|
2096
|
+
let score = 0;
|
|
2097
|
+
for (const { delta: d, hotspot } of indexed) {
|
|
2098
|
+
score += d.score ?? hotspot.score;
|
|
2099
|
+
for (const key of Object.keys(findings2)) findings2[key] += d.findings[key];
|
|
2100
|
+
}
|
|
2101
|
+
findings2.resolved += deleted;
|
|
2102
|
+
return { score, findings: findings2 };
|
|
2103
|
+
}
|
|
2104
|
+
function rollupOf(rows, comparison) {
|
|
2105
|
+
const indexed = rows.filter((r) => r.status === "indexed");
|
|
2106
|
+
const rollup = {
|
|
2107
|
+
indexed: indexed.length,
|
|
2108
|
+
notIndexed: rows.filter((r) => r.status === "not-indexed").length,
|
|
2109
|
+
outsideRepo: rows.filter((r) => r.status === "outside-repo").length,
|
|
2110
|
+
score: indexed.reduce((sum, r) => sum + r.hotspot.score, 0),
|
|
2111
|
+
maxComplexity: best(indexed.map((r) => r.complexity), Math.max),
|
|
2112
|
+
topRank: best(indexed.map((r) => r.hotspot.rank), Math.min),
|
|
2113
|
+
openFindings: indexed.reduce((sum, r) => sum + r.findings.length, 0)
|
|
2114
|
+
};
|
|
2115
|
+
if (comparison !== void 0) rollup.delta = comparison && rollupDelta(indexed, deletedResolved(rows, comparison));
|
|
2116
|
+
return rollup;
|
|
2117
|
+
}
|
|
2118
|
+
function pathsImpact(source, args) {
|
|
2119
|
+
checkRoot(args.root, source.repoRoot);
|
|
2120
|
+
const model = modelFor(source, args.snapshot);
|
|
2121
|
+
const baseline = openBaseline(source, args.baseline);
|
|
2122
|
+
const fields = baselineFields(model, baseline);
|
|
2123
|
+
const current = sideOf(model, args.window);
|
|
2124
|
+
const comparison = baseline && (fields.comparable ? compareTo(current, sideOf(baseline.model, args.window)) : null);
|
|
2125
|
+
const reading = { current, open: openByFile(model), comparison };
|
|
2126
|
+
const rows = rowsFor(args, reading);
|
|
2127
|
+
return { snapshotId: model.snapshot.id, ...fields, rows, ranked: current.hotspots.size, rollup: rollupOf(rows, comparison) };
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
// src/query/resolve.ts
|
|
2131
|
+
var SEARCHED_KINDS = /* @__PURE__ */ new Set(["directory", "file", "symbol"]);
|
|
2132
|
+
var PATH_LINE = /^([^#]+):(\d+)$/;
|
|
2133
|
+
function scoreNode(node, q) {
|
|
2134
|
+
const id = (node.ref.kind === "directory" ? node.ref.path : node.ref.id).toLowerCase();
|
|
2135
|
+
const name = node.ref.name.toLowerCase();
|
|
2136
|
+
if (id === q || name === q) return { score: 100, match: "exact" };
|
|
2137
|
+
if (id.endsWith(`/${q}`) || id.endsWith(`#${q}`)) return { score: 80, match: "suffix" };
|
|
2138
|
+
if (node.ref.kind === "symbol" && name.endsWith(`.${q}`)) return { score: 80, match: "suffix" };
|
|
2139
|
+
if (name.startsWith(q)) return { score: 60, match: "prefix" };
|
|
2140
|
+
if (id.includes(q)) return { score: 40, match: "substring" };
|
|
2141
|
+
if (name.includes(q)) return { score: 30, match: "substring" };
|
|
2142
|
+
return null;
|
|
2143
|
+
}
|
|
2144
|
+
function normalize(query) {
|
|
2145
|
+
const q = query.toLowerCase();
|
|
2146
|
+
return q.length > 1 && q.endsWith("/") ? q.slice(0, -1) : q;
|
|
2147
|
+
}
|
|
2148
|
+
function rank2(tree, query, kinds) {
|
|
2149
|
+
const q = normalize(query);
|
|
2150
|
+
const hits = [];
|
|
2151
|
+
for (const node of tree.byId.values()) {
|
|
2152
|
+
if (!kinds.has(node.ref.kind)) continue;
|
|
2153
|
+
const scored = scoreNode(node, q);
|
|
2154
|
+
if (scored) hits.push({ node: node.ref, ...scored });
|
|
2155
|
+
}
|
|
2156
|
+
return hits.sort((a, b) => b.score - a.score || (a.node.id < b.node.id ? -1 : a.node.id > b.node.id ? 1 : 0));
|
|
2157
|
+
}
|
|
2158
|
+
function innermostContaining(file, line) {
|
|
2159
|
+
let best2 = null;
|
|
2160
|
+
const visit = (node) => {
|
|
2161
|
+
for (const child of node.children) {
|
|
2162
|
+
const span = child.ref.span;
|
|
2163
|
+
if (span && span.startLine <= line && line <= span.endLine) {
|
|
2164
|
+
const bestSpan = best2?.ref.span;
|
|
2165
|
+
if (!bestSpan || span.endLine - span.startLine < bestSpan.endLine - bestSpan.startLine) best2 = child;
|
|
2166
|
+
}
|
|
2167
|
+
visit(child);
|
|
2168
|
+
}
|
|
2169
|
+
};
|
|
2170
|
+
visit(file);
|
|
2171
|
+
return best2;
|
|
2172
|
+
}
|
|
2173
|
+
function resolvePath(tree, path, line) {
|
|
2174
|
+
const files = rank2(tree, path, /* @__PURE__ */ new Set(["file"]));
|
|
2175
|
+
if (line === void 0) return files;
|
|
2176
|
+
return files.map((hit) => {
|
|
2177
|
+
const symbol = innermostContaining(tree.byId.get(hit.node.id), line);
|
|
2178
|
+
return symbol ? { node: symbol.ref, score: hit.score, match: "span" } : hit;
|
|
2179
|
+
});
|
|
2180
|
+
}
|
|
2181
|
+
function pathAndLine(args) {
|
|
2182
|
+
if (args.path !== void 0) return { path: args.path, line: args.line };
|
|
2183
|
+
const m = PATH_LINE.exec(args.query);
|
|
2184
|
+
return m ? { path: m[1], line: Number(m[2]) } : null;
|
|
2185
|
+
}
|
|
2186
|
+
function resolveNode(source, args) {
|
|
2187
|
+
if (args.query === void 0 === (args.path === void 0)) throw invalidArgs("pass exactly one of query or path");
|
|
2188
|
+
if (args.line !== void 0 && args.path === void 0) throw invalidArgs("line needs path");
|
|
2189
|
+
const tree = treeFor(modelFor(source, args.snapshot));
|
|
2190
|
+
const located = pathAndLine(args);
|
|
2191
|
+
const candidates = located ? resolvePath(tree, located.path, located.line) : rank2(tree, args.query, SEARCHED_KINDS);
|
|
2192
|
+
return { candidates: candidates.slice(0, args.limit) };
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
// src/query/commands.ts
|
|
2196
|
+
function describeApi(source) {
|
|
2197
|
+
const facts = source.facts();
|
|
2198
|
+
const snapshots = source.snapshots();
|
|
2199
|
+
const newest = snapshots[0] ?? null;
|
|
2200
|
+
return {
|
|
2201
|
+
api: CODE_READ_API_VERSION,
|
|
2202
|
+
dataset: facts.dataset,
|
|
2203
|
+
commands: [...facts.commands],
|
|
2204
|
+
newest,
|
|
2205
|
+
indexVersions: [...new Set(snapshots.map((s) => s.indexVersion))],
|
|
2206
|
+
capabilities: facts.capabilities,
|
|
2207
|
+
metrics: newest ? [...source.model(newest.id).metricCatalogue] : [],
|
|
2208
|
+
rules: [...facts.rules]
|
|
2209
|
+
};
|
|
2210
|
+
}
|
|
2211
|
+
function listSnapshots(source, args) {
|
|
2212
|
+
const matching = source.snapshots().filter((s) => args.ref === void 0 || s.ref === args.ref);
|
|
2213
|
+
return { snapshots: matching.slice(0, args.limit) };
|
|
2214
|
+
}
|
|
2215
|
+
var QUERIES = {
|
|
2216
|
+
"api.describe": describeApi,
|
|
2217
|
+
"snapshot.list": listSnapshots,
|
|
2218
|
+
"hierarchy.get": getHierarchy,
|
|
2219
|
+
"node.get": getNode,
|
|
2220
|
+
"node.resolve": resolveNode,
|
|
2221
|
+
"findings.list": listFindings,
|
|
2222
|
+
"finding.get": getFinding,
|
|
2223
|
+
"node.neighbors": getNeighbors,
|
|
2224
|
+
"hotspots.list": listHotspots,
|
|
2225
|
+
"overview.get": getOverview,
|
|
2226
|
+
"changes.get": getChanges,
|
|
2227
|
+
"paths.impact": pathsImpact,
|
|
2228
|
+
"packages.stats": packagesStats
|
|
2229
|
+
};
|
|
2230
|
+
|
|
2231
|
+
// src/query/resolver.ts
|
|
2232
|
+
import { EXIT as EXIT3, errorEnvelope, successEnvelope } from "@titan-design/rpc-protocol";
|
|
2233
|
+
function isCommandName(name) {
|
|
2234
|
+
return Object.hasOwn(CONTRACT, name);
|
|
2235
|
+
}
|
|
2236
|
+
function formatIssues(error) {
|
|
2237
|
+
return error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
2238
|
+
}
|
|
2239
|
+
function describeFailure(err) {
|
|
2240
|
+
const code = err?.code;
|
|
2241
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2242
|
+
return { message, code: typeof code === "number" ? code : EXIT3.GENERIC };
|
|
2243
|
+
}
|
|
2244
|
+
function createQueryResolver(source) {
|
|
2245
|
+
return (name, rawArgs) => {
|
|
2246
|
+
if (!isCommandName(name)) return errorEnvelope(`Unknown command: ${name}`, EXIT3.USAGE);
|
|
2247
|
+
if (!source.facts().commands.includes(name)) {
|
|
2248
|
+
return errorEnvelope(`${name} is not available from a ${source.facts().dataset} dataset`, EXIT3.UNAVAILABLE);
|
|
2249
|
+
}
|
|
2250
|
+
const parsed = CONTRACT[name].args.safeParse(rawArgs ?? {});
|
|
2251
|
+
if (!parsed.success) return errorEnvelope(`Invalid arguments: ${formatIssues(parsed.error)}`, EXIT3.DATAERR);
|
|
2252
|
+
try {
|
|
2253
|
+
const query = QUERIES[name];
|
|
2254
|
+
return successEnvelope(query(source, parsed.data));
|
|
2255
|
+
} catch (err) {
|
|
2256
|
+
const { message, code } = describeFailure(err);
|
|
2257
|
+
return errorEnvelope(message, code);
|
|
2258
|
+
}
|
|
2259
|
+
};
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
export {
|
|
2263
|
+
NodeKind,
|
|
2264
|
+
Severity,
|
|
2265
|
+
ProvenanceKind,
|
|
2266
|
+
Provenance,
|
|
2267
|
+
SnapshotInfo,
|
|
2268
|
+
MetricDescriptor,
|
|
2269
|
+
RuleSummary,
|
|
2270
|
+
SnapshotRef,
|
|
2271
|
+
Span,
|
|
2272
|
+
NodeRef,
|
|
2273
|
+
MissingReason,
|
|
2274
|
+
Capabilities,
|
|
2275
|
+
FINDINGS_PAGE_MAX,
|
|
2276
|
+
EXCERPT_LINE_CAP,
|
|
2277
|
+
FindingStatus,
|
|
2278
|
+
Finding,
|
|
2279
|
+
SourceExcerpt,
|
|
2280
|
+
HOTSPOTS_PAGE_MAX,
|
|
2281
|
+
ChurnWindow,
|
|
2282
|
+
HotspotMark,
|
|
2283
|
+
Hotspot,
|
|
2284
|
+
NodeLens,
|
|
2285
|
+
ExportRow,
|
|
2286
|
+
ScoreBreakdown,
|
|
2287
|
+
Centrality,
|
|
2288
|
+
CoupledPartners,
|
|
2289
|
+
LinkedTests,
|
|
2290
|
+
NodeLenses,
|
|
2291
|
+
HIERARCHY_ROW_CAP,
|
|
2292
|
+
OVERVIEW_ROWS_MAX,
|
|
2293
|
+
AttentionSignal,
|
|
2294
|
+
LookFirstRow,
|
|
2295
|
+
ReadingOrderRow,
|
|
2296
|
+
CHANGES_ROWS_MAX,
|
|
2297
|
+
ScoreChange,
|
|
2298
|
+
NewFile,
|
|
2299
|
+
FindingChange,
|
|
2300
|
+
Regression,
|
|
2301
|
+
CoChangePair,
|
|
2302
|
+
PATHS_IMPACT_MAX,
|
|
2303
|
+
FindingDelta,
|
|
2304
|
+
PathDelta,
|
|
2305
|
+
PathHotspot,
|
|
2306
|
+
PathImpact,
|
|
2307
|
+
ImpactRollup,
|
|
2308
|
+
PACKAGES_MAX,
|
|
2309
|
+
PackageLayer,
|
|
2310
|
+
PackageStatsRow,
|
|
2311
|
+
CrossEdge,
|
|
2312
|
+
CODE_READ_API_VERSION,
|
|
2313
|
+
CONTRACT,
|
|
2314
|
+
COMMAND_NAMES,
|
|
2315
|
+
AGENT_COMMANDS,
|
|
2316
|
+
serializeContract,
|
|
2317
|
+
buildReadModel,
|
|
2318
|
+
ReadError,
|
|
2319
|
+
snapshotNotFound,
|
|
2320
|
+
nodeNotFound,
|
|
2321
|
+
findingNotFound,
|
|
2322
|
+
invalidArgs,
|
|
2323
|
+
resolveSnapshot,
|
|
2324
|
+
excessOf,
|
|
2325
|
+
listHotspots,
|
|
2326
|
+
getChanges,
|
|
2327
|
+
getNeighbors,
|
|
2328
|
+
RELATED_CAP,
|
|
2329
|
+
getFinding,
|
|
2330
|
+
listFindings,
|
|
2331
|
+
getHierarchy,
|
|
2332
|
+
getNode,
|
|
2333
|
+
getOverview,
|
|
2334
|
+
declaredTiers,
|
|
2335
|
+
packagesStats,
|
|
2336
|
+
repoRelative,
|
|
2337
|
+
pathsImpact,
|
|
2338
|
+
resolveNode,
|
|
2339
|
+
describeApi,
|
|
2340
|
+
listSnapshots,
|
|
2341
|
+
QUERIES,
|
|
2342
|
+
createQueryResolver
|
|
2343
|
+
};
|
|
2344
|
+
//# sourceMappingURL=chunk-7FNTXO7C.js.map
|