@tryinget/runtime-trace-insights 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +78 -0
- package/README.md +155 -0
- package/package.json +41 -0
- package/src/.gitkeep +1 -0
- package/src/index.mjs +3 -0
- package/src/runtime_record/depdiet_runtime_record_adapter.mjs +80 -0
- package/src/runtime_record/index.mjs +18 -0
- package/src/runtime_record/runtime_record_flow.mjs +1577 -0
- package/src/runtime_trace_bundle/index.mjs +8 -0
- package/src/runtime_trace_bundle/runtime_trace_bundle_builder.mjs +679 -0
- package/src/runtime_trace_bundle/runtime_trace_bundle_flow.mjs +168 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export {
|
|
2
|
+
DEFAULT_RUNTIME_TRACE_BUNDLE_PATH,
|
|
3
|
+
RUNTIME_TRACE_BUNDLE_NON_AUTHORITY_NOTE,
|
|
4
|
+
RUNTIME_TRACE_BUNDLE_SCHEMA_VERSION,
|
|
5
|
+
RUNTIME_TRACE_REOBSERVATION_NON_AUTHORITY_NOTE,
|
|
6
|
+
RUNTIME_TRACE_REOBSERVATION_SCHEMA_VERSION,
|
|
7
|
+
runRuntimeTraceBundleFlow,
|
|
8
|
+
} from "./runtime_trace_bundle_flow.mjs";
|
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
export const RUNTIME_TRACE_BUNDLE_SCHEMA_VERSION = "runtime-trace-insights.runtime-trace-bundle.v1";
|
|
6
|
+
export const RUNTIME_TRACE_REOBSERVATION_SCHEMA_VERSION = "runtime-trace-insights.replacement-reobservation.v1";
|
|
7
|
+
export const RUNTIME_TRACE_BUNDLE_NON_AUTHORITY_NOTE = "Runtime observation is command-level evidence, not dependency removal, risk, vulnerability policy, or visualization authority.";
|
|
8
|
+
export const RUNTIME_TRACE_REOBSERVATION_NON_AUTHORITY_NOTE = "Replacement re-observation is before/after command-level evidence for candidate validation; it is not exhaustive coverage, replacement safety, merge, release, exploitability, disclosure, or trust authority.";
|
|
9
|
+
|
|
10
|
+
function isPlainObject(value) {
|
|
11
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function asArray(value) {
|
|
15
|
+
return Array.isArray(value) ? value : [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function asNonEmptyString(value) {
|
|
19
|
+
if (typeof value !== "string") {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const normalized = value.trim();
|
|
24
|
+
return normalized.length > 0 ? normalized : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizePathSlashes(value) {
|
|
28
|
+
return String(value).replaceAll("\\", "/");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function toRelativePath(basePath, targetPath) {
|
|
32
|
+
const relative = path.relative(basePath, targetPath);
|
|
33
|
+
return normalizePathSlashes(relative.length > 0 ? relative : ".");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function createDiagnostic({ code, level = "warning", message, source = "runtimeTraceBundle", details }) {
|
|
37
|
+
const diagnostic = { code, level, source, message };
|
|
38
|
+
if (details !== undefined) {
|
|
39
|
+
diagnostic.details = details;
|
|
40
|
+
}
|
|
41
|
+
return diagnostic;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function capitalize(value) {
|
|
45
|
+
return `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Who emits the bundle decides its diagnostic namespace and a few conventions. The bundle command
|
|
49
|
+
// (runtime_trace_bundle.mjs record) is this repo's own surface. The record profile reproduces
|
|
50
|
+
// dep-diet's `depdiet record` contract so dep-diet can load this repo as its runtime-record adapter
|
|
51
|
+
// without loss (AK #5892).
|
|
52
|
+
export const RUNTIME_TRACE_BUNDLE_PROFILES = Object.freeze({
|
|
53
|
+
bundleCommand: Object.freeze({
|
|
54
|
+
diagnosticCode: (name) => `runtimeTraceBundle.${name}`,
|
|
55
|
+
diagnosticSource: "runtimeTraceBundle",
|
|
56
|
+
readPackageJson: "always",
|
|
57
|
+
warnWhenNoPackagesDeclared: true,
|
|
58
|
+
warnOnArtifactPlacement: true,
|
|
59
|
+
emittedBy: null,
|
|
60
|
+
moduleId: ({ projectPathAbsolute }) => `js:${path.basename(projectPathAbsolute)}`,
|
|
61
|
+
referenceTool: "runtime-trace-insights",
|
|
62
|
+
}),
|
|
63
|
+
depdietRecord: Object.freeze({
|
|
64
|
+
diagnosticCode: (name) => `runtimeRecord.runtimeBundle${capitalize(name)}`,
|
|
65
|
+
diagnosticSource: "runtimeRecord",
|
|
66
|
+
readPackageJson: "when-declared",
|
|
67
|
+
warnWhenNoPackagesDeclared: false,
|
|
68
|
+
warnOnArtifactPlacement: false,
|
|
69
|
+
emittedBy: "dep-diet",
|
|
70
|
+
moduleId: ({ projectPathInput }) => `js:${normalizePathSlashes(projectPathInput)}`,
|
|
71
|
+
referenceTool: "dep-diet",
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
function profileDiagnostic(context, { name, level = "warning", message, details }) {
|
|
76
|
+
return createDiagnostic({
|
|
77
|
+
code: context.profile.diagnosticCode(name),
|
|
78
|
+
level,
|
|
79
|
+
message,
|
|
80
|
+
source: context.profile.diagnosticSource,
|
|
81
|
+
details,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function normalizeDiagnosticList(entries, source) {
|
|
86
|
+
return asArray(entries)
|
|
87
|
+
.filter((entry) => isPlainObject(entry))
|
|
88
|
+
.map((entry) => ({
|
|
89
|
+
...entry,
|
|
90
|
+
...(asNonEmptyString(entry.level) ? {} : { level: "warning" }),
|
|
91
|
+
...(asNonEmptyString(entry.source) ? {} : { source }),
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function parsePackageSpecifier(value) {
|
|
96
|
+
const token = asNonEmptyString(value);
|
|
97
|
+
if (!token) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const atIndex = token.lastIndexOf("@");
|
|
102
|
+
if (atIndex > 0) {
|
|
103
|
+
return {
|
|
104
|
+
name: token.slice(0, atIndex),
|
|
105
|
+
version: token.slice(atIndex + 1) || null,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
name: token,
|
|
111
|
+
version: null,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function readPackageJson(projectPathAbsolute, context) {
|
|
116
|
+
const packageJsonPath = path.join(projectPathAbsolute, "package.json");
|
|
117
|
+
|
|
118
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
119
|
+
context.diagnostics.push(profileDiagnostic(context, {
|
|
120
|
+
name: "packageJsonMissing",
|
|
121
|
+
message: "Could not resolve package versions because package.json was not found.",
|
|
122
|
+
details: { packageJsonPath },
|
|
123
|
+
}));
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
return JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
129
|
+
} catch (error) {
|
|
130
|
+
context.diagnostics.push(profileDiagnostic(context, {
|
|
131
|
+
name: "packageJsonReadFailed",
|
|
132
|
+
message: "Could not parse package.json while resolving observed package versions.",
|
|
133
|
+
details: {
|
|
134
|
+
packageJsonPath,
|
|
135
|
+
error: error instanceof Error ? error.message : String(error),
|
|
136
|
+
},
|
|
137
|
+
}));
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function isExactVersionSpecifier(value) {
|
|
143
|
+
const normalized = asNonEmptyString(value);
|
|
144
|
+
return Boolean(normalized && /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/u.test(normalized));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function readJsonFile(filePath) {
|
|
148
|
+
try {
|
|
149
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
150
|
+
} catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function ancestorDirectories(startAbsolute) {
|
|
156
|
+
const directories = [];
|
|
157
|
+
let current = path.resolve(startAbsolute);
|
|
158
|
+
for (;;) {
|
|
159
|
+
directories.push(current);
|
|
160
|
+
const parent = path.dirname(current);
|
|
161
|
+
if (parent === current) {
|
|
162
|
+
return directories;
|
|
163
|
+
}
|
|
164
|
+
current = parent;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// A package-lock.json at `directory` records a workspace member's own nested install as
|
|
169
|
+
// "<member>/node_modules/<name>" and a hoisted install as "node_modules/<name>".
|
|
170
|
+
function readPackageLockVersion(directory, projectPathAbsolute, packageName) {
|
|
171
|
+
const lockPath = path.join(directory, "package-lock.json");
|
|
172
|
+
if (!fs.existsSync(lockPath)) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const lockPackages = readJsonFile(lockPath)?.packages;
|
|
177
|
+
const memberPath = normalizePathSlashes(path.relative(directory, projectPathAbsolute));
|
|
178
|
+
const lockKeys = [
|
|
179
|
+
...(memberPath ? [`${memberPath}/node_modules/${packageName}`] : []),
|
|
180
|
+
`node_modules/${packageName}`,
|
|
181
|
+
];
|
|
182
|
+
for (const lockKey of lockKeys) {
|
|
183
|
+
const version = asNonEmptyString(lockPackages?.[lockKey]?.version);
|
|
184
|
+
if (version) {
|
|
185
|
+
return {
|
|
186
|
+
version,
|
|
187
|
+
sourcePath: `${toRelativePath(projectPathAbsolute, lockPath)}#packages.${lockKey}.version`,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function readInstalledPackageVersion(directory, projectPathAbsolute, packageName) {
|
|
195
|
+
const packageJsonPath = path.join(directory, "node_modules", packageName, "package.json");
|
|
196
|
+
const version = asNonEmptyString(readJsonFile(packageJsonPath)?.version);
|
|
197
|
+
return version
|
|
198
|
+
? { version, sourcePath: `${toRelativePath(projectPathAbsolute, packageJsonPath)}#version` }
|
|
199
|
+
: null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Walks from the project upward the way Node resolution does, so the nearest install wins and a
|
|
203
|
+
// distant ancestor lockfile (for example one in $HOME) never overrides it. At each level the
|
|
204
|
+
// lockfile is checked before the installed package.json.
|
|
205
|
+
function resolveInstalledPackageVersion(projectPathAbsolute, packageName) {
|
|
206
|
+
for (const directory of ancestorDirectories(projectPathAbsolute)) {
|
|
207
|
+
const resolved = readPackageLockVersion(directory, projectPathAbsolute, packageName)
|
|
208
|
+
?? readInstalledPackageVersion(directory, projectPathAbsolute, packageName);
|
|
209
|
+
if (resolved) {
|
|
210
|
+
return resolved;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Resolves the exact version a declared package runs at. A manifest range such as
|
|
217
|
+
// "^1.0.0" is never returned as a version: it cannot distinguish a before/after update.
|
|
218
|
+
function resolvePackageVersion({ packageJson, projectPathAbsolute, packageName }) {
|
|
219
|
+
const locked = resolveInstalledPackageVersion(projectPathAbsolute, packageName);
|
|
220
|
+
if (locked) {
|
|
221
|
+
return { ...locked, rawSpecifier: null };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
for (const sectionName of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) {
|
|
225
|
+
const section = packageJson?.[sectionName];
|
|
226
|
+
if (!isPlainObject(section)) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const specifier = asNonEmptyString(section[packageName]);
|
|
231
|
+
if (specifier) {
|
|
232
|
+
return {
|
|
233
|
+
version: isExactVersionSpecifier(specifier) ? specifier : null,
|
|
234
|
+
rawSpecifier: specifier,
|
|
235
|
+
sourcePath: `package.json#${sectionName}.${packageName}`,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
version: null,
|
|
242
|
+
rawSpecifier: null,
|
|
243
|
+
sourcePath: "package.json",
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function normalizeReplacementObservation(options) {
|
|
248
|
+
const phase = asNonEmptyString(options.observationPhase ?? options.reobservationPhase ?? options.phase);
|
|
249
|
+
const planRef = asNonEmptyString(options.replacementPlanRef ?? options.planRef);
|
|
250
|
+
const resultRef = asNonEmptyString(options.replacementResultRef ?? options.resultRef);
|
|
251
|
+
const candidateId = asNonEmptyString(options.replacementCandidateId ?? options.candidateId);
|
|
252
|
+
const intentRef = asNonEmptyString(options.replacementIntentRef ?? options.intentRef);
|
|
253
|
+
|
|
254
|
+
if (!phase && !planRef && !resultRef && !candidateId && !intentRef) {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const normalizedPhase = phase === "after" ? "after" : phase === "before" ? "before" : "unspecified";
|
|
259
|
+
return {
|
|
260
|
+
schemaVersion: RUNTIME_TRACE_REOBSERVATION_SCHEMA_VERSION,
|
|
261
|
+
phase: normalizedPhase,
|
|
262
|
+
...(intentRef ? { intentRef } : {}),
|
|
263
|
+
...(planRef ? { planRef } : {}),
|
|
264
|
+
...(resultRef ? { resultRef } : {}),
|
|
265
|
+
...(candidateId ? { candidateId } : {}),
|
|
266
|
+
comparisonKey: asNonEmptyString(options.comparisonKey)
|
|
267
|
+
?? ([intentRef, planRef, candidateId].filter(Boolean).join("::") || null),
|
|
268
|
+
requiredPairing: normalizedPhase === "before"
|
|
269
|
+
? "Record a matching after observation with the same comparisonKey after a candidate replacement is produced."
|
|
270
|
+
: normalizedPhase === "after"
|
|
271
|
+
? "Compare against the matching before observation with the same comparisonKey; do not treat this command as exhaustive coverage."
|
|
272
|
+
: "Pair before and after observations with the same comparisonKey for candidate validation.",
|
|
273
|
+
authority: {
|
|
274
|
+
exhaustiveCoverageAuthority: false,
|
|
275
|
+
replacementSafetyAuthority: false,
|
|
276
|
+
mergeAuthority: false,
|
|
277
|
+
releaseAuthority: false,
|
|
278
|
+
exploitabilityAuthority: false,
|
|
279
|
+
disclosureAuthority: false,
|
|
280
|
+
trustCertificationAuthority: false,
|
|
281
|
+
note: RUNTIME_TRACE_REOBSERVATION_NON_AUTHORITY_NOTE,
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function normalizeObservedPackage(entry, context) {
|
|
287
|
+
const diagnostics = context.diagnostics;
|
|
288
|
+
const raw = isPlainObject(entry)
|
|
289
|
+
? entry
|
|
290
|
+
: { specifier: entry };
|
|
291
|
+
|
|
292
|
+
const parsed = parsePackageSpecifier(raw.name ?? raw.packageName ?? raw.specifier ?? raw.package);
|
|
293
|
+
if (!parsed?.name) {
|
|
294
|
+
diagnostics.push(profileDiagnostic(context, {
|
|
295
|
+
name: "observedPackageSkipped",
|
|
296
|
+
message: "Skipped observed package because its name was missing.",
|
|
297
|
+
details: { entry },
|
|
298
|
+
}));
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const ecosystem = asNonEmptyString(raw.ecosystem) ?? asNonEmptyString(context.ecosystem) ?? "npm";
|
|
303
|
+
const resolved = resolvePackageVersion({
|
|
304
|
+
packageJson: context.packageJson,
|
|
305
|
+
projectPathAbsolute: context.projectPathAbsolute,
|
|
306
|
+
packageName: parsed.name,
|
|
307
|
+
});
|
|
308
|
+
const version = asNonEmptyString(raw.version) ?? parsed.version ?? resolved.version ?? "unknown";
|
|
309
|
+
const packageId = asNonEmptyString(raw.packageId) ?? `${ecosystem}:${parsed.name}@${version}`;
|
|
310
|
+
const moduleId = asNonEmptyString(raw.moduleId) ?? asNonEmptyString(context.moduleId) ?? context.profile.moduleId(context);
|
|
311
|
+
const sourcePath = asNonEmptyString(raw.sourcePath) ?? resolved.sourcePath;
|
|
312
|
+
|
|
313
|
+
if (!resolved.version && !parsed.version && !raw.version) {
|
|
314
|
+
diagnostics.push(profileDiagnostic(context, {
|
|
315
|
+
name: resolved.rawSpecifier ? "observedPackageVersionRange" : "observedPackageVersionUnknown",
|
|
316
|
+
message: resolved.rawSpecifier
|
|
317
|
+
? `Observed package ${JSON.stringify(parsed.name)} used non-exact manifest specifier ${JSON.stringify(resolved.rawSpecifier)} without a lockfile or installed version; using \"unknown\".`
|
|
318
|
+
: `Observed package ${JSON.stringify(parsed.name)} did not have a resolvable version; using \"unknown\".`,
|
|
319
|
+
details: {
|
|
320
|
+
packageName: parsed.name,
|
|
321
|
+
rawSpecifier: resolved.rawSpecifier ?? null,
|
|
322
|
+
},
|
|
323
|
+
}));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
name: parsed.name,
|
|
328
|
+
ecosystem,
|
|
329
|
+
packageId,
|
|
330
|
+
version,
|
|
331
|
+
moduleId,
|
|
332
|
+
observed: raw.observed === false ? false : true,
|
|
333
|
+
evidence: {
|
|
334
|
+
kind: asNonEmptyString(raw.evidenceKind) ?? asNonEmptyString(context.evidenceKind) ?? "runtime-command-observation",
|
|
335
|
+
sourcePath,
|
|
336
|
+
commandLine: context.commandLine,
|
|
337
|
+
...(context.replacementObservation
|
|
338
|
+
? {
|
|
339
|
+
replacementObservationPhase: context.replacementObservation.phase,
|
|
340
|
+
replacementObservationComparisonKey: context.replacementObservation.comparisonKey,
|
|
341
|
+
}
|
|
342
|
+
: {}),
|
|
343
|
+
},
|
|
344
|
+
authority: {
|
|
345
|
+
removalAuthority: false,
|
|
346
|
+
riskAuthority: false,
|
|
347
|
+
visualizationAuthority: false,
|
|
348
|
+
note: RUNTIME_TRACE_BUNDLE_NON_AUTHORITY_NOTE,
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function observedPackageInputs(options) {
|
|
354
|
+
const inputs = [];
|
|
355
|
+
for (const candidate of [options.observedPackage, options.observedPackages, options.packageName, options.packages]) {
|
|
356
|
+
if (Array.isArray(candidate)) {
|
|
357
|
+
inputs.push(...candidate);
|
|
358
|
+
} else if (typeof candidate === "string") {
|
|
359
|
+
inputs.push(...candidate.split(","));
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return inputs
|
|
363
|
+
.map((entry) => (isPlainObject(entry) ? entry : asNonEmptyString(entry)))
|
|
364
|
+
.filter(Boolean);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function normalizeObservedPackages(inputs, context) {
|
|
368
|
+
if (inputs.length === 0 && context.packageAutodiscoveryRequested !== true && context.profile.warnWhenNoPackagesDeclared) {
|
|
369
|
+
context.diagnostics.push(profileDiagnostic(context, {
|
|
370
|
+
name: "noObservedPackagesDeclared",
|
|
371
|
+
message: "Runtime trace bundle was produced without explicit observed package declarations.",
|
|
372
|
+
}));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const packages = inputs
|
|
376
|
+
.map((entry) => normalizeObservedPackage(entry, context))
|
|
377
|
+
.filter(Boolean);
|
|
378
|
+
|
|
379
|
+
packages.sort((left, right) => left.packageId.localeCompare(right.packageId));
|
|
380
|
+
return packages;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function eventPathFromAutodiscoveryEvent(event) {
|
|
384
|
+
const url = asNonEmptyString(event?.url);
|
|
385
|
+
if (url?.startsWith("file:")) {
|
|
386
|
+
try {
|
|
387
|
+
return fileURLToPath(url);
|
|
388
|
+
} catch {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return asNonEmptyString(event?.resolvedPath);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function resolveNodeModulePackageFromPath(absolutePath) {
|
|
397
|
+
const normalized = normalizePathSlashes(path.resolve(absolutePath));
|
|
398
|
+
const marker = "/node_modules/";
|
|
399
|
+
const markerIndex = normalized.lastIndexOf(marker);
|
|
400
|
+
if (markerIndex < 0) {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const prefix = normalized.slice(0, markerIndex + marker.length - 1);
|
|
405
|
+
const remainder = normalized.slice(markerIndex + marker.length);
|
|
406
|
+
const segments = remainder.split("/").filter(Boolean);
|
|
407
|
+
if (segments.length === 0) {
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const name = segments[0].startsWith("@")
|
|
412
|
+
? (segments.length >= 2 ? `${segments[0]}/${segments[1]}` : null)
|
|
413
|
+
: segments[0];
|
|
414
|
+
|
|
415
|
+
if (!name) {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const packageRoot = path.join(prefix, name);
|
|
420
|
+
return {
|
|
421
|
+
name,
|
|
422
|
+
packageRoot,
|
|
423
|
+
relativePathInPackage: normalizePathSlashes(path.relative(packageRoot, absolutePath)),
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function readPackageVersionFromPackageRoot(packageRoot) {
|
|
428
|
+
try {
|
|
429
|
+
const packageJsonPath = path.join(packageRoot, "package.json");
|
|
430
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
431
|
+
return asNonEmptyString(packageJson.version);
|
|
432
|
+
} catch {
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function normalizeAutodiscoveredPackages(packageAutodiscovery, context) {
|
|
438
|
+
const events = asArray(packageAutodiscovery?.events);
|
|
439
|
+
if (events.length === 0) {
|
|
440
|
+
if (packageAutodiscovery?.enabled === true) {
|
|
441
|
+
context.diagnostics.push(profileDiagnostic(context, {
|
|
442
|
+
name: "packageAutodiscoveryNoEvents",
|
|
443
|
+
message: "Package autodiscovery was enabled but emitted no loader/preload events.",
|
|
444
|
+
}));
|
|
445
|
+
}
|
|
446
|
+
return [];
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const byPackageId = new Map();
|
|
450
|
+
|
|
451
|
+
for (const event of events) {
|
|
452
|
+
const eventPath = eventPathFromAutodiscoveryEvent(event);
|
|
453
|
+
if (!eventPath) {
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const resolved = resolveNodeModulePackageFromPath(eventPath);
|
|
458
|
+
if (!resolved) {
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const packageJsonResolution = resolvePackageVersion({
|
|
463
|
+
packageJson: context.packageJson,
|
|
464
|
+
projectPathAbsolute: context.projectPathAbsolute,
|
|
465
|
+
packageName: resolved.name,
|
|
466
|
+
});
|
|
467
|
+
const version = readPackageVersionFromPackageRoot(resolved.packageRoot)
|
|
468
|
+
?? packageJsonResolution.version
|
|
469
|
+
?? "unknown";
|
|
470
|
+
const ecosystem = asNonEmptyString(context.ecosystem) ?? "npm";
|
|
471
|
+
const packageId = `${ecosystem}:${resolved.name}@${version}`;
|
|
472
|
+
const moduleId = asNonEmptyString(context.moduleId) ?? `js:${path.basename(context.projectPathAbsolute)}`;
|
|
473
|
+
|
|
474
|
+
if (!byPackageId.has(packageId)) {
|
|
475
|
+
byPackageId.set(packageId, {
|
|
476
|
+
name: resolved.name,
|
|
477
|
+
ecosystem,
|
|
478
|
+
packageId,
|
|
479
|
+
version,
|
|
480
|
+
moduleId,
|
|
481
|
+
observed: true,
|
|
482
|
+
evidence: {
|
|
483
|
+
kind: "runtime-package-autodiscovery",
|
|
484
|
+
sourcePath: normalizePathSlashes(path.relative(context.projectPathAbsolute, path.join(resolved.packageRoot, "package.json"))),
|
|
485
|
+
commandLine: context.commandLine,
|
|
486
|
+
discovery: {
|
|
487
|
+
eventKinds: [],
|
|
488
|
+
sampleResolvedPaths: [],
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
authority: {
|
|
492
|
+
removalAuthority: false,
|
|
493
|
+
riskAuthority: false,
|
|
494
|
+
visualizationAuthority: false,
|
|
495
|
+
note: RUNTIME_TRACE_BUNDLE_NON_AUTHORITY_NOTE,
|
|
496
|
+
},
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const observed = byPackageId.get(packageId);
|
|
501
|
+
if (!observed.evidence.discovery.eventKinds.includes(event.kind)) {
|
|
502
|
+
observed.evidence.discovery.eventKinds.push(event.kind);
|
|
503
|
+
}
|
|
504
|
+
const relativeEventPath = normalizePathSlashes(path.relative(context.projectPathAbsolute, eventPath));
|
|
505
|
+
if (observed.evidence.discovery.sampleResolvedPaths.length < 5
|
|
506
|
+
&& !observed.evidence.discovery.sampleResolvedPaths.includes(relativeEventPath)) {
|
|
507
|
+
observed.evidence.discovery.sampleResolvedPaths.push(relativeEventPath);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return [...byPackageId.values()]
|
|
512
|
+
.sort((left, right) => left.packageId.localeCompare(right.packageId));
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Autodiscovery records what the command actually loaded, so it outranks declarations for the
|
|
516
|
+
// same package. A declared entry for an autodiscovered package survives only through the
|
|
517
|
+
// autodiscovered entry; keeping a declared "unknown" or a version that was never loaded would
|
|
518
|
+
// give dep-diet a phantom package row.
|
|
519
|
+
function mergeObservedPackageEvidence({ declared, autodiscovered, context }) {
|
|
520
|
+
const diagnostics = context.diagnostics;
|
|
521
|
+
const unresolvedDeclaredVersionCodes = new Set(
|
|
522
|
+
["observedPackageVersionRange", "observedPackageVersionUnknown"].map((name) => context.profile.diagnosticCode(name)),
|
|
523
|
+
);
|
|
524
|
+
const observedVersionsByName = new Map();
|
|
525
|
+
for (const entry of autodiscovered) {
|
|
526
|
+
const key = `${entry.ecosystem}:${entry.name}`;
|
|
527
|
+
observedVersionsByName.set(key, [...(observedVersionsByName.get(key) ?? []), entry.version]);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const keptDeclared = [];
|
|
531
|
+
for (const entry of declared) {
|
|
532
|
+
const observedVersions = observedVersionsByName.get(`${entry.ecosystem}:${entry.name}`);
|
|
533
|
+
if (!observedVersions) {
|
|
534
|
+
keptDeclared.push(entry);
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (entry.version === "unknown") {
|
|
539
|
+
for (let index = diagnostics.length - 1; index >= 0; index -= 1) {
|
|
540
|
+
const diagnostic = diagnostics[index];
|
|
541
|
+
if (unresolvedDeclaredVersionCodes.has(diagnostic.code) && diagnostic.details?.packageName === entry.name) {
|
|
542
|
+
diagnostics.splice(index, 1);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
} else if (!observedVersions.includes(entry.version)) {
|
|
546
|
+
diagnostics.push(profileDiagnostic(context, {
|
|
547
|
+
name: "declaredVersionNotObserved",
|
|
548
|
+
message: `Declared observed package ${JSON.stringify(entry.name)} at ${JSON.stringify(entry.version)} was not loaded by the command; package autodiscovery observed ${observedVersions.map((version) => JSON.stringify(version)).join(", ")}. The declared version was dropped.`,
|
|
549
|
+
details: {
|
|
550
|
+
packageName: entry.name,
|
|
551
|
+
declaredVersion: entry.version,
|
|
552
|
+
observedVersions: [...observedVersions].sort(),
|
|
553
|
+
},
|
|
554
|
+
}));
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const byPackageId = new Map();
|
|
559
|
+
for (const entry of [...keptDeclared, ...autodiscovered]) {
|
|
560
|
+
byPackageId.set(entry.packageId, entry);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return [...byPackageId.values()]
|
|
564
|
+
.sort((left, right) => left.packageId.localeCompare(right.packageId));
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function isInsideDirectory(directoryAbsolute, targetAbsolute) {
|
|
568
|
+
const relative = path.relative(directoryAbsolute, targetAbsolute);
|
|
569
|
+
return relative.length > 0 && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Artifact paths resolve against the caller's cwd while the command runs in the project, so a
|
|
573
|
+
// caller standing inside the project writes the recorder's files into the observed checkout.
|
|
574
|
+
// See docs/decisions/2026-09-23-observed-checkout-artifact-placement-greats.md.
|
|
575
|
+
function createArtifactPlacementDiagnostic(context, { runDirectoryAbsolute, runtimeTraceManifestAbsolutePath, bundleAbsolutePath }) {
|
|
576
|
+
const projectPathAbsolute = context.projectPathAbsolute;
|
|
577
|
+
const artifacts = [
|
|
578
|
+
{ artifact: "runDirectory", absolutePath: runDirectoryAbsolute },
|
|
579
|
+
{ artifact: "runtimeTraceManifest", absolutePath: runtimeTraceManifestAbsolutePath },
|
|
580
|
+
{ artifact: "bundle", absolutePath: bundleAbsolutePath },
|
|
581
|
+
];
|
|
582
|
+
|
|
583
|
+
const insideObservedProject = artifacts
|
|
584
|
+
.filter((entry) => entry.absolutePath && isInsideDirectory(projectPathAbsolute, entry.absolutePath))
|
|
585
|
+
.map((entry) => ({ artifact: entry.artifact, path: toRelativePath(projectPathAbsolute, entry.absolutePath) }));
|
|
586
|
+
|
|
587
|
+
if (insideObservedProject.length === 0) {
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return profileDiagnostic(context, {
|
|
592
|
+
name: "artifactsInsideObservedProject",
|
|
593
|
+
message: "Recorder artifacts were written inside the observed project; a later static scan of the project can read them (including package autodiscovery loader modules) as project source. Record from a directory outside the project, or pass --artifact-directory, --runtime-trace-manifest, and --out outside it.",
|
|
594
|
+
details: { insideObservedProject },
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Builds a runtime-trace-insights.runtime-trace-bundle.v1 object from one recorded command.
|
|
600
|
+
* Diagnostics are appended to `diagnostics` (shared with the caller); the bundle carries a
|
|
601
|
+
* normalized snapshot of them.
|
|
602
|
+
*/
|
|
603
|
+
export function buildRuntimeTraceBundle({
|
|
604
|
+
profile = RUNTIME_TRACE_BUNDLE_PROFILES.bundleCommand,
|
|
605
|
+
diagnostics,
|
|
606
|
+
options = {},
|
|
607
|
+
generatedAt,
|
|
608
|
+
runId,
|
|
609
|
+
bundleId,
|
|
610
|
+
projectPathInput,
|
|
611
|
+
projectPathAbsolute,
|
|
612
|
+
observedCommand,
|
|
613
|
+
commandLine,
|
|
614
|
+
exitCode,
|
|
615
|
+
durationMs,
|
|
616
|
+
packageAutodiscovery,
|
|
617
|
+
artifactPaths = {},
|
|
618
|
+
references = [],
|
|
619
|
+
}) {
|
|
620
|
+
const inputs = observedPackageInputs(options);
|
|
621
|
+
const context = {
|
|
622
|
+
profile,
|
|
623
|
+
diagnostics,
|
|
624
|
+
projectPathInput,
|
|
625
|
+
projectPathAbsolute,
|
|
626
|
+
moduleId: options.moduleId,
|
|
627
|
+
ecosystem: options.ecosystem,
|
|
628
|
+
evidenceKind: options.evidenceKind,
|
|
629
|
+
commandLine,
|
|
630
|
+
replacementObservation: normalizeReplacementObservation(options),
|
|
631
|
+
packageAutodiscoveryRequested: packageAutodiscovery?.enabled === true,
|
|
632
|
+
packageJson: null,
|
|
633
|
+
};
|
|
634
|
+
if (profile.readPackageJson === "always" || inputs.length > 0) {
|
|
635
|
+
context.packageJson = readPackageJson(projectPathAbsolute, context);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const observedPackages = mergeObservedPackageEvidence({
|
|
639
|
+
declared: normalizeObservedPackages(inputs, context),
|
|
640
|
+
autodiscovered: normalizeAutodiscoveredPackages(packageAutodiscovery, context),
|
|
641
|
+
context,
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
if (profile.warnOnArtifactPlacement) {
|
|
645
|
+
const placementDiagnostic = createArtifactPlacementDiagnostic(context, artifactPaths);
|
|
646
|
+
if (placementDiagnostic) {
|
|
647
|
+
diagnostics.push(placementDiagnostic);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return {
|
|
652
|
+
schemaVersion: RUNTIME_TRACE_BUNDLE_SCHEMA_VERSION,
|
|
653
|
+
schema_version: RUNTIME_TRACE_BUNDLE_SCHEMA_VERSION,
|
|
654
|
+
bundleId: asNonEmptyString(bundleId) ?? `${runId}-runtime-trace-bundle`,
|
|
655
|
+
producer: "runtime-trace-insights",
|
|
656
|
+
...(profile.emittedBy ? { emittedBy: profile.emittedBy } : {}),
|
|
657
|
+
runId,
|
|
658
|
+
generatedAt,
|
|
659
|
+
command: {
|
|
660
|
+
projectPath: projectPathInput,
|
|
661
|
+
observedCommand,
|
|
662
|
+
commandLine,
|
|
663
|
+
exitCode,
|
|
664
|
+
durationMs,
|
|
665
|
+
},
|
|
666
|
+
authority: {
|
|
667
|
+
removalAuthority: false,
|
|
668
|
+
riskAuthority: false,
|
|
669
|
+
visualizationAuthority: false,
|
|
670
|
+
note: RUNTIME_TRACE_BUNDLE_NON_AUTHORITY_NOTE,
|
|
671
|
+
},
|
|
672
|
+
...(context.replacementObservation ? { replacementObservation: context.replacementObservation } : {}),
|
|
673
|
+
observedPackages,
|
|
674
|
+
diagnostics: normalizeDiagnosticList(diagnostics, profile.diagnosticSource),
|
|
675
|
+
references: references
|
|
676
|
+
.filter((reference) => asNonEmptyString(reference?.path))
|
|
677
|
+
.map((reference) => ({ kind: reference.kind, path: reference.path, tool: profile.referenceTool })),
|
|
678
|
+
};
|
|
679
|
+
}
|