@titan-design/code-read 0.1.9 → 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
package/dist/chunk-VXQFCVGB.js
DELETED
|
@@ -1,1326 +0,0 @@
|
|
|
1
|
-
// src/query/contract.ts
|
|
2
|
-
import { toJSONSchema, z as z4 } 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-nodes.ts
|
|
187
|
-
import { z as z3 } from "zod";
|
|
188
|
-
var HIERARCHY_ROW_CAP = 5e3;
|
|
189
|
-
var MetricNames = z3.array(z3.string().min(1)).max(40);
|
|
190
|
-
var BaselineFields2 = {
|
|
191
|
-
baselineSnapshotId: z3.number().int().optional(),
|
|
192
|
-
comparable: z3.boolean().optional()
|
|
193
|
-
};
|
|
194
|
-
var hierarchyArgs = z3.object({
|
|
195
|
-
snapshot: SnapshotRef.optional(),
|
|
196
|
-
root: z3.string().optional(),
|
|
197
|
-
depth: z3.number().int().min(1).max(8).default(2),
|
|
198
|
-
metrics: MetricNames.default(["loc"]),
|
|
199
|
-
baseline: SnapshotRef.optional(),
|
|
200
|
-
include_symbols: z3.boolean().default(false),
|
|
201
|
-
exclude_roles: z3.array(z3.string()).default([])
|
|
202
|
-
});
|
|
203
|
-
var HierarchyRow = NodeRef.extend({
|
|
204
|
-
parentId: z3.string().nullable(),
|
|
205
|
-
/** Relative to the requested root, which is 0. */
|
|
206
|
-
depth: z3.number().int(),
|
|
207
|
-
/** Children in the full tree, symbols included, whatever `depth` and `include_symbols` returned. */
|
|
208
|
-
childCount: z3.number().int(),
|
|
209
|
-
role: z3.string().optional(),
|
|
210
|
-
values: z3.record(z3.string(), z3.number().nullable()),
|
|
211
|
-
missing: z3.record(z3.string(), MissingReason).optional(),
|
|
212
|
-
deltas: z3.record(z3.string(), z3.number().nullable()).optional()
|
|
213
|
-
});
|
|
214
|
-
var hierarchyResult = z3.object({
|
|
215
|
-
snapshotId: z3.number().int(),
|
|
216
|
-
...BaselineFields2,
|
|
217
|
-
nodes: z3.array(HierarchyRow),
|
|
218
|
-
truncated: z3.boolean()
|
|
219
|
-
});
|
|
220
|
-
var HIERARCHY_GET = {
|
|
221
|
-
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`.",
|
|
222
|
-
args: hierarchyArgs,
|
|
223
|
-
result: hierarchyResult
|
|
224
|
-
};
|
|
225
|
-
var nodeGetArgs = z3.object({
|
|
226
|
-
snapshot: SnapshotRef.optional(),
|
|
227
|
-
id: z3.string(),
|
|
228
|
-
baseline: SnapshotRef.optional(),
|
|
229
|
-
metrics: MetricNames.default([])
|
|
230
|
-
});
|
|
231
|
-
var NodeMetric = z3.object({
|
|
232
|
-
name: z3.string(),
|
|
233
|
-
unit: z3.string().nullable(),
|
|
234
|
-
direction: MetricDescriptor.shape.direction,
|
|
235
|
-
rollup: MetricDescriptor.shape.rollup,
|
|
236
|
-
value: z3.number().nullable(),
|
|
237
|
-
missing: MissingReason.optional(),
|
|
238
|
-
percentile: z3.number().nullable(),
|
|
239
|
-
siblingMedian: z3.number().nullable(),
|
|
240
|
-
siblingRank: z3.number().int().nullable(),
|
|
241
|
-
siblingCount: z3.number().int(),
|
|
242
|
-
baseline: z3.number().nullable().optional(),
|
|
243
|
-
delta: z3.number().nullable().optional()
|
|
244
|
-
});
|
|
245
|
-
var nodeGetResult = z3.object({
|
|
246
|
-
snapshotId: z3.number().int(),
|
|
247
|
-
...BaselineFields2,
|
|
248
|
-
node: NodeRef.extend({
|
|
249
|
-
language: z3.string().optional(),
|
|
250
|
-
role: z3.string().optional(),
|
|
251
|
-
exported: z3.boolean().optional(),
|
|
252
|
-
signature: z3.string().optional(),
|
|
253
|
-
purpose: z3.string().optional()
|
|
254
|
-
}),
|
|
255
|
-
ancestors: z3.array(NodeRef),
|
|
256
|
-
childCounts: z3.record(z3.string(), z3.number().int()),
|
|
257
|
-
metrics: z3.array(NodeMetric)
|
|
258
|
-
});
|
|
259
|
-
var NODE_GET = {
|
|
260
|
-
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.",
|
|
261
|
-
args: nodeGetArgs,
|
|
262
|
-
result: nodeGetResult
|
|
263
|
-
};
|
|
264
|
-
var nodeResolveArgs = z3.object({
|
|
265
|
-
snapshot: SnapshotRef.optional(),
|
|
266
|
-
query: z3.string().min(1).optional(),
|
|
267
|
-
path: z3.string().min(1).optional(),
|
|
268
|
-
line: z3.number().int().min(1).optional(),
|
|
269
|
-
limit: z3.number().int().min(1).max(50).default(10)
|
|
270
|
-
});
|
|
271
|
-
var nodeResolveResult = z3.object({
|
|
272
|
-
candidates: z3.array(
|
|
273
|
-
z3.object({
|
|
274
|
-
node: NodeRef,
|
|
275
|
-
score: z3.number(),
|
|
276
|
-
/** Open string: "exact" | "span" | "suffix" | "prefix" | "substring" today. */
|
|
277
|
-
match: z3.string()
|
|
278
|
-
})
|
|
279
|
-
)
|
|
280
|
-
});
|
|
281
|
-
var NODE_RESOLVE = {
|
|
282
|
-
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.",
|
|
283
|
-
args: nodeResolveArgs,
|
|
284
|
-
result: nodeResolveResult
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
// src/query/contract.ts
|
|
288
|
-
var CODE_READ_API_VERSION = "0.1.2";
|
|
289
|
-
var describeArgs = z4.object({});
|
|
290
|
-
var describeResult = z4.object({
|
|
291
|
-
api: z4.string(),
|
|
292
|
-
dataset: z4.enum(["live", "static"]),
|
|
293
|
-
commands: z4.array(z4.string()),
|
|
294
|
-
newest: SnapshotInfo.nullable(),
|
|
295
|
-
indexVersions: z4.array(z4.string()),
|
|
296
|
-
capabilities: Capabilities,
|
|
297
|
-
metrics: z4.array(MetricDescriptor),
|
|
298
|
-
rules: z4.array(RuleSummary)
|
|
299
|
-
});
|
|
300
|
-
var snapshotListArgs = z4.object({
|
|
301
|
-
ref: z4.string().min(1).optional(),
|
|
302
|
-
limit: z4.number().int().min(1).max(500).default(50)
|
|
303
|
-
});
|
|
304
|
-
var snapshotListResult = z4.object({ snapshots: z4.array(SnapshotInfo) });
|
|
305
|
-
var CONTRACT = {
|
|
306
|
-
"api.describe": {
|
|
307
|
-
description: "Contract version, commands served, capabilities, the newest snapshot, and the metric and rule catalogues. Call first to pin compatibility.",
|
|
308
|
-
args: describeArgs,
|
|
309
|
-
result: describeResult
|
|
310
|
-
},
|
|
311
|
-
"snapshot.list": {
|
|
312
|
-
description: "Indexed snapshots, newest first, with ref, commit, time, and index version. Optionally one ref only.",
|
|
313
|
-
args: snapshotListArgs,
|
|
314
|
-
result: snapshotListResult
|
|
315
|
-
},
|
|
316
|
-
"hierarchy.get": HIERARCHY_GET,
|
|
317
|
-
"node.get": NODE_GET,
|
|
318
|
-
"node.resolve": NODE_RESOLVE,
|
|
319
|
-
"findings.list": FINDINGS_LIST,
|
|
320
|
-
"finding.get": FINDING_GET,
|
|
321
|
-
"node.neighbors": NODE_NEIGHBORS
|
|
322
|
-
};
|
|
323
|
-
var COMMAND_NAMES = Object.keys(CONTRACT).sort();
|
|
324
|
-
var AGENT_COMMANDS = [
|
|
325
|
-
"api.describe",
|
|
326
|
-
"finding.get",
|
|
327
|
-
"findings.list",
|
|
328
|
-
"node.get",
|
|
329
|
-
"node.neighbors",
|
|
330
|
-
"node.resolve"
|
|
331
|
-
];
|
|
332
|
-
function serializeContract() {
|
|
333
|
-
const commands = {};
|
|
334
|
-
for (const name of COMMAND_NAMES) {
|
|
335
|
-
const { args, result } = CONTRACT[name];
|
|
336
|
-
commands[name] = { args: toJSONSchema(args, { io: "input" }), result: toJSONSchema(result) };
|
|
337
|
-
}
|
|
338
|
-
return { api: CODE_READ_API_VERSION, commands };
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// src/query/model.ts
|
|
342
|
-
function buildReadModel(parts) {
|
|
343
|
-
const nodeById = new Map(parts.nodes.map((n) => [n.id, n]));
|
|
344
|
-
return {
|
|
345
|
-
snapshot: parts.snapshot,
|
|
346
|
-
nodes: parts.nodes,
|
|
347
|
-
nodeById,
|
|
348
|
-
edges: parts.edges,
|
|
349
|
-
aliases: parts.aliases,
|
|
350
|
-
metrics: indexMetrics(parts.metrics),
|
|
351
|
-
metricCatalogue: buildCatalogue(parts, nodeById),
|
|
352
|
-
findings: parts.findings ?? [],
|
|
353
|
-
rules: parts.rules ?? []
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
function indexMetrics(metrics) {
|
|
357
|
-
const byName = /* @__PURE__ */ new Map();
|
|
358
|
-
for (const m of metrics) {
|
|
359
|
-
let values = byName.get(m.name);
|
|
360
|
-
if (!values) byName.set(m.name, values = /* @__PURE__ */ new Map());
|
|
361
|
-
values.set(m.nodeId, m.value);
|
|
362
|
-
}
|
|
363
|
-
return byName;
|
|
364
|
-
}
|
|
365
|
-
function observeMetrics(metrics, nodeById) {
|
|
366
|
-
const observed = /* @__PURE__ */ new Map();
|
|
367
|
-
for (const m of metrics) {
|
|
368
|
-
let entry = observed.get(m.name);
|
|
369
|
-
if (!entry) observed.set(m.name, entry = { unit: m.unit ?? null, kinds: /* @__PURE__ */ new Set() });
|
|
370
|
-
const kind = nodeById.get(m.nodeId)?.kind;
|
|
371
|
-
if (kind) entry.kinds.add(kind);
|
|
372
|
-
}
|
|
373
|
-
return observed;
|
|
374
|
-
}
|
|
375
|
-
function buildCatalogue(parts, nodeById) {
|
|
376
|
-
const observed = observeMetrics(parts.metrics, nodeById);
|
|
377
|
-
const engine = `code-graph@${parts.snapshot.indexVersion}`;
|
|
378
|
-
return [...observed.keys()].sort().map((name) => {
|
|
379
|
-
const entry = parts.describe(name);
|
|
380
|
-
return entry ? fromCatalogue(entry, engine) : uncatalogued(name, observed.get(name), engine);
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
function fromCatalogue(entry, engine) {
|
|
384
|
-
const descriptor = {
|
|
385
|
-
name: entry.name,
|
|
386
|
-
unit: entry.unit,
|
|
387
|
-
appliesTo: [...entry.appliesTo],
|
|
388
|
-
rollup: entry.rollup,
|
|
389
|
-
direction: entry.direction,
|
|
390
|
-
absent: entry.absent,
|
|
391
|
-
description: entry.description,
|
|
392
|
-
provenance: { kind: "measured", source: `${engine}/${entry.source}` }
|
|
393
|
-
};
|
|
394
|
-
if (entry.window !== void 0) descriptor.window = entry.window;
|
|
395
|
-
return descriptor;
|
|
396
|
-
}
|
|
397
|
-
function uncatalogued(name, seen, engine) {
|
|
398
|
-
return {
|
|
399
|
-
name,
|
|
400
|
-
unit: seen.unit,
|
|
401
|
-
appliesTo: [...seen.kinds].sort(),
|
|
402
|
-
rollup: "none",
|
|
403
|
-
direction: "neutral",
|
|
404
|
-
absent: "exclude",
|
|
405
|
-
description: "Not in code-graph's metric catalogue.",
|
|
406
|
-
provenance: { kind: "measured", source: `${engine}/uncatalogued` }
|
|
407
|
-
};
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// src/query/source.ts
|
|
411
|
-
import { EXIT } from "@titan-design/rpc-protocol";
|
|
412
|
-
var ReadError = class extends Error {
|
|
413
|
-
constructor(message, code) {
|
|
414
|
-
super(message);
|
|
415
|
-
this.code = code;
|
|
416
|
-
this.name = "ReadError";
|
|
417
|
-
}
|
|
418
|
-
code;
|
|
419
|
-
};
|
|
420
|
-
function snapshotNotFound(snapshotId) {
|
|
421
|
-
return new ReadError(`No snapshot with id ${snapshotId}`, EXIT.NOINPUT);
|
|
422
|
-
}
|
|
423
|
-
function nodeNotFound(id, snapshotId) {
|
|
424
|
-
return new ReadError(`No node "${id}" in snapshot ${snapshotId}`, EXIT.NOINPUT);
|
|
425
|
-
}
|
|
426
|
-
function findingNotFound(id, snapshotId) {
|
|
427
|
-
return new ReadError(`No finding "${id}" in snapshot ${snapshotId}`, EXIT.NOINPUT);
|
|
428
|
-
}
|
|
429
|
-
function invalidArgs(message) {
|
|
430
|
-
return new ReadError(`Invalid arguments: ${message}`, EXIT.DATAERR);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// src/query/snapshot-ref.ts
|
|
434
|
-
import { EXIT as EXIT2 } from "@titan-design/rpc-protocol";
|
|
435
|
-
var DIGITS = /^\d+$/;
|
|
436
|
-
function resolveSnapshot(source, ref) {
|
|
437
|
-
const snapshots = source.snapshots();
|
|
438
|
-
if (ref === void 0) {
|
|
439
|
-
const newest = snapshots[0];
|
|
440
|
-
if (!newest) throw new ReadError("No snapshots have been indexed", EXIT2.NOINPUT);
|
|
441
|
-
return newest;
|
|
442
|
-
}
|
|
443
|
-
if (typeof ref === "number" || DIGITS.test(ref)) {
|
|
444
|
-
const id = Number(ref);
|
|
445
|
-
const found = snapshots.find((s) => s.id === id);
|
|
446
|
-
if (!found) throw snapshotNotFound(id);
|
|
447
|
-
return found;
|
|
448
|
-
}
|
|
449
|
-
const newestOfRef = snapshots.find((s) => s.ref === ref);
|
|
450
|
-
if (!newestOfRef) throw new ReadError(`No snapshot of ref "${ref}"`, EXIT2.NOINPUT);
|
|
451
|
-
return newestOfRef;
|
|
452
|
-
}
|
|
453
|
-
function modelFor(source, ref) {
|
|
454
|
-
return source.model(resolveSnapshot(source, ref).id);
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// src/query/rollup.ts
|
|
458
|
-
var KIND_RANK = { repo: 0, directory: 1, file: 2, symbol: 3 };
|
|
459
|
-
var EMPTY = { sum: 0, count: 0, max: -Infinity };
|
|
460
|
-
function leafKind(descriptor) {
|
|
461
|
-
if (descriptor.appliesTo.includes("file")) return "file";
|
|
462
|
-
return descriptor.appliesTo.includes("symbol") ? "symbol" : null;
|
|
463
|
-
}
|
|
464
|
-
function rank(kind) {
|
|
465
|
-
return KIND_RANK[kind] ?? Infinity;
|
|
466
|
-
}
|
|
467
|
-
function storedValue(descriptor, values, id) {
|
|
468
|
-
const value = values?.get(id);
|
|
469
|
-
if (value !== void 0 && value !== null) return { value };
|
|
470
|
-
if (value === void 0 && descriptor.absent === "zero") return { value: 0 };
|
|
471
|
-
return { value: null, missing: "not-measured" };
|
|
472
|
-
}
|
|
473
|
-
function merge(a, b) {
|
|
474
|
-
return { sum: a.sum + b.sum, count: a.count + b.count, max: Math.max(a.max, b.max) };
|
|
475
|
-
}
|
|
476
|
-
function finalValue(rule, agg) {
|
|
477
|
-
if (agg.count === 0) return { value: null, missing: "not-measured" };
|
|
478
|
-
if (rule === "max") return { value: agg.max };
|
|
479
|
-
if (rule === "mean") return { value: agg.sum / agg.count };
|
|
480
|
-
return { value: agg.sum };
|
|
481
|
-
}
|
|
482
|
-
function buildColumn(model, name) {
|
|
483
|
-
const descriptor = model.metricCatalogue.find((m) => m.name === name);
|
|
484
|
-
if (!descriptor) return { descriptor, valueOf: () => ({ value: null, missing: "not-in-snapshot" }) };
|
|
485
|
-
const values = model.metrics.get(name);
|
|
486
|
-
const leaf = leafKind(descriptor);
|
|
487
|
-
const aggregates = /* @__PURE__ */ new Map();
|
|
488
|
-
const own = (node) => storedValue(descriptor, values, node.ref.id);
|
|
489
|
-
const aggregate = (node) => {
|
|
490
|
-
const cached = aggregates.get(node);
|
|
491
|
-
if (cached) return cached;
|
|
492
|
-
let agg = EMPTY;
|
|
493
|
-
if (node.ref.kind === leaf) {
|
|
494
|
-
const v = own(node).value;
|
|
495
|
-
if (v !== null) agg = { sum: v, count: 1, max: v };
|
|
496
|
-
}
|
|
497
|
-
for (const child of node.children) if (rank(child.ref.kind) <= rank(leaf ?? "")) agg = merge(agg, aggregate(child));
|
|
498
|
-
aggregates.set(node, agg);
|
|
499
|
-
return agg;
|
|
500
|
-
};
|
|
501
|
-
const valueOf2 = (node) => {
|
|
502
|
-
if (node.stored && descriptor.appliesTo.includes(node.ref.kind)) return own(node);
|
|
503
|
-
if (leaf === null || rank(node.ref.kind) >= rank(leaf)) return { value: null, missing: "not-applicable" };
|
|
504
|
-
if (descriptor.rollup === "none") return { value: null, missing: "no-rollup" };
|
|
505
|
-
return finalValue(descriptor.rollup, aggregate(node));
|
|
506
|
-
};
|
|
507
|
-
return { descriptor, valueOf: valueOf2 };
|
|
508
|
-
}
|
|
509
|
-
var columns = /* @__PURE__ */ new WeakMap();
|
|
510
|
-
function columnFor(model, tree, name) {
|
|
511
|
-
let byName = columns.get(tree);
|
|
512
|
-
if (!byName) columns.set(tree, byName = /* @__PURE__ */ new Map());
|
|
513
|
-
let column = byName.get(name);
|
|
514
|
-
if (!column) byName.set(name, column = buildColumn(model, name));
|
|
515
|
-
return column;
|
|
516
|
-
}
|
|
517
|
-
function describesKind(descriptor, kind) {
|
|
518
|
-
if (descriptor.appliesTo.includes(kind)) return true;
|
|
519
|
-
const leaf = leafKind(descriptor);
|
|
520
|
-
return leaf !== null && rank(kind) < rank(leaf);
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// src/query/tree.ts
|
|
524
|
-
var REPO_ID = "";
|
|
525
|
-
var SYMBOL_SEP = "#";
|
|
526
|
-
function directoryId(path) {
|
|
527
|
-
return path === "" ? REPO_ID : `${path}/`;
|
|
528
|
-
}
|
|
529
|
-
function spanOf(node) {
|
|
530
|
-
const { startLine, endLine } = node.attrs;
|
|
531
|
-
return typeof startLine === "number" && typeof endLine === "number" ? { startLine, endLine } : void 0;
|
|
532
|
-
}
|
|
533
|
-
function toRef(node) {
|
|
534
|
-
const sep = node.kind === "symbol" ? node.id.indexOf(SYMBOL_SEP) : -1;
|
|
535
|
-
const ref = { id: node.id, kind: node.kind, name: node.name, path: sep < 0 ? node.id : node.id.slice(0, sep) };
|
|
536
|
-
const span = spanOf(node);
|
|
537
|
-
if (span) ref.span = span;
|
|
538
|
-
return ref;
|
|
539
|
-
}
|
|
540
|
-
function treeNode(ref, parentId, stored) {
|
|
541
|
-
return stored ? { ref, parentId, depth: 0, stored, children: [] } : { ref, parentId, depth: 0, children: [] };
|
|
542
|
-
}
|
|
543
|
-
function dirPath(fileId) {
|
|
544
|
-
const slash = fileId.lastIndexOf("/");
|
|
545
|
-
return slash < 0 ? "" : fileId.slice(0, slash);
|
|
546
|
-
}
|
|
547
|
-
function ensureDirectory(byId, path) {
|
|
548
|
-
const id = directoryId(path);
|
|
549
|
-
const existing = byId.get(id);
|
|
550
|
-
if (existing) return existing;
|
|
551
|
-
const parent = ensureDirectory(byId, dirPath(path));
|
|
552
|
-
const name = path.slice(path.lastIndexOf("/") + 1);
|
|
553
|
-
const dir = treeNode({ id, kind: "directory", name, path }, parent.ref.id);
|
|
554
|
-
parent.children.push(dir);
|
|
555
|
-
byId.set(id, dir);
|
|
556
|
-
return dir;
|
|
557
|
-
}
|
|
558
|
-
function attach(byId, parent, stored) {
|
|
559
|
-
const node = treeNode(toRef(stored), parent.ref.id, stored);
|
|
560
|
-
parent.children.push(node);
|
|
561
|
-
byId.set(stored.id, node);
|
|
562
|
-
}
|
|
563
|
-
function containsStrictly(outer, inner) {
|
|
564
|
-
const wider = outer.endLine - outer.startLine > inner.endLine - inner.startLine;
|
|
565
|
-
return wider && outer.startLine <= inner.startLine && inner.endLine <= outer.endLine;
|
|
566
|
-
}
|
|
567
|
-
function innermostEnclosing(symbol, siblings) {
|
|
568
|
-
const span = spanOf(symbol);
|
|
569
|
-
if (!span) return void 0;
|
|
570
|
-
let best;
|
|
571
|
-
let bestSpan;
|
|
572
|
-
for (const other of siblings) {
|
|
573
|
-
const outer = spanOf(other);
|
|
574
|
-
if (other === symbol || !outer || !containsStrictly(outer, span)) continue;
|
|
575
|
-
if (!bestSpan || outer.endLine - outer.startLine < bestSpan.endLine - bestSpan.startLine) [best, bestSpan] = [other, outer];
|
|
576
|
-
}
|
|
577
|
-
return best;
|
|
578
|
-
}
|
|
579
|
-
function symbolParentId(symbol, fileId, bySymbolId, siblings) {
|
|
580
|
-
const segments = symbol.name.split(".");
|
|
581
|
-
for (let n = segments.length - 1; n > 0; n--) {
|
|
582
|
-
const scopeId = `${fileId}${SYMBOL_SEP}${segments.slice(0, n).join(".")}`;
|
|
583
|
-
if (bySymbolId.has(scopeId)) return scopeId;
|
|
584
|
-
}
|
|
585
|
-
return innermostEnclosing(symbol, siblings)?.id ?? fileId;
|
|
586
|
-
}
|
|
587
|
-
function symbolsByFile(model, byId) {
|
|
588
|
-
const out = /* @__PURE__ */ new Map();
|
|
589
|
-
for (const node of model.nodes) {
|
|
590
|
-
if (node.kind !== "symbol" || node.parentId === null || !byId.has(node.parentId)) continue;
|
|
591
|
-
const list = out.get(node.parentId);
|
|
592
|
-
if (list) list.push(node);
|
|
593
|
-
else out.set(node.parentId, [node]);
|
|
594
|
-
}
|
|
595
|
-
return out;
|
|
596
|
-
}
|
|
597
|
-
function attachSymbols(model, byId) {
|
|
598
|
-
for (const [fileId, symbols] of symbolsByFile(model, byId)) {
|
|
599
|
-
const bySymbolId = new Map(symbols.map((s) => [s.id, s]));
|
|
600
|
-
const parentOf = new Map(symbols.map((s) => [s.id, symbolParentId(s, fileId, bySymbolId, symbols)]));
|
|
601
|
-
const pending = [...symbols];
|
|
602
|
-
while (pending.length > 0) {
|
|
603
|
-
const ready = pending.filter((s) => byId.has(parentOf.get(s.id)));
|
|
604
|
-
if (ready.length === 0) for (const s of pending) parentOf.set(s.id, fileId);
|
|
605
|
-
for (const s of ready) attach(byId, byId.get(parentOf.get(s.id)), s);
|
|
606
|
-
for (const s of ready) pending.splice(pending.indexOf(s), 1);
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
var KIND_ORDER = { directory: 0, file: 1, symbol: 2 };
|
|
611
|
-
function compareSiblings(a, b) {
|
|
612
|
-
const byKind = (KIND_ORDER[a.ref.kind] ?? 3) - (KIND_ORDER[b.ref.kind] ?? 3);
|
|
613
|
-
if (byKind !== 0) return byKind;
|
|
614
|
-
const byLine = (a.ref.span?.startLine ?? Infinity) - (b.ref.span?.startLine ?? Infinity);
|
|
615
|
-
if (byLine !== 0 && !Number.isNaN(byLine)) return byLine;
|
|
616
|
-
return a.ref.id < b.ref.id ? -1 : a.ref.id > b.ref.id ? 1 : 0;
|
|
617
|
-
}
|
|
618
|
-
function finish(node, depth) {
|
|
619
|
-
node.depth = depth;
|
|
620
|
-
node.children.sort(compareSiblings);
|
|
621
|
-
for (const child of node.children) finish(child, depth + 1);
|
|
622
|
-
}
|
|
623
|
-
function buildTree(model, excludeRoles) {
|
|
624
|
-
const root = treeNode({ id: REPO_ID, kind: "repo", name: ".", path: "" }, null);
|
|
625
|
-
const byId = /* @__PURE__ */ new Map([[REPO_ID, root]]);
|
|
626
|
-
for (const node of model.nodes) {
|
|
627
|
-
if (node.kind !== "file" || node.role !== void 0 && excludeRoles.has(node.role)) continue;
|
|
628
|
-
attach(byId, ensureDirectory(byId, dirPath(node.id)), node);
|
|
629
|
-
}
|
|
630
|
-
attachSymbols(model, byId);
|
|
631
|
-
finish(root, 0);
|
|
632
|
-
return { root, byId };
|
|
633
|
-
}
|
|
634
|
-
var trees = /* @__PURE__ */ new WeakMap();
|
|
635
|
-
function treeFor(model, excludeRoles = []) {
|
|
636
|
-
const roles = [...new Set(excludeRoles)].sort();
|
|
637
|
-
const key = roles.join("\n");
|
|
638
|
-
let byKey = trees.get(model);
|
|
639
|
-
if (!byKey) trees.set(model, byKey = /* @__PURE__ */ new Map());
|
|
640
|
-
let tree = byKey.get(key);
|
|
641
|
-
if (!tree) byKey.set(key, tree = buildTree(model, new Set(roles)));
|
|
642
|
-
return tree;
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
// src/query/baseline.ts
|
|
646
|
-
function openBaseline(source, ref, excludeRoles = []) {
|
|
647
|
-
if (ref === void 0) return void 0;
|
|
648
|
-
const model = modelFor(source, ref);
|
|
649
|
-
return { model, tree: treeFor(model, excludeRoles) };
|
|
650
|
-
}
|
|
651
|
-
function baselineValue(baseline, id, name) {
|
|
652
|
-
const node = baseline.tree.byId.get(id);
|
|
653
|
-
return node ? columnFor(baseline.model, baseline.tree, name).valueOf(node).value : null;
|
|
654
|
-
}
|
|
655
|
-
function baselineFields(current, baseline) {
|
|
656
|
-
if (!baseline) return {};
|
|
657
|
-
return {
|
|
658
|
-
baselineSnapshotId: baseline.model.snapshot.id,
|
|
659
|
-
comparable: baseline.model.snapshot.indexVersion === current.snapshot.indexVersion
|
|
660
|
-
};
|
|
661
|
-
}
|
|
662
|
-
function delta(value, before) {
|
|
663
|
-
return value !== null && before !== null ? value - before : null;
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
// src/query/excerpt.ts
|
|
667
|
-
function excerptWindow(ranges, context, lineCount) {
|
|
668
|
-
const from = ranges.length === 0 ? 1 : Math.max(1, Math.min(...ranges.map((r) => r.startLine)) - context);
|
|
669
|
-
const wanted = ranges.length === 0 ? lineCount : Math.min(lineCount, Math.max(...ranges.map((r) => r.endLine)) + context);
|
|
670
|
-
const to = Math.min(wanted, from + EXCERPT_LINE_CAP - 1);
|
|
671
|
-
return { from, to, truncated: to < wanted };
|
|
672
|
-
}
|
|
673
|
-
function clipped(ranges, from, to) {
|
|
674
|
-
return ranges.filter((r) => r.endLine >= from && r.startLine <= to).map((r) => ({ startLine: Math.max(r.startLine, from), endLine: Math.min(r.endLine, to) }));
|
|
675
|
-
}
|
|
676
|
-
function holds(window, from, to) {
|
|
677
|
-
return from >= window.startLine && to <= window.startLine + window.lines.length - 1;
|
|
678
|
-
}
|
|
679
|
-
function buildExcerpt(read, ranges, context) {
|
|
680
|
-
if ("unavailable" in read) return { excerpt: null, missing: read.unavailable };
|
|
681
|
-
const { from, to, truncated } = excerptWindow(ranges, context, read.lineCount);
|
|
682
|
-
if (to >= from && !holds(read, from, to)) return { excerpt: null, missing: "not-in-export" };
|
|
683
|
-
const text = read.lines.slice(from - read.startLine, to - read.startLine + 1).join("\n");
|
|
684
|
-
const excerpt = {
|
|
685
|
-
path: read.path,
|
|
686
|
-
startLine: from,
|
|
687
|
-
endLine: to,
|
|
688
|
-
text,
|
|
689
|
-
contentHash: read.contentHash,
|
|
690
|
-
origin: read.origin,
|
|
691
|
-
highlights: clipped(ranges, from, to),
|
|
692
|
-
truncated
|
|
693
|
-
};
|
|
694
|
-
return { excerpt };
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
// src/query/finding-rows.ts
|
|
698
|
-
var SEVERITY_RANK = { error: 2, warning: 1, info: 0 };
|
|
699
|
-
var CHECK_TOOL = "check";
|
|
700
|
-
function refFor(model, id) {
|
|
701
|
-
const node = model.nodeById.get(id);
|
|
702
|
-
return node ? toRef(node) : { id, kind: "unknown", name: id, path: id };
|
|
703
|
-
}
|
|
704
|
-
function excessOf(ruleType, value, threshold) {
|
|
705
|
-
if (value === void 0 || threshold === void 0) return null;
|
|
706
|
-
if (ruleType === "metric-min") return value > 0 ? threshold / value : null;
|
|
707
|
-
return threshold > 0 ? value / threshold : null;
|
|
708
|
-
}
|
|
709
|
-
function toFinding(model, f, rules) {
|
|
710
|
-
const out = {
|
|
711
|
-
id: f.id,
|
|
712
|
-
snapshotId: model.snapshot.id,
|
|
713
|
-
rule: f.rule,
|
|
714
|
-
tool: CHECK_TOOL,
|
|
715
|
-
severity: f.severity,
|
|
716
|
-
node: refFor(model, f.nodeId),
|
|
717
|
-
excess: excessOf(rules.get(f.rule)?.type, f.value, f.threshold),
|
|
718
|
-
message: f.message,
|
|
719
|
-
provenance: { kind: "derived", source: `${CHECK_TOOL}/${f.rule}` }
|
|
720
|
-
};
|
|
721
|
-
if (f.destinationId !== void 0) out.destination = refFor(model, f.destinationId);
|
|
722
|
-
if (f.ranges?.[0]) out.range = f.ranges[0];
|
|
723
|
-
if (f.metric !== void 0) out.metric = f.metric;
|
|
724
|
-
if (f.value !== void 0) out.value = f.value;
|
|
725
|
-
if (f.threshold !== void 0) out.threshold = f.threshold;
|
|
726
|
-
return out;
|
|
727
|
-
}
|
|
728
|
-
var rowsByModel = /* @__PURE__ */ new WeakMap();
|
|
729
|
-
function findingsFor(model) {
|
|
730
|
-
let rows = rowsByModel.get(model);
|
|
731
|
-
if (!rows) {
|
|
732
|
-
const rules = new Map(model.rules.map((r) => [r.id, r]));
|
|
733
|
-
rowsByModel.set(model, rows = model.findings.map((f) => toFinding(model, f, rules)));
|
|
734
|
-
}
|
|
735
|
-
return rows;
|
|
736
|
-
}
|
|
737
|
-
function statusAgainst(current, before) {
|
|
738
|
-
if (!before) return "new";
|
|
739
|
-
if (current.excess === null || before.excess === null || current.excess === before.excess) return "carryover";
|
|
740
|
-
return current.excess > before.excess ? "worsened" : "improved";
|
|
741
|
-
}
|
|
742
|
-
function withStatus(current, baseline) {
|
|
743
|
-
const before = new Map(baseline.map((f) => [f.id, f]));
|
|
744
|
-
const now = new Set(current.map((f) => f.id));
|
|
745
|
-
const rows = current.map((f) => ({ ...f, status: statusAgainst(f, before.get(f.id)) }));
|
|
746
|
-
for (const f of baseline) if (!now.has(f.id)) rows.push({ ...f, status: "resolved" });
|
|
747
|
-
return rows;
|
|
748
|
-
}
|
|
749
|
-
var severityRank = (f) => SEVERITY_RANK[f.severity] ?? -1;
|
|
750
|
-
var byText = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
751
|
-
function byNullable(a, b, sign) {
|
|
752
|
-
const [x, y] = [a ?? null, b ?? null];
|
|
753
|
-
if (x === null || y === null) return x === y ? 0 : x === null ? 1 : -1;
|
|
754
|
-
return sign * (x - y);
|
|
755
|
-
}
|
|
756
|
-
var PRIMARY = {
|
|
757
|
-
severity: (sign) => (a, b) => sign * (severityRank(a) - severityRank(b)),
|
|
758
|
-
excess: (sign) => (a, b) => byNullable(a.excess, b.excess, sign),
|
|
759
|
-
value: (sign) => (a, b) => byNullable(a.value, b.value, sign),
|
|
760
|
-
path: (sign) => (a, b) => sign * byText(a.node.path, b.node.path),
|
|
761
|
-
rule: (sign) => (a, b) => sign * byText(a.rule, b.rule)
|
|
762
|
-
};
|
|
763
|
-
var TIES = [
|
|
764
|
-
PRIMARY.severity(-1),
|
|
765
|
-
PRIMARY.excess(-1),
|
|
766
|
-
PRIMARY.path(1),
|
|
767
|
-
PRIMARY.rule(1),
|
|
768
|
-
(a, b) => byText(a.id, b.id)
|
|
769
|
-
];
|
|
770
|
-
function compareFindings(sort, order) {
|
|
771
|
-
const chain = [PRIMARY[sort](order === "asc" ? 1 : -1), ...TIES];
|
|
772
|
-
return (a, b) => {
|
|
773
|
-
for (const compare of chain) {
|
|
774
|
-
const c = compare(a, b);
|
|
775
|
-
if (c !== 0) return c;
|
|
776
|
-
}
|
|
777
|
-
return 0;
|
|
778
|
-
};
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
// src/query/neighbors.ts
|
|
782
|
-
var adjacencies = /* @__PURE__ */ new WeakMap();
|
|
783
|
-
function push(map, key, edge) {
|
|
784
|
-
const list = map.get(key);
|
|
785
|
-
if (list) list.push(edge);
|
|
786
|
-
else map.set(key, [edge]);
|
|
787
|
-
}
|
|
788
|
-
function adjacencyOf(model) {
|
|
789
|
-
let adjacency = adjacencies.get(model);
|
|
790
|
-
if (!adjacency) {
|
|
791
|
-
const inbound = /* @__PURE__ */ new Map();
|
|
792
|
-
const outbound = /* @__PURE__ */ new Map();
|
|
793
|
-
for (const edge of model.edges) {
|
|
794
|
-
push(inbound, edge.dstId, edge);
|
|
795
|
-
push(outbound, edge.srcId, edge);
|
|
796
|
-
}
|
|
797
|
-
adjacencies.set(model, adjacency = { inbound, outbound });
|
|
798
|
-
}
|
|
799
|
-
return adjacency;
|
|
800
|
-
}
|
|
801
|
-
var weightOf = (edge) => typeof edge.attrs.weight === "number" ? edge.attrs.weight : null;
|
|
802
|
-
function valueOf(model, tree, id, name) {
|
|
803
|
-
const node = tree.byId.get(id);
|
|
804
|
-
if (node) return columnFor(model, tree, name).valueOf(node).value;
|
|
805
|
-
return model.metrics.get(name)?.get(id) ?? null;
|
|
806
|
-
}
|
|
807
|
-
function toNeighbor(model, tree, edge, otherId, metrics) {
|
|
808
|
-
const neighbor = { node: refFor(model, otherId), kind: edge.kind, weight: weightOf(edge), values: {} };
|
|
809
|
-
if (typeof edge.attrs.specifier === "string") neighbor.specifier = edge.attrs.specifier;
|
|
810
|
-
for (const name of metrics) neighbor.values[name] = valueOf(model, tree, otherId, name);
|
|
811
|
-
return neighbor;
|
|
812
|
-
}
|
|
813
|
-
function compareNeighbors(a, b) {
|
|
814
|
-
const [x, y] = [a.weight ?? -Infinity, b.weight ?? -Infinity];
|
|
815
|
-
if (x !== y) return y - x;
|
|
816
|
-
if (a.node.id !== b.node.id) return a.node.id < b.node.id ? -1 : 1;
|
|
817
|
-
return a.kind < b.kind ? -1 : a.kind > b.kind ? 1 : 0;
|
|
818
|
-
}
|
|
819
|
-
function edgeFilter(kinds, nodeKind) {
|
|
820
|
-
if (kinds.length > 0) return (edge) => kinds.includes(edge.kind);
|
|
821
|
-
return nodeKind === "symbol" ? () => true : (edge) => edge.kind !== "references";
|
|
822
|
-
}
|
|
823
|
-
function side(edges, other, p) {
|
|
824
|
-
const all = (edges ?? []).filter(p.keep).map((e) => p.build(e, other(e))).sort(compareNeighbors);
|
|
825
|
-
return { rows: all.slice(p.offset, p.offset + p.limit), total: all.length };
|
|
826
|
-
}
|
|
827
|
-
function getNeighbors(source, args) {
|
|
828
|
-
const model = modelFor(source, args.snapshot);
|
|
829
|
-
const tree = treeFor(model);
|
|
830
|
-
const node = model.nodeById.get(args.id);
|
|
831
|
-
if (!node && tree.byId.has(args.id)) throw invalidArgs(`${args.id} is synthesized; node.neighbors needs a stored node`);
|
|
832
|
-
if (!node) throw nodeNotFound(args.id, model.snapshot.id);
|
|
833
|
-
const { inbound, outbound } = adjacencyOf(model);
|
|
834
|
-
const metrics = [...new Set(args.metrics)];
|
|
835
|
-
const paging = {
|
|
836
|
-
keep: edgeFilter(args.edge_kinds, node.kind),
|
|
837
|
-
build: (e, id) => toNeighbor(model, tree, e, id, metrics),
|
|
838
|
-
offset: args.offset,
|
|
839
|
-
limit: args.limit
|
|
840
|
-
};
|
|
841
|
-
const none = { rows: [], total: 0 };
|
|
842
|
-
const ins = args.direction === "out" ? none : side(inbound.get(node.id), (e) => e.srcId, paging);
|
|
843
|
-
const outs = args.direction === "in" ? none : side(outbound.get(node.id), (e) => e.dstId, paging);
|
|
844
|
-
return {
|
|
845
|
-
snapshotId: model.snapshot.id,
|
|
846
|
-
node: refFor(model, node.id),
|
|
847
|
-
inbound: ins.rows,
|
|
848
|
-
outbound: outs.rows,
|
|
849
|
-
total: { inbound: ins.total, outbound: outs.total }
|
|
850
|
-
};
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
// src/query/stats.ts
|
|
854
|
-
var sortedByKind = /* @__PURE__ */ new WeakMap();
|
|
855
|
-
function valuesOf(column, nodes, kind) {
|
|
856
|
-
const out = [];
|
|
857
|
-
for (const node of nodes) {
|
|
858
|
-
if (node.ref.kind !== kind) continue;
|
|
859
|
-
const { value } = column.valueOf(node);
|
|
860
|
-
if (value !== null) out.push(value);
|
|
861
|
-
}
|
|
862
|
-
return out;
|
|
863
|
-
}
|
|
864
|
-
function sortedPeers(column, tree, kind) {
|
|
865
|
-
let byKind = sortedByKind.get(column);
|
|
866
|
-
if (!byKind) sortedByKind.set(column, byKind = /* @__PURE__ */ new Map());
|
|
867
|
-
let sorted = byKind.get(kind);
|
|
868
|
-
if (!sorted) byKind.set(kind, sorted = valuesOf(column, tree.byId.values(), kind).sort((a, b) => a - b));
|
|
869
|
-
return sorted;
|
|
870
|
-
}
|
|
871
|
-
function countAtMost(sorted, value) {
|
|
872
|
-
let lo = 0;
|
|
873
|
-
let hi = sorted.length;
|
|
874
|
-
while (lo < hi) {
|
|
875
|
-
const mid = lo + hi >>> 1;
|
|
876
|
-
if (sorted[mid] <= value) lo = mid + 1;
|
|
877
|
-
else hi = mid;
|
|
878
|
-
}
|
|
879
|
-
return lo;
|
|
880
|
-
}
|
|
881
|
-
function median(values) {
|
|
882
|
-
if (values.length === 0) return null;
|
|
883
|
-
const sorted = [...values].sort((a, b) => a - b);
|
|
884
|
-
const mid = sorted.length >>> 1;
|
|
885
|
-
return sorted.length % 2 === 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
886
|
-
}
|
|
887
|
-
var round1 = (n) => Math.round(n * 10) / 10;
|
|
888
|
-
function peerStats(column, tree, node, value) {
|
|
889
|
-
const parent = node.parentId === null ? void 0 : tree.byId.get(node.parentId);
|
|
890
|
-
const siblings = valuesOf(column, parent ? parent.children : [node], node.ref.kind);
|
|
891
|
-
if (value === null) return { percentile: null, siblingMedian: median(siblings), siblingRank: null, siblingCount: siblings.length };
|
|
892
|
-
const peers = sortedPeers(column, tree, node.ref.kind);
|
|
893
|
-
return {
|
|
894
|
-
percentile: peers.length === 0 ? null : round1(100 * countAtMost(peers, value) / peers.length),
|
|
895
|
-
siblingMedian: median(siblings),
|
|
896
|
-
siblingRank: 1 + siblings.filter((v) => v > value).length,
|
|
897
|
-
siblingCount: siblings.length
|
|
898
|
-
};
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
// src/query/finding-get.ts
|
|
902
|
-
var RELATED_CAP = 10;
|
|
903
|
-
function measure(model, finding, baseline) {
|
|
904
|
-
const measured = {
|
|
905
|
-
value: finding.value ?? null,
|
|
906
|
-
threshold: finding.threshold ?? null,
|
|
907
|
-
percentile: null,
|
|
908
|
-
siblingMedian: null
|
|
909
|
-
};
|
|
910
|
-
const tree = treeFor(model);
|
|
911
|
-
const node = tree.byId.get(finding.node.id);
|
|
912
|
-
const metric = finding.metric;
|
|
913
|
-
if (node && metric !== void 0 && model.metrics.has(metric)) {
|
|
914
|
-
const column = columnFor(model, tree, metric);
|
|
915
|
-
const stats = peerStats(column, tree, node, column.valueOf(node).value);
|
|
916
|
-
measured.percentile = stats.percentile;
|
|
917
|
-
measured.siblingMedian = stats.siblingMedian;
|
|
918
|
-
}
|
|
919
|
-
if (baseline && metric !== void 0) measured.baselineValue = baselineValue(baseline, finding.node.id, metric);
|
|
920
|
-
return measured;
|
|
921
|
-
}
|
|
922
|
-
function neighbourIds(model, id) {
|
|
923
|
-
const { inbound, outbound } = adjacencyOf(model);
|
|
924
|
-
const ids = /* @__PURE__ */ new Set();
|
|
925
|
-
for (const e of inbound.get(id) ?? []) if (e.kind !== "references") ids.add(e.srcId);
|
|
926
|
-
for (const e of outbound.get(id) ?? []) if (e.kind !== "references") ids.add(e.dstId);
|
|
927
|
-
return ids;
|
|
928
|
-
}
|
|
929
|
-
function relatedTo(model, rows, finding) {
|
|
930
|
-
const near = neighbourIds(model, finding.node.id);
|
|
931
|
-
const order = compareFindings("severity", "desc");
|
|
932
|
-
const sameNode = rows.filter((f) => f.id !== finding.id && f.node.id === finding.node.id).sort(order);
|
|
933
|
-
const sameRule = rows.filter((f) => f.rule === finding.rule && f.node.id !== finding.node.id && near.has(f.node.id)).sort(order);
|
|
934
|
-
return [...sameNode, ...sameRule].slice(0, RELATED_CAP);
|
|
935
|
-
}
|
|
936
|
-
function ruleOf(model, finding) {
|
|
937
|
-
return model.rules.find((r) => r.id === finding.rule) ?? { id: finding.rule, type: "unknown", severity: finding.severity, text: "" };
|
|
938
|
-
}
|
|
939
|
-
function excerptOf(source, model, finding, context) {
|
|
940
|
-
if (!source.readSource) return { excerpt: null, missing: "no-source" };
|
|
941
|
-
const stored = model.findings.find((f) => f.id === finding.id);
|
|
942
|
-
return buildExcerpt(source.readSource(model.snapshot.id, finding.node.path), stored?.ranges ?? [], context);
|
|
943
|
-
}
|
|
944
|
-
function getFinding(source, args) {
|
|
945
|
-
const model = modelFor(source, args.snapshot);
|
|
946
|
-
const baseline = openBaseline(source, args.baseline);
|
|
947
|
-
const current = findingsFor(model);
|
|
948
|
-
const rows = baseline ? withStatus(current, findingsFor(baseline.model)).filter((f) => f.status !== "resolved") : current;
|
|
949
|
-
const finding = rows.find((f) => f.id === args.id);
|
|
950
|
-
if (!finding) throw findingNotFound(args.id, model.snapshot.id);
|
|
951
|
-
const rule = ruleOf(model, finding);
|
|
952
|
-
const { excerpt, missing } = excerptOf(source, model, finding, args.context_lines);
|
|
953
|
-
const result = {
|
|
954
|
-
snapshotId: model.snapshot.id,
|
|
955
|
-
...baselineFields(model, baseline),
|
|
956
|
-
finding,
|
|
957
|
-
rule,
|
|
958
|
-
measured: measure(model, finding, baseline),
|
|
959
|
-
why: rule.text ? `${rule.text} Here: ${finding.message}.` : finding.message,
|
|
960
|
-
excerpt,
|
|
961
|
-
related: relatedTo(model, rows, finding)
|
|
962
|
-
};
|
|
963
|
-
if (missing !== void 0) result.excerptMissing = missing;
|
|
964
|
-
return result;
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
// src/query/findings-list.ts
|
|
968
|
-
function childUnder(prefix, ref) {
|
|
969
|
-
if (ref.kind === "external" || !ref.path.startsWith(prefix)) return ref.id;
|
|
970
|
-
const rest = ref.path.slice(prefix.length);
|
|
971
|
-
const slash = rest.indexOf("/");
|
|
972
|
-
return slash < 0 ? ref.path : directoryId(prefix + rest.slice(0, slash));
|
|
973
|
-
}
|
|
974
|
-
function scopeOf(node) {
|
|
975
|
-
const { kind, id, path } = node.ref;
|
|
976
|
-
if (kind === "repo") return { contains: () => true, childOf: (ref) => childUnder("", ref) };
|
|
977
|
-
if (kind === "directory") {
|
|
978
|
-
const prefix = `${path}/`;
|
|
979
|
-
return { contains: (ref) => ref.path.startsWith(prefix), childOf: (ref) => childUnder(prefix, ref) };
|
|
980
|
-
}
|
|
981
|
-
if (kind === "file") return { contains: (ref) => ref.path === id, childOf: (ref) => ref.id };
|
|
982
|
-
return { contains: (ref) => ref.id === id || ref.id.startsWith(`${id}.`), childOf: (ref) => ref.id };
|
|
983
|
-
}
|
|
984
|
-
function oneOf(allowed, pick) {
|
|
985
|
-
if (allowed.length === 0) return () => true;
|
|
986
|
-
const set = new Set(allowed);
|
|
987
|
-
return (f) => {
|
|
988
|
-
const value = pick(f);
|
|
989
|
-
return value !== void 0 && set.has(value);
|
|
990
|
-
};
|
|
991
|
-
}
|
|
992
|
-
function filtersFor(args, scope) {
|
|
993
|
-
return [
|
|
994
|
-
(f) => scope.contains(f.node),
|
|
995
|
-
oneOf(args.rule, (f) => f.rule),
|
|
996
|
-
oneOf(args.severity, (f) => f.severity),
|
|
997
|
-
oneOf(args.tool, (f) => f.tool),
|
|
998
|
-
oneOf(args.provenance, (f) => f.provenance.kind),
|
|
999
|
-
oneOf(args.kind, (f) => f.node.kind),
|
|
1000
|
-
oneOf(args.status, (f) => f.status)
|
|
1001
|
-
];
|
|
1002
|
-
}
|
|
1003
|
-
var FACETS = {
|
|
1004
|
-
rule: (f) => f.rule,
|
|
1005
|
-
severity: (f) => f.severity,
|
|
1006
|
-
tool: (f) => f.tool,
|
|
1007
|
-
provenance: (f) => f.provenance.kind,
|
|
1008
|
-
kind: (f) => f.node.kind,
|
|
1009
|
-
child: (f, scope) => scope.childOf(f.node),
|
|
1010
|
-
status: (f) => f.status
|
|
1011
|
-
};
|
|
1012
|
-
function sortedCounts(counts) {
|
|
1013
|
-
return Object.fromEntries([...counts].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
|
|
1014
|
-
}
|
|
1015
|
-
function facetCounts(rows, scope, withBaseline) {
|
|
1016
|
-
const out = {};
|
|
1017
|
-
for (const [name, pick] of Object.entries(FACETS)) {
|
|
1018
|
-
if (name === "status" && !withBaseline) continue;
|
|
1019
|
-
const counts = /* @__PURE__ */ new Map();
|
|
1020
|
-
for (const f of rows) {
|
|
1021
|
-
const value = pick(f, scope) ?? "";
|
|
1022
|
-
counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
1023
|
-
}
|
|
1024
|
-
out[name] = sortedCounts(counts);
|
|
1025
|
-
}
|
|
1026
|
-
return out;
|
|
1027
|
-
}
|
|
1028
|
-
function listFindings(source, args) {
|
|
1029
|
-
const model = modelFor(source, args.snapshot);
|
|
1030
|
-
const baseline = openBaseline(source, args.baseline);
|
|
1031
|
-
if (args.status.length > 0 && !baseline) throw invalidArgs("status needs a baseline");
|
|
1032
|
-
const root = treeFor(model).byId.get(args.scope ?? REPO_ID);
|
|
1033
|
-
if (!root) throw nodeNotFound(args.scope ?? REPO_ID, model.snapshot.id);
|
|
1034
|
-
const scope = scopeOf(root);
|
|
1035
|
-
const all = baseline ? withStatus(findingsFor(model), findingsFor(baseline.model)) : findingsFor(model);
|
|
1036
|
-
const filters = filtersFor(args, scope);
|
|
1037
|
-
const kept = all.filter((f) => filters.every((keep) => keep(f))).sort(compareFindings(args.sort, args.order));
|
|
1038
|
-
const result = {
|
|
1039
|
-
snapshotId: model.snapshot.id,
|
|
1040
|
-
...baselineFields(model, baseline),
|
|
1041
|
-
rows: kept.slice(args.offset, args.offset + args.limit),
|
|
1042
|
-
total: kept.length
|
|
1043
|
-
};
|
|
1044
|
-
if (args.facets) result.facets = facetCounts(kept, scope, baseline !== void 0);
|
|
1045
|
-
return result;
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
// src/query/hierarchy.ts
|
|
1049
|
-
function collectRows(root, depth, includeSymbols) {
|
|
1050
|
-
const rows = [];
|
|
1051
|
-
const queue = [root];
|
|
1052
|
-
for (let i = 0; i < queue.length; i++) {
|
|
1053
|
-
const node = queue[i];
|
|
1054
|
-
if (rows.length === HIERARCHY_ROW_CAP) return { rows, truncated: true };
|
|
1055
|
-
rows.push(node);
|
|
1056
|
-
if (node.depth - root.depth >= depth) continue;
|
|
1057
|
-
for (const child of node.children) if (includeSymbols || child.ref.kind !== "symbol") queue.push(child);
|
|
1058
|
-
}
|
|
1059
|
-
return { rows, truncated: false };
|
|
1060
|
-
}
|
|
1061
|
-
function toRow(node, rootDepth, columns2, names, baseline) {
|
|
1062
|
-
const row = { ...node.ref, parentId: node.parentId, depth: node.depth - rootDepth, childCount: node.children.length, values: {} };
|
|
1063
|
-
if (node.stored?.role !== void 0) row.role = node.stored.role;
|
|
1064
|
-
const missing = {};
|
|
1065
|
-
const deltas = {};
|
|
1066
|
-
names.forEach((name, i) => {
|
|
1067
|
-
const { value, missing: reason } = columns2[i].valueOf(node);
|
|
1068
|
-
row.values[name] = value;
|
|
1069
|
-
if (reason) missing[name] = reason;
|
|
1070
|
-
if (!baseline) return;
|
|
1071
|
-
deltas[name] = delta(value, baselineValue(baseline, node.ref.id, name));
|
|
1072
|
-
});
|
|
1073
|
-
if (Object.keys(missing).length > 0) row.missing = missing;
|
|
1074
|
-
if (baseline) row.deltas = deltas;
|
|
1075
|
-
return row;
|
|
1076
|
-
}
|
|
1077
|
-
function getHierarchy(source, args) {
|
|
1078
|
-
const model = modelFor(source, args.snapshot);
|
|
1079
|
-
const tree = treeFor(model, args.exclude_roles);
|
|
1080
|
-
const root = tree.byId.get(args.root ?? REPO_ID);
|
|
1081
|
-
if (!root) throw nodeNotFound(args.root ?? REPO_ID, model.snapshot.id);
|
|
1082
|
-
const baseline = openBaseline(source, args.baseline, args.exclude_roles);
|
|
1083
|
-
const names = [...new Set(args.metrics)];
|
|
1084
|
-
const columns2 = names.map((name) => columnFor(model, tree, name));
|
|
1085
|
-
const { rows, truncated } = collectRows(root, args.depth, args.include_symbols);
|
|
1086
|
-
const nodes = rows.map((node) => toRow(node, root.depth, columns2, names, baseline));
|
|
1087
|
-
return { snapshotId: model.snapshot.id, ...baselineFields(model, baseline), nodes, truncated };
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
// src/query/node-get.ts
|
|
1091
|
-
function ancestorsOf(tree, node) {
|
|
1092
|
-
const chain = [];
|
|
1093
|
-
for (let id = node.parentId; id !== null; ) {
|
|
1094
|
-
const parent = tree.byId.get(id);
|
|
1095
|
-
chain.push(parent.ref);
|
|
1096
|
-
id = parent.parentId;
|
|
1097
|
-
}
|
|
1098
|
-
return chain.reverse();
|
|
1099
|
-
}
|
|
1100
|
-
function childCounts(node) {
|
|
1101
|
-
const counts = {};
|
|
1102
|
-
for (const child of node.children) counts[child.ref.kind] = (counts[child.ref.kind] ?? 0) + 1;
|
|
1103
|
-
return counts;
|
|
1104
|
-
}
|
|
1105
|
-
function describeNode(node) {
|
|
1106
|
-
const out = { ...node.ref };
|
|
1107
|
-
const stored = node.stored;
|
|
1108
|
-
if (!stored) return out;
|
|
1109
|
-
if (stored.language !== void 0) out.language = stored.language;
|
|
1110
|
-
if (stored.role !== void 0) out.role = stored.role;
|
|
1111
|
-
const { exported, signature, purpose } = stored.attrs;
|
|
1112
|
-
if (typeof exported === "boolean") out.exported = exported;
|
|
1113
|
-
if (typeof signature === "string") out.signature = signature;
|
|
1114
|
-
if (typeof purpose === "string") out.purpose = purpose;
|
|
1115
|
-
return out;
|
|
1116
|
-
}
|
|
1117
|
-
function metricNames(model, kind, requested) {
|
|
1118
|
-
if (requested.length > 0) return [...new Set(requested)];
|
|
1119
|
-
return model.metricCatalogue.filter((d) => describesKind(d, kind)).map((d) => d.name);
|
|
1120
|
-
}
|
|
1121
|
-
function measure2(model, tree, node, name, baseline) {
|
|
1122
|
-
const column = columnFor(model, tree, name);
|
|
1123
|
-
const { value, missing } = column.valueOf(node);
|
|
1124
|
-
const d = column.descriptor;
|
|
1125
|
-
const metric = {
|
|
1126
|
-
name,
|
|
1127
|
-
unit: d?.unit ?? null,
|
|
1128
|
-
direction: d?.direction ?? "neutral",
|
|
1129
|
-
rollup: d?.rollup ?? "none",
|
|
1130
|
-
value,
|
|
1131
|
-
...missing ? { missing } : {},
|
|
1132
|
-
...peerStats(column, tree, node, value)
|
|
1133
|
-
};
|
|
1134
|
-
if (!baseline) return metric;
|
|
1135
|
-
metric.baseline = baselineValue(baseline, node.ref.id, name);
|
|
1136
|
-
metric.delta = delta(value, metric.baseline);
|
|
1137
|
-
return metric;
|
|
1138
|
-
}
|
|
1139
|
-
function getNode(source, args) {
|
|
1140
|
-
const model = modelFor(source, args.snapshot);
|
|
1141
|
-
const tree = treeFor(model);
|
|
1142
|
-
const node = tree.byId.get(args.id);
|
|
1143
|
-
if (!node) throw nodeNotFound(args.id, model.snapshot.id);
|
|
1144
|
-
const baseline = openBaseline(source, args.baseline);
|
|
1145
|
-
return {
|
|
1146
|
-
snapshotId: model.snapshot.id,
|
|
1147
|
-
...baselineFields(model, baseline),
|
|
1148
|
-
node: describeNode(node),
|
|
1149
|
-
ancestors: ancestorsOf(tree, node),
|
|
1150
|
-
childCounts: childCounts(node),
|
|
1151
|
-
metrics: metricNames(model, node.ref.kind, args.metrics).map((name) => measure2(model, tree, node, name, baseline))
|
|
1152
|
-
};
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
// src/query/resolve.ts
|
|
1156
|
-
var SEARCHED_KINDS = /* @__PURE__ */ new Set(["directory", "file", "symbol"]);
|
|
1157
|
-
var PATH_LINE = /^([^#]+):(\d+)$/;
|
|
1158
|
-
function scoreNode(node, q) {
|
|
1159
|
-
const id = (node.ref.kind === "directory" ? node.ref.path : node.ref.id).toLowerCase();
|
|
1160
|
-
const name = node.ref.name.toLowerCase();
|
|
1161
|
-
if (id === q || name === q) return { score: 100, match: "exact" };
|
|
1162
|
-
if (id.endsWith(`/${q}`) || id.endsWith(`#${q}`)) return { score: 80, match: "suffix" };
|
|
1163
|
-
if (node.ref.kind === "symbol" && name.endsWith(`.${q}`)) return { score: 80, match: "suffix" };
|
|
1164
|
-
if (name.startsWith(q)) return { score: 60, match: "prefix" };
|
|
1165
|
-
if (id.includes(q)) return { score: 40, match: "substring" };
|
|
1166
|
-
if (name.includes(q)) return { score: 30, match: "substring" };
|
|
1167
|
-
return null;
|
|
1168
|
-
}
|
|
1169
|
-
function normalize(query) {
|
|
1170
|
-
const q = query.toLowerCase();
|
|
1171
|
-
return q.length > 1 && q.endsWith("/") ? q.slice(0, -1) : q;
|
|
1172
|
-
}
|
|
1173
|
-
function rank2(tree, query, kinds) {
|
|
1174
|
-
const q = normalize(query);
|
|
1175
|
-
const hits = [];
|
|
1176
|
-
for (const node of tree.byId.values()) {
|
|
1177
|
-
if (!kinds.has(node.ref.kind)) continue;
|
|
1178
|
-
const scored = scoreNode(node, q);
|
|
1179
|
-
if (scored) hits.push({ node: node.ref, ...scored });
|
|
1180
|
-
}
|
|
1181
|
-
return hits.sort((a, b) => b.score - a.score || (a.node.id < b.node.id ? -1 : a.node.id > b.node.id ? 1 : 0));
|
|
1182
|
-
}
|
|
1183
|
-
function innermostContaining(file, line) {
|
|
1184
|
-
let best = null;
|
|
1185
|
-
const visit = (node) => {
|
|
1186
|
-
for (const child of node.children) {
|
|
1187
|
-
const span = child.ref.span;
|
|
1188
|
-
if (span && span.startLine <= line && line <= span.endLine) {
|
|
1189
|
-
const bestSpan = best?.ref.span;
|
|
1190
|
-
if (!bestSpan || span.endLine - span.startLine < bestSpan.endLine - bestSpan.startLine) best = child;
|
|
1191
|
-
}
|
|
1192
|
-
visit(child);
|
|
1193
|
-
}
|
|
1194
|
-
};
|
|
1195
|
-
visit(file);
|
|
1196
|
-
return best;
|
|
1197
|
-
}
|
|
1198
|
-
function resolvePath(tree, path, line) {
|
|
1199
|
-
const files = rank2(tree, path, /* @__PURE__ */ new Set(["file"]));
|
|
1200
|
-
if (line === void 0) return files;
|
|
1201
|
-
return files.map((hit) => {
|
|
1202
|
-
const symbol = innermostContaining(tree.byId.get(hit.node.id), line);
|
|
1203
|
-
return symbol ? { node: symbol.ref, score: hit.score, match: "span" } : hit;
|
|
1204
|
-
});
|
|
1205
|
-
}
|
|
1206
|
-
function pathAndLine(args) {
|
|
1207
|
-
if (args.path !== void 0) return { path: args.path, line: args.line };
|
|
1208
|
-
const m = PATH_LINE.exec(args.query);
|
|
1209
|
-
return m ? { path: m[1], line: Number(m[2]) } : null;
|
|
1210
|
-
}
|
|
1211
|
-
function resolveNode(source, args) {
|
|
1212
|
-
if (args.query === void 0 === (args.path === void 0)) throw invalidArgs("pass exactly one of query or path");
|
|
1213
|
-
if (args.line !== void 0 && args.path === void 0) throw invalidArgs("line needs path");
|
|
1214
|
-
const tree = treeFor(modelFor(source, args.snapshot));
|
|
1215
|
-
const located = pathAndLine(args);
|
|
1216
|
-
const candidates = located ? resolvePath(tree, located.path, located.line) : rank2(tree, args.query, SEARCHED_KINDS);
|
|
1217
|
-
return { candidates: candidates.slice(0, args.limit) };
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
// src/query/commands.ts
|
|
1221
|
-
function describeApi(source) {
|
|
1222
|
-
const facts = source.facts();
|
|
1223
|
-
const snapshots = source.snapshots();
|
|
1224
|
-
const newest = snapshots[0] ?? null;
|
|
1225
|
-
return {
|
|
1226
|
-
api: CODE_READ_API_VERSION,
|
|
1227
|
-
dataset: facts.dataset,
|
|
1228
|
-
commands: [...facts.commands],
|
|
1229
|
-
newest,
|
|
1230
|
-
indexVersions: [...new Set(snapshots.map((s) => s.indexVersion))],
|
|
1231
|
-
capabilities: facts.capabilities,
|
|
1232
|
-
metrics: newest ? [...source.model(newest.id).metricCatalogue] : [],
|
|
1233
|
-
rules: [...facts.rules]
|
|
1234
|
-
};
|
|
1235
|
-
}
|
|
1236
|
-
function listSnapshots(source, args) {
|
|
1237
|
-
const matching = source.snapshots().filter((s) => args.ref === void 0 || s.ref === args.ref);
|
|
1238
|
-
return { snapshots: matching.slice(0, args.limit) };
|
|
1239
|
-
}
|
|
1240
|
-
var QUERIES = {
|
|
1241
|
-
"api.describe": describeApi,
|
|
1242
|
-
"snapshot.list": listSnapshots,
|
|
1243
|
-
"hierarchy.get": getHierarchy,
|
|
1244
|
-
"node.get": getNode,
|
|
1245
|
-
"node.resolve": resolveNode,
|
|
1246
|
-
"findings.list": listFindings,
|
|
1247
|
-
"finding.get": getFinding,
|
|
1248
|
-
"node.neighbors": getNeighbors
|
|
1249
|
-
};
|
|
1250
|
-
|
|
1251
|
-
// src/query/resolver.ts
|
|
1252
|
-
import { EXIT as EXIT3, errorEnvelope, successEnvelope } from "@titan-design/rpc-protocol";
|
|
1253
|
-
function isCommandName(name) {
|
|
1254
|
-
return Object.hasOwn(CONTRACT, name);
|
|
1255
|
-
}
|
|
1256
|
-
function formatIssues(error) {
|
|
1257
|
-
return error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
1258
|
-
}
|
|
1259
|
-
function describeFailure(err) {
|
|
1260
|
-
const code = err?.code;
|
|
1261
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1262
|
-
return { message, code: typeof code === "number" ? code : EXIT3.GENERIC };
|
|
1263
|
-
}
|
|
1264
|
-
function createQueryResolver(source) {
|
|
1265
|
-
return (name, rawArgs) => {
|
|
1266
|
-
if (!isCommandName(name)) return errorEnvelope(`Unknown command: ${name}`, EXIT3.USAGE);
|
|
1267
|
-
if (!source.facts().commands.includes(name)) {
|
|
1268
|
-
return errorEnvelope(`${name} is not available from a ${source.facts().dataset} dataset`, EXIT3.UNAVAILABLE);
|
|
1269
|
-
}
|
|
1270
|
-
const parsed = CONTRACT[name].args.safeParse(rawArgs ?? {});
|
|
1271
|
-
if (!parsed.success) return errorEnvelope(`Invalid arguments: ${formatIssues(parsed.error)}`, EXIT3.DATAERR);
|
|
1272
|
-
try {
|
|
1273
|
-
const query = QUERIES[name];
|
|
1274
|
-
return successEnvelope(query(source, parsed.data));
|
|
1275
|
-
} catch (err) {
|
|
1276
|
-
const { message, code } = describeFailure(err);
|
|
1277
|
-
return errorEnvelope(message, code);
|
|
1278
|
-
}
|
|
1279
|
-
};
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
export {
|
|
1283
|
-
NodeKind,
|
|
1284
|
-
Severity,
|
|
1285
|
-
ProvenanceKind,
|
|
1286
|
-
Provenance,
|
|
1287
|
-
SnapshotInfo,
|
|
1288
|
-
MetricDescriptor,
|
|
1289
|
-
RuleSummary,
|
|
1290
|
-
SnapshotRef,
|
|
1291
|
-
Span,
|
|
1292
|
-
NodeRef,
|
|
1293
|
-
MissingReason,
|
|
1294
|
-
Capabilities,
|
|
1295
|
-
FINDINGS_PAGE_MAX,
|
|
1296
|
-
EXCERPT_LINE_CAP,
|
|
1297
|
-
FindingStatus,
|
|
1298
|
-
Finding,
|
|
1299
|
-
SourceExcerpt,
|
|
1300
|
-
HIERARCHY_ROW_CAP,
|
|
1301
|
-
CODE_READ_API_VERSION,
|
|
1302
|
-
CONTRACT,
|
|
1303
|
-
COMMAND_NAMES,
|
|
1304
|
-
AGENT_COMMANDS,
|
|
1305
|
-
serializeContract,
|
|
1306
|
-
buildReadModel,
|
|
1307
|
-
ReadError,
|
|
1308
|
-
snapshotNotFound,
|
|
1309
|
-
nodeNotFound,
|
|
1310
|
-
findingNotFound,
|
|
1311
|
-
invalidArgs,
|
|
1312
|
-
resolveSnapshot,
|
|
1313
|
-
excessOf,
|
|
1314
|
-
getNeighbors,
|
|
1315
|
-
RELATED_CAP,
|
|
1316
|
-
getFinding,
|
|
1317
|
-
listFindings,
|
|
1318
|
-
getHierarchy,
|
|
1319
|
-
getNode,
|
|
1320
|
-
resolveNode,
|
|
1321
|
-
describeApi,
|
|
1322
|
-
listSnapshots,
|
|
1323
|
-
QUERIES,
|
|
1324
|
-
createQueryResolver
|
|
1325
|
-
};
|
|
1326
|
-
//# sourceMappingURL=chunk-VXQFCVGB.js.map
|