rea-agents 1.1.0 → 1.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.
Files changed (48) hide show
  1. package/README.md +21 -2
  2. package/dist/application/ArtifactInventory.js +80 -44
  3. package/dist/application/CommandShimReplay.js +136 -0
  4. package/dist/application/EvidenceBundleFiles.js +9 -1
  5. package/dist/application/ProcessCaptureAuthority.js +25 -0
  6. package/dist/application/ProcessCaptureCapability.js +23 -0
  7. package/dist/application/ProcessCaptureError.js +5 -0
  8. package/dist/application/ProcessCaptureLifecycle.js +269 -0
  9. package/dist/application/ProcessCheckpoints.js +129 -0
  10. package/dist/application/ProcessCli.js +61 -0
  11. package/dist/application/ProcessEvidence.js +38 -0
  12. package/dist/application/ProcessHarness.js +252 -272
  13. package/dist/application/ProcessNormalization.js +10 -1
  14. package/dist/application/ProcessOwnership.js +39 -1
  15. package/dist/application/ProcessSampling.js +39 -23
  16. package/dist/application/Setup.js +51 -76
  17. package/dist/application/SetupSkill.js +44 -0
  18. package/dist/application/TerminalRenderer.js +92 -0
  19. package/dist/application/runtime.js +1 -1
  20. package/dist/artifacts/ArtifactProvider.js +18 -6
  21. package/dist/artifacts/ArtifactReader.js +3 -1
  22. package/dist/artifacts/AsarArtifactReader.js +8 -1
  23. package/dist/artifacts/DirectoryArtifactReader.js +1 -0
  24. package/dist/artifacts/MachOSliceArtifactReader.js +1 -0
  25. package/dist/artifacts/NativeDmgArtifactReader.js +151 -0
  26. package/dist/artifacts/ZipArtifactReader.js +1 -0
  27. package/dist/cli.js +29 -24
  28. package/dist/cliProcessCommands.js +19 -0
  29. package/dist/config.js +2 -0
  30. package/dist/contracts/artifactToolContracts.js +1 -0
  31. package/dist/contracts/investigationExamples.js +3 -3
  32. package/dist/contracts/processCaptureExample.js +50 -7
  33. package/dist/contracts/toolContracts.js +3 -2
  34. package/dist/domain/changedBehavior.js +2 -2
  35. package/dist/domain/errors.js +11 -3
  36. package/dist/domain/processCapture.js +99 -293
  37. package/dist/domain/processCaptureValidation.js +124 -0
  38. package/dist/domain/processComparison.js +176 -32
  39. package/dist/domain/processScenario.js +370 -0
  40. package/dist/domain/reconstructionVerification.js +3 -3
  41. package/dist/domain/staticRuntimeCorrelation.js +3 -2
  42. package/dist/identity.js +1 -0
  43. package/dist/server/registerProcessComparisonTool.js +28 -8
  44. package/dist/server/registerSessionTools.js +3 -25
  45. package/dist/server/sessionToolPolicies.js +1 -6
  46. package/package.json +5 -1
  47. package/scripts/prepare-node-pty.mjs +35 -0
  48. package/skills/rea-analysis/SKILL.md +19 -4
@@ -1,265 +1,105 @@
1
- import { resolve } from "node:path";
2
1
  import { z } from "zod";
3
- const positiveBudget = z.number().int().positive();
4
- const timedEventBase = { at_ms: z.number().int().nonnegative() };
5
- const environmentName = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/u);
6
- const reservedEnvironment = new Set([
7
- "HOME",
8
- "TERM",
9
- "REA_PROCESS_RUN_ID",
10
- "REA_REPLAY_HTTP_URL",
11
- "REA_REPLAY_WEBSOCKET_URL",
12
- ]);
13
- const normalizationSchema = z.object({
14
- paths: z.boolean(),
15
- pids: z.boolean(),
16
- ports: z.boolean(),
17
- time_bucket_ms: positiveBudget.max(60_000),
18
- patterns: z.array(z.object({
19
- pattern: z.string().max(500),
20
- replacement: z.string().max(100),
21
- })),
2
+ import { LEGACY_PROCESS_CAPTURE_MESSAGE, normalizationSchema, } from "./processScenario.js";
3
+ export * from "./processScenario.js";
4
+ const fileStateSchema = z.object({
5
+ path: z.string(),
6
+ type: z.enum(["file", "directory", "symlink", "other"]),
7
+ mode: z.number().int().nonnegative(),
8
+ size: z.number().int().nonnegative(),
9
+ sha256: z
10
+ .string()
11
+ .regex(/^[a-f0-9]{64}$/u)
12
+ .nullable(),
13
+ symlink_target: z.string().nullable(),
22
14
  });
23
- /** Exact boundary schema for bounded dynamic process scenarios. */
24
- export const processScenarioSchema = z
25
- .object({
26
- approved: z
27
- .literal(true)
28
- .describe("Explicit per-call acknowledgement that this operation launches the target"),
29
- unknown_registry_approved: z
30
- .literal(true)
31
- .optional()
32
- .describe("Explicit approval to record capture residuals durably"),
33
- executable: z.string().startsWith("/"),
34
- arguments: z.array(z.string()).max(256).default([]),
35
- working_directory: z.string().startsWith("/"),
36
- environment: z.record(environmentName, z.string()).default({}),
37
- inherit_environment: z.array(environmentName).max(64).default([]),
38
- secret_aliases: z.array(environmentName).max(64).default([]),
39
- network_access: z.literal("host").default("host"),
40
- filesystem_roots: z.array(z.string().startsWith("/")).max(16).default([]),
41
- events: z
42
- .array(z.discriminatedUnion("type", [
43
- z.object({
44
- ...timedEventBase,
45
- type: z.literal("input"),
46
- data: z.string(),
47
- }),
48
- z.object({
49
- ...timedEventBase,
50
- type: z.literal("resize"),
51
- columns: z.number().int().min(1).max(1_000),
52
- rows: z.number().int().min(1).max(1_000),
53
- }),
54
- z.object({
55
- ...timedEventBase,
56
- type: z.literal("signal"),
57
- signal: z.enum(["SIGINT", "SIGTERM", "SIGKILL"]),
58
- }),
59
- ]))
60
- .max(1_000)
61
- .default([]),
62
- timeout_ms: positiveBudget.max(300_000).default(30_000),
63
- idle_timeout_ms: positiveBudget.max(300_000).default(30_000),
64
- settle_ms: z.number().int().nonnegative().max(10_000).default(100),
65
- limits: z
66
- .object({
67
- output_bytes: positiveBudget.max(10_000_000).default(1_000_000),
68
- frames: positiveBudget.max(100_000).default(10_000),
69
- files: positiveBudget.max(100_000).default(10_000),
70
- file_bytes: positiveBudget.max(100_000_000).default(10_000_000),
71
- processes: positiveBudget.max(10_000).default(1_000),
72
- protocol_events: positiveBudget.max(100_000).default(10_000),
73
- protocol_body_bytes: positiveBudget.max(10_000_000).default(1_000_000),
74
- connections: positiveBudget.max(1_000).default(100),
75
- filesystem_depth: z.number().int().min(1).max(64).default(16),
76
- })
77
- .default({
78
- output_bytes: 1_000_000,
79
- frames: 10_000,
80
- files: 10_000,
81
- file_bytes: 10_000_000,
82
- processes: 1_000,
83
- protocol_events: 10_000,
84
- protocol_body_bytes: 1_000_000,
85
- connections: 100,
86
- filesystem_depth: 16,
87
- }),
88
- normalization: z
89
- .object({
90
- paths: z.boolean().default(true),
91
- pids: z.boolean().default(true),
92
- ports: z.boolean().default(true),
93
- time_bucket_ms: positiveBudget.max(60_000).default(10),
94
- patterns: z
95
- .array(z.object({
96
- pattern: z.string().max(500),
97
- replacement: z.string().max(100),
98
- }))
99
- .max(32)
100
- .default([]),
101
- })
102
- .default({
103
- paths: true,
104
- pids: true,
105
- ports: true,
106
- time_bucket_ms: 10,
107
- patterns: [],
108
- }),
109
- replay: z
110
- .object({
111
- http: z
112
- .array(z.object({
113
- method: z.string().max(16),
114
- path: z.string().startsWith("/"),
115
- status: z.number().int().min(100).max(599),
116
- body: z.string().max(1_000_000),
117
- request_headers: z.record(z.string(), z.string()).default({}),
118
- request_body: z.string().max(1_000_000).optional(),
119
- response_headers: z.record(z.string(), z.string()).default({}),
120
- delay_ms: z.number().int().nonnegative().max(30_000).default(0),
121
- disconnect: z.boolean().default(false),
122
- max_calls: z.number().int().min(1).max(100).default(1),
123
- }))
124
- .max(100)
125
- .default([]),
126
- websocket_messages: z
127
- .array(z.string().max(1_000_000))
128
- .max(100)
129
- .default([]),
130
- websocket_connections: z
131
- .array(z.object({
132
- messages: z
133
- .array(z.object({
134
- data: z.string().max(1_000_000),
135
- delay_ms: z
136
- .number()
137
- .int()
138
- .nonnegative()
139
- .max(30_000)
140
- .default(0),
141
- }))
142
- .max(100),
143
- disconnect_after: z.boolean().default(false),
144
- }))
145
- .max(100)
146
- .default([]),
147
- })
148
- .default({
149
- http: [],
150
- websocket_messages: [],
151
- websocket_connections: [],
152
- }),
153
- })
154
- .strict()
155
- .superRefine((scenario, context) => {
156
- for (let index = 1; index < scenario.events.length; index += 1) {
157
- if (scenario.events[index].at_ms < scenario.events[index - 1].at_ms) {
158
- context.addIssue({
159
- code: "custom",
160
- message: "events must be ordered by at_ms",
161
- path: ["events", index],
162
- });
163
- }
164
- }
165
- const secrets = new Set(scenario.secret_aliases);
166
- for (const alias of secrets) {
167
- if (!(alias in scenario.environment)) {
168
- context.addIssue({
169
- code: "custom",
170
- message: "secret alias has no environment value",
171
- path: ["secret_aliases"],
172
- });
173
- }
174
- }
175
- const explicit = new Set(Object.keys(scenario.environment));
176
- for (const name of [...explicit, ...scenario.inherit_environment]) {
177
- if (reservedEnvironment.has(name)) {
178
- context.addIssue({
179
- code: "custom",
180
- message: `${name} is reserved by the process adapter`,
181
- path: ["environment", name],
182
- });
183
- }
184
- }
185
- for (const name of scenario.inherit_environment) {
186
- if (explicit.has(name)) {
187
- context.addIssue({
188
- code: "custom",
189
- message: "an environment name cannot be explicit and inherited",
190
- path: ["inherit_environment"],
191
- });
192
- }
193
- }
194
- for (const event of scenario.events) {
195
- if (event.at_ms > scenario.timeout_ms) {
196
- context.addIssue({
197
- code: "custom",
198
- message: "event occurs after the scenario timeout",
199
- path: ["events"],
200
- });
201
- }
202
- }
15
+ const fileEffectSchema = z.object({
16
+ path: z.string(),
17
+ status: z.enum(["created", "modified", "deleted", "unchanged"]),
18
+ before: fileStateSchema.nullable(),
19
+ after: fileStateSchema.nullable(),
203
20
  });
204
- /** Parse untrusted process scenario input and apply safe default budgets. */
205
- export const parseProcessScenario = (input) => processScenarioSchema.parse(input);
206
- const isWithin = (candidate, root) => resolve(candidate) === resolve(root) ||
207
- resolve(candidate).startsWith(`${resolve(root)}${resolve(root).endsWith("/") ? "" : "/"}`);
208
- /** Evaluate scenario authority before any process or filesystem side effect occurs. */
209
- export const authorizeProcessScenario = (scenario, policy) => {
210
- if (!policy.enabled)
211
- return { allowed: false, reason: "process capture is disabled" };
212
- if (scenario.network_access === "host" && !policy.allowExternalNetwork)
213
- return {
214
- allowed: false,
215
- reason: "host network access is not approved by operator policy",
216
- };
217
- if (!policy.executableRoots.some((root) => isWithin(scenario.executable, root))) {
218
- return { allowed: false, reason: "executable is outside approved roots" };
219
- }
220
- if (!policy.workingRoots.some((root) => isWithin(scenario.working_directory, root))) {
221
- return {
222
- allowed: false,
223
- reason: "working directory is outside approved roots",
224
- };
225
- }
226
- const requestedNames = [
227
- ...Object.keys(scenario.environment),
228
- ...scenario.inherit_environment,
229
- ];
230
- if (requestedNames.some((name) => !policy.allowedEnvironment.includes(name))) {
231
- return {
232
- allowed: false,
233
- reason: "scenario requests an environment variable not allowed by policy",
234
- };
235
- }
236
- if (scenario.filesystem_roots.some((path) => !policy.workingRoots.some((root) => isWithin(path, root)))) {
237
- return {
238
- allowed: false,
239
- reason: "filesystem root is outside approved roots",
240
- };
241
- }
242
- return { allowed: true };
243
- };
244
21
  /** Exact serialized shape of a bounded process capture. */
245
22
  export const processCaptureSchema = z.object({
246
- schema_version: z.literal(2),
23
+ schema_version: z.literal(4, {
24
+ error: LEGACY_PROCESS_CAPTURE_MESSAGE,
25
+ }),
26
+ manifest: z.object({
27
+ rea_version: z.string().min(1),
28
+ provider_version: z.string().min(1),
29
+ platform: z.string().min(1),
30
+ architecture: z.string().min(1),
31
+ pty_backend: z.literal("node-pty"),
32
+ started_at: z.iso.datetime(),
33
+ completed_at: z.iso.datetime(),
34
+ scenario: z.record(z.string(), z.unknown()),
35
+ comparison_contract: z.record(z.string(), z.unknown()),
36
+ shim_plan: z.array(z.unknown()),
37
+ replay_plan: z.record(z.string(), z.unknown()),
38
+ full_scenario_sha256: z.string().regex(/^[a-f0-9]{64}$/u),
39
+ comparison_contract_sha256: z.string().regex(/^[a-f0-9]{64}$/u),
40
+ executable_sha256: z.string().regex(/^[a-f0-9]{64}$/u),
41
+ normalization_sha256: z.string().regex(/^[a-f0-9]{64}$/u),
42
+ shim_plan_sha256: z.string().regex(/^[a-f0-9]{64}$/u),
43
+ replay_plan_sha256: z.string().regex(/^[a-f0-9]{64}$/u),
44
+ }),
247
45
  normalization: normalizationSchema,
248
46
  frames: z.array(z.object({
249
47
  sequence: z.number().int().nonnegative(),
250
48
  at_ms: z.number().int().nonnegative(),
251
49
  data: z.string(),
252
50
  })),
51
+ rendered_frames: z.array(z.object({
52
+ sequence: z.number().int().nonnegative(),
53
+ at_ms: z.number().int().nonnegative(),
54
+ columns: z.number().int().positive(),
55
+ rows: z.number().int().positive(),
56
+ cursor_x: z.number().int().nonnegative(),
57
+ cursor_y: z.number().int().nonnegative(),
58
+ active_buffer: z.enum(["normal", "alternate"]),
59
+ lines: z.array(z.string()),
60
+ serialized_state: z.string(),
61
+ })),
62
+ interaction_events: z.array(z.object({
63
+ sequence: z.number().int().nonnegative(),
64
+ scheduled_at_ms: z.number().int().nonnegative(),
65
+ dispatched_at_ms: z.number().int().nonnegative(),
66
+ type: z.enum(["input", "resize", "signal"]),
67
+ data: z.string(),
68
+ outcome: z.enum(["dispatched", "target_exited", "failed"]),
69
+ })),
253
70
  exit: z.object({
254
71
  code: z.number().int().nullable(),
255
72
  signal: z.number().int().nullable(),
256
73
  reason: z.enum(["exited", "timeout", "idle_timeout"]),
257
74
  }),
75
+ settlement: z.object({
76
+ state: z.enum(["quiesced", "alive_at_deadline", "unverifiable"]),
77
+ elapsed_ms: z.number().int().nonnegative(),
78
+ cleanup_outcome: z.enum(["not_required", "cleaned", "failed"]),
79
+ }),
258
80
  process_samples: z.array(z.object({
259
81
  at_ms: z.number().int().nonnegative(),
260
82
  pid: z.number().int().positive(),
261
83
  parent_pid: z.number().int().nonnegative(),
262
84
  command: z.string(),
85
+ process_group_id: z.number().int().positive().nullable(),
86
+ session_id: z.number().int().nonnegative().nullable(),
87
+ })),
88
+ filesystem_checkpoints: z.array(z.object({
89
+ name: z.string(),
90
+ at_ms: z.number().int().nonnegative(),
91
+ files: z.array(fileStateSchema),
92
+ effects: z.array(fileEffectSchema),
93
+ truncated: z.boolean(),
94
+ })),
95
+ shim_events: z.array(z.object({
96
+ sequence: z.number().int().nonnegative(),
97
+ at_ms: z.number().int().nonnegative(),
98
+ command: z.string(),
99
+ route_index: z.number().int().nonnegative().nullable(),
100
+ arguments: z.array(z.string()),
101
+ working_directory: z.string(),
102
+ outcome: z.enum(["matched", "unmatched", "exhausted"]),
263
103
  })),
264
104
  protocol_events: z.array(z.object({
265
105
  sequence: z.number().int().nonnegative(),
@@ -276,58 +116,9 @@ export const processCaptureSchema = z.object({
276
116
  "disconnected",
277
117
  ]),
278
118
  })),
279
- files_before: z.array(z.object({
280
- path: z.string(),
281
- type: z.enum(["file", "directory", "symlink", "other"]),
282
- mode: z.number().int().nonnegative(),
283
- size: z.number().int().nonnegative(),
284
- sha256: z
285
- .string()
286
- .regex(/^[a-f0-9]{64}$/u)
287
- .nullable(),
288
- symlink_target: z.string().nullable(),
289
- })),
290
- files_after: z.array(z.object({
291
- path: z.string(),
292
- type: z.enum(["file", "directory", "symlink", "other"]),
293
- mode: z.number().int().nonnegative(),
294
- size: z.number().int().nonnegative(),
295
- sha256: z
296
- .string()
297
- .regex(/^[a-f0-9]{64}$/u)
298
- .nullable(),
299
- symlink_target: z.string().nullable(),
300
- })),
301
- filesystem_effects: z.array(z.object({
302
- path: z.string(),
303
- status: z.enum(["created", "modified", "deleted", "unchanged"]),
304
- before: z
305
- .object({
306
- path: z.string(),
307
- type: z.enum(["file", "directory", "symlink", "other"]),
308
- mode: z.number().int().nonnegative(),
309
- size: z.number().int().nonnegative(),
310
- sha256: z
311
- .string()
312
- .regex(/^[a-f0-9]{64}$/u)
313
- .nullable(),
314
- symlink_target: z.string().nullable(),
315
- })
316
- .nullable(),
317
- after: z
318
- .object({
319
- path: z.string(),
320
- type: z.enum(["file", "directory", "symlink", "other"]),
321
- mode: z.number().int().nonnegative(),
322
- size: z.number().int().nonnegative(),
323
- sha256: z
324
- .string()
325
- .regex(/^[a-f0-9]{64}$/u)
326
- .nullable(),
327
- symlink_target: z.string().nullable(),
328
- })
329
- .nullable(),
330
- })),
119
+ files_before: z.array(fileStateSchema),
120
+ files_after: z.array(fileStateSchema),
121
+ filesystem_effects: z.array(fileEffectSchema),
331
122
  truncated: z.boolean(),
332
123
  limitations: z.array(z.string()),
333
124
  residual_unknowns: z.array(z.object({
@@ -347,4 +138,19 @@ export const processCaptureSchema = z.object({
347
138
  temporary_root: z.literal("removed"),
348
139
  }),
349
140
  });
141
+ export { validateProcessCapture } from "./processCaptureValidation.js";
142
+ import { validateProcessCapture } from "./processCaptureValidation.js";
143
+ /** Parse unknown input as v4 and reject invalid commitments or semantics. */
144
+ export const parseProcessCapture = (input) => {
145
+ if (typeof input === "object" &&
146
+ input !== null &&
147
+ "schema_version" in input &&
148
+ input.schema_version === 3)
149
+ throw new TypeError(LEGACY_PROCESS_CAPTURE_MESSAGE);
150
+ const capture = processCaptureSchema.parse(input);
151
+ const issues = validateProcessCapture(capture);
152
+ if (issues.length > 0)
153
+ throw new TypeError(`Invalid Process Capture v4: ${issues.map(({ path, message }) => `${path}: ${message}`).join("; ")}`);
154
+ return capture;
155
+ };
350
156
  export { compareProcessCaptures, comparisonStatusSchema, processCaptureComparisonSchema, } from "./processComparison.js";
@@ -0,0 +1,124 @@
1
+ import { digestProcessCommitment } from "./processScenario.js";
2
+ const orderedTimestamps = (values) => values.every((value, index) => index === 0 || value.at_ms >= values[index - 1].at_ms);
3
+ const validateCommitments = (capture, require) => {
4
+ const { manifest } = capture;
5
+ require(manifest.scenario.executable_sha256 ===
6
+ manifest.executable_sha256, "manifest.executable_sha256", "executable commitment does not match the scenario projection");
7
+ for (const [field, value] of [
8
+ ["full_scenario_sha256", manifest.scenario],
9
+ ["comparison_contract_sha256", manifest.comparison_contract],
10
+ ["normalization_sha256", capture.normalization],
11
+ ["shim_plan_sha256", manifest.shim_plan],
12
+ ["replay_plan_sha256", manifest.replay_plan],
13
+ ])
14
+ require(manifest[field] ===
15
+ digestProcessCommitment(value), `manifest.${field}`, "commitment does not match its canonical value");
16
+ require(Date.parse(manifest.started_at) <=
17
+ Date.parse(manifest.completed_at), "manifest.completed_at", "completion precedes start");
18
+ };
19
+ const validateOrdering = (capture, require) => {
20
+ for (const [name, values] of [
21
+ ["frames", capture.frames],
22
+ ["rendered_frames", capture.rendered_frames],
23
+ ["interaction_events", capture.interaction_events],
24
+ ["shim_events", capture.shim_events],
25
+ ["protocol_events", capture.protocol_events],
26
+ ])
27
+ require(values.every(({ sequence }, index) => sequence === index), name, "sequence values must be contiguous from zero");
28
+ for (const [name, values] of [
29
+ ["frames", capture.frames],
30
+ ["rendered_frames", capture.rendered_frames],
31
+ ["process_samples", capture.process_samples],
32
+ ["filesystem_checkpoints", capture.filesystem_checkpoints],
33
+ ["shim_events", capture.shim_events],
34
+ ["protocol_events", capture.protocol_events],
35
+ ])
36
+ require(orderedTimestamps(values), name, "timestamps must be ordered");
37
+ };
38
+ const validateLifecycle = (capture, require) => {
39
+ const checkpointNames = capture.filesystem_checkpoints.map(({ name }) => name);
40
+ require(new Set(checkpointNames).size ===
41
+ checkpointNames.length, "filesystem_checkpoints", "checkpoint names must be unique");
42
+ require(checkpointNames[0] ===
43
+ "before", "filesystem_checkpoints", "first checkpoint must be before");
44
+ require(checkpointNames.at(-1) ===
45
+ "after_settlement", "filesystem_checkpoints", "last checkpoint must be after_settlement");
46
+ require(!capture.filesystem_checkpoints.some(({ truncated }) => truncated) ||
47
+ capture.truncated, "truncated", "checkpoint truncation must propagate to the capture");
48
+ require(capture.exit.reason === "exited" ||
49
+ capture.exit.code ===
50
+ null, "exit", "deadline termination cannot declare a normal exit code");
51
+ require(capture.settlement.state !== "quiesced" ||
52
+ capture.settlement.cleanup_outcome ===
53
+ "not_required", "settlement.cleanup_outcome", "quiesced settlement cannot require cleanup");
54
+ require(capture.settlement.state === "quiesced" ||
55
+ capture.settlement.cleanup_outcome !==
56
+ "not_required", "settlement.cleanup_outcome", "non-quiesced settlement must report cleanup");
57
+ };
58
+ const shimPlans = (capture) => capture.manifest.shim_plan.flatMap((shim) => typeof shim === "object" &&
59
+ shim !== null &&
60
+ "name" in shim &&
61
+ "routes" in shim &&
62
+ typeof shim.name === "string" &&
63
+ Array.isArray(shim.routes)
64
+ ? shim.routes.map((route, routeIndex) => ({
65
+ command: shim.name,
66
+ route,
67
+ routeIndex,
68
+ }))
69
+ : []);
70
+ const validateShimEvents = (capture, require) => {
71
+ const plans = shimPlans(capture);
72
+ for (const event of capture.shim_events) {
73
+ const declared = plans.some(({ command, routeIndex }) => command === event.command && routeIndex === event.route_index);
74
+ require(event.outcome === "unmatched"
75
+ ? event.route_index === null
76
+ : declared, "shim_events", "shim event has no declared route");
77
+ }
78
+ for (const plan of plans) {
79
+ const maximum = typeof plan.route === "object" &&
80
+ plan.route !== null &&
81
+ "max_calls" in plan.route &&
82
+ typeof plan.route.max_calls === "number"
83
+ ? plan.route.max_calls
84
+ : 0;
85
+ const matches = capture.shim_events.filter((event) => event.command === plan.command &&
86
+ event.route_index === plan.routeIndex &&
87
+ event.outcome === "matched").length;
88
+ require(matches <=
89
+ maximum, "shim_events", "matched shim events exceed the declared route max_calls");
90
+ }
91
+ };
92
+ const validateReplayEvents = (capture, require) => {
93
+ const { replay_plan: replayPlan } = capture.manifest;
94
+ const routes = "http" in replayPlan && Array.isArray(replayPlan.http)
95
+ ? replayPlan.http
96
+ : [];
97
+ for (const event of capture.protocol_events) {
98
+ if (event.protocol !== "http" ||
99
+ event.direction !== "request" ||
100
+ event.outcome !== "matched")
101
+ continue;
102
+ const declared = routes.some((route) => typeof route === "object" &&
103
+ route !== null &&
104
+ "method" in route &&
105
+ route.method === event.method &&
106
+ "path" in route &&
107
+ route.path === event.path);
108
+ require(declared, "protocol_events", "matched HTTP event has no declared replay route");
109
+ }
110
+ };
111
+ /** Recompute v4 commitments and cross-field invariants without side effects. */
112
+ export const validateProcessCapture = (capture) => {
113
+ const issues = [];
114
+ const require = (condition, path, message) => {
115
+ if (!condition)
116
+ issues.push({ path, message });
117
+ };
118
+ validateCommitments(capture, require);
119
+ validateOrdering(capture, require);
120
+ validateLifecycle(capture, require);
121
+ validateShimEvents(capture, require);
122
+ validateReplayEvents(capture, require);
123
+ return issues;
124
+ };