rea-agents 0.3.0 → 0.5.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 +139 -21
- package/bridge/hopper_bridge.py +156 -14
- package/dist/application/AnalysisProvider.js +12 -1
- package/dist/application/ArtifactExtraction.js +166 -0
- package/dist/application/ArtifactGraphConstruction.js +257 -0
- package/dist/application/ArtifactInventory.js +253 -0
- package/dist/application/BinarySession.js +174 -14
- package/dist/application/CompositeProvider.js +73 -0
- package/dist/application/DirectAnalysis.js +44 -3
- package/dist/application/Doctor.js +35 -6
- package/dist/application/EnhancedTools.js +10 -7
- package/dist/application/EvidenceBundleCommands.js +51 -0
- package/dist/application/EvidenceBundleFiles.js +127 -0
- package/dist/application/EvidenceLedger.js +225 -18
- package/dist/application/FilesystemSnapshot.js +124 -0
- package/dist/application/LinuxHopper.js +186 -0
- package/dist/application/LoopbackReplay.js +195 -37
- package/dist/application/ProcessHarness.js +200 -191
- package/dist/application/ProcessNormalization.js +44 -0
- package/dist/application/ProcessOwnership.js +105 -0
- package/dist/application/ProcessSampling.js +284 -0
- package/dist/application/RealHopperAssertions.js +116 -0
- package/dist/application/ReferenceSourceImport.js +182 -0
- package/dist/application/ReferenceSourceImportEntries.js +122 -0
- package/dist/application/ReferenceSourceImportPolicy.js +73 -0
- package/dist/application/ReferenceSourceImportTypes.js +18 -0
- package/dist/application/ReferenceSourceVcsAdapter.js +34 -0
- package/dist/application/Setup.js +192 -41
- package/dist/application/Uninstall.js +130 -0
- package/dist/application/runtime.js +8 -1
- package/dist/artifacts/ArtifactPaths.js +51 -0
- package/dist/artifacts/ArtifactProvider.js +130 -0
- package/dist/artifacts/ArtifactReader.js +9 -0
- package/dist/artifacts/AsarArtifactReader.js +62 -0
- package/dist/artifacts/DirectoryArtifactReader.js +107 -0
- package/dist/artifacts/MachOSliceArtifactReader.js +66 -0
- package/dist/artifacts/SafeOutputTree.js +199 -0
- package/dist/artifacts/StreamBytes.js +10 -0
- package/dist/artifacts/ZipArtifactReader.js +109 -0
- package/dist/cli.js +229 -21
- package/dist/cliEvidenceCommands.js +68 -0
- package/dist/cliLogging.js +21 -0
- package/dist/config.js +35 -1
- package/dist/contracts/artifactComparisonExample.js +95 -0
- package/dist/contracts/artifactToolContracts.js +84 -0
- package/dist/contracts/enhancedInputs.js +4 -0
- package/dist/contracts/functionComparisonExample.js +57 -0
- package/dist/contracts/investigationExamples.js +118 -0
- package/dist/contracts/nativeToolContracts.js +52 -0
- package/dist/contracts/processCaptureExample.js +25 -0
- package/dist/contracts/toolContractExamples.js +69 -0
- package/dist/contracts/toolContracts.js +99 -36
- package/dist/contracts/toolOutputSchemas.js +156 -82
- package/dist/contracts/unknownContractExamples.js +33 -0
- package/dist/domain/artifactComparison.js +273 -0
- package/dist/domain/artifactGraph.js +194 -0
- package/dist/domain/artifactInventoryEvidence.js +150 -0
- package/dist/domain/binaryTarget.js +55 -0
- package/dist/domain/bundleComparison.js +266 -0
- package/dist/domain/callPath.js +346 -0
- package/dist/domain/changedBehavior.js +294 -0
- package/dist/domain/errors.js +199 -5
- package/dist/domain/evidence.js +41 -10
- package/dist/domain/evidenceBundle.js +187 -6
- package/dist/domain/functionComparison.js +201 -0
- package/dist/domain/functionComparisonNormalization.js +112 -0
- package/dist/domain/functionComparisonResults.js +54 -0
- package/dist/domain/functionComparisonSchemas.js +82 -0
- package/dist/domain/functionDossierEvidence.js +171 -0
- package/dist/domain/hopperValues.js +35 -8
- package/dist/domain/nativeInspection.js +142 -0
- package/dist/domain/processCapture.js +152 -54
- package/dist/domain/processComparison.js +106 -0
- package/dist/domain/reconstructionUnknowns.js +90 -0
- package/dist/domain/reconstructionVerification.js +285 -0
- package/dist/domain/reconstructionVerificationSchemas.js +126 -0
- package/dist/domain/referenceSourceClassification.js +496 -0
- package/dist/domain/referenceSourceGraph.js +376 -0
- package/dist/domain/referenceSourceImportParsing.js +235 -0
- package/dist/domain/referenceSourcePolicy.js +1 -0
- package/dist/domain/residualUnknown.js +239 -0
- package/dist/domain/staticRuntimeCorrelation.js +375 -0
- package/dist/hopper/BridgeLauncher.js +39 -3
- package/dist/hopper/HopperClient.js +14 -5
- package/dist/hopper/HopperProvider.js +57 -22
- package/dist/hopper/protocol.js +13 -2
- package/dist/identity.js +1 -0
- package/dist/main.js +5 -1
- package/dist/native/CommandRunner.js +156 -0
- package/dist/native/NativeMacOSProvider.js +306 -0
- package/dist/native/NativeMachoInspection.js +135 -0
- package/dist/native/parsers/codesign.js +55 -0
- package/dist/native/parsers/demangle.js +26 -0
- package/dist/native/parsers/dyldInfo.js +25 -0
- package/dist/native/parsers/lipo.js +67 -0
- package/dist/native/parsers/otool.js +193 -0
- package/dist/native/parsers/plist.js +23 -0
- package/dist/reference/ReferenceSourceReader.js +73 -0
- package/dist/reference/ReferenceSourceReaderEntries.js +206 -0
- package/dist/reference/ReferenceSourceReaderErrors.js +19 -0
- package/dist/reference/ReferenceSourceReaderFile.js +119 -0
- package/dist/reference/ReferenceSourceReaderPaths.js +23 -0
- package/dist/reference/ReferenceSourceReaderTypes.js +2 -0
- package/dist/reference/ReferenceSourceReaderValidate.js +71 -0
- package/dist/server/createServer.js +31 -5
- package/dist/server/recordDerivedEvidence.js +10 -0
- package/dist/server/registerArtifactComparisonTool.js +62 -0
- package/dist/server/registerArtifactTools.js +6 -0
- package/dist/server/registerBundleComparisonTool.js +47 -0
- package/dist/server/registerEnhancedTools.js +64 -12
- package/dist/server/registerEvidenceTools.js +36 -0
- package/dist/server/registerFunctionComparisonTool.js +68 -0
- package/dist/server/registerInvestigationTools.js +224 -0
- package/dist/server/registerNativeTools.js +6 -0
- package/dist/server/registerOfficialTools.js +47 -14
- package/dist/server/registerProcessComparisonTool.js +106 -0
- package/dist/server/registerSessionTools.js +179 -70
- package/dist/server/sessionEvidence.js +28 -0
- package/dist/server/sessionToolPolicies.js +64 -0
- package/dist/server/toolRegistrationOptions.js +7 -0
- package/dist/server/toolResult.js +8 -5
- package/install.sh +198 -0
- package/package.json +18 -1
- package/scripts/rea.mjs +5 -1
- package/skills/rea-analysis/SKILL.md +77 -2
|
@@ -2,18 +2,41 @@ import { resolve } from "node:path";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
const positiveBudget = z.number().int().positive();
|
|
4
4
|
const timedEventBase = { at_ms: z.number().int().nonnegative() };
|
|
5
|
+
const environmentName = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/u);
|
|
6
|
+
const reservedEnvironment = new Set([
|
|
7
|
+
"HOME",
|
|
8
|
+
"TERM",
|
|
9
|
+
"REA_PROCESS_RUN_ID",
|
|
10
|
+
"REA_REPLAY_HTTP_URL",
|
|
11
|
+
"REA_REPLAY_WEBSOCKET_URL",
|
|
12
|
+
]);
|
|
13
|
+
const normalizationSchema = z.object({
|
|
14
|
+
paths: z.boolean(),
|
|
15
|
+
pids: z.boolean(),
|
|
16
|
+
ports: z.boolean(),
|
|
17
|
+
time_bucket_ms: positiveBudget.max(60_000),
|
|
18
|
+
patterns: z.array(z.object({
|
|
19
|
+
pattern: z.string().max(500),
|
|
20
|
+
replacement: z.string().max(100),
|
|
21
|
+
})),
|
|
22
|
+
});
|
|
5
23
|
/** Exact boundary schema for bounded dynamic process scenarios. */
|
|
6
24
|
export const processScenarioSchema = z
|
|
7
25
|
.object({
|
|
8
26
|
approved: z
|
|
9
27
|
.literal(true)
|
|
10
28
|
.describe("Explicit per-call acknowledgement that this operation launches the target"),
|
|
29
|
+
unknown_registry_approved: z
|
|
30
|
+
.literal(true)
|
|
31
|
+
.optional()
|
|
32
|
+
.describe("Explicit approval to record capture residuals durably"),
|
|
11
33
|
executable: z.string().startsWith("/"),
|
|
12
34
|
arguments: z.array(z.string()).max(256).default([]),
|
|
13
35
|
working_directory: z.string().startsWith("/"),
|
|
14
|
-
environment: z.record(
|
|
15
|
-
inherit_environment: z.array(
|
|
16
|
-
secret_aliases: z.array(
|
|
36
|
+
environment: z.record(environmentName, z.string()).default({}),
|
|
37
|
+
inherit_environment: z.array(environmentName).max(64).default([]),
|
|
38
|
+
secret_aliases: z.array(environmentName).max(64).default([]),
|
|
39
|
+
network_access: z.literal("host").default("host"),
|
|
17
40
|
filesystem_roots: z.array(z.string().startsWith("/")).max(16).default([]),
|
|
18
41
|
events: z
|
|
19
42
|
.array(z.discriminatedUnion("type", [
|
|
@@ -46,6 +69,10 @@ export const processScenarioSchema = z
|
|
|
46
69
|
files: positiveBudget.max(100_000).default(10_000),
|
|
47
70
|
file_bytes: positiveBudget.max(100_000_000).default(10_000_000),
|
|
48
71
|
processes: positiveBudget.max(10_000).default(1_000),
|
|
72
|
+
protocol_events: positiveBudget.max(100_000).default(10_000),
|
|
73
|
+
protocol_body_bytes: positiveBudget.max(10_000_000).default(1_000_000),
|
|
74
|
+
connections: positiveBudget.max(1_000).default(100),
|
|
75
|
+
filesystem_depth: z.number().int().min(1).max(64).default(16),
|
|
49
76
|
})
|
|
50
77
|
.default({
|
|
51
78
|
output_bytes: 1_000_000,
|
|
@@ -53,6 +80,10 @@ export const processScenarioSchema = z
|
|
|
53
80
|
files: 10_000,
|
|
54
81
|
file_bytes: 10_000_000,
|
|
55
82
|
processes: 1_000,
|
|
83
|
+
protocol_events: 10_000,
|
|
84
|
+
protocol_body_bytes: 1_000_000,
|
|
85
|
+
connections: 100,
|
|
86
|
+
filesystem_depth: 16,
|
|
56
87
|
}),
|
|
57
88
|
normalization: z
|
|
58
89
|
.object({
|
|
@@ -83,6 +114,12 @@ export const processScenarioSchema = z
|
|
|
83
114
|
path: z.string().startsWith("/"),
|
|
84
115
|
status: z.number().int().min(100).max(599),
|
|
85
116
|
body: z.string().max(1_000_000),
|
|
117
|
+
request_headers: z.record(z.string(), z.string()).default({}),
|
|
118
|
+
request_body: z.string().max(1_000_000).optional(),
|
|
119
|
+
response_headers: z.record(z.string(), z.string()).default({}),
|
|
120
|
+
delay_ms: z.number().int().nonnegative().max(30_000).default(0),
|
|
121
|
+
disconnect: z.boolean().default(false),
|
|
122
|
+
max_calls: z.number().int().min(1).max(100).default(1),
|
|
86
123
|
}))
|
|
87
124
|
.max(100)
|
|
88
125
|
.default([]),
|
|
@@ -90,8 +127,29 @@ export const processScenarioSchema = z
|
|
|
90
127
|
.array(z.string().max(1_000_000))
|
|
91
128
|
.max(100)
|
|
92
129
|
.default([]),
|
|
130
|
+
websocket_connections: z
|
|
131
|
+
.array(z.object({
|
|
132
|
+
messages: z
|
|
133
|
+
.array(z.object({
|
|
134
|
+
data: z.string().max(1_000_000),
|
|
135
|
+
delay_ms: z
|
|
136
|
+
.number()
|
|
137
|
+
.int()
|
|
138
|
+
.nonnegative()
|
|
139
|
+
.max(30_000)
|
|
140
|
+
.default(0),
|
|
141
|
+
}))
|
|
142
|
+
.max(100),
|
|
143
|
+
disconnect_after: z.boolean().default(false),
|
|
144
|
+
}))
|
|
145
|
+
.max(100)
|
|
146
|
+
.default([]),
|
|
93
147
|
})
|
|
94
|
-
.default({
|
|
148
|
+
.default({
|
|
149
|
+
http: [],
|
|
150
|
+
websocket_messages: [],
|
|
151
|
+
websocket_connections: [],
|
|
152
|
+
}),
|
|
95
153
|
})
|
|
96
154
|
.strict()
|
|
97
155
|
.superRefine((scenario, context) => {
|
|
@@ -114,6 +172,34 @@ export const processScenarioSchema = z
|
|
|
114
172
|
});
|
|
115
173
|
}
|
|
116
174
|
}
|
|
175
|
+
const explicit = new Set(Object.keys(scenario.environment));
|
|
176
|
+
for (const name of [...explicit, ...scenario.inherit_environment]) {
|
|
177
|
+
if (reservedEnvironment.has(name)) {
|
|
178
|
+
context.addIssue({
|
|
179
|
+
code: "custom",
|
|
180
|
+
message: `${name} is reserved by the process adapter`,
|
|
181
|
+
path: ["environment", name],
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
for (const name of scenario.inherit_environment) {
|
|
186
|
+
if (explicit.has(name)) {
|
|
187
|
+
context.addIssue({
|
|
188
|
+
code: "custom",
|
|
189
|
+
message: "an environment name cannot be explicit and inherited",
|
|
190
|
+
path: ["inherit_environment"],
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
for (const event of scenario.events) {
|
|
195
|
+
if (event.at_ms > scenario.timeout_ms) {
|
|
196
|
+
context.addIssue({
|
|
197
|
+
code: "custom",
|
|
198
|
+
message: "event occurs after the scenario timeout",
|
|
199
|
+
path: ["events"],
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
117
203
|
});
|
|
118
204
|
/** Parse untrusted process scenario input and apply safe default budgets. */
|
|
119
205
|
export const parseProcessScenario = (input) => processScenarioSchema.parse(input);
|
|
@@ -123,6 +209,11 @@ const isWithin = (candidate, root) => resolve(candidate) === resolve(root) ||
|
|
|
123
209
|
export const authorizeProcessScenario = (scenario, policy) => {
|
|
124
210
|
if (!policy.enabled)
|
|
125
211
|
return { allowed: false, reason: "process capture is disabled" };
|
|
212
|
+
if (scenario.network_access === "host" && !policy.allowExternalNetwork)
|
|
213
|
+
return {
|
|
214
|
+
allowed: false,
|
|
215
|
+
reason: "host network access is not approved by operator policy",
|
|
216
|
+
};
|
|
126
217
|
if (!policy.executableRoots.some((root) => isWithin(scenario.executable, root))) {
|
|
127
218
|
return { allowed: false, reason: "executable is outside approved roots" };
|
|
128
219
|
}
|
|
@@ -152,7 +243,8 @@ export const authorizeProcessScenario = (scenario, policy) => {
|
|
|
152
243
|
};
|
|
153
244
|
/** Exact serialized shape of a bounded process capture. */
|
|
154
245
|
export const processCaptureSchema = z.object({
|
|
155
|
-
schema_version: z.literal(
|
|
246
|
+
schema_version: z.literal(2),
|
|
247
|
+
normalization: normalizationSchema,
|
|
156
248
|
frames: z.array(z.object({
|
|
157
249
|
sequence: z.number().int().nonnegative(),
|
|
158
250
|
at_ms: z.number().int().nonnegative(),
|
|
@@ -161,6 +253,7 @@ export const processCaptureSchema = z.object({
|
|
|
161
253
|
exit: z.object({
|
|
162
254
|
code: z.number().int().nullable(),
|
|
163
255
|
signal: z.number().int().nullable(),
|
|
256
|
+
reason: z.enum(["exited", "timeout", "idle_timeout"]),
|
|
164
257
|
}),
|
|
165
258
|
process_samples: z.array(z.object({
|
|
166
259
|
at_ms: z.number().int().nonnegative(),
|
|
@@ -170,11 +263,18 @@ export const processCaptureSchema = z.object({
|
|
|
170
263
|
})),
|
|
171
264
|
protocol_events: z.array(z.object({
|
|
172
265
|
sequence: z.number().int().nonnegative(),
|
|
266
|
+
at_ms: z.number().int().nonnegative(),
|
|
173
267
|
protocol: z.enum(["http", "websocket"]),
|
|
174
268
|
direction: z.enum(["request", "response", "received", "sent"]),
|
|
175
269
|
method: z.string().nullable(),
|
|
176
270
|
path: z.string().nullable(),
|
|
177
271
|
data: z.string(),
|
|
272
|
+
outcome: z.enum([
|
|
273
|
+
"matched",
|
|
274
|
+
"unmatched",
|
|
275
|
+
"script_exhausted",
|
|
276
|
+
"disconnected",
|
|
277
|
+
]),
|
|
178
278
|
})),
|
|
179
279
|
files_before: z.array(z.object({
|
|
180
280
|
path: z.string(),
|
|
@@ -198,55 +298,53 @@ export const processCaptureSchema = z.object({
|
|
|
198
298
|
.nullable(),
|
|
199
299
|
symlink_target: z.string().nullable(),
|
|
200
300
|
})),
|
|
301
|
+
filesystem_effects: z.array(z.object({
|
|
302
|
+
path: z.string(),
|
|
303
|
+
status: z.enum(["created", "modified", "deleted", "unchanged"]),
|
|
304
|
+
before: z
|
|
305
|
+
.object({
|
|
306
|
+
path: z.string(),
|
|
307
|
+
type: z.enum(["file", "directory", "symlink", "other"]),
|
|
308
|
+
mode: z.number().int().nonnegative(),
|
|
309
|
+
size: z.number().int().nonnegative(),
|
|
310
|
+
sha256: z
|
|
311
|
+
.string()
|
|
312
|
+
.regex(/^[a-f0-9]{64}$/u)
|
|
313
|
+
.nullable(),
|
|
314
|
+
symlink_target: z.string().nullable(),
|
|
315
|
+
})
|
|
316
|
+
.nullable(),
|
|
317
|
+
after: z
|
|
318
|
+
.object({
|
|
319
|
+
path: z.string(),
|
|
320
|
+
type: z.enum(["file", "directory", "symlink", "other"]),
|
|
321
|
+
mode: z.number().int().nonnegative(),
|
|
322
|
+
size: z.number().int().nonnegative(),
|
|
323
|
+
sha256: z
|
|
324
|
+
.string()
|
|
325
|
+
.regex(/^[a-f0-9]{64}$/u)
|
|
326
|
+
.nullable(),
|
|
327
|
+
symlink_target: z.string().nullable(),
|
|
328
|
+
})
|
|
329
|
+
.nullable(),
|
|
330
|
+
})),
|
|
201
331
|
truncated: z.boolean(),
|
|
202
332
|
limitations: z.array(z.string()),
|
|
333
|
+
residual_unknowns: z.array(z.object({
|
|
334
|
+
scope: z.enum([
|
|
335
|
+
"terminal",
|
|
336
|
+
"exit",
|
|
337
|
+
"process",
|
|
338
|
+
"filesystem",
|
|
339
|
+
"protocol",
|
|
340
|
+
"cleanup",
|
|
341
|
+
"network",
|
|
342
|
+
]),
|
|
343
|
+
reason: z.string(),
|
|
344
|
+
})),
|
|
345
|
+
cleanup: z.object({
|
|
346
|
+
owned_process_group: z.literal("verified"),
|
|
347
|
+
temporary_root: z.literal("removed"),
|
|
348
|
+
}),
|
|
203
349
|
});
|
|
204
|
-
|
|
205
|
-
/** Compare bounded captures without claiming equality for incomplete observations. */
|
|
206
|
-
export const compareProcessCaptures = (left, right) => {
|
|
207
|
-
if (left.truncated || right.truncated) {
|
|
208
|
-
return {
|
|
209
|
-
status: "truncated",
|
|
210
|
-
terminal: "truncated",
|
|
211
|
-
exit: "truncated",
|
|
212
|
-
filesystem: "truncated",
|
|
213
|
-
protocol: "truncated",
|
|
214
|
-
process: "truncated",
|
|
215
|
-
limitations: ["At least one capture is truncated."],
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
const terminal = left.frames.map(({ data }) => data).join("") ===
|
|
219
|
-
right.frames.map(({ data }) => data).join("")
|
|
220
|
-
? "unchanged"
|
|
221
|
-
: "changed";
|
|
222
|
-
const exit = JSON.stringify(left.exit) === JSON.stringify(right.exit)
|
|
223
|
-
? "unchanged"
|
|
224
|
-
: "changed";
|
|
225
|
-
const filesystem = stableFiles(left.files_after) === stableFiles(right.files_after)
|
|
226
|
-
? "unchanged"
|
|
227
|
-
: "changed";
|
|
228
|
-
const protocol = JSON.stringify(left.protocol_events) ===
|
|
229
|
-
JSON.stringify(right.protocol_events)
|
|
230
|
-
? "unchanged"
|
|
231
|
-
: "changed";
|
|
232
|
-
const process = JSON.stringify(left.process_samples.map(({ command }) => command)) ===
|
|
233
|
-
JSON.stringify(right.process_samples.map(({ command }) => command))
|
|
234
|
-
? "unchanged"
|
|
235
|
-
: "changed";
|
|
236
|
-
const status = terminal === "unchanged" &&
|
|
237
|
-
exit === "unchanged" &&
|
|
238
|
-
filesystem === "unchanged" &&
|
|
239
|
-
protocol === "unchanged" &&
|
|
240
|
-
process === "unchanged"
|
|
241
|
-
? "unchanged"
|
|
242
|
-
: "changed";
|
|
243
|
-
return {
|
|
244
|
-
status,
|
|
245
|
-
terminal,
|
|
246
|
-
exit,
|
|
247
|
-
filesystem,
|
|
248
|
-
protocol,
|
|
249
|
-
process,
|
|
250
|
-
limitations: [...left.limitations, ...right.limitations],
|
|
251
|
-
};
|
|
252
|
-
};
|
|
350
|
+
export { compareProcessCaptures, comparisonStatusSchema, processCaptureComparisonSchema, } from "./processComparison.js";
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** Comparison classification that never equates incomplete evidence. */
|
|
3
|
+
export const comparisonStatusSchema = z.enum([
|
|
4
|
+
"unchanged",
|
|
5
|
+
"added",
|
|
6
|
+
"removed",
|
|
7
|
+
"changed",
|
|
8
|
+
"truncated",
|
|
9
|
+
"unknown",
|
|
10
|
+
]);
|
|
11
|
+
/** Pure normalized comparison between two captures. */
|
|
12
|
+
export const processCaptureComparisonSchema = z
|
|
13
|
+
.object({
|
|
14
|
+
status: comparisonStatusSchema,
|
|
15
|
+
terminal: comparisonStatusSchema,
|
|
16
|
+
exit: comparisonStatusSchema,
|
|
17
|
+
filesystem: comparisonStatusSchema,
|
|
18
|
+
protocol: comparisonStatusSchema,
|
|
19
|
+
process: comparisonStatusSchema,
|
|
20
|
+
limitations: z.array(z.string()),
|
|
21
|
+
})
|
|
22
|
+
.superRefine((comparison, context) => {
|
|
23
|
+
const dimensions = [
|
|
24
|
+
comparison.terminal,
|
|
25
|
+
comparison.exit,
|
|
26
|
+
comparison.filesystem,
|
|
27
|
+
comparison.protocol,
|
|
28
|
+
comparison.process,
|
|
29
|
+
];
|
|
30
|
+
const expected = dimensions.includes("truncated")
|
|
31
|
+
? "truncated"
|
|
32
|
+
: dimensions.some((status) => ["added", "removed", "changed"].includes(status))
|
|
33
|
+
? "changed"
|
|
34
|
+
: dimensions.includes("unknown")
|
|
35
|
+
? "unknown"
|
|
36
|
+
: "unchanged";
|
|
37
|
+
if (comparison.status !== expected)
|
|
38
|
+
context.addIssue({
|
|
39
|
+
code: "custom",
|
|
40
|
+
message: "Process comparison status contradicts its dimensions",
|
|
41
|
+
path: ["status"],
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
const stableFiles = (capture) => JSON.stringify([...capture.files_after].sort((left, right) => left.path.localeCompare(right.path)));
|
|
45
|
+
const classifyCollection = (left, right) => {
|
|
46
|
+
if (JSON.stringify(left) === JSON.stringify(right))
|
|
47
|
+
return "unchanged";
|
|
48
|
+
if (left.length === 0)
|
|
49
|
+
return "added";
|
|
50
|
+
if (right.length === 0)
|
|
51
|
+
return "removed";
|
|
52
|
+
return "changed";
|
|
53
|
+
};
|
|
54
|
+
const hasUnknown = (capture, scope) => capture.residual_unknowns.some((item) => item.scope === scope);
|
|
55
|
+
const comparisonStatus = (dimensions) => {
|
|
56
|
+
if (dimensions.some((status) => ["changed", "added", "removed"].includes(status)))
|
|
57
|
+
return "changed";
|
|
58
|
+
return dimensions.includes("unknown") ? "unknown" : "unchanged";
|
|
59
|
+
};
|
|
60
|
+
/** Compare bounded captures without equating missing or incompatible evidence. */
|
|
61
|
+
export const compareProcessCaptures = (left, right) => {
|
|
62
|
+
if (left.truncated || right.truncated) {
|
|
63
|
+
return {
|
|
64
|
+
status: "truncated",
|
|
65
|
+
terminal: "truncated",
|
|
66
|
+
exit: "truncated",
|
|
67
|
+
filesystem: "truncated",
|
|
68
|
+
protocol: "truncated",
|
|
69
|
+
process: "truncated",
|
|
70
|
+
limitations: ["At least one capture is truncated."],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const sameNormalization = JSON.stringify(left.normalization) === JSON.stringify(right.normalization);
|
|
74
|
+
const classify = (scope, leftValues, rightValues) => !sameNormalization || hasUnknown(left, scope) || hasUnknown(right, scope)
|
|
75
|
+
? "unknown"
|
|
76
|
+
: classifyCollection(leftValues, rightValues);
|
|
77
|
+
const terminal = classify("terminal", left.frames.map(({ data }) => data), right.frames.map(({ data }) => data));
|
|
78
|
+
const exit = hasUnknown(left, "exit") || hasUnknown(right, "exit")
|
|
79
|
+
? "unknown"
|
|
80
|
+
: JSON.stringify(left.exit) === JSON.stringify(right.exit)
|
|
81
|
+
? "unchanged"
|
|
82
|
+
: "changed";
|
|
83
|
+
const filesystem = !hasUnknown(left, "filesystem") &&
|
|
84
|
+
!hasUnknown(right, "filesystem") &&
|
|
85
|
+
sameNormalization &&
|
|
86
|
+
stableFiles(left) === stableFiles(right)
|
|
87
|
+
? "unchanged"
|
|
88
|
+
: classify("filesystem", left.filesystem_effects, right.filesystem_effects);
|
|
89
|
+
const protocol = classify("protocol", left.protocol_events, right.protocol_events);
|
|
90
|
+
const process = classify("process", left.process_samples, right.process_samples);
|
|
91
|
+
return {
|
|
92
|
+
status: comparisonStatus([terminal, exit, filesystem, protocol, process]),
|
|
93
|
+
terminal,
|
|
94
|
+
exit,
|
|
95
|
+
filesystem,
|
|
96
|
+
protocol,
|
|
97
|
+
process,
|
|
98
|
+
limitations: [
|
|
99
|
+
...left.limitations,
|
|
100
|
+
...right.limitations,
|
|
101
|
+
...left.residual_unknowns.map(({ reason }) => reason),
|
|
102
|
+
...right.residual_unknowns.map(({ reason }) => reason),
|
|
103
|
+
...(sameNormalization ? [] : ["Capture normalization rules differ."]),
|
|
104
|
+
],
|
|
105
|
+
};
|
|
106
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/** Select the latest revision of each residual unknown. */
|
|
2
|
+
export const reconstructionUnknownHeads = (revisions) => {
|
|
3
|
+
const heads = new Map();
|
|
4
|
+
for (const item of revisions) {
|
|
5
|
+
const current = heads.get(item.unknown_id);
|
|
6
|
+
if (current === undefined || current.revision < item.revision)
|
|
7
|
+
heads.set(item.unknown_id, item);
|
|
8
|
+
}
|
|
9
|
+
return heads;
|
|
10
|
+
};
|
|
11
|
+
/** Find active unknowns that affect a comparison or one of its sources. */
|
|
12
|
+
export const reconstructionClaimUnknowns = (heads, comparisonId, sourceIds) => {
|
|
13
|
+
const evidenceIds = new Set([comparisonId, ...sourceIds]);
|
|
14
|
+
const relevantIds = new Set([...heads.values()]
|
|
15
|
+
.filter((item) => item.status !== "resolved" &&
|
|
16
|
+
[
|
|
17
|
+
...item.supporting_evidence_ids,
|
|
18
|
+
...item.contradicting_evidence_ids,
|
|
19
|
+
].some((id) => evidenceIds.has(id)))
|
|
20
|
+
.map(({ unknown_id: id }) => id));
|
|
21
|
+
const pending = [...relevantIds];
|
|
22
|
+
while (pending.length > 0) {
|
|
23
|
+
const currentId = pending.pop();
|
|
24
|
+
if (currentId === undefined)
|
|
25
|
+
continue;
|
|
26
|
+
const current = heads.get(currentId);
|
|
27
|
+
if (current === undefined)
|
|
28
|
+
continue;
|
|
29
|
+
for (const relationship of current.relationships) {
|
|
30
|
+
if (relationship.type === "related-to")
|
|
31
|
+
continue;
|
|
32
|
+
const dependency = heads.get(relationship.unknown_id);
|
|
33
|
+
if (dependency === undefined ||
|
|
34
|
+
dependency.status === "resolved" ||
|
|
35
|
+
relevantIds.has(dependency.unknown_id))
|
|
36
|
+
continue;
|
|
37
|
+
relevantIds.add(dependency.unknown_id);
|
|
38
|
+
pending.push(dependency.unknown_id);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return [...relevantIds]
|
|
42
|
+
.flatMap((id) => {
|
|
43
|
+
const unknown = heads.get(id);
|
|
44
|
+
return unknown === undefined ? [] : [unknown];
|
|
45
|
+
})
|
|
46
|
+
.sort((left, right) => left.unknown_id.localeCompare(right.unknown_id, "en"));
|
|
47
|
+
};
|
|
48
|
+
/** Aggregate and bound deterministic probes for unresolved claims. */
|
|
49
|
+
export const reconstructionProbes = (results, heads) => {
|
|
50
|
+
const output = new Map();
|
|
51
|
+
for (const result of results.filter(({ status }) => status === "unknown")) {
|
|
52
|
+
const unknowns = result.unknown_ids.flatMap((id) => {
|
|
53
|
+
const item = heads.get(id);
|
|
54
|
+
return item === undefined ? [] : [item];
|
|
55
|
+
});
|
|
56
|
+
const probes = unknowns.flatMap(({ recommended_probes: items }) => items);
|
|
57
|
+
const candidates = probes.length > 0 ? probes : [defaultProbe(result.kind)];
|
|
58
|
+
for (const probe of candidates) {
|
|
59
|
+
const key = `${probe.operation}\0${probe.rationale}`;
|
|
60
|
+
const current = output.get(key) ?? {
|
|
61
|
+
...probe,
|
|
62
|
+
claim_ids: [],
|
|
63
|
+
unknown_ids: [],
|
|
64
|
+
};
|
|
65
|
+
current.claim_ids = uniqueSorted([...current.claim_ids, result.claim_id]);
|
|
66
|
+
current.unknown_ids = uniqueSorted([
|
|
67
|
+
...current.unknown_ids,
|
|
68
|
+
...result.unknown_ids,
|
|
69
|
+
]);
|
|
70
|
+
output.set(key, current);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const sorted = [...output.values()].sort((left, right) => `${left.operation}\0${left.rationale}`.localeCompare(`${right.operation}\0${right.rationale}`, "en"));
|
|
74
|
+
return { items: sorted.slice(0, 2_000), truncated: sorted.length > 2_000 };
|
|
75
|
+
};
|
|
76
|
+
const defaultProbe = (kind) => kind === "behavioral"
|
|
77
|
+
? {
|
|
78
|
+
operation: "capture_process_scenario",
|
|
79
|
+
rationale: "Repeat both scenarios under one controlled environment.",
|
|
80
|
+
}
|
|
81
|
+
: kind === "structural-function"
|
|
82
|
+
? {
|
|
83
|
+
operation: "analyze_function",
|
|
84
|
+
rationale: "Capture complete function dossiers under equal limits.",
|
|
85
|
+
}
|
|
86
|
+
: {
|
|
87
|
+
operation: "inventory_artifact",
|
|
88
|
+
rationale: "Capture complete artifact inventories under equal limits.",
|
|
89
|
+
};
|
|
90
|
+
const uniqueSorted = (values) => [...new Set(values)].sort((left, right) => left.localeCompare(right, "en"));
|