rea-agents 2.1.0 → 2.3.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 (193) hide show
  1. package/README.md +84 -18
  2. package/dist/application/AndroidApplicationService.js +17 -0
  3. package/dist/application/AppleApplicationService.js +17 -0
  4. package/dist/application/ApplicationWorkflowEvidenceResolver.js +162 -0
  5. package/dist/application/BinarySession.js +9 -2
  6. package/dist/application/BinarySessionStatus.js +39 -1
  7. package/dist/application/CapabilityInventory.js +15 -0
  8. package/dist/application/ClientRegistrationStatus.js +11 -4
  9. package/dist/application/CommandShimReplay.js +6 -3
  10. package/dist/application/CompositeProvider.js +5 -2
  11. package/dist/application/DirectAnalysis.js +1 -20
  12. package/dist/application/DirectAnalysisStatus.js +83 -0
  13. package/dist/application/Doctor.js +34 -25
  14. package/dist/application/DoctorDiagnostics.js +1 -4
  15. package/dist/application/DoctorProjection.js +43 -0
  16. package/dist/application/DoctorScope.js +102 -0
  17. package/dist/application/EvidenceReferenceResolver.js +26 -0
  18. package/dist/application/InventoryProjectionEvidence.js +41 -0
  19. package/dist/application/InvestigationProviders.js +12 -0
  20. package/dist/application/JavaScriptApplicationEvidence.js +1 -1
  21. package/dist/application/JavaScriptApplicationEvidenceGraph.js +10 -1
  22. package/dist/application/JavaScriptApplicationService.js +3 -2
  23. package/dist/application/JavaScriptApplicationWorkflowEvidence.js +26 -0
  24. package/dist/application/JavaScriptApplicationWorkflowService.js +49 -1
  25. package/dist/application/JavaScriptArtifactReconstruction.js +7 -0
  26. package/dist/application/JavaScriptModuleRelationships.js +25 -1
  27. package/dist/application/JavaScriptReturnShapeProjection.js +111 -0
  28. package/dist/application/JavaScriptSemanticGraphBuilder.js +294 -0
  29. package/dist/application/JavaScriptSemanticGraphConstruction.js +144 -0
  30. package/dist/application/JavaScriptSemanticGraphEvidence.js +86 -0
  31. package/dist/application/JavaScriptSemanticGraphFlowProjection.js +72 -0
  32. package/dist/application/JavaScriptSemanticGraphProjection.js +33 -0
  33. package/dist/application/JavaScriptSemanticTraceService.js +45 -0
  34. package/dist/application/LoopbackReplay.js +277 -133
  35. package/dist/application/LoopbackReplayRecorder.js +101 -0
  36. package/dist/application/PermissionAuthority.js +7 -3
  37. package/dist/application/ProcessCaptureJournal.js +95 -0
  38. package/dist/application/ProcessCaptureLifecycle.js +24 -13
  39. package/dist/application/ProcessCheckpoints.js +30 -4
  40. package/dist/application/ProcessCli.js +26 -4
  41. package/dist/application/ProcessHarness.js +20 -50
  42. package/dist/application/ProcessNormalization.js +3 -2
  43. package/dist/application/ProcessPairedExperiment.js +72 -0
  44. package/dist/application/ProcessReactiveCoordinator.js +218 -0
  45. package/dist/application/ProcessReactiveEffects.js +133 -0
  46. package/dist/application/ProcessReactiveObservations.js +20 -0
  47. package/dist/application/ProcessSampling.js +2 -0
  48. package/dist/application/SessionProviderRouter.js +7 -6
  49. package/dist/application/Setup.js +14 -13
  50. package/dist/application/SetupClientConfiguration.js +10 -6
  51. package/dist/application/SetupPlan.js +4 -3
  52. package/dist/application/SetupRegistrationEnvironment.js +12 -0
  53. package/dist/application/SetupSkill.js +44 -40
  54. package/dist/application/TerminalRenderer.js +3 -1
  55. package/dist/application/Uninstall.js +7 -1
  56. package/dist/application/Upgrade.js +16 -1
  57. package/dist/browser/CdpEndpoint.js +37 -4
  58. package/dist/catalogIdentity.js +16 -7
  59. package/dist/cli/coreAnalysisCommands.js +34 -2
  60. package/dist/cli/javascriptApplicationAnalysis.js +19 -0
  61. package/dist/cli/setupCommands.js +84 -30
  62. package/dist/cli/utilityCommands.js +10 -2
  63. package/dist/cliApplicationCommands.js +79 -16
  64. package/dist/cliCommandNames.js +3 -0
  65. package/dist/cliElectronCommands.js +23 -33
  66. package/dist/cliObservationOptions.js +1 -1
  67. package/dist/cliOutput.js +56 -0
  68. package/dist/cliProcessCommands.js +32 -1
  69. package/dist/cliSetup.js +55 -29
  70. package/dist/contracts/applicationToolContracts.js +54 -7
  71. package/dist/contracts/applicationWorkflowInputContracts.js +119 -0
  72. package/dist/contracts/electronToolContracts.js +73 -4
  73. package/dist/contracts/javascriptApplicationWorkflowExamples.js +32 -9
  74. package/dist/contracts/managedToolContracts.js +7 -2
  75. package/dist/contracts/processCaptureExample.js +1 -0
  76. package/dist/contracts/replayMachineExample.js +37 -0
  77. package/dist/contracts/sessionStatusContract.js +26 -0
  78. package/dist/contracts/toolContractExamples.js +2 -0
  79. package/dist/contracts/toolContractTypes.js +12 -1
  80. package/dist/contracts/toolContracts.js +8 -11
  81. package/dist/contracts/toolEffects.js +3 -0
  82. package/dist/contracts/toolOutputSchemaGroups.js +53 -9
  83. package/dist/contracts/toolOutputSchemaPrimitives.js +73 -32
  84. package/dist/doctorRuntime.js +41 -0
  85. package/dist/domain/analysisErrorPresentation.js +5 -3
  86. package/dist/domain/analysisErrorProjection.js +7 -1
  87. package/dist/domain/androidApplication.js +206 -0
  88. package/dist/domain/appleApplication.js +221 -0
  89. package/dist/domain/boundedCartesianProjection.js +15 -0
  90. package/dist/domain/changedBehavior.js +5 -4
  91. package/dist/domain/completionLedgerGeneration.js +190 -0
  92. package/dist/domain/errors.js +4 -2
  93. package/dist/domain/evidenceCompletionLedger.js +113 -0
  94. package/dist/domain/hopperStartupFailure.js +9 -0
  95. package/dist/domain/javascriptApplicationAnalysis.js +44 -2
  96. package/dist/domain/javascriptExportShapeComparison.js +145 -0
  97. package/dist/domain/javascriptExportShapeComparisonIdentity.js +11 -0
  98. package/dist/domain/javascriptExportShapeComparisonSchemas.js +277 -0
  99. package/dist/domain/javascriptExportShapeSelection.js +132 -0
  100. package/dist/domain/javascriptExportShapeVariants.js +272 -0
  101. package/dist/domain/javascriptRuntimeReconciliationParsing.js +2 -2
  102. package/dist/domain/javascriptSemanticAnalysis.js +17 -36
  103. package/dist/domain/javascriptSemanticCallResolution.js +129 -0
  104. package/dist/domain/javascriptSemanticCalls.js +269 -0
  105. package/dist/domain/javascriptSemanticGraph.js +356 -0
  106. package/dist/domain/javascriptSemanticGraphSchemas.js +329 -0
  107. package/dist/domain/javascriptSemanticGraphSerialization.js +23 -0
  108. package/dist/domain/javascriptSemanticIr.js +14 -1
  109. package/dist/domain/javascriptSemanticPrimitives.js +36 -0
  110. package/dist/domain/javascriptSemanticProjection.js +36 -4
  111. package/dist/domain/javascriptSemanticProvenance.js +29 -0
  112. package/dist/domain/javascriptSemanticQuery.js +294 -0
  113. package/dist/domain/javascriptSemanticQueryAssessment.js +74 -0
  114. package/dist/domain/javascriptSemanticQueryIdentity.js +86 -0
  115. package/dist/domain/javascriptSemanticQueryRelations.js +15 -0
  116. package/dist/domain/javascriptSemanticQuerySchemas.js +188 -0
  117. package/dist/domain/javascriptSemanticReturns.js +187 -0
  118. package/dist/domain/javascriptSemanticState.js +32 -0
  119. package/dist/domain/javascriptSemanticTraceSchemas.js +14 -0
  120. package/dist/domain/javascriptSemanticValues.js +54 -70
  121. package/dist/domain/processCapture.js +36 -0
  122. package/dist/domain/processCaptureValidation.js +101 -12
  123. package/dist/domain/processComparison.js +96 -24
  124. package/dist/domain/processObservation.js +153 -0
  125. package/dist/domain/processPairedExperiment.js +128 -0
  126. package/dist/domain/processReactiveCheckpointDataflow.js +32 -0
  127. package/dist/domain/processReactiveMatching.js +210 -0
  128. package/dist/domain/processReactiveRuntime.js +109 -0
  129. package/dist/domain/processReactiveScenario.js +361 -0
  130. package/dist/domain/processReactiveScenarioPreflight.js +120 -0
  131. package/dist/domain/processReactiveTransition.js +159 -0
  132. package/dist/domain/processScenario.js +12 -0
  133. package/dist/domain/processTraceComparison.js +43 -0
  134. package/dist/domain/processTraceDimensionProjection.js +50 -0
  135. package/dist/domain/processTraceEvaluation.js +312 -0
  136. package/dist/domain/processTraceSpecification.js +373 -0
  137. package/dist/domain/reconstructionVerification.js +5 -4
  138. package/dist/domain/replayMachine.js +332 -0
  139. package/dist/domain/replayMachineRun.js +216 -0
  140. package/dist/domain/replayMachineRuntime.js +294 -0
  141. package/dist/domain/runtimeIdentification.js +210 -0
  142. package/dist/domain/staticRuntimeCorrelation.js +4 -2
  143. package/dist/evaluation/CodexAgentEval.js +156 -0
  144. package/dist/generatedPackageMetadata.js +2 -2
  145. package/dist/ghidra/GhidraClient.js +13 -2
  146. package/dist/ghidra/GhidraDiagnostics.js +1 -0
  147. package/dist/ghidra/GhidraLauncher.js +7 -3
  148. package/dist/ghidra/GhidraProvider.js +8 -1
  149. package/dist/hopper/BridgeLauncher.js +56 -14
  150. package/dist/hopper/HopperClient.js +23 -10
  151. package/dist/hopper/HopperDiagnostics.js +14 -0
  152. package/dist/hopper/HopperProcessDiagnostic.js +14 -0
  153. package/dist/hopper/HopperProvider.js +8 -1
  154. package/dist/hopper/LinuxPrivateDisplayDiagnostic.js +72 -0
  155. package/dist/hopper/LinuxPrivateDisplayProbe.js +217 -0
  156. package/dist/identity.js +2 -1
  157. package/dist/main/transport.js +2 -0
  158. package/dist/mcpDoctor.js +310 -0
  159. package/dist/process/ProcessOwnership.js +200 -53
  160. package/dist/process/ProviderRunLineage.js +21 -0
  161. package/dist/server/createServer.js +8 -2
  162. package/dist/server/javascriptApplicationResult.js +106 -0
  163. package/dist/server/registerApplicationTools/characterization.js +3 -3
  164. package/dist/server/registerApplicationTools/compareExportShapes.js +37 -0
  165. package/dist/server/registerApplicationTools/compareVersions.js +12 -6
  166. package/dist/server/registerApplicationTools/controlledReplay.js +2 -2
  167. package/dist/server/registerApplicationTools/coverage.js +3 -3
  168. package/dist/server/registerApplicationTools/helpers.js +37 -25
  169. package/dist/server/registerApplicationTools/traceFeature.js +9 -5
  170. package/dist/server/registerApplicationTools/traceSemantics.js +30 -0
  171. package/dist/server/registerApplicationTools.js +4 -0
  172. package/dist/server/registerElectronTools.js +19 -5
  173. package/dist/server/registerEvidenceResources.js +7 -1
  174. package/dist/server/registerJavaScriptApplicationGraphResource.js +69 -0
  175. package/dist/server/registerManagedTools.js +18 -1
  176. package/dist/server/registerProcessComparisonTool.js +17 -7
  177. package/dist/server/registerReplayMachineTool.js +20 -0
  178. package/dist/server/registerSessionStatusTool.js +95 -25
  179. package/dist/server/registerSessionTools.js +12 -7
  180. package/dist/server/sessionAvailabilityPolicy.js +2 -1
  181. package/dist/server/toolResult.js +10 -4
  182. package/dist/serverIdentity.js +19 -0
  183. package/package.json +62 -29
  184. package/scripts/hopper-demo-x11.py +459 -61
  185. package/scripts/prepack.mjs +55 -0
  186. package/scripts/prepare.mjs +28 -0
  187. package/scripts/rea.mjs +26 -5
  188. package/skills/reverse-engineer-anything/SKILL.md +69 -228
  189. package/skills/reverse-engineer-anything/references/controlled-replay.md +12 -0
  190. package/skills/reverse-engineer-anything/references/evidence-workflows.md +33 -0
  191. package/skills/reverse-engineer-anything/references/javascript-applications.md +35 -0
  192. package/skills/reverse-engineer-anything/references/native-and-artifacts.md +36 -0
  193. package/skills/reverse-engineer-anything/references/runtime-observation.md +24 -0
@@ -0,0 +1,95 @@
1
+ import { normalizeProcessElapsedTime, normalizeProcessText, } from "./ProcessNormalization.js";
2
+ /** Create one monotonic event journal for a process capture. */
3
+ export const createProcessCaptureJournal = () => {
4
+ const entries = [];
5
+ const listeners = new Set();
6
+ const notifications = [];
7
+ let delivering = false;
8
+ const deliverNotifications = () => {
9
+ if (delivering)
10
+ return;
11
+ delivering = true;
12
+ const failures = [];
13
+ try {
14
+ for (;;) {
15
+ const notification = notifications.shift();
16
+ if (notification === undefined)
17
+ break;
18
+ for (const listener of Array.from(listeners))
19
+ try {
20
+ listener(notification);
21
+ }
22
+ catch (cause) {
23
+ failures.push(cause);
24
+ }
25
+ }
26
+ }
27
+ finally {
28
+ delivering = false;
29
+ }
30
+ if (failures.length > 0)
31
+ throw new AggregateError(failures, "process capture journal subscriber failed");
32
+ };
33
+ const record = (collection, index) => {
34
+ const entry = { capture_order: entries.length, collection, index };
35
+ entries.push(entry);
36
+ notifications.push(entry);
37
+ deliverNotifications();
38
+ return entry;
39
+ };
40
+ return {
41
+ entries,
42
+ record,
43
+ recordEvent: (collection, index) => void record(collection, index),
44
+ subscribe: (listener) => {
45
+ listeners.add(listener);
46
+ return () => listeners.delete(listener);
47
+ },
48
+ };
49
+ };
50
+ /** Schedule scenario interactions and record each observed dispatch atomically. */
51
+ export const scheduleScenarioInteractions = (options) => {
52
+ const { scenario, getTerminal, timers, interactions, renderer, started } = options;
53
+ for (const [eventIndex, event] of scenario.events.entries()) {
54
+ const timer = setTimeout(() => {
55
+ options.dispatchedEventIndexes.add(eventIndex);
56
+ const terminal = getTerminal();
57
+ const dispatchedAt = Math.max(0, Date.now() - started);
58
+ let outcome = "dispatched";
59
+ if (terminal === undefined)
60
+ outcome = "target_exited";
61
+ else {
62
+ try {
63
+ if (event.type === "input")
64
+ terminal.write(event.data);
65
+ else if (event.type === "resize") {
66
+ terminal.resize(event.columns, event.rows);
67
+ renderer.resize(event.columns, event.rows, normalizeProcessElapsedTime(dispatchedAt, scenario.normalization.time_bucket_ms));
68
+ }
69
+ else
70
+ terminal.kill(event.signal);
71
+ }
72
+ catch {
73
+ outcome = "failed";
74
+ }
75
+ }
76
+ const sequence = interactions.length;
77
+ interactions.push({
78
+ sequence,
79
+ scheduled_at_ms: event.at_ms,
80
+ dispatched_at_ms: dispatchedAt,
81
+ type: event.type,
82
+ data: event.type === "input"
83
+ ? event.sensitive
84
+ ? `<redacted-input:${String(Buffer.byteLength(event.data))}-bytes>`
85
+ : normalizeProcessText(event.data, scenario, "<no-temporary-root>", -1)
86
+ : event.type === "resize"
87
+ ? `${String(event.columns)}x${String(event.rows)}`
88
+ : event.signal,
89
+ outcome,
90
+ });
91
+ options.recordEvent("interaction_events", sequence);
92
+ }, event.at_ms);
93
+ timers.add(timer);
94
+ }
95
+ };
@@ -10,7 +10,7 @@ import { cleanupOwnedProcessGroup, observeOwnedProcessGroup, } from "../process/
10
10
  import { classifyFilesystemEffects, ProcessCheckpoints, } from "./ProcessCheckpoints.js";
11
11
  import { ProcessCaptureError } from "./ProcessCaptureError.js";
12
12
  import { assertNotCancelled, assertRealPathAuthority, } from "./ProcessCaptureAuthority.js";
13
- import { normalizeProcessSamples, normalizeProcessText, redactProtocolEvents, } from "./ProcessNormalization.js";
13
+ import { normalizeProcessElapsedTime, normalizeProcessSamples, normalizeProcessText, redactProtocolEvents, } from "./ProcessNormalization.js";
14
14
  import { PROCESS_PROVIDER } from "./ProcessEvidence.js";
15
15
  import { TerminalRenderer } from "./TerminalRenderer.js";
16
16
  export const buildCaptureResult = (options) => ({
@@ -18,9 +18,7 @@ export const buildCaptureResult = (options) => ({
18
18
  manifest: options.manifest,
19
19
  normalization: options.scenario.normalization,
20
20
  frames: options.frames,
21
- rendered_frames: [...options.renderedFrames]
22
- .sort((left, right) => left.at_ms - right.at_ms || left.sequence - right.sequence)
23
- .map((frame, sequence) => ({ ...frame, sequence })),
21
+ rendered_frames: options.renderedFrames,
24
22
  interaction_events: options.interactions,
25
23
  exit: {
26
24
  code: options.exit.reason === "exited" && options.exit.exitCode >= 0
@@ -34,6 +32,8 @@ export const buildCaptureResult = (options) => ({
34
32
  filesystem_checkpoints: options.checkpoints,
35
33
  shim_events: options.shimEvents,
36
34
  protocol_events: redactProtocolEvents(options.replay.events, options.scenario),
35
+ replay_transitions: options.replay.transitions,
36
+ event_journal: options.eventJournal,
37
37
  files_before: options.before.files,
38
38
  files_after: options.after.files,
39
39
  filesystem_effects: classifyFilesystemEffects(options.before.files, options.after.files),
@@ -91,9 +91,11 @@ export const createRunManifest = async (scenario, startedAt, completedAt) => {
91
91
  replay_plan_sha256: digestProcessCommitment(scenario.replay),
92
92
  };
93
93
  };
94
- export const observeSettlement = async (runId, processGroupIds, settleMs) => {
95
- if (process.platform === "win32")
94
+ export const observeSettlement = async (runId, processGroupIds, settleMs, recordEvent = () => undefined) => {
95
+ if (process.platform === "win32") {
96
+ recordEvent("lifecycle", 1);
96
97
  return { state: "unverifiable", elapsed_ms: 0 };
98
+ }
97
99
  const started = Date.now();
98
100
  let consecutiveEmpty = 0;
99
101
  let deadlineReached = false;
@@ -103,12 +105,16 @@ export const observeSettlement = async (runId, processGroupIds, settleMs) => {
103
105
  leaderPid: processGroupId,
104
106
  processGroupId,
105
107
  })));
106
- if (observations.some(({ state }) => state === "unverifiable"))
108
+ if (observations.some(({ state }) => state === "unverifiable")) {
109
+ recordEvent("lifecycle", 1);
107
110
  return { state: "unverifiable", elapsed_ms: Date.now() - started };
111
+ }
108
112
  if (observations.every(({ state }) => state === "empty")) {
109
113
  consecutiveEmpty += 1;
110
- if (consecutiveEmpty >= 2)
114
+ if (consecutiveEmpty >= 2) {
115
+ recordEvent("lifecycle", 1);
111
116
  return { state: "quiesced", elapsed_ms: Date.now() - started };
117
+ }
112
118
  }
113
119
  else
114
120
  consecutiveEmpty = 0;
@@ -117,19 +123,22 @@ export const observeSettlement = async (runId, processGroupIds, settleMs) => {
117
123
  break;
118
124
  await new Promise((resolve) => setTimeout(resolve, 50));
119
125
  }
126
+ recordEvent("lifecycle", 1);
120
127
  return { state: "alive_at_deadline", elapsed_ms: Date.now() - started };
121
128
  };
122
- export const awaitTerminalExit = async ({ terminal, scenario, started, lastOutput, signal, timers, interactions, dispatchedEventIndexes, }) => new Promise((resolveExit) => {
129
+ export const awaitTerminalExit = async ({ terminal, scenario, started, lastOutput, signal, timers, interactions, dispatchedEventIndexes, recordEvent, }) => new Promise((resolveExit) => {
123
130
  // The kill caused by a deadline is observed later as an ordinary PTY exit.
124
131
  // Keep the initiating lifecycle reason so comparisons distinguish a target
125
132
  // exit from harness-owned timeout, idle-timeout, and cancellation cleanup.
126
133
  let reason = "exited";
127
134
  terminal.onExit((exit) => {
135
+ recordEvent("lifecycle", 0);
128
136
  for (const [eventIndex, event] of scenario.events.entries()) {
129
137
  if (dispatchedEventIndexes.has(eventIndex))
130
138
  continue;
139
+ const sequence = interactions.length;
131
140
  interactions.push({
132
- sequence: interactions.length,
141
+ sequence,
133
142
  scheduled_at_ms: event.at_ms,
134
143
  dispatched_at_ms: Math.max(0, Date.now() - started),
135
144
  type: event.type,
@@ -140,6 +149,7 @@ export const awaitTerminalExit = async ({ terminal, scenario, started, lastOutpu
140
149
  : event.signal,
141
150
  outcome: "target_exited",
142
151
  });
152
+ recordEvent("interaction_events", sequence);
143
153
  }
144
154
  for (const timer of timers) {
145
155
  clearTimeout(timer);
@@ -223,14 +233,15 @@ export const captureTerminalFrames = (options) => {
223
233
  return;
224
234
  }
225
235
  outputBytes += bytes;
226
- const atMs = Math.floor((Date.now() - options.started) /
227
- options.scenario.normalization.time_bucket_ms) * options.scenario.normalization.time_bucket_ms;
236
+ const atMs = normalizeProcessElapsedTime(Date.now() - options.started, options.scenario.normalization.time_bucket_ms);
228
237
  const normalized = normalizeProcessText(data, options.scenario, options.temporaryRoot, options.terminal.pid);
238
+ const sequence = options.frames.length;
229
239
  options.frames.push({
230
- sequence: options.frames.length,
240
+ sequence,
231
241
  at_ms: atMs,
232
242
  data: normalized,
233
243
  });
244
+ options.recordEvent("frames", sequence);
234
245
  options.renderer.write(data, atMs);
235
246
  options.checkpoints.observeTerminal(normalized);
236
247
  });
@@ -22,17 +22,19 @@ export const classifyFilesystemEffects = (before, after) => {
22
22
  export class ProcessCheckpoints {
23
23
  scenario;
24
24
  started;
25
- signal;
26
25
  #captures = [];
27
26
  #captured = new Set();
28
27
  #terminalCounts = new Map();
29
28
  #terminalTails = new Map();
30
29
  #timers = new Set();
30
+ #signal;
31
+ #recordEvent;
31
32
  #pending = Promise.resolve();
32
- constructor(scenario, started, before, signal) {
33
+ constructor(scenario, started, before, options = { signal: undefined }) {
33
34
  this.scenario = scenario;
34
35
  this.started = started;
35
- this.signal = signal;
36
+ this.#signal = options.signal;
37
+ this.#recordEvent = options.recordEvent ?? (() => undefined);
36
38
  this.#captures.push({
37
39
  name: "before",
38
40
  at_ms: 0,
@@ -40,6 +42,7 @@ export class ProcessCheckpoints {
40
42
  effects: [],
41
43
  truncated: before.truncated,
42
44
  });
45
+ this.#recordEvent("filesystem_checkpoints", 0);
43
46
  this.#captured.add("before");
44
47
  for (const checkpoint of scenario.checkpoints) {
45
48
  if (checkpoint.trigger.type !== "time")
@@ -74,6 +77,9 @@ export class ProcessCheckpoints {
74
77
  }
75
78
  /** Queue one idempotent named snapshot. */
76
79
  capture(name) {
80
+ this.#scheduleCapture(name, this.#signal);
81
+ }
82
+ #scheduleCapture(name, signal) {
77
83
  if (this.#captured.has(name))
78
84
  return;
79
85
  this.#captured.add(name);
@@ -81,8 +87,9 @@ export class ProcessCheckpoints {
81
87
  // together. Each effect set is intentionally relative to the immediately
82
88
  // preceding checkpoint, which preserves transient create/delete behavior.
83
89
  this.#pending = this.#pending.then(async () => {
84
- const snapshot = await snapshotRoots(this.scenario, this.signal);
90
+ const snapshot = await snapshotRoots(this.scenario, signal);
85
91
  const previous = this.#captures.at(-1)?.files ?? [];
92
+ const index = this.#captures.length;
86
93
  this.#captures.push({
87
94
  name,
88
95
  at_ms: Math.max(0, Date.now() - this.started),
@@ -90,14 +97,32 @@ export class ProcessCheckpoints {
90
97
  effects: classifyFilesystemEffects(previous, snapshot.files),
91
98
  truncated: snapshot.truncated,
92
99
  });
100
+ this.#recordEvent("filesystem_checkpoints", index);
93
101
  });
94
102
  }
103
+ /** Queue one named snapshot and return its exact captured record. */
104
+ async captureAndRead(name, signal) {
105
+ const captureSignal = signal === undefined
106
+ ? this.#signal
107
+ : this.#signal === undefined
108
+ ? signal
109
+ : AbortSignal.any([this.#signal, signal]);
110
+ this.#scheduleCapture(name, captureSignal);
111
+ const scheduled = this.#pending;
112
+ await scheduled;
113
+ const index = this.#captures.findIndex(({ name: capturedName }) => capturedName === name);
114
+ const checkpoint = this.#captures[index];
115
+ if (index < 0 || checkpoint === undefined)
116
+ throw new Error(`filesystem checkpoint was not captured: ${name}`);
117
+ return { index, checkpoint: structuredClone(checkpoint) };
118
+ }
95
119
  /** Finish pending snapshots and return them in observation order. */
96
120
  async finish(after) {
97
121
  for (const timer of this.#timers)
98
122
  clearTimeout(timer);
99
123
  await this.#pending;
100
124
  const previous = this.#captures.at(-1)?.files ?? [];
125
+ const index = this.#captures.length;
101
126
  this.#captures.push({
102
127
  name: "after_settlement",
103
128
  at_ms: Math.max(0, Date.now() - this.started),
@@ -105,6 +130,7 @@ export class ProcessCheckpoints {
105
130
  effects: classifyFilesystemEffects(previous, after.files),
106
131
  truncated: after.truncated,
107
132
  });
133
+ this.#recordEvent("filesystem_checkpoints", index);
108
134
  return structuredClone(this.#captures);
109
135
  }
110
136
  /** Cancel scheduled captures and await any snapshot already in progress. */
@@ -1,8 +1,10 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { parseConfig } from "../config.js";
3
- import { AnalysisError, AnalysisProtocolError, PermissionRequiredError, projectAnalysisError, } from "../domain/errors.js";
3
+ import { AnalysisError, AnalysisInputError, AnalysisProtocolError, PermissionRequiredError, projectAnalysisError, } from "../domain/errors.js";
4
4
  import { createEvidence, parseEvidence } from "../domain/evidence.js";
5
5
  import { jsonValueSchema } from "../domain/jsonValue.js";
6
+ import { projectInputIssues } from "../domain/inputIssueProjection.js";
7
+ import { processTraceSpecificationSchema } from "../domain/processTraceComparison.js";
6
8
  import { compareProcessCaptures, LEGACY_PROCESS_CAPTURE_MESSAGE, parseProcessScenario, parseProcessCapture, } from "../domain/processCapture.js";
7
9
  import { captureProcessScenario } from "./ProcessHarness.js";
8
10
  import { processCapturePermissionRequest } from "./ProcessCapturePermission.js";
@@ -43,24 +45,37 @@ export const captureProcessScenarioFile = async (path) => {
43
45
  }
44
46
  };
45
47
  /** Compare two capture Evidence files and emit derived comparison Evidence. */
46
- export const compareProcessEvidenceFiles = async (leftPath, rightPath) => {
48
+ export const compareProcessEvidenceFiles = async (leftPath, rightPath, traceSpecPath) => {
47
49
  try {
48
50
  const left = parseCaptureEvidence(await readJson(leftPath));
49
51
  const right = parseCaptureEvidence(await readJson(rightPath));
50
- const comparison = compareProcessCaptures(left.capture, right.capture);
52
+ const traceSpecification = traceSpecPath === undefined
53
+ ? undefined
54
+ : parseTraceSpecification(await readJson(traceSpecPath));
55
+ const comparison = traceSpecification === undefined
56
+ ? compareProcessCaptures(left.capture, right.capture)
57
+ : compareProcessCaptures(left.capture, right.capture, {
58
+ traceSpecification,
59
+ });
51
60
  return createEvidence(undefined, PROCESS_PROVIDER, {
52
- predicateType: "rea.process-comparison/v3",
61
+ predicateType: traceSpecification === undefined
62
+ ? "rea.process-comparison/v3"
63
+ : "rea.process-comparison/v4",
53
64
  operation: "compare_process_captures",
54
65
  parameters: {
55
66
  left_evidence_id: left.id,
56
67
  right_evidence_id: right.id,
57
68
  left_normalization: left.capture.normalization,
58
69
  right_normalization: right.capture.normalization,
70
+ ...(traceSpecification === undefined
71
+ ? {}
72
+ : { trace_spec: jsonValueSchema.parse(traceSpecification) }),
59
73
  },
60
74
  result: jsonValueSchema.parse(comparison),
61
75
  confidence: "derived",
62
76
  authority: "analyst-inference",
63
77
  limitations: comparison.limitations,
78
+ locations: [...left.locations, ...right.locations],
64
79
  evidenceLinks: [left.id, right.id],
65
80
  });
66
81
  }
@@ -88,12 +103,19 @@ const parseCaptureEvidence = (input) => {
88
103
  return {
89
104
  id: evidence.evidence_id,
90
105
  capture: parseProcessCapture(evidence.normalized_result),
106
+ locations: evidence.locations,
91
107
  };
92
108
  }
93
109
  catch {
94
110
  throw invalidCaptureEvidence();
95
111
  }
96
112
  };
113
+ const parseTraceSpecification = (input) => {
114
+ const parsed = processTraceSpecificationSchema.safeParse(input);
115
+ if (parsed.success)
116
+ return parsed.data;
117
+ throw new AnalysisInputError("compare-process-captures", { cause: parsed.error }, projectInputIssues(parsed.error.issues, input));
118
+ };
97
119
  const invalidCaptureEvidence = () => new ProcessCliFailure("invalid_input", "Capture evidence is malformed. Create new capture evidence, then try again.");
98
120
  const readJson = async (path) => {
99
121
  let bytes;
@@ -12,6 +12,7 @@ import { normalizeProcessText } from "./ProcessNormalization.js";
12
12
  import { selectCapturedProcessGroupIds } from "../process/ProcessOwnership.js";
13
13
  import { awaitTerminalExit, buildCaptureResult, captureTerminalFrames, createRunManifest, observeSettlement, prepareProcessCapture, releaseProcessResources, resolveProcessResult, } from "./ProcessCaptureLifecycle.js";
14
14
  import { assertNotCancelled } from "./ProcessCaptureAuthority.js";
15
+ import { createProcessCaptureJournal, scheduleScenarioInteractions, } from "./ProcessCaptureJournal.js";
15
16
  export { probeProcessCaptureCapability } from "./ProcessCaptureCapability.js";
16
17
  const makeEnvironment = (options) => {
17
18
  const { scenario, home, replay, shimReplay, runId } = options;
@@ -38,49 +39,6 @@ const makeEnvironment = (options) => {
38
39
  .join(":");
39
40
  return environment;
40
41
  };
41
- const scheduleScenarioEvents = (options) => {
42
- const { scenario, getTerminal, timers, interactions, renderer, started } = options;
43
- for (const [eventIndex, event] of scenario.events.entries()) {
44
- const timer = setTimeout(() => {
45
- options.dispatchedEventIndexes.add(eventIndex);
46
- const terminal = getTerminal();
47
- const dispatchedAt = Math.max(0, Date.now() - started);
48
- let outcome = "dispatched";
49
- if (terminal === undefined)
50
- outcome = "target_exited";
51
- else {
52
- try {
53
- if (event.type === "input")
54
- terminal.write(event.data);
55
- else if (event.type === "resize") {
56
- terminal.resize(event.columns, event.rows);
57
- renderer.resize(event.columns, event.rows, dispatchedAt);
58
- }
59
- else
60
- terminal.kill(event.signal);
61
- }
62
- catch {
63
- outcome = "failed";
64
- }
65
- }
66
- interactions.push({
67
- sequence: interactions.length,
68
- scheduled_at_ms: event.at_ms,
69
- dispatched_at_ms: dispatchedAt,
70
- type: event.type,
71
- data: event.type === "input"
72
- ? event.sensitive
73
- ? `<redacted-input:${String(Buffer.byteLength(event.data))}-bytes>`
74
- : normalizeProcessText(event.data, scenario, "<no-temporary-root>", -1)
75
- : event.type === "resize"
76
- ? `${String(event.columns)}x${String(event.rows)}`
77
- : event.signal,
78
- outcome,
79
- });
80
- }, event.at_ms);
81
- timers.add(timer);
82
- }
83
- };
84
42
  const cleanupFailedStartup = async (options) => {
85
43
  options.terminal?.kill("SIGKILL");
86
44
  const cleanupFailure = await releaseProcessResources({
@@ -94,13 +52,14 @@ const cleanupFailedStartup = async (options) => {
94
52
  });
95
53
  throw options.cause;
96
54
  };
97
- const createTerminalRenderer = (scenario, temporaryRoot, terminalPid) => new TerminalRenderer({
55
+ const createTerminalRenderer = (scenario, temporaryRoot, terminalPid, recordEvent) => new TerminalRenderer({
98
56
  columns: scenario.terminal.columns,
99
57
  rows: scenario.terminal.rows,
100
58
  scrollback: scenario.terminal.scrollback,
101
59
  maxFrames: scenario.limits.frames,
102
60
  maxBytes: scenario.limits.output_bytes,
103
61
  normalize: (value) => normalizeProcessText(value, scenario, temporaryRoot, terminalPid()),
62
+ recordEvent,
104
63
  });
105
64
  const startCaptureRuntime = async (options) => {
106
65
  const { scenario } = options;
@@ -111,13 +70,16 @@ const startCaptureRuntime = async (options) => {
111
70
  let terminal;
112
71
  try {
113
72
  const { spawn } = await import("@lydell/node-pty");
114
- replay = await startLoopbackReplay(scenario);
73
+ replay = await startLoopbackReplay(scenario, options.recordEvent);
115
74
  const startedAt = new Date();
116
75
  const started = Date.now();
117
76
  let lastOutput = started;
118
- renderer = createTerminalRenderer(scenario, options.temporaryRoot, () => terminal?.pid ?? -1);
119
- checkpoints = new ProcessCheckpoints(scenario, started, options.before, options.signal);
120
- shimReplay = await startCommandShimReplay(scenario, options.temporaryRoot, started);
77
+ renderer = createTerminalRenderer(scenario, options.temporaryRoot, () => terminal?.pid ?? -1, options.recordEvent);
78
+ checkpoints = new ProcessCheckpoints(scenario, started, options.before, {
79
+ signal: options.signal,
80
+ recordEvent: options.recordEvent,
81
+ });
82
+ shimReplay = await startCommandShimReplay(scenario, options.temporaryRoot, started, options.recordEvent);
121
83
  terminal = spawn(scenario.executable, [...scenario.arguments], {
122
84
  cwd: scenario.working_directory,
123
85
  env: makeEnvironment({ ...options, replay, shimReplay }),
@@ -133,7 +95,7 @@ const startCaptureRuntime = async (options) => {
133
95
  renderer,
134
96
  checkpoints,
135
97
  });
136
- scheduleScenarioEvents({
98
+ scheduleScenarioInteractions({
137
99
  ...options,
138
100
  getTerminal: () => terminal,
139
101
  renderer,
@@ -145,6 +107,7 @@ const startCaptureRuntime = async (options) => {
145
107
  started,
146
108
  limit: scenario.limits.processes,
147
109
  samples: options.samples,
110
+ recordEvent: options.recordEvent,
148
111
  });
149
112
  return {
150
113
  replay,
@@ -218,13 +181,14 @@ const completeCapture = async (options) => {
218
181
  const { reason } = options.exit;
219
182
  if (reason === "cancelled")
220
183
  throw processCaptureCancelled();
221
- const settlement = await observeSettlement(options.runId, selectCapturedProcessGroupIds(runtime.terminal.pid, options.samples), scenario.settle_ms);
184
+ const settlement = await observeSettlement(options.runId, selectCapturedProcessGroupIds(runtime.terminal.pid, options.samples), scenario.settle_ms, options.recordEvent);
222
185
  runtime.checkpoints.trigger("settled");
223
186
  const samplingPartial = (await runtime.stopSampler()).partial;
224
187
  assertNotCancelled(options.signal);
225
188
  const after = await snapshotRoots(scenario, options.signal);
226
189
  const renderedFrames = await runtime.renderer.frames();
227
190
  const checkpoints = await runtime.checkpoints.finish(after);
191
+ await runtime.replay.close();
228
192
  const manifest = await createRunManifest(scenario, runtime.startedAt, new Date());
229
193
  const truncated = options.initiallyTruncated ||
230
194
  after.truncated ||
@@ -251,6 +215,7 @@ const completeCapture = async (options) => {
251
215
  shimEvents: normalizedShimEvents(runtime, scenario, options.temporaryRoot),
252
216
  settlement: { ...settlement, cleanup_outcome: "not_required" },
253
217
  manifest,
218
+ eventJournal: options.eventJournal,
254
219
  });
255
220
  };
256
221
  /** Execute one authorized scenario and return bounded observations. */
@@ -258,6 +223,7 @@ const runProcessScenario = async (scenario, policy, signal) => {
258
223
  const { temporaryRoot, runId, home, before } = await prepareProcessCapture(scenario, policy, signal);
259
224
  const frames = [];
260
225
  const samples = [];
226
+ const { entries: eventJournal, recordEvent } = createProcessCaptureJournal();
261
227
  let runtime;
262
228
  const timers = new Set();
263
229
  let capture;
@@ -277,6 +243,7 @@ const runProcessScenario = async (scenario, policy, signal) => {
277
243
  interactions,
278
244
  timers,
279
245
  dispatchedEventIndexes,
246
+ recordEvent,
280
247
  ...(signal === undefined ? {} : { signal }),
281
248
  });
282
249
  stopSampler = runtime.stopSampler;
@@ -289,6 +256,7 @@ const runProcessScenario = async (scenario, policy, signal) => {
289
256
  timers,
290
257
  interactions,
291
258
  dispatchedEventIndexes,
259
+ recordEvent,
292
260
  });
293
261
  capture = await completeCapture({
294
262
  scenario,
@@ -301,6 +269,8 @@ const runProcessScenario = async (scenario, policy, signal) => {
301
269
  interactions,
302
270
  exit,
303
271
  initiallyTruncated: before.truncated,
272
+ eventJournal,
273
+ recordEvent,
304
274
  ...(signal === undefined ? {} : { signal }),
305
275
  });
306
276
  }
@@ -1,3 +1,5 @@
1
+ /** Bucket one elapsed process-capture timestamp under scenario normalization. */
2
+ export const normalizeProcessElapsedTime = (elapsedMs, timeBucketMs) => Math.floor(elapsedMs / timeBucketMs) * timeBucketMs;
1
3
  /** Normalize and redact one terminal/protocol payload under scenario rules. */
2
4
  export const normalizeProcessText = (value, scenario, temporaryRoot, pid) => {
3
5
  let normalized = value;
@@ -42,8 +44,7 @@ export const normalizeProcessSamples = (samples, scenario, rootPid) => {
42
44
  : normalized;
43
45
  };
44
46
  return samples.map((sample) => ({
45
- at_ms: Math.floor(sample.at_ms / scenario.normalization.time_bucket_ms) *
46
- scenario.normalization.time_bucket_ms,
47
+ at_ms: normalizeProcessElapsedTime(sample.at_ms, scenario.normalization.time_bucket_ms),
47
48
  pid: mapping.get(sample.pid) ?? 1,
48
49
  parent_pid: mapping.get(sample.parent_pid) ?? 0,
49
50
  process_group_id: sample.process_group_id === null
@@ -0,0 +1,72 @@
1
+ import { processCaptureCancelled, } from "./ProcessCaptureError.js";
2
+ import { captureProcessScenario } from "./ProcessHarness.js";
3
+ import { compareProcessCaptures } from "../domain/processComparison.js";
4
+ import { PROCESS_COMPARISON_DIMENSIONS } from "../domain/processComparison.js";
5
+ import { analyzeProcessRepeatability, bindProcessScenario, } from "../domain/processPairedExperiment.js";
6
+ import { err, ok } from "../domain/result.js";
7
+ const productionCapturePort = {
8
+ capture: captureProcessScenario,
9
+ };
10
+ const captureRepeats = async (input) => {
11
+ const captures = [];
12
+ for (let index = 0; index < input.repeatCount; index += 1) {
13
+ if (input.signal?.aborted === true)
14
+ return err(processCaptureCancelled());
15
+ const capture = await input.port.capture(input.scenario, input.policy, input.signal);
16
+ if (!capture.ok)
17
+ return err(capture.error);
18
+ captures.push(capture.value);
19
+ }
20
+ return ok(captures);
21
+ };
22
+ /** Execute repeatability-first authority/candidate captures under one contract. */
23
+ export const runPairedProcessExperiment = async (experiment, policy, options = {}) => {
24
+ const port = options.capturePort ?? productionCapturePort;
25
+ const authorityScenario = bindProcessScenario(experiment.shared_scenario, experiment.authority);
26
+ const candidateScenario = bindProcessScenario(experiment.shared_scenario, experiment.candidate);
27
+ const authorityRuns = await captureRepeats({
28
+ scenario: authorityScenario,
29
+ repeatCount: experiment.repeat_count,
30
+ policy,
31
+ port,
32
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
33
+ });
34
+ if (!authorityRuns.ok)
35
+ return authorityRuns;
36
+ const candidateRuns = await captureRepeats({
37
+ scenario: candidateScenario,
38
+ repeatCount: experiment.repeat_count,
39
+ policy,
40
+ port,
41
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
42
+ });
43
+ if (!candidateRuns.ok)
44
+ return candidateRuns;
45
+ const authorityRepeatability = analyzeProcessRepeatability(authorityRuns.value, experiment.required_dimensions);
46
+ const candidateRepeatability = analyzeProcessRepeatability(candidateRuns.value, experiment.required_dimensions);
47
+ const stable = authorityRepeatability.stable && candidateRepeatability.stable;
48
+ const allDimensionsRequired = PROCESS_COMPARISON_DIMENSIONS.every((dimension) => experiment.required_dimensions.includes(dimension));
49
+ const authorityLatest = authorityRuns.value.at(-1);
50
+ const candidateLatest = candidateRuns.value.at(-1);
51
+ const crossSide = stable &&
52
+ allDimensionsRequired &&
53
+ authorityLatest !== undefined &&
54
+ candidateLatest !== undefined
55
+ ? compareProcessCaptures(authorityLatest, candidateLatest, {
56
+ maxCaptureAgeMs: experiment.freshness_policy.max_capture_age_ms,
57
+ ...(options.now === undefined ? {} : { now: options.now }),
58
+ })
59
+ : null;
60
+ return ok({
61
+ authority_runs: authorityRuns.value,
62
+ candidate_runs: candidateRuns.value,
63
+ authority_repeatability: authorityRepeatability,
64
+ candidate_repeatability: candidateRepeatability,
65
+ cross_side: crossSide,
66
+ cross_side_blocked_reason: crossSide !== null
67
+ ? null
68
+ : !allDimensionsRequired
69
+ ? "Cross-side comparison is blocked until every returned dimension is required and repeatable."
70
+ : "Cross-side comparison is blocked because a required same-side dimension is unstable.",
71
+ });
72
+ };