rea-agents 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -10
- package/dist/application/AndroidApplicationService.js +17 -0
- package/dist/application/AppleApplicationService.js +17 -0
- package/dist/application/ClientRegistrationStatus.js +11 -4
- package/dist/application/InventoryProjectionEvidence.js +41 -0
- package/dist/application/InvestigationProviders.js +12 -0
- package/dist/application/ProcessPairedExperiment.js +72 -0
- package/dist/application/Setup.js +3 -3
- package/dist/application/SetupClientConfiguration.js +10 -6
- package/dist/application/SetupPlan.js +3 -2
- package/dist/application/SetupSkill.js +44 -40
- package/dist/application/Uninstall.js +7 -1
- package/dist/application/Upgrade.js +16 -1
- package/dist/cli/setupCommands.js +8 -3
- package/dist/cliSetup.js +29 -12
- package/dist/contracts/electronToolContracts.js +51 -3
- package/dist/contracts/managedToolContracts.js +7 -2
- package/dist/contracts/sessionStatusContract.js +26 -0
- package/dist/contracts/toolContractTypes.js +12 -1
- package/dist/contracts/toolContracts.js +3 -10
- package/dist/contracts/toolOutputSchemaGroups.js +51 -9
- package/dist/contracts/toolOutputSchemaPrimitives.js +33 -32
- package/dist/domain/androidApplication.js +206 -0
- package/dist/domain/appleApplication.js +221 -0
- package/dist/domain/boundedCartesianProjection.js +15 -0
- package/dist/domain/processComparison.js +19 -0
- package/dist/domain/processPairedExperiment.js +128 -0
- package/dist/domain/replayMachine.js +316 -0
- package/dist/domain/replayMachineRuntime.js +215 -0
- package/dist/domain/runtimeIdentification.js +210 -0
- package/dist/evaluation/CodexAgentEval.js +132 -0
- package/dist/generatedPackageMetadata.js +2 -2
- package/dist/identity.js +2 -1
- package/dist/server/createServer.js +5 -2
- package/dist/server/javascriptApplicationResult.js +85 -0
- package/dist/server/registerElectronTools.js +19 -5
- package/dist/server/registerEvidenceResources.js +2 -0
- package/dist/server/registerJavaScriptApplicationGraphResource.js +69 -0
- package/dist/server/registerManagedTools.js +18 -1
- package/dist/server/registerSessionStatusTool.js +95 -25
- package/dist/server/toolResult.js +10 -4
- package/package.json +2 -1
- package/skills/reverse-engineer-anything/SKILL.md +68 -227
- 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 +25 -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,221 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import canonicalize from "canonicalize";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { parseArtifactInventoryEvidence } from "./artifactInventoryEvidence.js";
|
|
5
|
+
import { projectBoundedCartesian } from "./boundedCartesianProjection.js";
|
|
6
|
+
import { evidenceSchema } from "./evidence.js";
|
|
7
|
+
const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u);
|
|
8
|
+
const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u);
|
|
9
|
+
const boundedPathSchema = z.string().min(1).max(4_096);
|
|
10
|
+
const componentSchema = z.strictObject({
|
|
11
|
+
path: boundedPathSchema,
|
|
12
|
+
artifact_id: z.string().regex(/^art_[a-f0-9]{64}$/u),
|
|
13
|
+
sha256: digestSchema,
|
|
14
|
+
format: z.string().min(1).max(100),
|
|
15
|
+
});
|
|
16
|
+
/** Authenticated artifact inventory pages projected as one Apple application. */
|
|
17
|
+
export const appleApplicationProjectionInputSchema = z.strictObject({
|
|
18
|
+
inventory_evidence: z.array(evidenceSchema).min(1).max(100),
|
|
19
|
+
limits: z
|
|
20
|
+
.strictObject({
|
|
21
|
+
max_components: z.number().int().min(1).max(10_000).default(1_000),
|
|
22
|
+
})
|
|
23
|
+
.default({ max_components: 1_000 }),
|
|
24
|
+
});
|
|
25
|
+
/** Deterministic, execution-free Apple application inventory projection. */
|
|
26
|
+
export const appleApplicationProjectionResultSchema = z.strictObject({
|
|
27
|
+
schema_version: z.literal(1),
|
|
28
|
+
projection_id: z.string().regex(/^aap_[a-f0-9]{64}$/u),
|
|
29
|
+
root_sha256: digestSchema,
|
|
30
|
+
root_format: z.literal("ipa"),
|
|
31
|
+
source_evidence_ids: z.array(evidenceIdSchema).min(1).max(100),
|
|
32
|
+
application_roots: z.array(boundedPathSchema),
|
|
33
|
+
components: z.strictObject({
|
|
34
|
+
bundle_metadata: z.array(componentSchema),
|
|
35
|
+
executables: z.array(componentSchema),
|
|
36
|
+
frameworks: z.array(componentSchema),
|
|
37
|
+
native_libraries: z.array(componentSchema),
|
|
38
|
+
javascript: z.array(componentSchema),
|
|
39
|
+
signing: z.array(componentSchema),
|
|
40
|
+
}),
|
|
41
|
+
runtime_families: z.array(z.enum([
|
|
42
|
+
"native",
|
|
43
|
+
"swift-objective-c",
|
|
44
|
+
"javascript",
|
|
45
|
+
"react-native",
|
|
46
|
+
"flutter",
|
|
47
|
+
"unity",
|
|
48
|
+
])),
|
|
49
|
+
bridge_candidates: z.array(z.strictObject({
|
|
50
|
+
source_path: boundedPathSchema,
|
|
51
|
+
native_path: boundedPathSchema,
|
|
52
|
+
basis: z.enum([
|
|
53
|
+
"javascript-and-native-content",
|
|
54
|
+
"react-native-convention",
|
|
55
|
+
"flutter-convention",
|
|
56
|
+
"unity-convention",
|
|
57
|
+
]),
|
|
58
|
+
})),
|
|
59
|
+
coverage: z.strictObject({
|
|
60
|
+
status: z.enum(["complete-within-inventory", "partial", "truncated"]),
|
|
61
|
+
inventory_complete: z.boolean(),
|
|
62
|
+
omitted_components: z.number().int().min(0),
|
|
63
|
+
omitted_bridge_candidates: z.number().int().min(0),
|
|
64
|
+
}),
|
|
65
|
+
limitations: z.array(z.string().min(1).max(4_096)).max(100),
|
|
66
|
+
});
|
|
67
|
+
/** Project exact IPA paths and hashes without parsing or executing target code. */
|
|
68
|
+
export const projectAppleApplication = (input) => {
|
|
69
|
+
const parsed = appleApplicationProjectionInputSchema.parse(input);
|
|
70
|
+
const { evidence, inventory } = parseArtifactInventoryEvidence(parsed.inventory_evidence);
|
|
71
|
+
if (inventory.manifest.root_format !== "ipa")
|
|
72
|
+
throw new TypeError("Apple application projection requires IPA Evidence");
|
|
73
|
+
const nodes = new Map(inventory.nodes.map((node) => [node.artifact_id, node]));
|
|
74
|
+
const all = inventory.occurrences
|
|
75
|
+
.filter((occurrence) => occurrence.artifact_id !== null && occurrence.logical_path !== ".")
|
|
76
|
+
.map((occurrence) => {
|
|
77
|
+
const node = nodes.get(occurrence.artifact_id ?? "");
|
|
78
|
+
if (node === undefined)
|
|
79
|
+
throw new TypeError("Apple application occurrence has no artifact node");
|
|
80
|
+
return {
|
|
81
|
+
path: occurrence.logical_path,
|
|
82
|
+
artifact_id: node.artifact_id,
|
|
83
|
+
sha256: node.sha256,
|
|
84
|
+
format: node.format,
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
const roots = [
|
|
88
|
+
...new Set(inventory.occurrences.flatMap(({ logical_path: path }) => {
|
|
89
|
+
const match = /^(Payload\/[^/]+\.app)(?:\/|$)/u.exec(path);
|
|
90
|
+
return match?.[1] === undefined ? [] : [match[1]];
|
|
91
|
+
})),
|
|
92
|
+
].sort(compare);
|
|
93
|
+
const classified = classifyComponents(all, roots);
|
|
94
|
+
const retained = retainComponents(classified, parsed.limits.max_components);
|
|
95
|
+
const omitted = componentCount(classified) - componentCount(retained);
|
|
96
|
+
const runtimeFamilies = identifyRuntimeFamilies(all);
|
|
97
|
+
const bridgeProjection = identifyBridgeCandidates(retained.javascript, deduplicateComponents([
|
|
98
|
+
...retained.executables,
|
|
99
|
+
...retained.native_libraries,
|
|
100
|
+
]), parsed.limits.max_components);
|
|
101
|
+
const limitations = projectionLimitations(inventory.complete, omitted, bridgeProjection.omitted, roots);
|
|
102
|
+
const withoutId = {
|
|
103
|
+
schema_version: 1,
|
|
104
|
+
root_sha256: inventory.manifest.root_sha256,
|
|
105
|
+
root_format: "ipa",
|
|
106
|
+
source_evidence_ids: evidence
|
|
107
|
+
.map(({ evidence_id: id }) => id)
|
|
108
|
+
.sort(compare),
|
|
109
|
+
application_roots: roots,
|
|
110
|
+
components: retained,
|
|
111
|
+
runtime_families: runtimeFamilies,
|
|
112
|
+
bridge_candidates: bridgeProjection.candidates,
|
|
113
|
+
coverage: {
|
|
114
|
+
status: omitted > 0 || bridgeProjection.omitted > 0
|
|
115
|
+
? "truncated"
|
|
116
|
+
: inventory.complete
|
|
117
|
+
? "complete-within-inventory"
|
|
118
|
+
: "partial",
|
|
119
|
+
inventory_complete: inventory.complete,
|
|
120
|
+
omitted_components: omitted,
|
|
121
|
+
omitted_bridge_candidates: bridgeProjection.omitted,
|
|
122
|
+
},
|
|
123
|
+
limitations,
|
|
124
|
+
};
|
|
125
|
+
return appleApplicationProjectionResultSchema.parse({
|
|
126
|
+
...withoutId,
|
|
127
|
+
projection_id: `aap_${digest(withoutId)}`,
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
const classifyComponents = (all, roots) => {
|
|
131
|
+
const withinApp = (path) => roots.some((root) => path.startsWith(`${root}/`));
|
|
132
|
+
return {
|
|
133
|
+
bundle_metadata: all.filter(({ path }) => /(?:^|\/)Info\.plist$/u.test(path)),
|
|
134
|
+
executables: all.filter(({ path, format }) => withinApp(path) && ["mach-o", "mach-o-universal"].includes(format)),
|
|
135
|
+
frameworks: all.filter(({ path }) => /\.framework\//u.test(path)),
|
|
136
|
+
native_libraries: all.filter(({ path }) => /\.(?:dylib|so)$/iu.test(path)),
|
|
137
|
+
javascript: all.filter(({ format }) => format === "javascript-bundle"),
|
|
138
|
+
signing: all.filter(({ path }) => /(?:^|\/)(?:embedded\.mobileprovision|_CodeSignature\/CodeResources)$/u.test(path)),
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
const retainComponents = (groups, maximum) => {
|
|
142
|
+
let remaining = maximum;
|
|
143
|
+
return Object.fromEntries(Object.entries(groups).map(([key, values]) => {
|
|
144
|
+
const retained = [...values]
|
|
145
|
+
.sort((left, right) => compare(left.path, right.path))
|
|
146
|
+
.slice(0, remaining);
|
|
147
|
+
remaining -= retained.length;
|
|
148
|
+
return [key, retained];
|
|
149
|
+
}));
|
|
150
|
+
};
|
|
151
|
+
const componentCount = (groups) => Object.values(groups).reduce((total, values) => total + values.length, 0);
|
|
152
|
+
const deduplicateComponents = (values) => [...new Map(values.map((value) => [value.path, value])).values()].sort((left, right) => compare(left.path, right.path));
|
|
153
|
+
const identifyRuntimeFamilies = (all) => {
|
|
154
|
+
const paths = all.map(({ path }) => path.toLowerCase());
|
|
155
|
+
const families = new Set();
|
|
156
|
+
if (all.some(({ format }) => ["mach-o", "mach-o-universal"].includes(format)))
|
|
157
|
+
families.add("native");
|
|
158
|
+
if (paths.some((path) => /\.(?:dylib|framework\/[^/]+)$/u.test(path)))
|
|
159
|
+
families.add("swift-objective-c");
|
|
160
|
+
if (all.some(({ format }) => format === "javascript-bundle"))
|
|
161
|
+
families.add("javascript");
|
|
162
|
+
if (paths.some((path) => path.includes("reactnative") ||
|
|
163
|
+
path.includes("react.framework") ||
|
|
164
|
+
path.includes("hermes")))
|
|
165
|
+
families.add("react-native");
|
|
166
|
+
if (paths.some((path) => path.includes("flutter.framework") || path.includes("app.framework")))
|
|
167
|
+
families.add("flutter");
|
|
168
|
+
if (paths.some((path) => path.includes("unityframework.framework")))
|
|
169
|
+
families.add("unity");
|
|
170
|
+
return [...families].sort(compare);
|
|
171
|
+
};
|
|
172
|
+
const identifyBridgeCandidates = (scripts, native, maximum) => {
|
|
173
|
+
const projection = projectBoundedCartesian(scripts, native, maximum, (script, item) => ({
|
|
174
|
+
source_path: script.path,
|
|
175
|
+
native_path: item.path,
|
|
176
|
+
basis: bridgeBasis(item.path),
|
|
177
|
+
}));
|
|
178
|
+
return {
|
|
179
|
+
candidates: projection.values,
|
|
180
|
+
omitted: projection.omitted,
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
const bridgeBasis = (path) => {
|
|
184
|
+
const lower = path.toLowerCase();
|
|
185
|
+
if (lower.includes("react"))
|
|
186
|
+
return "react-native-convention";
|
|
187
|
+
if (lower.includes("flutter") || lower.includes("app.framework"))
|
|
188
|
+
return "flutter-convention";
|
|
189
|
+
if (lower.includes("unity"))
|
|
190
|
+
return "unity-convention";
|
|
191
|
+
return "javascript-and-native-content";
|
|
192
|
+
};
|
|
193
|
+
const projectionLimitations = (complete, omitted, omittedBridges, roots) => [
|
|
194
|
+
...(!complete
|
|
195
|
+
? ["Source inventory pages are incomplete; absence is unknown."]
|
|
196
|
+
: []),
|
|
197
|
+
...(omitted > 0
|
|
198
|
+
? [
|
|
199
|
+
`${String(omitted)} component observations were omitted by the projection limit.`,
|
|
200
|
+
]
|
|
201
|
+
: []),
|
|
202
|
+
...(omittedBridges > 0
|
|
203
|
+
? [
|
|
204
|
+
`${String(omittedBridges)} bridge hypotheses were omitted by the projection limit.`,
|
|
205
|
+
]
|
|
206
|
+
: []),
|
|
207
|
+
...(roots.length === 0
|
|
208
|
+
? [
|
|
209
|
+
"No Payload/*.app directory was present in the supplied inventory pages.",
|
|
210
|
+
]
|
|
211
|
+
: []),
|
|
212
|
+
"Bundle identifiers and signing claims require dedicated plist and CMS parsing; this projection reports only exact paths and hashes.",
|
|
213
|
+
"Bridge candidates are path-based hypotheses, not observed runtime calls.",
|
|
214
|
+
];
|
|
215
|
+
const compare = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
216
|
+
const digest = (value) => {
|
|
217
|
+
const encoded = canonicalize(value);
|
|
218
|
+
if (encoded === undefined)
|
|
219
|
+
throw new TypeError("Apple application projection is not canonical JSON");
|
|
220
|
+
return createHash("sha256").update(encoded).digest("hex");
|
|
221
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Project a Cartesian product without materializing values beyond the output limit. */
|
|
2
|
+
export const projectBoundedCartesian = (left, right, maximum, project) => {
|
|
3
|
+
const values = [];
|
|
4
|
+
for (const leftValue of left) {
|
|
5
|
+
for (const rightValue of right) {
|
|
6
|
+
if (values.length === maximum)
|
|
7
|
+
return {
|
|
8
|
+
values,
|
|
9
|
+
omitted: left.length * right.length - values.length,
|
|
10
|
+
};
|
|
11
|
+
values.push(project(leftValue, rightValue));
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return { values, omitted: 0 };
|
|
15
|
+
};
|
|
@@ -94,6 +94,25 @@ const filesystemObservations = (capture) => [
|
|
|
94
94
|
effects: capture.filesystem_effects,
|
|
95
95
|
},
|
|
96
96
|
];
|
|
97
|
+
/** Canonical observations for one process-comparison dimension. */
|
|
98
|
+
export const processDimensionObservations = (capture, dimension) => {
|
|
99
|
+
switch (dimension) {
|
|
100
|
+
case "terminal":
|
|
101
|
+
return terminalObservations(capture);
|
|
102
|
+
case "interaction":
|
|
103
|
+
return capture.interaction_events;
|
|
104
|
+
case "exit":
|
|
105
|
+
return [{ ...capture.exit, settlement: capture.settlement }];
|
|
106
|
+
case "filesystem":
|
|
107
|
+
return filesystemObservations(capture);
|
|
108
|
+
case "protocol":
|
|
109
|
+
return capture.protocol_events;
|
|
110
|
+
case "process":
|
|
111
|
+
return capture.process_samples;
|
|
112
|
+
case "shim":
|
|
113
|
+
return capture.shim_events;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
97
116
|
const classifyCollection = (left, right) => {
|
|
98
117
|
if (JSON.stringify(left) === JSON.stringify(right))
|
|
99
118
|
return "unchanged";
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { PROCESS_COMPARISON_DIMENSIONS, compareProcessCaptures, processDimensionObservations, } from "./processComparison.js";
|
|
3
|
+
import { digestProcessCommitment, processScenarioSchema, } from "./processScenario.js";
|
|
4
|
+
const sideIdentifierSchema = z
|
|
5
|
+
.string()
|
|
6
|
+
.regex(/^[A-Za-z][A-Za-z0-9._-]{0,127}$/u);
|
|
7
|
+
const environmentNameSchema = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/u);
|
|
8
|
+
const processDimensionSchema = z.enum(PROCESS_COMPARISON_DIMENSIONS);
|
|
9
|
+
const sideBindingSchema = z
|
|
10
|
+
.object({
|
|
11
|
+
id: sideIdentifierSchema,
|
|
12
|
+
executable: z.string().startsWith("/"),
|
|
13
|
+
arguments: z.array(z.string()).max(256).optional(),
|
|
14
|
+
environment: z.record(environmentNameSchema, z.string()).default({}),
|
|
15
|
+
})
|
|
16
|
+
.strict();
|
|
17
|
+
/** Boundary contract for one bounded authority/candidate process experiment. */
|
|
18
|
+
export const processPairedExperimentSchema = z
|
|
19
|
+
.object({
|
|
20
|
+
approved: z.literal(true),
|
|
21
|
+
shared_scenario: processScenarioSchema,
|
|
22
|
+
authority: sideBindingSchema,
|
|
23
|
+
candidate: sideBindingSchema,
|
|
24
|
+
repeat_count: z.number().int().min(2).max(10).default(3),
|
|
25
|
+
freshness_policy: z
|
|
26
|
+
.object({
|
|
27
|
+
max_capture_age_ms: z.number().int().positive().max(86_400_000),
|
|
28
|
+
})
|
|
29
|
+
.strict(),
|
|
30
|
+
required_dimensions: z
|
|
31
|
+
.array(processDimensionSchema)
|
|
32
|
+
.min(1)
|
|
33
|
+
.max(PROCESS_COMPARISON_DIMENSIONS.length)
|
|
34
|
+
.default([...PROCESS_COMPARISON_DIMENSIONS]),
|
|
35
|
+
})
|
|
36
|
+
.strict()
|
|
37
|
+
.superRefine((experiment, context) => {
|
|
38
|
+
if (experiment.authority.id === experiment.candidate.id)
|
|
39
|
+
context.addIssue({
|
|
40
|
+
code: "custom",
|
|
41
|
+
message: "authority and candidate identities must differ",
|
|
42
|
+
path: ["candidate", "id"],
|
|
43
|
+
});
|
|
44
|
+
if (new Set(experiment.required_dimensions).size !==
|
|
45
|
+
experiment.required_dimensions.length)
|
|
46
|
+
context.addIssue({
|
|
47
|
+
code: "custom",
|
|
48
|
+
message: "required process dimensions must be unique",
|
|
49
|
+
path: ["required_dimensions"],
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
/** Apply a side binding without changing the shared observation contract. */
|
|
53
|
+
export const bindProcessScenario = (shared, side) => processScenarioSchema.parse({
|
|
54
|
+
...shared,
|
|
55
|
+
executable: side.executable,
|
|
56
|
+
arguments: side.arguments ?? shared.arguments,
|
|
57
|
+
environment: { ...shared.environment, ...side.environment },
|
|
58
|
+
});
|
|
59
|
+
const classifyInstability = (dimensions) => {
|
|
60
|
+
if (dimensions.has("exit"))
|
|
61
|
+
return "flaky_exit";
|
|
62
|
+
if (dimensions.has("filesystem") || dimensions.has("process"))
|
|
63
|
+
return "environmental_drift";
|
|
64
|
+
return "nondeterministic_output";
|
|
65
|
+
};
|
|
66
|
+
const timingKeys = new Set([
|
|
67
|
+
"at_ms",
|
|
68
|
+
"scheduled_at_ms",
|
|
69
|
+
"dispatched_at_ms",
|
|
70
|
+
"elapsed_ms",
|
|
71
|
+
]);
|
|
72
|
+
const withoutTiming = (value) => typeof value !== "object" || value === null || Array.isArray(value)
|
|
73
|
+
? value
|
|
74
|
+
: Object.fromEntries(Object.entries(value).filter(([key]) => !timingKeys.has(key)));
|
|
75
|
+
const observationsWithoutTiming = (capture, dimension) => dimension === "exit"
|
|
76
|
+
? [
|
|
77
|
+
{
|
|
78
|
+
...capture.exit,
|
|
79
|
+
settlement: {
|
|
80
|
+
state: capture.settlement.state,
|
|
81
|
+
cleanup_outcome: capture.settlement.cleanup_outcome,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
]
|
|
85
|
+
: processDimensionObservations(capture, dimension).map(withoutTiming);
|
|
86
|
+
const hasOnlyTimingVariance = (captures, dimensions) => dimensions.every((dimension) => {
|
|
87
|
+
const first = captures[0];
|
|
88
|
+
if (first === undefined)
|
|
89
|
+
return false;
|
|
90
|
+
const baseline = JSON.stringify(observationsWithoutTiming(first, dimension));
|
|
91
|
+
return captures
|
|
92
|
+
.slice(1)
|
|
93
|
+
.every((capture) => JSON.stringify(observationsWithoutTiming(capture, dimension)) ===
|
|
94
|
+
baseline);
|
|
95
|
+
});
|
|
96
|
+
/** Compare same-side repeats before permitting a cross-side claim. */
|
|
97
|
+
export const analyzeProcessRepeatability = (captures, requiredDimensions) => {
|
|
98
|
+
if (captures.length < 2)
|
|
99
|
+
return {
|
|
100
|
+
classification: "insufficient_samples",
|
|
101
|
+
stable: false,
|
|
102
|
+
digest: digestProcessCommitment({ captures: [], requiredDimensions }),
|
|
103
|
+
comparisons: [],
|
|
104
|
+
unstable_dimensions: [],
|
|
105
|
+
};
|
|
106
|
+
const baseline = captures[0];
|
|
107
|
+
if (baseline === undefined)
|
|
108
|
+
throw new TypeError("Repeatability analysis lost its baseline capture");
|
|
109
|
+
const comparisons = captures
|
|
110
|
+
.slice(1)
|
|
111
|
+
.map((capture) => compareProcessCaptures(baseline, capture));
|
|
112
|
+
const unstable = new Set(requiredDimensions.filter((dimension) => comparisons.some((comparison) => comparison[dimension] !== "unchanged")));
|
|
113
|
+
const unstableDimensions = PROCESS_COMPARISON_DIMENSIONS.filter((dimension) => unstable.has(dimension));
|
|
114
|
+
return {
|
|
115
|
+
classification: unstable.size === 0
|
|
116
|
+
? "stable"
|
|
117
|
+
: hasOnlyTimingVariance(captures, unstableDimensions)
|
|
118
|
+
? "timing_variance"
|
|
119
|
+
: classifyInstability(unstable),
|
|
120
|
+
stable: unstable.size === 0,
|
|
121
|
+
digest: digestProcessCommitment({
|
|
122
|
+
requiredDimensions,
|
|
123
|
+
comparisons,
|
|
124
|
+
}),
|
|
125
|
+
comparisons,
|
|
126
|
+
unstable_dimensions: unstableDimensions,
|
|
127
|
+
};
|
|
128
|
+
};
|