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.
- package/README.md +84 -18
- package/dist/application/AndroidApplicationService.js +17 -0
- package/dist/application/AppleApplicationService.js +17 -0
- package/dist/application/ApplicationWorkflowEvidenceResolver.js +162 -0
- package/dist/application/BinarySession.js +9 -2
- package/dist/application/BinarySessionStatus.js +39 -1
- package/dist/application/CapabilityInventory.js +15 -0
- package/dist/application/ClientRegistrationStatus.js +11 -4
- package/dist/application/CommandShimReplay.js +6 -3
- package/dist/application/CompositeProvider.js +5 -2
- package/dist/application/DirectAnalysis.js +1 -20
- package/dist/application/DirectAnalysisStatus.js +83 -0
- package/dist/application/Doctor.js +34 -25
- package/dist/application/DoctorDiagnostics.js +1 -4
- package/dist/application/DoctorProjection.js +43 -0
- package/dist/application/DoctorScope.js +102 -0
- package/dist/application/EvidenceReferenceResolver.js +26 -0
- package/dist/application/InventoryProjectionEvidence.js +41 -0
- package/dist/application/InvestigationProviders.js +12 -0
- package/dist/application/JavaScriptApplicationEvidence.js +1 -1
- package/dist/application/JavaScriptApplicationEvidenceGraph.js +10 -1
- package/dist/application/JavaScriptApplicationService.js +3 -2
- package/dist/application/JavaScriptApplicationWorkflowEvidence.js +26 -0
- package/dist/application/JavaScriptApplicationWorkflowService.js +49 -1
- package/dist/application/JavaScriptArtifactReconstruction.js +7 -0
- package/dist/application/JavaScriptModuleRelationships.js +25 -1
- package/dist/application/JavaScriptReturnShapeProjection.js +111 -0
- package/dist/application/JavaScriptSemanticGraphBuilder.js +294 -0
- package/dist/application/JavaScriptSemanticGraphConstruction.js +144 -0
- package/dist/application/JavaScriptSemanticGraphEvidence.js +86 -0
- package/dist/application/JavaScriptSemanticGraphFlowProjection.js +72 -0
- package/dist/application/JavaScriptSemanticGraphProjection.js +33 -0
- package/dist/application/JavaScriptSemanticTraceService.js +45 -0
- package/dist/application/LoopbackReplay.js +277 -133
- package/dist/application/LoopbackReplayRecorder.js +101 -0
- package/dist/application/PermissionAuthority.js +7 -3
- package/dist/application/ProcessCaptureJournal.js +95 -0
- package/dist/application/ProcessCaptureLifecycle.js +24 -13
- package/dist/application/ProcessCheckpoints.js +30 -4
- package/dist/application/ProcessCli.js +26 -4
- package/dist/application/ProcessHarness.js +20 -50
- package/dist/application/ProcessNormalization.js +3 -2
- package/dist/application/ProcessPairedExperiment.js +72 -0
- package/dist/application/ProcessReactiveCoordinator.js +218 -0
- package/dist/application/ProcessReactiveEffects.js +133 -0
- package/dist/application/ProcessReactiveObservations.js +20 -0
- package/dist/application/ProcessSampling.js +2 -0
- package/dist/application/SessionProviderRouter.js +7 -6
- package/dist/application/Setup.js +14 -13
- package/dist/application/SetupClientConfiguration.js +10 -6
- package/dist/application/SetupPlan.js +4 -3
- package/dist/application/SetupRegistrationEnvironment.js +12 -0
- package/dist/application/SetupSkill.js +44 -40
- package/dist/application/TerminalRenderer.js +3 -1
- package/dist/application/Uninstall.js +7 -1
- package/dist/application/Upgrade.js +16 -1
- package/dist/browser/CdpEndpoint.js +37 -4
- package/dist/catalogIdentity.js +16 -7
- package/dist/cli/coreAnalysisCommands.js +34 -2
- package/dist/cli/javascriptApplicationAnalysis.js +19 -0
- package/dist/cli/setupCommands.js +84 -30
- package/dist/cli/utilityCommands.js +10 -2
- package/dist/cliApplicationCommands.js +79 -16
- package/dist/cliCommandNames.js +3 -0
- package/dist/cliElectronCommands.js +23 -33
- package/dist/cliObservationOptions.js +1 -1
- package/dist/cliOutput.js +56 -0
- package/dist/cliProcessCommands.js +32 -1
- package/dist/cliSetup.js +55 -29
- package/dist/contracts/applicationToolContracts.js +54 -7
- package/dist/contracts/applicationWorkflowInputContracts.js +119 -0
- package/dist/contracts/electronToolContracts.js +73 -4
- package/dist/contracts/javascriptApplicationWorkflowExamples.js +32 -9
- package/dist/contracts/managedToolContracts.js +7 -2
- package/dist/contracts/processCaptureExample.js +1 -0
- package/dist/contracts/replayMachineExample.js +37 -0
- package/dist/contracts/sessionStatusContract.js +26 -0
- package/dist/contracts/toolContractExamples.js +2 -0
- package/dist/contracts/toolContractTypes.js +12 -1
- package/dist/contracts/toolContracts.js +8 -11
- package/dist/contracts/toolEffects.js +3 -0
- package/dist/contracts/toolOutputSchemaGroups.js +53 -9
- package/dist/contracts/toolOutputSchemaPrimitives.js +73 -32
- package/dist/doctorRuntime.js +41 -0
- package/dist/domain/analysisErrorPresentation.js +5 -3
- package/dist/domain/analysisErrorProjection.js +7 -1
- package/dist/domain/androidApplication.js +206 -0
- package/dist/domain/appleApplication.js +221 -0
- package/dist/domain/boundedCartesianProjection.js +15 -0
- package/dist/domain/changedBehavior.js +5 -4
- package/dist/domain/completionLedgerGeneration.js +190 -0
- package/dist/domain/errors.js +4 -2
- package/dist/domain/evidenceCompletionLedger.js +113 -0
- package/dist/domain/hopperStartupFailure.js +9 -0
- package/dist/domain/javascriptApplicationAnalysis.js +44 -2
- package/dist/domain/javascriptExportShapeComparison.js +145 -0
- package/dist/domain/javascriptExportShapeComparisonIdentity.js +11 -0
- package/dist/domain/javascriptExportShapeComparisonSchemas.js +277 -0
- package/dist/domain/javascriptExportShapeSelection.js +132 -0
- package/dist/domain/javascriptExportShapeVariants.js +272 -0
- package/dist/domain/javascriptRuntimeReconciliationParsing.js +2 -2
- package/dist/domain/javascriptSemanticAnalysis.js +17 -36
- package/dist/domain/javascriptSemanticCallResolution.js +129 -0
- package/dist/domain/javascriptSemanticCalls.js +269 -0
- package/dist/domain/javascriptSemanticGraph.js +356 -0
- package/dist/domain/javascriptSemanticGraphSchemas.js +329 -0
- package/dist/domain/javascriptSemanticGraphSerialization.js +23 -0
- package/dist/domain/javascriptSemanticIr.js +14 -1
- package/dist/domain/javascriptSemanticPrimitives.js +36 -0
- package/dist/domain/javascriptSemanticProjection.js +36 -4
- package/dist/domain/javascriptSemanticProvenance.js +29 -0
- package/dist/domain/javascriptSemanticQuery.js +294 -0
- package/dist/domain/javascriptSemanticQueryAssessment.js +74 -0
- package/dist/domain/javascriptSemanticQueryIdentity.js +86 -0
- package/dist/domain/javascriptSemanticQueryRelations.js +15 -0
- package/dist/domain/javascriptSemanticQuerySchemas.js +188 -0
- package/dist/domain/javascriptSemanticReturns.js +187 -0
- package/dist/domain/javascriptSemanticState.js +32 -0
- package/dist/domain/javascriptSemanticTraceSchemas.js +14 -0
- package/dist/domain/javascriptSemanticValues.js +54 -70
- package/dist/domain/processCapture.js +36 -0
- package/dist/domain/processCaptureValidation.js +101 -12
- package/dist/domain/processComparison.js +96 -24
- package/dist/domain/processObservation.js +153 -0
- package/dist/domain/processPairedExperiment.js +128 -0
- package/dist/domain/processReactiveCheckpointDataflow.js +32 -0
- package/dist/domain/processReactiveMatching.js +210 -0
- package/dist/domain/processReactiveRuntime.js +109 -0
- package/dist/domain/processReactiveScenario.js +361 -0
- package/dist/domain/processReactiveScenarioPreflight.js +120 -0
- package/dist/domain/processReactiveTransition.js +159 -0
- package/dist/domain/processScenario.js +12 -0
- package/dist/domain/processTraceComparison.js +43 -0
- package/dist/domain/processTraceDimensionProjection.js +50 -0
- package/dist/domain/processTraceEvaluation.js +312 -0
- package/dist/domain/processTraceSpecification.js +373 -0
- package/dist/domain/reconstructionVerification.js +5 -4
- package/dist/domain/replayMachine.js +332 -0
- package/dist/domain/replayMachineRun.js +216 -0
- package/dist/domain/replayMachineRuntime.js +294 -0
- package/dist/domain/runtimeIdentification.js +210 -0
- package/dist/domain/staticRuntimeCorrelation.js +4 -2
- package/dist/evaluation/CodexAgentEval.js +156 -0
- package/dist/generatedPackageMetadata.js +2 -2
- package/dist/ghidra/GhidraClient.js +13 -2
- package/dist/ghidra/GhidraDiagnostics.js +1 -0
- package/dist/ghidra/GhidraLauncher.js +7 -3
- package/dist/ghidra/GhidraProvider.js +8 -1
- package/dist/hopper/BridgeLauncher.js +56 -14
- package/dist/hopper/HopperClient.js +23 -10
- package/dist/hopper/HopperDiagnostics.js +14 -0
- package/dist/hopper/HopperProcessDiagnostic.js +14 -0
- package/dist/hopper/HopperProvider.js +8 -1
- package/dist/hopper/LinuxPrivateDisplayDiagnostic.js +72 -0
- package/dist/hopper/LinuxPrivateDisplayProbe.js +217 -0
- package/dist/identity.js +2 -1
- package/dist/main/transport.js +2 -0
- package/dist/mcpDoctor.js +310 -0
- package/dist/process/ProcessOwnership.js +200 -53
- package/dist/process/ProviderRunLineage.js +21 -0
- package/dist/server/createServer.js +8 -2
- package/dist/server/javascriptApplicationResult.js +106 -0
- package/dist/server/registerApplicationTools/characterization.js +3 -3
- package/dist/server/registerApplicationTools/compareExportShapes.js +37 -0
- package/dist/server/registerApplicationTools/compareVersions.js +12 -6
- package/dist/server/registerApplicationTools/controlledReplay.js +2 -2
- package/dist/server/registerApplicationTools/coverage.js +3 -3
- package/dist/server/registerApplicationTools/helpers.js +37 -25
- package/dist/server/registerApplicationTools/traceFeature.js +9 -5
- package/dist/server/registerApplicationTools/traceSemantics.js +30 -0
- package/dist/server/registerApplicationTools.js +4 -0
- package/dist/server/registerElectronTools.js +19 -5
- package/dist/server/registerEvidenceResources.js +7 -1
- package/dist/server/registerJavaScriptApplicationGraphResource.js +69 -0
- package/dist/server/registerManagedTools.js +18 -1
- package/dist/server/registerProcessComparisonTool.js +17 -7
- package/dist/server/registerReplayMachineTool.js +20 -0
- package/dist/server/registerSessionStatusTool.js +95 -25
- package/dist/server/registerSessionTools.js +12 -7
- package/dist/server/sessionAvailabilityPolicy.js +2 -1
- package/dist/server/toolResult.js +10 -4
- package/dist/serverIdentity.js +19 -0
- package/package.json +62 -29
- package/scripts/hopper-demo-x11.py +459 -61
- package/scripts/prepack.mjs +55 -0
- package/scripts/prepare.mjs +28 -0
- package/scripts/rea.mjs +26 -5
- package/skills/reverse-engineer-anything/SKILL.md +69 -228
- package/skills/reverse-engineer-anything/references/controlled-replay.md +12 -0
- package/skills/reverse-engineer-anything/references/evidence-workflows.md +33 -0
- package/skills/reverse-engineer-anything/references/javascript-applications.md +35 -0
- package/skills/reverse-engineer-anything/references/native-and-artifacts.md +36 -0
- 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:
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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:
|
|
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,
|
|
120
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
+
};
|