rea-agents 0.3.0 → 0.4.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.
Files changed (122) hide show
  1. package/README.md +124 -20
  2. package/bridge/hopper_bridge.py +141 -13
  3. package/dist/application/AnalysisProvider.js +12 -1
  4. package/dist/application/ArtifactExtraction.js +166 -0
  5. package/dist/application/ArtifactGraphConstruction.js +257 -0
  6. package/dist/application/ArtifactInventory.js +253 -0
  7. package/dist/application/BinarySession.js +164 -14
  8. package/dist/application/CompositeProvider.js +73 -0
  9. package/dist/application/DirectAnalysis.js +27 -3
  10. package/dist/application/Doctor.js +35 -6
  11. package/dist/application/EnhancedTools.js +8 -7
  12. package/dist/application/EvidenceBundleCommands.js +29 -0
  13. package/dist/application/EvidenceBundleFiles.js +127 -0
  14. package/dist/application/EvidenceLedger.js +225 -18
  15. package/dist/application/FilesystemSnapshot.js +124 -0
  16. package/dist/application/LinuxHopper.js +186 -0
  17. package/dist/application/LoopbackReplay.js +195 -37
  18. package/dist/application/ProcessHarness.js +200 -191
  19. package/dist/application/ProcessNormalization.js +44 -0
  20. package/dist/application/ProcessOwnership.js +84 -0
  21. package/dist/application/ProcessSampling.js +284 -0
  22. package/dist/application/RealHopperAssertions.js +105 -0
  23. package/dist/application/ReferenceSourceImport.js +182 -0
  24. package/dist/application/ReferenceSourceImportEntries.js +122 -0
  25. package/dist/application/ReferenceSourceImportPolicy.js +73 -0
  26. package/dist/application/ReferenceSourceImportTypes.js +18 -0
  27. package/dist/application/ReferenceSourceVcsAdapter.js +34 -0
  28. package/dist/application/Setup.js +192 -41
  29. package/dist/application/Uninstall.js +130 -0
  30. package/dist/application/runtime.js +8 -1
  31. package/dist/artifacts/ArtifactPaths.js +51 -0
  32. package/dist/artifacts/ArtifactProvider.js +130 -0
  33. package/dist/artifacts/ArtifactReader.js +9 -0
  34. package/dist/artifacts/AsarArtifactReader.js +62 -0
  35. package/dist/artifacts/DirectoryArtifactReader.js +107 -0
  36. package/dist/artifacts/MachOSliceArtifactReader.js +66 -0
  37. package/dist/artifacts/SafeOutputTree.js +199 -0
  38. package/dist/artifacts/StreamBytes.js +10 -0
  39. package/dist/artifacts/ZipArtifactReader.js +109 -0
  40. package/dist/cli.js +149 -2
  41. package/dist/config.js +35 -1
  42. package/dist/contracts/artifactComparisonExample.js +95 -0
  43. package/dist/contracts/artifactToolContracts.js +84 -0
  44. package/dist/contracts/enhancedInputs.js +4 -0
  45. package/dist/contracts/functionComparisonExample.js +57 -0
  46. package/dist/contracts/investigationExamples.js +118 -0
  47. package/dist/contracts/nativeToolContracts.js +52 -0
  48. package/dist/contracts/processCaptureExample.js +25 -0
  49. package/dist/contracts/toolContractExamples.js +69 -0
  50. package/dist/contracts/toolContracts.js +99 -36
  51. package/dist/contracts/toolOutputSchemas.js +154 -82
  52. package/dist/contracts/unknownContractExamples.js +33 -0
  53. package/dist/domain/artifactComparison.js +273 -0
  54. package/dist/domain/artifactGraph.js +194 -0
  55. package/dist/domain/artifactInventoryEvidence.js +150 -0
  56. package/dist/domain/binaryTarget.js +55 -0
  57. package/dist/domain/bundleComparison.js +266 -0
  58. package/dist/domain/callPath.js +346 -0
  59. package/dist/domain/changedBehavior.js +294 -0
  60. package/dist/domain/errors.js +192 -4
  61. package/dist/domain/evidence.js +41 -10
  62. package/dist/domain/evidenceBundle.js +187 -6
  63. package/dist/domain/functionComparison.js +201 -0
  64. package/dist/domain/functionComparisonNormalization.js +112 -0
  65. package/dist/domain/functionComparisonResults.js +54 -0
  66. package/dist/domain/functionComparisonSchemas.js +82 -0
  67. package/dist/domain/functionDossierEvidence.js +171 -0
  68. package/dist/domain/hopperValues.js +14 -7
  69. package/dist/domain/nativeInspection.js +142 -0
  70. package/dist/domain/processCapture.js +152 -54
  71. package/dist/domain/processComparison.js +106 -0
  72. package/dist/domain/reconstructionUnknowns.js +90 -0
  73. package/dist/domain/reconstructionVerification.js +285 -0
  74. package/dist/domain/reconstructionVerificationSchemas.js +126 -0
  75. package/dist/domain/referenceSourceClassification.js +496 -0
  76. package/dist/domain/referenceSourceGraph.js +376 -0
  77. package/dist/domain/referenceSourceImportParsing.js +235 -0
  78. package/dist/domain/referenceSourcePolicy.js +1 -0
  79. package/dist/domain/residualUnknown.js +239 -0
  80. package/dist/domain/staticRuntimeCorrelation.js +375 -0
  81. package/dist/hopper/BridgeLauncher.js +40 -3
  82. package/dist/hopper/HopperClient.js +14 -5
  83. package/dist/hopper/HopperProvider.js +57 -22
  84. package/dist/identity.js +1 -0
  85. package/dist/main.js +5 -1
  86. package/dist/native/CommandRunner.js +156 -0
  87. package/dist/native/NativeMacOSProvider.js +306 -0
  88. package/dist/native/NativeMachoInspection.js +135 -0
  89. package/dist/native/parsers/codesign.js +55 -0
  90. package/dist/native/parsers/demangle.js +26 -0
  91. package/dist/native/parsers/dyldInfo.js +25 -0
  92. package/dist/native/parsers/lipo.js +67 -0
  93. package/dist/native/parsers/otool.js +193 -0
  94. package/dist/native/parsers/plist.js +23 -0
  95. package/dist/reference/ReferenceSourceReader.js +73 -0
  96. package/dist/reference/ReferenceSourceReaderEntries.js +206 -0
  97. package/dist/reference/ReferenceSourceReaderErrors.js +19 -0
  98. package/dist/reference/ReferenceSourceReaderFile.js +119 -0
  99. package/dist/reference/ReferenceSourceReaderPaths.js +23 -0
  100. package/dist/reference/ReferenceSourceReaderTypes.js +2 -0
  101. package/dist/reference/ReferenceSourceReaderValidate.js +71 -0
  102. package/dist/server/createServer.js +31 -5
  103. package/dist/server/recordDerivedEvidence.js +10 -0
  104. package/dist/server/registerArtifactComparisonTool.js +62 -0
  105. package/dist/server/registerArtifactTools.js +6 -0
  106. package/dist/server/registerBundleComparisonTool.js +47 -0
  107. package/dist/server/registerEnhancedTools.js +64 -12
  108. package/dist/server/registerEvidenceTools.js +36 -0
  109. package/dist/server/registerFunctionComparisonTool.js +68 -0
  110. package/dist/server/registerInvestigationTools.js +224 -0
  111. package/dist/server/registerNativeTools.js +6 -0
  112. package/dist/server/registerOfficialTools.js +47 -14
  113. package/dist/server/registerProcessComparisonTool.js +106 -0
  114. package/dist/server/registerSessionTools.js +179 -70
  115. package/dist/server/sessionEvidence.js +28 -0
  116. package/dist/server/sessionToolPolicies.js +64 -0
  117. package/dist/server/toolRegistrationOptions.js +7 -0
  118. package/dist/server/toolResult.js +8 -5
  119. package/install.sh +198 -0
  120. package/package.json +18 -1
  121. package/scripts/rea.mjs +5 -1
  122. package/skills/rea-analysis/SKILL.md +77 -2
@@ -0,0 +1,376 @@
1
+ import { createHash } from "node:crypto";
2
+ import { posix } from "node:path";
3
+ import canonicalize from "canonicalize";
4
+ import { z } from "zod";
5
+ const MAX_PATH = 4_096;
6
+ const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u);
7
+ const boundedTextSchema = z.string().min(1).max(1_024);
8
+ const relativePathSchema = z
9
+ .string()
10
+ .min(1)
11
+ .max(MAX_PATH)
12
+ .refine((path) => !path.startsWith("/") &&
13
+ !path.includes("\\") &&
14
+ path
15
+ .split("/")
16
+ .every((part) => part !== "" && part !== "." && part !== ".."), "Expected a normalized relative POSIX path without traversal");
17
+ const classificationSchema = z.enum([
18
+ "source",
19
+ "test",
20
+ "config",
21
+ "manifest",
22
+ "generated",
23
+ "vendor",
24
+ "documentation",
25
+ "unknown",
26
+ ]);
27
+ const entryBaseShape = {
28
+ path: relativePathSchema,
29
+ classifications: z.array(classificationSchema).min(1).max(8),
30
+ limitations: z.array(boundedTextSchema).max(100),
31
+ };
32
+ const sourceFileSchema = z
33
+ .strictObject({
34
+ ...entryBaseShape,
35
+ kind: z.literal("file"),
36
+ sha256: digestSchema.nullable(),
37
+ size: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER).nullable(),
38
+ language: z.string().min(1).max(100).nullable(),
39
+ content_state: z.enum([
40
+ "hashed",
41
+ "redacted-secret",
42
+ "excluded",
43
+ "unreadable",
44
+ "too-large",
45
+ "unknown",
46
+ ]),
47
+ })
48
+ .superRefine((file, context) => {
49
+ checkSortedUnique(file.classifications, "classifications", context);
50
+ if ((file.content_state === "hashed") !== (file.sha256 !== null))
51
+ context.addIssue({
52
+ code: "custom",
53
+ message: "Only hashed files have a content digest",
54
+ path: ["sha256"],
55
+ });
56
+ if (file.content_state === "hashed" && file.size === null)
57
+ context.addIssue({
58
+ code: "custom",
59
+ message: "Hashed files require a byte size",
60
+ path: ["size"],
61
+ });
62
+ });
63
+ const sourceDirectorySchema = z.strictObject({
64
+ ...entryBaseShape,
65
+ kind: z.literal("directory"),
66
+ tree_state: z.enum([
67
+ "enumerated",
68
+ "partial",
69
+ "excluded",
70
+ "unreadable",
71
+ "unknown",
72
+ ]),
73
+ });
74
+ const symlinkTargetSchema = z
75
+ .string()
76
+ .min(1)
77
+ .max(MAX_PATH)
78
+ .refine((target) => target === "<outside-root>" ||
79
+ (!target.startsWith("/") &&
80
+ !target.includes("\\") &&
81
+ posix.normalize(target) === target), "Expected a normalized POSIX symlink target");
82
+ const sourceSymlinkSchema = z.strictObject({
83
+ ...entryBaseShape,
84
+ kind: z.literal("symlink"),
85
+ target: symlinkTargetSchema,
86
+ target_state: z.enum([
87
+ "internal",
88
+ "external",
89
+ "missing",
90
+ "unreadable",
91
+ "unknown",
92
+ ]),
93
+ });
94
+ const sourceEntrySchema = z.discriminatedUnion("kind", [
95
+ sourceFileSchema,
96
+ sourceDirectorySchema,
97
+ sourceSymlinkSchema,
98
+ ]);
99
+ const sourceRelationshipSchema = z.strictObject({
100
+ from_path: relativePathSchema,
101
+ to: z.string().min(1).max(MAX_PATH),
102
+ kind: z.enum(["imports", "requires", "references", "declares-module"]),
103
+ resolution: z.enum(["internal", "external", "unresolved", "unknown"]),
104
+ parse_state: z.enum(["parsed", "partial", "unknown"]),
105
+ });
106
+ const parseFailureSchema = z.strictObject({
107
+ path: relativePathSchema,
108
+ parser: z.string().min(1).max(100),
109
+ reason: boundedTextSchema,
110
+ });
111
+ const exclusionSchema = z.strictObject({
112
+ path: relativePathSchema,
113
+ reason: z.enum([
114
+ "configured-secret",
115
+ "symlink-escape",
116
+ "size-limit",
117
+ "inventory-limit",
118
+ "unreadable",
119
+ "caller-excluded",
120
+ ]),
121
+ });
122
+ const vcsSchema = z
123
+ .discriminatedUnion("kind", [
124
+ z.strictObject({
125
+ kind: z.literal("git"),
126
+ head: z.string().regex(/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u),
127
+ dirty: z.boolean().nullable(),
128
+ }),
129
+ z.strictObject({
130
+ kind: z.literal("none"),
131
+ head: z.null(),
132
+ dirty: z.null(),
133
+ }),
134
+ z.strictObject({
135
+ kind: z.literal("unknown"),
136
+ head: z.null(),
137
+ dirty: z.null(),
138
+ }),
139
+ ])
140
+ .nullable();
141
+ const graphShape = {
142
+ schema: z.literal("HistoricalSourceGraph/v1"),
143
+ authority: z.literal("historical-reference"),
144
+ root_alias: z.literal("$REFERENCE_ROOT"),
145
+ root_sha256: digestSchema,
146
+ inventory_state: z.enum(["complete", "partial", "unknown"]),
147
+ entries: z.array(sourceEntrySchema).max(100_000),
148
+ relationships: z.array(sourceRelationshipSchema).max(200_000),
149
+ parse_failures: z.array(parseFailureSchema).max(10_000),
150
+ exclusions: z.array(exclusionSchema).max(100_000),
151
+ languages: z.array(z.string().min(1).max(100)).max(1_000),
152
+ manifests: z.array(relativePathSchema).max(10_000),
153
+ vcs: vcsSchema,
154
+ provenance: z.strictObject({
155
+ importer: z.string().min(1).max(100),
156
+ importer_version: z.string().min(1).max(100).nullable(),
157
+ caller: z
158
+ .string()
159
+ .min(1)
160
+ .max(200)
161
+ .regex(/^[\w .:@/+-]+$/u),
162
+ }),
163
+ limitations: z.array(boundedTextSchema).max(1_000),
164
+ };
165
+ const historicalSourceGraphStructureSchema = z
166
+ .strictObject(graphShape)
167
+ .superRefine(checkGraphInvariants);
168
+ export const historicalSourceGraphSchema = historicalSourceGraphStructureSchema.superRefine((graph, context) => {
169
+ if (computeRootSha256(graph.entries, graph.exclusions) !== graph.root_sha256)
170
+ context.addIssue({
171
+ code: "custom",
172
+ message: "Historical source root commitment does not match entries",
173
+ path: ["root_sha256"],
174
+ });
175
+ });
176
+ const historicalSourceGraphInputSchema = z.strictObject({
177
+ ...graphShape,
178
+ root_sha256: z.never().optional(),
179
+ });
180
+ const historicalSourceManifestBaseSchema = z.strictObject({
181
+ schema: z.literal("HistoricalSourceManifest/v1"),
182
+ authority: z.literal("historical-reference"),
183
+ manifest_id: z.string().regex(/^hsm_[a-f0-9]{64}$/u),
184
+ graph_sha256: digestSchema,
185
+ root_sha256: digestSchema,
186
+ inventory_state: z.enum(["complete", "partial", "unknown"]),
187
+ entry_count: z.number().int().min(0).max(100_000),
188
+ relationship_count: z.number().int().min(0).max(200_000),
189
+ });
190
+ const canonicalJson = (value) => {
191
+ const serialized = canonicalize(value);
192
+ if (serialized === undefined)
193
+ throw new TypeError("RFC 8785 canonicalization rejected a graph value");
194
+ return serialized;
195
+ };
196
+ const digestCanonical = (value) => createHash("sha256").update(canonicalJson(value)).digest("hex");
197
+ const relationshipKey = (relationship) => `${relationship.from_path}\u0000${relationship.to}\u0000${relationship.kind}\u0000${relationship.resolution}\u0000${relationship.parse_state}`;
198
+ const compareCodePoints = (left, right) => left < right ? -1 : left > right ? 1 : 0;
199
+ const checkSortedUnique = (values, field, context) => {
200
+ for (let index = 1; index < values.length; index += 1)
201
+ if (compareCodePoints(values[index - 1] ?? "", values[index] ?? "") >= 0) {
202
+ context.addIssue({
203
+ code: "custom",
204
+ message: `${field} must be sorted and unique by Unicode code point`,
205
+ path: [field, index],
206
+ });
207
+ return;
208
+ }
209
+ };
210
+ const expectedLanguages = (entries) => [
211
+ ...new Set(entries.flatMap((entry) => entry.kind === "file" && entry.language !== null
212
+ ? [entry.language]
213
+ : [])),
214
+ ].sort(compareCodePoints);
215
+ const expectedManifests = (entries) => entries
216
+ .flatMap((entry) => entry.kind === "file" && entry.classifications.includes("manifest")
217
+ ? [entry.path]
218
+ : [])
219
+ .sort(compareCodePoints);
220
+ const checkRelationships = (graph, paths, context) => {
221
+ const files = new Set(graph.entries.flatMap((entry) => entry.kind === "file" ? [entry.path] : []));
222
+ for (const [index, relationship] of graph.relationships.entries()) {
223
+ if (!files.has(relationship.from_path))
224
+ context.addIssue({
225
+ code: "custom",
226
+ message: "Relationship source must name an inventoried file",
227
+ path: ["relationships", index, "from_path"],
228
+ });
229
+ if (relationship.resolution !== "internal")
230
+ continue;
231
+ const parsed = relativePathSchema.safeParse(relationship.to);
232
+ if (!parsed.success || !paths.has(parsed.data))
233
+ context.addIssue({
234
+ code: "custom",
235
+ message: "Internal relationship target must be normalized and inventoried",
236
+ path: ["relationships", index, "to"],
237
+ });
238
+ }
239
+ };
240
+ const checkSymlinks = (graph, paths, context) => {
241
+ for (const [index, entry] of graph.entries.entries()) {
242
+ if (entry.kind !== "symlink")
243
+ continue;
244
+ if ((entry.target_state === "external") !==
245
+ (entry.target === "<outside-root>"))
246
+ context.addIssue({
247
+ code: "custom",
248
+ message: "External symlink targets must use the sanitized <outside-root> sentinel",
249
+ path: ["entries", index, "target"],
250
+ });
251
+ if (entry.target_state !== "internal")
252
+ continue;
253
+ const resolved = posix.normalize(posix.join(posix.dirname(entry.path), entry.target));
254
+ if (resolved.startsWith("../") || resolved === ".." || !paths.has(resolved))
255
+ context.addIssue({
256
+ code: "custom",
257
+ message: "Internal symlink target must resolve to an inventoried entry",
258
+ path: ["entries", index, "target"],
259
+ });
260
+ }
261
+ };
262
+ const checkCompleteInventory = (graph, context) => {
263
+ if (graph.inventory_state !== "complete")
264
+ return;
265
+ const partialEntry = graph.entries.some((entry) => entry.limitations.length > 0 ||
266
+ (entry.kind === "file" && entry.content_state !== "hashed") ||
267
+ (entry.kind === "directory" && entry.tree_state !== "enumerated") ||
268
+ (entry.kind === "symlink" && entry.target_state !== "internal"));
269
+ const partialRelationship = graph.relationships.some(({ parse_state, resolution }) => parse_state !== "parsed" ||
270
+ ["unresolved", "unknown"].includes(resolution));
271
+ if (!partialEntry &&
272
+ !partialRelationship &&
273
+ graph.parse_failures.length === 0 &&
274
+ graph.exclusions.length === 0 &&
275
+ graph.limitations.length === 0)
276
+ return;
277
+ context.addIssue({
278
+ code: "custom",
279
+ message: "Complete inventory cannot contain partial states, exclusions, failures, or limitations",
280
+ path: ["inventory_state"],
281
+ });
282
+ };
283
+ function checkGraphInvariants(graph, context) {
284
+ checkSortedUnique(graph.entries.map(({ path }) => path), "entries", context);
285
+ for (const [index, entry] of graph.entries.entries())
286
+ checkSortedUnique(entry.classifications, `entries.${index}.classifications`, context);
287
+ checkSortedUnique(graph.languages, "languages", context);
288
+ checkSortedUnique(graph.manifests, "manifests", context);
289
+ checkSortedUnique(graph.relationships.map(relationshipKey), "relationships", context);
290
+ checkSortedUnique(graph.parse_failures.map(({ path, parser }) => `${path}\u0000${parser}`), "parse_failures", context);
291
+ checkSortedUnique(graph.exclusions.map(({ path, reason }) => `${path}\u0000${reason}`), "exclusions", context);
292
+ if (canonicalJson(graph.languages) !==
293
+ canonicalJson(expectedLanguages(graph.entries)))
294
+ context.addIssue({
295
+ code: "custom",
296
+ message: "languages must be derived from file entries",
297
+ path: ["languages"],
298
+ });
299
+ if (canonicalJson(graph.manifests) !==
300
+ canonicalJson(expectedManifests(graph.entries)))
301
+ context.addIssue({
302
+ code: "custom",
303
+ message: "manifests must be derived from classified file entries",
304
+ path: ["manifests"],
305
+ });
306
+ const paths = new Set(graph.entries.map(({ path }) => path));
307
+ checkRelationships(graph, paths, context);
308
+ checkSymlinks(graph, paths, context);
309
+ checkCompleteInventory(graph, context);
310
+ }
311
+ const rootCommitment = (entries, exclusions) => ({
312
+ entries: entries.map((entry) => {
313
+ switch (entry.kind) {
314
+ case "file":
315
+ return {
316
+ kind: entry.kind,
317
+ path: entry.path,
318
+ sha256: entry.sha256,
319
+ size: entry.size,
320
+ content_state: entry.content_state,
321
+ };
322
+ case "directory":
323
+ return {
324
+ kind: entry.kind,
325
+ path: entry.path,
326
+ tree_state: entry.tree_state,
327
+ };
328
+ case "symlink":
329
+ return {
330
+ kind: entry.kind,
331
+ path: entry.path,
332
+ target: entry.target,
333
+ target_state: entry.target_state,
334
+ };
335
+ }
336
+ }),
337
+ exclusions,
338
+ });
339
+ const computeRootSha256 = (entries, exclusions) => digestCanonical(rootCommitment(entries, exclusions));
340
+ /** Build and internally commit a normalized historical source graph. */
341
+ export const createHistoricalSourceGraph = (input) => {
342
+ const parsed = historicalSourceGraphInputSchema.parse(input);
343
+ return historicalSourceGraphSchema.parse({
344
+ ...parsed,
345
+ root_sha256: computeRootSha256(parsed.entries, parsed.exclusions),
346
+ });
347
+ };
348
+ /** Parse a graph and reject stale or caller-invented root commitments. */
349
+ export const parseHistoricalSourceGraph = (input) => historicalSourceGraphSchema.parse(input);
350
+ /** Compute a deterministic graph commitment containing no absolute root path. */
351
+ export const computeHistoricalSourceGraphSha256 = (input) => digestCanonical(parseHistoricalSourceGraph(input));
352
+ /** Build a deterministic, relocation-independent manifest for a source graph. */
353
+ export const createHistoricalSourceManifest = (input) => {
354
+ const graph = parseHistoricalSourceGraph(input);
355
+ const semantic = {
356
+ schema: "HistoricalSourceManifest/v1",
357
+ authority: "historical-reference",
358
+ graph_sha256: computeHistoricalSourceGraphSha256(graph),
359
+ root_sha256: graph.root_sha256,
360
+ inventory_state: graph.inventory_state,
361
+ entry_count: graph.entries.length,
362
+ relationship_count: graph.relationships.length,
363
+ };
364
+ return historicalSourceManifestBaseSchema.parse({
365
+ ...semantic,
366
+ manifest_id: `hsm_${digestCanonical(semantic)}`,
367
+ });
368
+ };
369
+ /** Parse and verify a historical source manifest's semantic identifier. */
370
+ export const parseHistoricalSourceManifest = (input) => {
371
+ const manifest = historicalSourceManifestBaseSchema.parse(input);
372
+ const { manifest_id: manifestId, ...semantic } = manifest;
373
+ if (`hsm_${digestCanonical(semantic)}` !== manifestId)
374
+ throw new TypeError("Historical source manifest identifier does not match");
375
+ return manifest;
376
+ };
@@ -0,0 +1,235 @@
1
+ import { parse } from "@babel/parser";
2
+ import { isCallExpression, isExportAllDeclaration, isExportNamedDeclaration, isIdentifier, isImport, isImportDeclaration, isMemberExpression, isNode, isStringLiteral, isTSExternalModuleReference, isTSImportEqualsDeclaration, isTSModuleDeclaration, } from "@babel/types";
3
+ const MAX_REASON_LENGTH = 1_024;
4
+ const codeSourceLanguages = new Set(["JavaScript", "TypeScript", "JSX", "TSX"]);
5
+ const truncateReason = (reason) => reason.length > MAX_REASON_LENGTH
6
+ ? `${reason.slice(0, MAX_REASON_LENGTH - 3)}...`
7
+ : reason;
8
+ const safeModuleName = (node) => {
9
+ if (isStringLiteral(node))
10
+ return node.value;
11
+ return undefined;
12
+ };
13
+ const resolveSpecifier = (specifier) => {
14
+ if (specifier.startsWith(".") || specifier.startsWith("/"))
15
+ return "internal";
16
+ if (specifier.startsWith("node:") || specifier.startsWith("data:"))
17
+ return "external";
18
+ return "external";
19
+ };
20
+ const isModuleExpression = (expression) => isStringLiteral(expression) || isCallExpression(expression);
21
+ const moduleSpecifierFromExpression = (expression) => {
22
+ if (isStringLiteral(expression))
23
+ return { specifier: expression.value, parseState: "parsed" };
24
+ if (isCallExpression(expression) &&
25
+ isImport(expression.callee) &&
26
+ expression.arguments.length > 0) {
27
+ const first = expression.arguments[0];
28
+ if (isStringLiteral(first)) {
29
+ return { specifier: first.value, parseState: "parsed" };
30
+ }
31
+ return { specifier: "<dynamic-import>", parseState: "partial" };
32
+ }
33
+ return undefined;
34
+ };
35
+ const appendRelationship = (relationships, input) => {
36
+ if (input.to === undefined)
37
+ return;
38
+ relationships.push({
39
+ from_path: input.fromPath,
40
+ to: input.to,
41
+ kind: input.kind,
42
+ resolution: input.resolution ?? resolveSpecifier(input.to),
43
+ parse_state: input.parseState,
44
+ });
45
+ };
46
+ const extractImportDeclarations = (body, from_path, relationships) => {
47
+ for (const statement of body) {
48
+ if (isImportDeclaration(statement)) {
49
+ appendRelationship(relationships, {
50
+ fromPath: from_path,
51
+ to: safeModuleName(statement.source),
52
+ kind: "imports",
53
+ parseState: "parsed",
54
+ });
55
+ continue;
56
+ }
57
+ if (isExportNamedDeclaration(statement)) {
58
+ if (statement.source !== null && statement.source !== undefined) {
59
+ appendRelationship(relationships, {
60
+ fromPath: from_path,
61
+ to: safeModuleName(statement.source),
62
+ kind: "imports",
63
+ parseState: "parsed",
64
+ });
65
+ }
66
+ continue;
67
+ }
68
+ if (isExportAllDeclaration(statement)) {
69
+ appendRelationship(relationships, {
70
+ fromPath: from_path,
71
+ to: safeModuleName(statement.source),
72
+ kind: "imports",
73
+ parseState: "parsed",
74
+ });
75
+ continue;
76
+ }
77
+ if (isTSImportEqualsDeclaration(statement)) {
78
+ const reference = statement.moduleReference;
79
+ if (isTSExternalModuleReference(reference)) {
80
+ appendRelationship(relationships, {
81
+ fromPath: from_path,
82
+ to: safeModuleName(reference.expression),
83
+ kind: "requires",
84
+ parseState: "parsed",
85
+ });
86
+ }
87
+ continue;
88
+ }
89
+ if (isTSModuleDeclaration(statement)) {
90
+ if (statement.declare === true && isStringLiteral(statement.id)) {
91
+ appendRelationship(relationships, {
92
+ fromPath: from_path,
93
+ to: statement.id.value,
94
+ kind: "declares-module",
95
+ parseState: "parsed",
96
+ resolution: "unknown",
97
+ });
98
+ }
99
+ }
100
+ }
101
+ };
102
+ const isRequireCallee = (callee) => {
103
+ if (isIdentifier(callee) && callee.name === "require")
104
+ return true;
105
+ if (isMemberExpression(callee) &&
106
+ isIdentifier(callee.object) &&
107
+ callee.object.name === "require" &&
108
+ (isIdentifier(callee.property) || isStringLiteral(callee.property))) {
109
+ const propertyName = isIdentifier(callee.property)
110
+ ? callee.property.name
111
+ : isStringLiteral(callee.property)
112
+ ? callee.property.value
113
+ : undefined;
114
+ return propertyName === "resolve" || propertyName === "main";
115
+ }
116
+ return false;
117
+ };
118
+ const isImportCallee = (callee) => isImport(callee);
119
+ const collectCallExpressions = (node, targets) => {
120
+ if (node === null || node === undefined)
121
+ return;
122
+ if (typeof node !== "object")
123
+ return;
124
+ if (Array.isArray(node)) {
125
+ for (const item of node)
126
+ collectCallExpressions(item, targets);
127
+ return;
128
+ }
129
+ if (!isNode(node))
130
+ return;
131
+ if (isCallExpression(node)) {
132
+ targets.push(node);
133
+ }
134
+ for (const value of Object.values(node)) {
135
+ if (value !== null &&
136
+ value !== undefined &&
137
+ typeof value === "object" &&
138
+ !isSourceLocation(value)) {
139
+ collectCallExpressions(value, targets);
140
+ }
141
+ }
142
+ };
143
+ const isSourceLocation = (value) => typeof value === "object" &&
144
+ value !== null &&
145
+ "start" in value &&
146
+ "end" in value &&
147
+ !("type" in value);
148
+ const extractRequireAndDynamicImports = (body, from_path, relationships) => {
149
+ const calls = [];
150
+ for (const statement of body)
151
+ collectCallExpressions(statement, calls);
152
+ for (const call of calls) {
153
+ const first = call.arguments[0];
154
+ if (isRequireCallee(call.callee) && isModuleExpression(first)) {
155
+ const result = moduleSpecifierFromExpression(first);
156
+ if (result !== undefined) {
157
+ appendRelationship(relationships, {
158
+ fromPath: from_path,
159
+ to: result.specifier,
160
+ kind: "requires",
161
+ parseState: result.parseState,
162
+ });
163
+ }
164
+ continue;
165
+ }
166
+ if (isImportCallee(call.callee) && isModuleExpression(first)) {
167
+ const result = moduleSpecifierFromExpression(first);
168
+ if (result !== undefined) {
169
+ appendRelationship(relationships, {
170
+ fromPath: from_path,
171
+ to: result.specifier,
172
+ kind: "imports",
173
+ parseState: result.parseState,
174
+ });
175
+ }
176
+ }
177
+ }
178
+ };
179
+ const parseWithBabel = (path, source, language) => {
180
+ const plugins = ["jsx"];
181
+ if (language === "TypeScript" || language === "TSX") {
182
+ plugins.push([
183
+ "typescript",
184
+ {
185
+ dts: path.endsWith(".d.ts") ||
186
+ path.endsWith(".d.mts") ||
187
+ path.endsWith(".d.cts"),
188
+ },
189
+ ]);
190
+ }
191
+ try {
192
+ const ast = parse(source, {
193
+ sourceType: "unambiguous",
194
+ allowImportExportEverywhere: false,
195
+ allowReturnOutsideFunction: true,
196
+ allowUndeclaredExports: true,
197
+ errorRecovery: true,
198
+ plugins,
199
+ });
200
+ return { ast, parseState: "parsed" };
201
+ }
202
+ catch (error) {
203
+ return {
204
+ ast: undefined,
205
+ parseState: "unknown",
206
+ reason: error instanceof Error ? error.message : "Parse failed",
207
+ };
208
+ }
209
+ };
210
+ /** Parse ESM/CommonJS imports and dynamic imports from source bytes. */
211
+ export const parseReferenceSourceImports = (path, bytes, language) => {
212
+ if (language !== null && !codeSourceLanguages.has(language))
213
+ return { relationships: [], parse_failures: [] };
214
+ const source = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
215
+ const { ast, parseState, reason } = parseWithBabel(path, source, language);
216
+ if (ast === undefined || parseState === "unknown") {
217
+ return {
218
+ relationships: [],
219
+ parse_failures: [
220
+ {
221
+ path,
222
+ parser: "babel",
223
+ reason: truncateReason(reason ?? "Unknown parse failure"),
224
+ },
225
+ ],
226
+ };
227
+ }
228
+ const relationships = [];
229
+ extractImportDeclarations(ast.program.body, path, relationships);
230
+ extractRequireAndDynamicImports(ast.program.body, path, relationships);
231
+ return {
232
+ relationships,
233
+ parse_failures: [],
234
+ };
235
+ };
@@ -0,0 +1 @@
1
+ export {};