fallow-type-aware 0.0.0-bootstrap.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.
@@ -0,0 +1,407 @@
1
+ import { createRequire } from "node:module";
2
+ import path from "node:path";
3
+
4
+ import { version as typescriptVersion } from "typescript";
5
+
6
+ import {
7
+ ANALYSIS_OPERATION,
8
+ BACKEND_FAMILY,
9
+ BACKEND_VERSION,
10
+ QUERY_OPERATIONS as MANIFEST_QUERY_OPERATIONS,
11
+ WIRE_PROTOCOL_VERSION,
12
+ } from "./generated-protocol.mjs";
13
+ import { normalizePhaseTimings, normalizeSemanticResult } from "./response-normalization.mjs";
14
+
15
+ const require = createRequire(import.meta.url);
16
+ const { version: SIDECAR_VERSION } = require("../package.json");
17
+ const QUERY_OPERATIONS = new Set(MANIFEST_QUERY_OPERATIONS);
18
+
19
+ if (typescriptVersion !== BACKEND_VERSION) {
20
+ throw new Error(
21
+ `typescript backend version ${typescriptVersion} does not match protocol ${BACKEND_VERSION}`,
22
+ );
23
+ }
24
+
25
+ export const createStatusResponse = () => ({
26
+ package_version: SIDECAR_VERSION,
27
+ protocol_version: WIRE_PROTOCOL_VERSION,
28
+ backend_family: BACKEND_FAMILY,
29
+ backend_version: BACKEND_VERSION,
30
+ });
31
+
32
+ const BATCH_REQUEST_KEYS = new Set([
33
+ "protocol_version",
34
+ "operation",
35
+ "root",
36
+ "projects",
37
+ "queries",
38
+ "evidence_limit",
39
+ ]);
40
+ const SYMBOL_QUERY_KEYS = new Set(["id", "operation", "symbol"]);
41
+ const SYMBOL_USE_QUERY_KEYS = new Set(["id", "operation", "symbol", "framework_contracts"]);
42
+ const FRAMEWORK_CONTRACT_KEYS = new Set([
43
+ "framework",
44
+ "package",
45
+ "heritage_symbol",
46
+ "heritage_names",
47
+ "relation",
48
+ "members",
49
+ ]);
50
+ const API_SURFACE_QUERY_KEYS = new Set([
51
+ "id",
52
+ "operation",
53
+ "entry_points",
54
+ "include_cycles",
55
+ "private_leak_candidates",
56
+ ]);
57
+ const TYPE_COUPLING_QUERY_KEYS = new Set(["id", "operation", "entry_points", "include_cycles"]);
58
+ const PRIVATE_LEAK_CANDIDATE_KEYS = new Set(["id", "path", "export_name", "type_name"]);
59
+ const SYMBOL_KEYS = new Set([
60
+ "path",
61
+ "namespace",
62
+ "declaration_kind",
63
+ "exported_name",
64
+ "local_name",
65
+ "line",
66
+ "col",
67
+ "owner",
68
+ ]);
69
+ const SYMBOL_OPERATIONS = new Set(["symbol-use", "symbol-trace", "symbol-impact"]);
70
+ const SYMBOL_NAMESPACES = new Set(["value", "type"]);
71
+ const MAX_WARNINGS = 20;
72
+ const MAX_WARNING_CHARS = 512;
73
+ const MAX_PROJECTS = 256;
74
+ const MAX_QUERIES = 25_000;
75
+ const MAX_GRAPH_QUERIES = 256;
76
+ const MAX_PRIVATE_LEAK_CANDIDATES = 25_000;
77
+ const MAX_EVIDENCE_PER_RESULT = 40;
78
+ const MAX_STRING_CHARS = 4_096;
79
+
80
+ const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
81
+ const compareText = (left, right) => Buffer.compare(Buffer.from(left), Buffer.from(right));
82
+ const normalizeWarnings = (warnings) =>
83
+ [
84
+ ...new Set(
85
+ warnings
86
+ .map((warning) =>
87
+ [...warning.replace(/\s+/g, " ").trim()].slice(0, MAX_WARNING_CHARS).join(""),
88
+ )
89
+ .filter(Boolean),
90
+ ),
91
+ ]
92
+ .toSorted(compareText)
93
+ .slice(0, MAX_WARNINGS);
94
+
95
+ const requireString = (value, field) => {
96
+ if (typeof value !== "string" || value.length === 0) {
97
+ throw new Error(`${field} must be a non-empty string`);
98
+ }
99
+ if ([...value].length > MAX_STRING_CHARS) {
100
+ throw new Error(`${field} exceeds the ${MAX_STRING_CHARS} character limit`);
101
+ }
102
+ return value;
103
+ };
104
+
105
+ const requireInteger = (value, field, minimum) => {
106
+ if (!Number.isSafeInteger(value) || value < minimum) {
107
+ throw new Error(`${field} must be an integer greater than or equal to ${minimum}`);
108
+ }
109
+ return value;
110
+ };
111
+
112
+ const requireExactKeys = (value, allowed, field) => {
113
+ const unexpected = Object.keys(value).filter((key) => !allowed.has(key));
114
+ if (unexpected.length > 0) {
115
+ throw new Error(`${field} contains unknown field ${unexpected.toSorted().join(", ")}`);
116
+ }
117
+ };
118
+
119
+ const requireObject = (value, field) => {
120
+ if (!isObject(value)) {
121
+ throw new Error(`${field} must be a JSON object`);
122
+ }
123
+ return value;
124
+ };
125
+
126
+ const requireLiteral = (value, expected, field) => {
127
+ if (value !== expected) {
128
+ throw new Error(`unsupported ${field} ${String(value)}`);
129
+ }
130
+ };
131
+
132
+ const requireBoolean = (value, field) => {
133
+ if (typeof value !== "boolean") {
134
+ throw new Error(`${field} must be a boolean`);
135
+ }
136
+ return value;
137
+ };
138
+
139
+ const requireArray = (value, field) => {
140
+ if (!Array.isArray(value)) {
141
+ throw new Error(`${field} must be an array`);
142
+ }
143
+ return value;
144
+ };
145
+
146
+ const requireBoundedArray = (value, field, maximum) => {
147
+ const array = requireArray(value, field);
148
+ if (array.length > maximum) {
149
+ throw new Error(`${field} exceeds the ${maximum} item limit`);
150
+ }
151
+ return array;
152
+ };
153
+
154
+ const isWithinRoot = (root, file) => {
155
+ const relative = path.relative(root, file);
156
+ return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== "..");
157
+ };
158
+
159
+ const parseCandidatePath = (value, field, root) => {
160
+ const candidatePath = requireString(value, field);
161
+ if (path.isAbsolute(candidatePath)) {
162
+ throw new Error(`${field} must be project-relative`);
163
+ }
164
+ const absolutePath = path.resolve(root, candidatePath);
165
+ if (!isWithinRoot(root, absolutePath)) {
166
+ throw new Error(`${field} must resolve within root`);
167
+ }
168
+ return { candidatePath, absolutePath };
169
+ };
170
+
171
+ const parseSymbolIdentity = (value, field, root) => {
172
+ requireObject(value, field);
173
+ requireExactKeys(value, SYMBOL_KEYS, field);
174
+ const { candidatePath, absolutePath } = parseCandidatePath(value.path, `${field}.path`, root);
175
+ const namespace = requireString(value.namespace, `${field}.namespace`);
176
+ if (!SYMBOL_NAMESPACES.has(namespace)) {
177
+ throw new Error(`${field}.namespace must be value or type`);
178
+ }
179
+ const owner = value.owner === undefined ? null : requireString(value.owner, `${field}.owner`);
180
+ return {
181
+ path: candidatePath,
182
+ absolutePath,
183
+ namespace,
184
+ declarationKind: requireString(value.declaration_kind, `${field}.declaration_kind`),
185
+ exportedName: requireString(value.exported_name, `${field}.exported_name`),
186
+ localName: requireString(value.local_name, `${field}.local_name`),
187
+ line: requireInteger(value.line, `${field}.line`, 1),
188
+ col: requireInteger(value.col, `${field}.col`, 0),
189
+ owner,
190
+ };
191
+ };
192
+
193
+ const parseEntryPoints = (value, field, root) =>
194
+ requireBoundedArray(value ?? [], field, MAX_PROJECTS).map((entryPoint, index) =>
195
+ parseCandidatePath(entryPoint, `${field}[${index}]`, root),
196
+ );
197
+
198
+ const parsePrivateLeakCandidates = (value, field, root) => {
199
+ const candidates = requireBoundedArray(value ?? [], field, MAX_PRIVATE_LEAK_CANDIDATES).map(
200
+ (candidate, index) => {
201
+ const candidateField = `${field}[${index}]`;
202
+ requireObject(candidate, candidateField);
203
+ requireExactKeys(candidate, PRIVATE_LEAK_CANDIDATE_KEYS, candidateField);
204
+ const { candidatePath, absolutePath } = parseCandidatePath(
205
+ candidate.path,
206
+ `${candidateField}.path`,
207
+ root,
208
+ );
209
+ return {
210
+ id: requireInteger(candidate.id, `${candidateField}.id`, 0),
211
+ path: candidatePath,
212
+ absolutePath,
213
+ exportName: requireString(candidate.export_name, `${candidateField}.export_name`),
214
+ typeName: requireString(candidate.type_name, `${candidateField}.type_name`),
215
+ };
216
+ },
217
+ );
218
+ requireUniqueCandidateIds(candidates);
219
+ return candidates;
220
+ };
221
+
222
+ const queryKeys = (operation) => {
223
+ if (operation === "symbol-use") return SYMBOL_USE_QUERY_KEYS;
224
+ if (SYMBOL_OPERATIONS.has(operation)) return SYMBOL_QUERY_KEYS;
225
+ return operation === "api-surface" ? API_SURFACE_QUERY_KEYS : TYPE_COUPLING_QUERY_KEYS;
226
+ };
227
+
228
+ const parseFrameworkContracts = (value, field) =>
229
+ requireBoundedArray(value ?? [], field, MAX_PROJECTS).map((contract, index) => {
230
+ const contractField = `${field}[${index}]`;
231
+ requireObject(contract, contractField);
232
+ requireExactKeys(contract, FRAMEWORK_CONTRACT_KEYS, contractField);
233
+ const relation = requireString(contract.relation, `${contractField}.relation`);
234
+ if (relation !== "extends" && relation !== "implements") {
235
+ throw new Error(`${contractField}.relation must be extends or implements`);
236
+ }
237
+ return {
238
+ framework: requireString(contract.framework, `${contractField}.framework`),
239
+ package: requireString(contract.package, `${contractField}.package`),
240
+ heritageSymbol: requireString(contract.heritage_symbol, `${contractField}.heritage_symbol`),
241
+ heritageNames: requireBoundedArray(
242
+ contract.heritage_names,
243
+ `${contractField}.heritage_names`,
244
+ MAX_PROJECTS,
245
+ ).map((name, nameIndex) =>
246
+ requireString(name, `${contractField}.heritage_names[${nameIndex}]`),
247
+ ),
248
+ relation,
249
+ members: requireBoundedArray(contract.members, `${contractField}.members`, MAX_PROJECTS).map(
250
+ (member, memberIndex) => requireString(member, `${contractField}.members[${memberIndex}]`),
251
+ ),
252
+ };
253
+ });
254
+
255
+ const parseIncludeCycles = (value, field) =>
256
+ value === undefined ? false : requireBoolean(value, field);
257
+
258
+ const parseGraphQuery = (value, field, root, query) => {
259
+ const graphQuery = {
260
+ ...query,
261
+ entryPoints: parseEntryPoints(value.entry_points, `${field}.entry_points`, root),
262
+ includeCycles: parseIncludeCycles(value.include_cycles, `${field}.include_cycles`),
263
+ };
264
+ if (query.operation !== "api-surface") return graphQuery;
265
+ return {
266
+ ...graphQuery,
267
+ privateLeakCandidates: parsePrivateLeakCandidates(
268
+ value.private_leak_candidates,
269
+ `${field}.private_leak_candidates`,
270
+ root,
271
+ ),
272
+ };
273
+ };
274
+
275
+ const parseQuery = (value, index, root) => {
276
+ const field = `queries[${index}]`;
277
+ requireObject(value, field);
278
+ const operation = requireString(value.operation, `${field}.operation`);
279
+ if (!QUERY_OPERATIONS.has(operation)) {
280
+ throw new Error(`unsupported ${field}.operation ${operation}`);
281
+ }
282
+ requireExactKeys(value, queryKeys(operation), field);
283
+ const query = {
284
+ id: requireInteger(value.id, `${field}.id`, 0),
285
+ operation,
286
+ };
287
+ if (SYMBOL_OPERATIONS.has(operation)) {
288
+ return {
289
+ ...query,
290
+ symbol: parseSymbolIdentity(value.symbol, `${field}.symbol`, root),
291
+ ...(operation === "symbol-use"
292
+ ? {
293
+ frameworkContracts: parseFrameworkContracts(
294
+ value.framework_contracts,
295
+ `${field}.framework_contracts`,
296
+ ),
297
+ }
298
+ : {}),
299
+ };
300
+ }
301
+ return parseGraphQuery(value, field, root, query);
302
+ };
303
+
304
+ const parseRoot = (value) => {
305
+ const root = requireString(value, "root");
306
+ if (!path.isAbsolute(root)) {
307
+ throw new Error("root must be an absolute path");
308
+ }
309
+ return path.resolve(root);
310
+ };
311
+
312
+ const requireUniqueCandidateIds = (candidates) => {
313
+ const candidateIds = new Set();
314
+ for (const candidate of candidates) {
315
+ if (candidateIds.has(candidate.id)) {
316
+ throw new Error(`duplicate candidate id ${candidate.id}`);
317
+ }
318
+ candidateIds.add(candidate.id);
319
+ }
320
+ };
321
+
322
+ const requireUniqueProjects = (projects) => {
323
+ const projectPaths = new Set();
324
+ for (const project of projects) {
325
+ if (projectPaths.has(project.absolutePath)) {
326
+ throw new Error(`duplicate project path ${project.path}`);
327
+ }
328
+ projectPaths.add(project.absolutePath);
329
+ }
330
+ };
331
+
332
+ const parseProjects = (value, root) => {
333
+ const projects = requireBoundedArray(value, "projects", MAX_PROJECTS).map((project, index) => {
334
+ const projectPath = requireString(project, `projects[${index}]`);
335
+ return {
336
+ path: projectPath,
337
+ absolutePath: path.resolve(root, projectPath),
338
+ };
339
+ });
340
+ requireUniqueProjects(projects);
341
+ return projects;
342
+ };
343
+
344
+ const validateGraphQueryCount = (queries) => {
345
+ const count = queries.filter(
346
+ (query) => query.operation === "api-surface" || query.operation === "type-coupling",
347
+ ).length;
348
+ if (count > MAX_GRAPH_QUERIES) {
349
+ throw new Error(`graph queries exceed the ${MAX_GRAPH_QUERIES} item limit`);
350
+ }
351
+ };
352
+
353
+ const parseEvidenceLimit = (value) => {
354
+ const limit =
355
+ value === undefined ? MAX_EVIDENCE_PER_RESULT : requireInteger(value, "evidence_limit", 1);
356
+ if (limit > MAX_EVIDENCE_PER_RESULT) {
357
+ throw new Error(`evidence_limit exceeds the ${MAX_EVIDENCE_PER_RESULT} item limit`);
358
+ }
359
+ return limit;
360
+ };
361
+
362
+ const parseBatchRequest = (value) => {
363
+ requireObject(value, "request");
364
+ requireExactKeys(value, BATCH_REQUEST_KEYS, "request");
365
+ requireLiteral(value.protocol_version, WIRE_PROTOCOL_VERSION, "protocol_version");
366
+ requireLiteral(value.operation, ANALYSIS_OPERATION, "operation");
367
+ const root = parseRoot(value.root);
368
+ const projects = parseProjects(value.projects, root);
369
+ const queries = requireBoundedArray(value.queries, "queries", MAX_QUERIES).map((query, index) =>
370
+ parseQuery(query, index, root),
371
+ );
372
+ validateGraphQueryCount(queries);
373
+ requireUniqueCandidateIds(queries);
374
+ const evidenceLimit = parseEvidenceLimit(value.evidence_limit);
375
+ return { protocolVersion: WIRE_PROTOCOL_VERSION, root, projects, queries, evidenceLimit };
376
+ };
377
+
378
+ export const parseRequest = (value) => {
379
+ requireObject(value, "request");
380
+ if (value.protocol_version === WIRE_PROTOCOL_VERSION) {
381
+ return parseBatchRequest(value);
382
+ }
383
+ throw new Error(`unsupported protocol_version ${String(value.protocol_version)}`);
384
+ };
385
+
386
+ export const createSemanticResponse = ({
387
+ selectedTsconfigs,
388
+ projectResults,
389
+ results,
390
+ phaseTimings,
391
+ warnings,
392
+ elapsedMs,
393
+ }) => ({
394
+ protocol_version: WIRE_PROTOCOL_VERSION,
395
+ operation: ANALYSIS_OPERATION,
396
+ sidecar_version: SIDECAR_VERSION,
397
+ backend: BACKEND_FAMILY,
398
+ backend_version: BACKEND_VERSION,
399
+ selected_tsconfigs: [...selectedTsconfigs].toSorted(compareText),
400
+ projects: [...projectResults].toSorted((left, right) => compareText(left.config, right.config)),
401
+ results: [...results]
402
+ .map(normalizeSemanticResult)
403
+ .toSorted((left, right) => left.query_id - right.query_id),
404
+ phase_timings_ms: normalizePhaseTimings(phaseTimings),
405
+ warnings: normalizeWarnings(warnings),
406
+ elapsed_ms: Math.max(0, Math.round(elapsedMs)),
407
+ });
@@ -0,0 +1,28 @@
1
+ const copyArray = (value) => [...(value ?? [])];
2
+
3
+ const evidenceCount = (result, evidence) => result.totalEvidenceCount ?? evidence.length;
4
+
5
+ export const normalizePhaseTimings = (phaseTimings) =>
6
+ Object.fromEntries(
7
+ Object.entries(phaseTimings).map(([name, duration]) => [
8
+ name,
9
+ Math.max(0, Math.round(duration)),
10
+ ]),
11
+ );
12
+
13
+ export const normalizeSemanticResult = (result) => {
14
+ const evidence = copyArray(result.evidence);
15
+ return {
16
+ query_id: result.queryId,
17
+ operation: result.operation,
18
+ assertion: result.assertion,
19
+ status: result.status,
20
+ reason_code: result.reasonCode ?? null,
21
+ actions: copyArray(result.actions).slice(0, 3),
22
+ evidence,
23
+ total_evidence_count: evidenceCount(result, evidence),
24
+ truncated: Boolean(result.truncated),
25
+ omissions: copyArray(result.omissions),
26
+ data: result.data ?? {},
27
+ };
28
+ };