rea-agents 2.2.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 +56 -9
- 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/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/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/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 +11 -10
- package/dist/application/SetupPlan.js +1 -1
- package/dist/application/SetupRegistrationEnvironment.js +12 -0
- package/dist/application/TerminalRenderer.js +3 -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 +81 -32
- 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 +30 -21
- package/dist/contracts/applicationToolContracts.js +54 -7
- package/dist/contracts/applicationWorkflowInputContracts.js +119 -0
- package/dist/contracts/electronToolContracts.js +53 -32
- package/dist/contracts/javascriptApplicationWorkflowExamples.js +32 -9
- package/dist/contracts/processCaptureExample.js +1 -0
- package/dist/contracts/replayMachineExample.js +37 -0
- package/dist/contracts/toolContractExamples.js +2 -0
- package/dist/contracts/toolContracts.js +5 -1
- package/dist/contracts/toolEffects.js +3 -0
- package/dist/contracts/toolOutputSchemaGroups.js +2 -0
- package/dist/contracts/toolOutputSchemaPrimitives.js +40 -0
- package/dist/doctorRuntime.js +41 -0
- package/dist/domain/analysisErrorPresentation.js +5 -3
- package/dist/domain/analysisErrorProjection.js +7 -1
- 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 +78 -25
- package/dist/domain/processObservation.js +153 -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 +18 -2
- package/dist/domain/replayMachineRun.js +216 -0
- package/dist/domain/replayMachineRuntime.js +88 -9
- package/dist/domain/staticRuntimeCorrelation.js +4 -2
- package/dist/evaluation/CodexAgentEval.js +24 -0
- package/dist/generatedPackageMetadata.js +1 -1
- 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/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 +4 -1
- package/dist/server/javascriptApplicationResult.js +22 -1
- 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/registerEvidenceResources.js +5 -1
- package/dist/server/registerProcessComparisonTool.js +17 -7
- package/dist/server/registerReplayMachineTool.js +20 -0
- package/dist/server/registerSessionTools.js +12 -7
- package/dist/server/sessionAvailabilityPolicy.js +2 -1
- package/dist/serverIdentity.js +19 -0
- package/package.json +62 -30
- 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 +2 -2
- package/skills/reverse-engineer-anything/references/javascript-applications.md +10 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { processObservationLocationSchema, projectProcessObservation, } from "./processObservation.js";
|
|
3
|
+
import { canonicalTraceJson, comparableTracePayload, processTraceCardinalityBounds, } from "./processTraceSpecification.js";
|
|
4
|
+
const identifierSchema = z.string().regex(/^[A-Za-z][A-Za-z0-9._-]{0,63}$/u);
|
|
5
|
+
const locationSchema = processObservationLocationSchema;
|
|
6
|
+
const matchedEventSchema = z.strictObject({
|
|
7
|
+
event_id: identifierSchema,
|
|
8
|
+
location: locationSchema,
|
|
9
|
+
});
|
|
10
|
+
const sideResultSchema = z.strictObject({
|
|
11
|
+
status: z.enum(["pass", "fail", "unknown"]),
|
|
12
|
+
matched_variant: identifierSchema.nullable(),
|
|
13
|
+
satisfied_constraints: z.array(z.string()),
|
|
14
|
+
raw_trace: z.array(matchedEventSchema),
|
|
15
|
+
});
|
|
16
|
+
const diagnosticSchema = z.strictObject({
|
|
17
|
+
kind: z.enum([
|
|
18
|
+
"journal",
|
|
19
|
+
"predicate",
|
|
20
|
+
"cardinality",
|
|
21
|
+
"edge",
|
|
22
|
+
"prefix",
|
|
23
|
+
"suffix",
|
|
24
|
+
"trace",
|
|
25
|
+
]),
|
|
26
|
+
side: z.enum(["left", "right"]),
|
|
27
|
+
message: z.string(),
|
|
28
|
+
event_ids: z.array(identifierSchema),
|
|
29
|
+
locations: z.array(locationSchema),
|
|
30
|
+
});
|
|
31
|
+
/** Structured verdict for one declared process trace language. */
|
|
32
|
+
export const processTraceComparisonResultSchema = z.strictObject({
|
|
33
|
+
verdict: z.enum(["equivalent", "different", "nonconforming", "unknown"]),
|
|
34
|
+
left: sideResultSchema,
|
|
35
|
+
right: sideResultSchema,
|
|
36
|
+
diagnostic: diagnosticSchema.nullable(),
|
|
37
|
+
});
|
|
38
|
+
const scopesFor = (sources) => new Set([...sources].map((source) => {
|
|
39
|
+
if (source.startsWith("terminal_"))
|
|
40
|
+
return "terminal";
|
|
41
|
+
if (source === "lifecycle")
|
|
42
|
+
return "exit";
|
|
43
|
+
if (source === "http" ||
|
|
44
|
+
source === "websocket" ||
|
|
45
|
+
source === "replay_transition")
|
|
46
|
+
return "protocol";
|
|
47
|
+
if (source === "filesystem")
|
|
48
|
+
return "filesystem";
|
|
49
|
+
if (source === "process")
|
|
50
|
+
return "process";
|
|
51
|
+
if (source === "shim")
|
|
52
|
+
return "shim";
|
|
53
|
+
return "interaction";
|
|
54
|
+
}));
|
|
55
|
+
const unknownSide = () => ({
|
|
56
|
+
status: "unknown",
|
|
57
|
+
matched_variant: null,
|
|
58
|
+
satisfied_constraints: [],
|
|
59
|
+
raw_trace: [],
|
|
60
|
+
});
|
|
61
|
+
const failure = (input) => ({
|
|
62
|
+
result: {
|
|
63
|
+
status: "fail",
|
|
64
|
+
matched_variant: null,
|
|
65
|
+
satisfied_constraints: [],
|
|
66
|
+
raw_trace: input.rawTrace,
|
|
67
|
+
},
|
|
68
|
+
diagnostic: {
|
|
69
|
+
kind: input.kind,
|
|
70
|
+
message: input.message,
|
|
71
|
+
event_ids: [...input.eventIds],
|
|
72
|
+
locations: [...input.locations],
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
const evaluateFiniteTrace = (ids, specification) => {
|
|
76
|
+
if (specification.language.kind !== "finite_traces")
|
|
77
|
+
return null;
|
|
78
|
+
for (const variant of specification.language.variants) {
|
|
79
|
+
let offset = 0;
|
|
80
|
+
let accepted = true;
|
|
81
|
+
for (const token of variant.trace) {
|
|
82
|
+
if (typeof token === "string") {
|
|
83
|
+
if (ids[offset] !== token)
|
|
84
|
+
accepted = false;
|
|
85
|
+
offset += 1;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const actual = ids.slice(offset, offset + token.unordered.length).sort();
|
|
89
|
+
const expected = [...token.unordered].sort();
|
|
90
|
+
if (canonicalTraceJson(actual) !== canonicalTraceJson(expected))
|
|
91
|
+
accepted = false;
|
|
92
|
+
offset += token.unordered.length;
|
|
93
|
+
}
|
|
94
|
+
if (accepted && offset === ids.length)
|
|
95
|
+
return variant.id;
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
};
|
|
99
|
+
const collectRecords = (capture, sources) => {
|
|
100
|
+
const records = [];
|
|
101
|
+
for (const location of capture.event_journal ?? []) {
|
|
102
|
+
const record = projectProcessObservation(capture, location);
|
|
103
|
+
if (record !== null && sources.has(record.source))
|
|
104
|
+
records.push({
|
|
105
|
+
source: record.source,
|
|
106
|
+
payload: record.payload,
|
|
107
|
+
location,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return records;
|
|
111
|
+
};
|
|
112
|
+
const matchRecords = (records, specification) => {
|
|
113
|
+
const rawTrace = [];
|
|
114
|
+
const locationsById = new Map();
|
|
115
|
+
const ignoredFieldsBySource = new Map();
|
|
116
|
+
const eventsByKey = new Map();
|
|
117
|
+
for (const event of specification.events) {
|
|
118
|
+
const ignoredFields = event.ignore_fields ?? [];
|
|
119
|
+
ignoredFieldsBySource.set(event.source, ignoredFields);
|
|
120
|
+
eventsByKey.set(`${event.source}\0${canonicalTraceJson(comparableTracePayload(event.exact, ignoredFields))}`, event);
|
|
121
|
+
}
|
|
122
|
+
for (const record of records) {
|
|
123
|
+
const match = eventsByKey.get(`${record.source}\0${canonicalTraceJson(comparableTracePayload(record.payload, ignoredFieldsBySource.get(record.source)))}`);
|
|
124
|
+
if (match === undefined)
|
|
125
|
+
return failure({
|
|
126
|
+
kind: "predicate",
|
|
127
|
+
message: "Observed event does not match any declared exact predicate.",
|
|
128
|
+
eventIds: [],
|
|
129
|
+
locations: [record.location],
|
|
130
|
+
rawTrace,
|
|
131
|
+
});
|
|
132
|
+
rawTrace.push({ event_id: match.id, location: record.location });
|
|
133
|
+
const locations = locationsById.get(match.id) ?? [];
|
|
134
|
+
locations.push(record.location);
|
|
135
|
+
locationsById.set(match.id, locations);
|
|
136
|
+
}
|
|
137
|
+
return { rawTrace, locationsById };
|
|
138
|
+
};
|
|
139
|
+
const cardinalityFailure = (trace, specification) => {
|
|
140
|
+
for (const event of specification.events) {
|
|
141
|
+
const [minimum, maximum] = processTraceCardinalityBounds(event.cardinality);
|
|
142
|
+
const locations = trace.locationsById.get(event.id) ?? [];
|
|
143
|
+
if (locations.length < minimum || locations.length > maximum)
|
|
144
|
+
return failure({
|
|
145
|
+
kind: "cardinality",
|
|
146
|
+
message: `${event.id} observed ${String(locations.length)} times; expected ${String(minimum)}..${String(maximum)}.`,
|
|
147
|
+
eventIds: [event.id],
|
|
148
|
+
locations,
|
|
149
|
+
rawTrace: trace.rawTrace,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
};
|
|
154
|
+
const evaluateOrderingConstraints = (trace, language) => {
|
|
155
|
+
const satisfied = [];
|
|
156
|
+
for (const edge of language.happens_before) {
|
|
157
|
+
const before = trace.locationsById.get(edge.before) ?? [];
|
|
158
|
+
const after = trace.locationsById.get(edge.after) ?? [];
|
|
159
|
+
const lastBefore = before.at(-1);
|
|
160
|
+
const firstAfter = after[0];
|
|
161
|
+
if (lastBefore === undefined && firstAfter !== undefined)
|
|
162
|
+
return {
|
|
163
|
+
failure: failure({
|
|
164
|
+
kind: "edge",
|
|
165
|
+
message: `${edge.before} is required when ${edge.after} occurs.`,
|
|
166
|
+
eventIds: [edge.before, edge.after],
|
|
167
|
+
locations: [firstAfter],
|
|
168
|
+
rawTrace: trace.rawTrace,
|
|
169
|
+
}),
|
|
170
|
+
satisfied,
|
|
171
|
+
};
|
|
172
|
+
if (lastBefore !== undefined &&
|
|
173
|
+
firstAfter !== undefined &&
|
|
174
|
+
lastBefore.capture_order >= firstAfter.capture_order)
|
|
175
|
+
return {
|
|
176
|
+
failure: failure({
|
|
177
|
+
kind: "edge",
|
|
178
|
+
message: `${edge.before} must happen before ${edge.after}.`,
|
|
179
|
+
eventIds: [edge.before, edge.after],
|
|
180
|
+
locations: [lastBefore, firstAfter],
|
|
181
|
+
rawTrace: trace.rawTrace,
|
|
182
|
+
}),
|
|
183
|
+
satisfied,
|
|
184
|
+
};
|
|
185
|
+
satisfied.push(`happens_before:${edge.before}:${edge.after}`);
|
|
186
|
+
}
|
|
187
|
+
for (const constraint of language.not_before) {
|
|
188
|
+
const events = trace.locationsById.get(constraint.event) ?? [];
|
|
189
|
+
const anchors = trace.locationsById.get(constraint.anchor) ?? [];
|
|
190
|
+
const firstEvent = events[0];
|
|
191
|
+
const firstAnchor = anchors[0];
|
|
192
|
+
if (firstEvent !== undefined && firstAnchor === undefined)
|
|
193
|
+
return {
|
|
194
|
+
failure: failure({
|
|
195
|
+
kind: "edge",
|
|
196
|
+
message: `${constraint.anchor} is required when ${constraint.event} occurs.`,
|
|
197
|
+
eventIds: [constraint.event, constraint.anchor],
|
|
198
|
+
locations: [firstEvent],
|
|
199
|
+
rawTrace: trace.rawTrace,
|
|
200
|
+
}),
|
|
201
|
+
satisfied,
|
|
202
|
+
};
|
|
203
|
+
if (firstEvent !== undefined &&
|
|
204
|
+
firstAnchor !== undefined &&
|
|
205
|
+
firstEvent.capture_order < firstAnchor.capture_order)
|
|
206
|
+
return {
|
|
207
|
+
failure: failure({
|
|
208
|
+
kind: "edge",
|
|
209
|
+
message: `${constraint.event} must not occur before ${constraint.anchor}.`,
|
|
210
|
+
eventIds: [constraint.event, constraint.anchor],
|
|
211
|
+
locations: [firstEvent, firstAnchor],
|
|
212
|
+
rawTrace: trace.rawTrace,
|
|
213
|
+
}),
|
|
214
|
+
satisfied,
|
|
215
|
+
};
|
|
216
|
+
satisfied.push(`not_before:${constraint.event}:${constraint.anchor}`);
|
|
217
|
+
}
|
|
218
|
+
return { failure: null, satisfied };
|
|
219
|
+
};
|
|
220
|
+
const boundaryFailure = (trace, kind, expected) => {
|
|
221
|
+
const ids = trace.rawTrace.map(({ event_id }) => event_id);
|
|
222
|
+
const actual = kind === "prefix"
|
|
223
|
+
? ids.slice(0, expected.length)
|
|
224
|
+
: expected.length === 0
|
|
225
|
+
? []
|
|
226
|
+
: ids.slice(-expected.length);
|
|
227
|
+
if (canonicalTraceJson(actual) === canonicalTraceJson(expected))
|
|
228
|
+
return null;
|
|
229
|
+
const locations = kind === "prefix"
|
|
230
|
+
? trace.rawTrace.slice(0, expected.length)
|
|
231
|
+
: trace.rawTrace.slice(-expected.length);
|
|
232
|
+
return failure({
|
|
233
|
+
kind,
|
|
234
|
+
message: `Observed trace does not satisfy the declared ${kind}.`,
|
|
235
|
+
eventIds: expected,
|
|
236
|
+
locations: locations.map(({ location }) => location),
|
|
237
|
+
rawTrace: trace.rawTrace,
|
|
238
|
+
});
|
|
239
|
+
};
|
|
240
|
+
const evaluatePartialOrder = (trace, language) => {
|
|
241
|
+
const ordering = evaluateOrderingConstraints(trace, language);
|
|
242
|
+
if (ordering.failure !== null)
|
|
243
|
+
return ordering.failure;
|
|
244
|
+
const prefixFailure = boundaryFailure(trace, "prefix", language.prefix);
|
|
245
|
+
if (prefixFailure !== null)
|
|
246
|
+
return prefixFailure;
|
|
247
|
+
const suffixFailure = boundaryFailure(trace, "suffix", language.suffix);
|
|
248
|
+
if (suffixFailure !== null)
|
|
249
|
+
return suffixFailure;
|
|
250
|
+
const satisfied = ordering.satisfied;
|
|
251
|
+
satisfied.push(...language.unordered_groups.map(({ events }) => `unordered:${events.join(":")}`));
|
|
252
|
+
if (language.prefix.length > 0)
|
|
253
|
+
satisfied.push(`prefix:${language.prefix.join(":")}`);
|
|
254
|
+
if (language.suffix.length > 0)
|
|
255
|
+
satisfied.push(`suffix:${language.suffix.join(":")}`);
|
|
256
|
+
return {
|
|
257
|
+
result: {
|
|
258
|
+
status: "pass",
|
|
259
|
+
matched_variant: null,
|
|
260
|
+
satisfied_constraints: satisfied,
|
|
261
|
+
raw_trace: trace.rawTrace,
|
|
262
|
+
},
|
|
263
|
+
diagnostic: null,
|
|
264
|
+
};
|
|
265
|
+
};
|
|
266
|
+
const evaluateMatchedTrace = (trace, specification) => {
|
|
267
|
+
const invalidCardinality = cardinalityFailure(trace, specification);
|
|
268
|
+
if (invalidCardinality !== null)
|
|
269
|
+
return invalidCardinality;
|
|
270
|
+
if (specification.language.kind === "partial_order")
|
|
271
|
+
return evaluatePartialOrder(trace, specification.language);
|
|
272
|
+
const ids = trace.rawTrace.map(({ event_id }) => event_id);
|
|
273
|
+
const variant = evaluateFiniteTrace(ids, specification);
|
|
274
|
+
return variant === null
|
|
275
|
+
? failure({
|
|
276
|
+
kind: "trace",
|
|
277
|
+
message: "Observed events do not match any finite trace variant.",
|
|
278
|
+
eventIds: ids,
|
|
279
|
+
locations: trace.rawTrace.map(({ location }) => location),
|
|
280
|
+
rawTrace: trace.rawTrace,
|
|
281
|
+
})
|
|
282
|
+
: {
|
|
283
|
+
result: {
|
|
284
|
+
status: "pass",
|
|
285
|
+
matched_variant: variant,
|
|
286
|
+
satisfied_constraints: [`variant:${variant}`],
|
|
287
|
+
raw_trace: trace.rawTrace,
|
|
288
|
+
},
|
|
289
|
+
diagnostic: null,
|
|
290
|
+
};
|
|
291
|
+
};
|
|
292
|
+
export const evaluateProcessTraceSide = (capture, specification) => {
|
|
293
|
+
const sources = new Set(specification.events.map(({ source }) => source));
|
|
294
|
+
const relevantScopes = scopesFor(sources);
|
|
295
|
+
if (capture.truncated ||
|
|
296
|
+
capture.residual_unknowns.some(({ scope }) => relevantScopes.has(scope)))
|
|
297
|
+
return { result: unknownSide(), diagnostic: null };
|
|
298
|
+
if ((capture.event_journal ?? []).length === 0)
|
|
299
|
+
return {
|
|
300
|
+
result: unknownSide(),
|
|
301
|
+
diagnostic: {
|
|
302
|
+
kind: "journal",
|
|
303
|
+
message: "Capture has no cross-source event journal.",
|
|
304
|
+
event_ids: [],
|
|
305
|
+
locations: [],
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
const matched = matchRecords(collectRecords(capture, sources), specification);
|
|
309
|
+
return "rawTrace" in matched
|
|
310
|
+
? evaluateMatchedTrace(matched, specification)
|
|
311
|
+
: matched;
|
|
312
|
+
};
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import canonicalize from "canonicalize";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { jsonValueSchema } from "./jsonValue.js";
|
|
4
|
+
import { comparableProcessObservationPayload, processObservationSourceSchema, } from "./processObservation.js";
|
|
5
|
+
const identifierSchema = z.string().regex(/^[A-Za-z][A-Za-z0-9._-]{0,63}$/u);
|
|
6
|
+
/** Process observation families admitted by a declared trace specification. */
|
|
7
|
+
export const processTraceSourceSchema = processObservationSourceSchema;
|
|
8
|
+
const cardinalitySchema = z.discriminatedUnion("kind", [
|
|
9
|
+
z.strictObject({ kind: z.literal("required") }),
|
|
10
|
+
z.strictObject({ kind: z.literal("optional") }),
|
|
11
|
+
z.strictObject({
|
|
12
|
+
kind: z.literal("exact"),
|
|
13
|
+
count: z.number().int().min(0).max(10_000),
|
|
14
|
+
}),
|
|
15
|
+
z
|
|
16
|
+
.strictObject({
|
|
17
|
+
kind: z.literal("range"),
|
|
18
|
+
min: z.number().int().min(0).max(10_000),
|
|
19
|
+
max: z.number().int().min(0).max(10_000),
|
|
20
|
+
})
|
|
21
|
+
.refine(({ min, max }) => min <= max, {
|
|
22
|
+
message: "cardinality min must not exceed max",
|
|
23
|
+
path: ["min"],
|
|
24
|
+
}),
|
|
25
|
+
]);
|
|
26
|
+
const eventDeclarationSchema = z.strictObject({
|
|
27
|
+
id: identifierSchema,
|
|
28
|
+
source: processTraceSourceSchema,
|
|
29
|
+
exact: jsonValueSchema,
|
|
30
|
+
ignore_fields: z
|
|
31
|
+
.array(z.enum([
|
|
32
|
+
"sequence",
|
|
33
|
+
"at_ms",
|
|
34
|
+
"scheduled_at_ms",
|
|
35
|
+
"dispatched_at_ms",
|
|
36
|
+
"elapsed_ms",
|
|
37
|
+
]))
|
|
38
|
+
.max(5)
|
|
39
|
+
.refine((values) => new Set(values).size === values.length, {
|
|
40
|
+
message: "ignored event fields must be unique",
|
|
41
|
+
})
|
|
42
|
+
.optional(),
|
|
43
|
+
cardinality: cardinalitySchema.default({ kind: "required" }),
|
|
44
|
+
});
|
|
45
|
+
const partialOrderLanguageSchema = z.strictObject({
|
|
46
|
+
kind: z.literal("partial_order"),
|
|
47
|
+
happens_before: z
|
|
48
|
+
.array(z.strictObject({
|
|
49
|
+
before: identifierSchema,
|
|
50
|
+
after: identifierSchema,
|
|
51
|
+
}))
|
|
52
|
+
.max(4_096)
|
|
53
|
+
.default([]),
|
|
54
|
+
not_before: z
|
|
55
|
+
.array(z.strictObject({
|
|
56
|
+
event: identifierSchema,
|
|
57
|
+
anchor: identifierSchema,
|
|
58
|
+
}))
|
|
59
|
+
.max(4_096)
|
|
60
|
+
.default([]),
|
|
61
|
+
unordered_groups: z
|
|
62
|
+
.array(z.strictObject({ events: z.array(identifierSchema).min(2).max(256) }))
|
|
63
|
+
.max(256)
|
|
64
|
+
.default([]),
|
|
65
|
+
prefix: z.array(identifierSchema).max(4_096).default([]),
|
|
66
|
+
suffix: z.array(identifierSchema).max(4_096).default([]),
|
|
67
|
+
});
|
|
68
|
+
const finiteTraceTokenSchema = z.union([
|
|
69
|
+
identifierSchema,
|
|
70
|
+
z.strictObject({ unordered: z.array(identifierSchema).min(2).max(256) }),
|
|
71
|
+
]);
|
|
72
|
+
const finiteTraceLanguageSchema = z.strictObject({
|
|
73
|
+
kind: z.literal("finite_traces"),
|
|
74
|
+
variants: z
|
|
75
|
+
.array(z.strictObject({
|
|
76
|
+
id: identifierSchema,
|
|
77
|
+
trace: z.array(finiteTraceTokenSchema).max(4_096),
|
|
78
|
+
}))
|
|
79
|
+
.min(1)
|
|
80
|
+
.max(64),
|
|
81
|
+
});
|
|
82
|
+
const specificationShapeSchema = z.strictObject({
|
|
83
|
+
version: z.literal(1),
|
|
84
|
+
events: z.array(eventDeclarationSchema).min(1).max(256),
|
|
85
|
+
language: z.discriminatedUnion("kind", [
|
|
86
|
+
partialOrderLanguageSchema,
|
|
87
|
+
finiteTraceLanguageSchema,
|
|
88
|
+
]),
|
|
89
|
+
});
|
|
90
|
+
export const canonicalTraceJson = (value) => {
|
|
91
|
+
const serialized = canonicalize(value);
|
|
92
|
+
if (serialized === undefined)
|
|
93
|
+
throw new TypeError("Trace specification contains a non-JSON value");
|
|
94
|
+
return serialized;
|
|
95
|
+
};
|
|
96
|
+
/** Remove only explicitly declared schedule metadata before exact matching. */
|
|
97
|
+
export const comparableTracePayload = (value, ignoredFields = []) => comparableProcessObservationPayload(value, ignoredFields);
|
|
98
|
+
export const processTraceCardinalityBounds = (cardinality) => {
|
|
99
|
+
switch (cardinality.kind) {
|
|
100
|
+
case "required":
|
|
101
|
+
return [1, 1];
|
|
102
|
+
case "optional":
|
|
103
|
+
return [0, 1];
|
|
104
|
+
case "exact":
|
|
105
|
+
return [cardinality.count, cardinality.count];
|
|
106
|
+
case "range":
|
|
107
|
+
return [cardinality.min, cardinality.max];
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
const addReferenceIssue = (context, known, id, path) => {
|
|
111
|
+
if (!known.has(id))
|
|
112
|
+
context.addIssue({
|
|
113
|
+
code: "custom",
|
|
114
|
+
message: `unknown event id: ${id}`,
|
|
115
|
+
path,
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
const pairKey = (left, right) => [left, right].sort().join("\0");
|
|
119
|
+
const buildAdjacency = (language, context, known) => {
|
|
120
|
+
const adjacency = new Map([...known].map((id) => [id, new Set()]));
|
|
121
|
+
for (const [index, edge] of language.happens_before.entries()) {
|
|
122
|
+
const path = ["language", "happens_before", index];
|
|
123
|
+
addReferenceIssue(context, known, edge.before, [...path, "before"]);
|
|
124
|
+
addReferenceIssue(context, known, edge.after, [...path, "after"]);
|
|
125
|
+
if (edge.before === edge.after)
|
|
126
|
+
context.addIssue({
|
|
127
|
+
code: "custom",
|
|
128
|
+
message: "happens-before edges must reference distinct events",
|
|
129
|
+
path,
|
|
130
|
+
});
|
|
131
|
+
adjacency.get(edge.before)?.add(edge.after);
|
|
132
|
+
}
|
|
133
|
+
return adjacency;
|
|
134
|
+
};
|
|
135
|
+
const validateNotBefore = (language, context, known, reachable) => {
|
|
136
|
+
const pairs = new Set();
|
|
137
|
+
for (const [index, constraint] of language.not_before.entries()) {
|
|
138
|
+
const path = ["language", "not_before", index];
|
|
139
|
+
addReferenceIssue(context, known, constraint.event, [...path, "event"]);
|
|
140
|
+
addReferenceIssue(context, known, constraint.anchor, [...path, "anchor"]);
|
|
141
|
+
if (constraint.event === constraint.anchor)
|
|
142
|
+
context.addIssue({
|
|
143
|
+
code: "custom",
|
|
144
|
+
message: "not-before constraints must reference distinct events",
|
|
145
|
+
path,
|
|
146
|
+
});
|
|
147
|
+
const key = pairKey(constraint.event, constraint.anchor);
|
|
148
|
+
if (pairs.has(key))
|
|
149
|
+
context.addIssue({
|
|
150
|
+
code: "custom",
|
|
151
|
+
message: "not-before event pairs must be declared once",
|
|
152
|
+
path,
|
|
153
|
+
});
|
|
154
|
+
pairs.add(key);
|
|
155
|
+
if (reachable.get(constraint.event)?.has(constraint.anchor) === true)
|
|
156
|
+
context.addIssue({
|
|
157
|
+
code: "custom",
|
|
158
|
+
message: "not-before constraint conflicts with happens-before order",
|
|
159
|
+
path,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return pairs;
|
|
163
|
+
};
|
|
164
|
+
const graphHasCycle = (ids, adjacency) => {
|
|
165
|
+
const active = new Set();
|
|
166
|
+
const complete = new Set();
|
|
167
|
+
const visit = (current) => {
|
|
168
|
+
if (active.has(current))
|
|
169
|
+
return true;
|
|
170
|
+
if (complete.has(current))
|
|
171
|
+
return false;
|
|
172
|
+
active.add(current);
|
|
173
|
+
const cyclic = [...(adjacency.get(current) ?? [])].some(visit);
|
|
174
|
+
active.delete(current);
|
|
175
|
+
complete.add(current);
|
|
176
|
+
return cyclic;
|
|
177
|
+
};
|
|
178
|
+
return ids.some(visit);
|
|
179
|
+
};
|
|
180
|
+
const computeReachability = (ids, adjacency) => {
|
|
181
|
+
const reachable = new Map();
|
|
182
|
+
for (const origin of ids) {
|
|
183
|
+
const seen = new Set();
|
|
184
|
+
const pending = [...(adjacency.get(origin) ?? [])];
|
|
185
|
+
while (pending.length > 0) {
|
|
186
|
+
const current = pending.pop();
|
|
187
|
+
if (current === undefined || seen.has(current))
|
|
188
|
+
continue;
|
|
189
|
+
seen.add(current);
|
|
190
|
+
pending.push(...(adjacency.get(current) ?? []));
|
|
191
|
+
}
|
|
192
|
+
reachable.set(origin, seen);
|
|
193
|
+
}
|
|
194
|
+
return reachable;
|
|
195
|
+
};
|
|
196
|
+
const isOrdered = (reachable, left, right) => reachable.get(left)?.has(right) === true ||
|
|
197
|
+
reachable.get(right)?.has(left) === true;
|
|
198
|
+
const validateUnorderedGroups = (language, context, known, relations) => {
|
|
199
|
+
const pairs = new Set();
|
|
200
|
+
for (const [groupIndex, group] of language.unordered_groups.entries()) {
|
|
201
|
+
const path = ["language", "unordered_groups", groupIndex];
|
|
202
|
+
if (new Set(group.events).size !== group.events.length)
|
|
203
|
+
context.addIssue({
|
|
204
|
+
code: "custom",
|
|
205
|
+
message: "unordered groups must not repeat event ids",
|
|
206
|
+
path: [...path, "events"],
|
|
207
|
+
});
|
|
208
|
+
for (const [eventIndex, id] of group.events.entries())
|
|
209
|
+
addReferenceIssue(context, known, id, [...path, "events", eventIndex]);
|
|
210
|
+
for (const [leftIndex, leftId] of group.events.entries()) {
|
|
211
|
+
for (const rightId of group.events.slice(leftIndex + 1)) {
|
|
212
|
+
const key = pairKey(leftId, rightId);
|
|
213
|
+
if (pairs.has(key))
|
|
214
|
+
context.addIssue({
|
|
215
|
+
code: "custom",
|
|
216
|
+
message: "unordered event pairs must be declared once",
|
|
217
|
+
path,
|
|
218
|
+
});
|
|
219
|
+
pairs.add(key);
|
|
220
|
+
if (isOrdered(relations.reachable, leftId, rightId) ||
|
|
221
|
+
relations.notBeforePairs.has(key))
|
|
222
|
+
context.addIssue({
|
|
223
|
+
code: "custom",
|
|
224
|
+
message: "ordered events cannot also be declared unordered",
|
|
225
|
+
path,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return pairs;
|
|
231
|
+
};
|
|
232
|
+
const validateCompletePairDeclarations = (ids, relations, unorderedPairs, context) => {
|
|
233
|
+
for (const [leftIndex, leftId] of ids.entries())
|
|
234
|
+
for (const rightId of ids.slice(leftIndex + 1))
|
|
235
|
+
if (!isOrdered(relations.reachable, leftId, rightId) &&
|
|
236
|
+
!relations.notBeforePairs.has(pairKey(leftId, rightId)) &&
|
|
237
|
+
!unorderedPairs.has(pairKey(leftId, rightId)))
|
|
238
|
+
context.addIssue({
|
|
239
|
+
code: "custom",
|
|
240
|
+
message: `event pair ${leftId}/${rightId} must be explicitly ordered or unordered`,
|
|
241
|
+
path: ["language"],
|
|
242
|
+
});
|
|
243
|
+
};
|
|
244
|
+
const validatePartialOrder = (specification, context) => {
|
|
245
|
+
if (specification.language.kind !== "partial_order")
|
|
246
|
+
return;
|
|
247
|
+
const ids = specification.events.map(({ id }) => id);
|
|
248
|
+
const known = new Set(ids);
|
|
249
|
+
const adjacency = buildAdjacency(specification.language, context, known);
|
|
250
|
+
if (graphHasCycle(ids, adjacency))
|
|
251
|
+
context.addIssue({
|
|
252
|
+
code: "custom",
|
|
253
|
+
message: "happens-before graph must be acyclic",
|
|
254
|
+
path: ["language"],
|
|
255
|
+
});
|
|
256
|
+
const reachable = computeReachability(ids, adjacency);
|
|
257
|
+
const notBeforePairs = validateNotBefore(specification.language, context, known, reachable);
|
|
258
|
+
const relations = { reachable, notBeforePairs };
|
|
259
|
+
const unorderedPairs = validateUnorderedGroups(specification.language, context, known, relations);
|
|
260
|
+
validateCompletePairDeclarations(ids, relations, unorderedPairs, context);
|
|
261
|
+
for (const [kind, values] of [
|
|
262
|
+
["prefix", specification.language.prefix],
|
|
263
|
+
["suffix", specification.language.suffix],
|
|
264
|
+
])
|
|
265
|
+
for (const [index, id] of values.entries())
|
|
266
|
+
addReferenceIssue(context, known, id, ["language", kind, index]);
|
|
267
|
+
};
|
|
268
|
+
const countVariantEvents = (trace) => {
|
|
269
|
+
const counts = new Map();
|
|
270
|
+
for (const token of trace)
|
|
271
|
+
for (const id of typeof token === "string" ? [token] : token.unordered)
|
|
272
|
+
counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
273
|
+
return counts;
|
|
274
|
+
};
|
|
275
|
+
const validateFiniteTraces = (specification, context) => {
|
|
276
|
+
if (specification.language.kind !== "finite_traces")
|
|
277
|
+
return;
|
|
278
|
+
const known = new Set(specification.events.map(({ id }) => id));
|
|
279
|
+
const variantIds = new Set();
|
|
280
|
+
const variantTraces = new Set();
|
|
281
|
+
for (const [variantIndex, variant,] of specification.language.variants.entries()) {
|
|
282
|
+
const variantPath = ["language", "variants", variantIndex];
|
|
283
|
+
if (variantIds.has(variant.id))
|
|
284
|
+
context.addIssue({
|
|
285
|
+
code: "custom",
|
|
286
|
+
message: "finite trace variant ids must be unique",
|
|
287
|
+
path: [...variantPath, "id"],
|
|
288
|
+
});
|
|
289
|
+
variantIds.add(variant.id);
|
|
290
|
+
const canonicalTrace = canonicalTraceJson(variant.trace);
|
|
291
|
+
if (variantTraces.has(canonicalTrace))
|
|
292
|
+
context.addIssue({
|
|
293
|
+
code: "custom",
|
|
294
|
+
message: "finite trace variants must not duplicate a trace",
|
|
295
|
+
path: [...variantPath, "trace"],
|
|
296
|
+
});
|
|
297
|
+
variantTraces.add(canonicalTrace);
|
|
298
|
+
for (const [tokenIndex, token] of variant.trace.entries()) {
|
|
299
|
+
const values = typeof token === "string" ? [token] : token.unordered;
|
|
300
|
+
for (const [eventIndex, id] of values.entries())
|
|
301
|
+
addReferenceIssue(context, known, id, [
|
|
302
|
+
...variantPath,
|
|
303
|
+
"trace",
|
|
304
|
+
tokenIndex,
|
|
305
|
+
...(typeof token === "string" ? [] : ["unordered", eventIndex]),
|
|
306
|
+
]);
|
|
307
|
+
}
|
|
308
|
+
const counts = countVariantEvents(variant.trace);
|
|
309
|
+
for (const event of specification.events) {
|
|
310
|
+
const [minimum, maximum] = processTraceCardinalityBounds(event.cardinality);
|
|
311
|
+
const count = counts.get(event.id) ?? 0;
|
|
312
|
+
if (count < minimum || count > maximum)
|
|
313
|
+
context.addIssue({
|
|
314
|
+
code: "custom",
|
|
315
|
+
message: `variant ${variant.id} violates ${event.id} cardinality`,
|
|
316
|
+
path: [...variantPath, "trace"],
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
/** Strict, bounded concurrency model for Process Capture observations. */
|
|
322
|
+
export const processTraceSpecificationSchema = specificationShapeSchema.superRefine((specification, context) => {
|
|
323
|
+
const ids = new Set();
|
|
324
|
+
const predicates = new Set();
|
|
325
|
+
const ignoredFieldsBySource = new Map();
|
|
326
|
+
for (const [index, event] of specification.events.entries()) {
|
|
327
|
+
if (ids.has(event.id))
|
|
328
|
+
context.addIssue({
|
|
329
|
+
code: "custom",
|
|
330
|
+
message: "event ids must be unique",
|
|
331
|
+
path: ["events", index, "id"],
|
|
332
|
+
});
|
|
333
|
+
ids.add(event.id);
|
|
334
|
+
const ignoredFields = [...(event.ignore_fields ?? [])].sort();
|
|
335
|
+
if (ignoredFields.length > 0 &&
|
|
336
|
+
(typeof event.exact !== "object" ||
|
|
337
|
+
event.exact === null ||
|
|
338
|
+
Array.isArray(event.exact)))
|
|
339
|
+
context.addIssue({
|
|
340
|
+
code: "custom",
|
|
341
|
+
message: "ignored schedule fields require an exact object payload",
|
|
342
|
+
path: ["events", index, "ignore_fields"],
|
|
343
|
+
});
|
|
344
|
+
const ignoredKey = canonicalTraceJson(ignoredFields);
|
|
345
|
+
const priorIgnoredKey = ignoredFieldsBySource.get(event.source);
|
|
346
|
+
if (priorIgnoredKey !== undefined && priorIgnoredKey !== ignoredKey)
|
|
347
|
+
context.addIssue({
|
|
348
|
+
code: "custom",
|
|
349
|
+
message: "events from one source must use the same ignored schedule fields",
|
|
350
|
+
path: ["events", index, "ignore_fields"],
|
|
351
|
+
});
|
|
352
|
+
ignoredFieldsBySource.set(event.source, ignoredKey);
|
|
353
|
+
if (ignoredFields.some((field) => typeof event.exact === "object" &&
|
|
354
|
+
event.exact !== null &&
|
|
355
|
+
!Array.isArray(event.exact) &&
|
|
356
|
+
field in event.exact))
|
|
357
|
+
context.addIssue({
|
|
358
|
+
code: "custom",
|
|
359
|
+
message: "exact payload must omit every ignored schedule field",
|
|
360
|
+
path: ["events", index, "exact"],
|
|
361
|
+
});
|
|
362
|
+
const predicate = `${event.source}\0${canonicalTraceJson(comparableTracePayload(event.exact, ignoredFields))}`;
|
|
363
|
+
if (predicates.has(predicate))
|
|
364
|
+
context.addIssue({
|
|
365
|
+
code: "custom",
|
|
366
|
+
message: "exact event predicates must not overlap",
|
|
367
|
+
path: ["events", index],
|
|
368
|
+
});
|
|
369
|
+
predicates.add(predicate);
|
|
370
|
+
}
|
|
371
|
+
validatePartialOrder(specification, context);
|
|
372
|
+
validateFiniteTraces(specification, context);
|
|
373
|
+
});
|