rea-agents 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +124 -20
  2. package/bridge/hopper_bridge.py +141 -13
  3. package/dist/application/AnalysisProvider.js +12 -1
  4. package/dist/application/ArtifactExtraction.js +166 -0
  5. package/dist/application/ArtifactGraphConstruction.js +257 -0
  6. package/dist/application/ArtifactInventory.js +253 -0
  7. package/dist/application/BinarySession.js +164 -14
  8. package/dist/application/CompositeProvider.js +73 -0
  9. package/dist/application/DirectAnalysis.js +27 -3
  10. package/dist/application/Doctor.js +35 -6
  11. package/dist/application/EnhancedTools.js +8 -7
  12. package/dist/application/EvidenceBundleCommands.js +29 -0
  13. package/dist/application/EvidenceBundleFiles.js +127 -0
  14. package/dist/application/EvidenceLedger.js +225 -18
  15. package/dist/application/FilesystemSnapshot.js +124 -0
  16. package/dist/application/LinuxHopper.js +186 -0
  17. package/dist/application/LoopbackReplay.js +195 -37
  18. package/dist/application/ProcessHarness.js +200 -191
  19. package/dist/application/ProcessNormalization.js +44 -0
  20. package/dist/application/ProcessOwnership.js +84 -0
  21. package/dist/application/ProcessSampling.js +284 -0
  22. package/dist/application/RealHopperAssertions.js +105 -0
  23. package/dist/application/ReferenceSourceImport.js +182 -0
  24. package/dist/application/ReferenceSourceImportEntries.js +122 -0
  25. package/dist/application/ReferenceSourceImportPolicy.js +73 -0
  26. package/dist/application/ReferenceSourceImportTypes.js +18 -0
  27. package/dist/application/ReferenceSourceVcsAdapter.js +34 -0
  28. package/dist/application/Setup.js +192 -41
  29. package/dist/application/Uninstall.js +130 -0
  30. package/dist/application/runtime.js +8 -1
  31. package/dist/artifacts/ArtifactPaths.js +51 -0
  32. package/dist/artifacts/ArtifactProvider.js +130 -0
  33. package/dist/artifacts/ArtifactReader.js +9 -0
  34. package/dist/artifacts/AsarArtifactReader.js +62 -0
  35. package/dist/artifacts/DirectoryArtifactReader.js +107 -0
  36. package/dist/artifacts/MachOSliceArtifactReader.js +66 -0
  37. package/dist/artifacts/SafeOutputTree.js +199 -0
  38. package/dist/artifacts/StreamBytes.js +10 -0
  39. package/dist/artifacts/ZipArtifactReader.js +109 -0
  40. package/dist/cli.js +153 -6
  41. package/dist/config.js +35 -1
  42. package/dist/contracts/artifactComparisonExample.js +95 -0
  43. package/dist/contracts/artifactToolContracts.js +84 -0
  44. package/dist/contracts/enhancedInputs.js +4 -0
  45. package/dist/contracts/functionComparisonExample.js +57 -0
  46. package/dist/contracts/investigationExamples.js +118 -0
  47. package/dist/contracts/nativeToolContracts.js +52 -0
  48. package/dist/contracts/processCaptureExample.js +25 -0
  49. package/dist/contracts/toolContractExamples.js +69 -0
  50. package/dist/contracts/toolContracts.js +99 -36
  51. package/dist/contracts/toolOutputSchemas.js +154 -82
  52. package/dist/contracts/unknownContractExamples.js +33 -0
  53. package/dist/domain/artifactComparison.js +273 -0
  54. package/dist/domain/artifactGraph.js +194 -0
  55. package/dist/domain/artifactInventoryEvidence.js +150 -0
  56. package/dist/domain/binaryTarget.js +55 -0
  57. package/dist/domain/bundleComparison.js +266 -0
  58. package/dist/domain/callPath.js +346 -0
  59. package/dist/domain/changedBehavior.js +294 -0
  60. package/dist/domain/errors.js +192 -4
  61. package/dist/domain/evidence.js +41 -10
  62. package/dist/domain/evidenceBundle.js +187 -6
  63. package/dist/domain/functionComparison.js +201 -0
  64. package/dist/domain/functionComparisonNormalization.js +112 -0
  65. package/dist/domain/functionComparisonResults.js +54 -0
  66. package/dist/domain/functionComparisonSchemas.js +82 -0
  67. package/dist/domain/functionDossierEvidence.js +171 -0
  68. package/dist/domain/hopperValues.js +14 -7
  69. package/dist/domain/nativeInspection.js +142 -0
  70. package/dist/domain/processCapture.js +152 -54
  71. package/dist/domain/processComparison.js +106 -0
  72. package/dist/domain/reconstructionUnknowns.js +90 -0
  73. package/dist/domain/reconstructionVerification.js +285 -0
  74. package/dist/domain/reconstructionVerificationSchemas.js +126 -0
  75. package/dist/domain/referenceSourceClassification.js +496 -0
  76. package/dist/domain/referenceSourceGraph.js +376 -0
  77. package/dist/domain/referenceSourceImportParsing.js +235 -0
  78. package/dist/domain/residualUnknown.js +239 -0
  79. package/dist/domain/staticRuntimeCorrelation.js +375 -0
  80. package/dist/hopper/BridgeLauncher.js +40 -3
  81. package/dist/hopper/HopperClient.js +14 -5
  82. package/dist/hopper/HopperProvider.js +57 -22
  83. package/dist/identity.js +1 -0
  84. package/dist/main.js +5 -1
  85. package/dist/native/CommandRunner.js +156 -0
  86. package/dist/native/NativeMacOSProvider.js +306 -0
  87. package/dist/native/NativeMachoInspection.js +135 -0
  88. package/dist/native/parsers/codesign.js +55 -0
  89. package/dist/native/parsers/demangle.js +26 -0
  90. package/dist/native/parsers/dyldInfo.js +25 -0
  91. package/dist/native/parsers/lipo.js +67 -0
  92. package/dist/native/parsers/otool.js +193 -0
  93. package/dist/native/parsers/plist.js +23 -0
  94. package/dist/reference/ReferenceSourceReader.js +73 -0
  95. package/dist/reference/ReferenceSourceReaderEntries.js +206 -0
  96. package/dist/reference/ReferenceSourceReaderErrors.js +19 -0
  97. package/dist/reference/ReferenceSourceReaderFile.js +119 -0
  98. package/dist/reference/ReferenceSourceReaderPaths.js +23 -0
  99. package/dist/reference/ReferenceSourceReaderTypes.js +2 -0
  100. package/dist/reference/ReferenceSourceReaderValidate.js +71 -0
  101. package/dist/server/createServer.js +31 -5
  102. package/dist/server/recordDerivedEvidence.js +10 -0
  103. package/dist/server/registerArtifactComparisonTool.js +62 -0
  104. package/dist/server/registerArtifactTools.js +6 -0
  105. package/dist/server/registerBundleComparisonTool.js +47 -0
  106. package/dist/server/registerEnhancedTools.js +64 -12
  107. package/dist/server/registerEvidenceTools.js +36 -0
  108. package/dist/server/registerFunctionComparisonTool.js +68 -0
  109. package/dist/server/registerInvestigationTools.js +224 -0
  110. package/dist/server/registerNativeTools.js +6 -0
  111. package/dist/server/registerOfficialTools.js +47 -14
  112. package/dist/server/registerProcessComparisonTool.js +106 -0
  113. package/dist/server/registerSessionTools.js +179 -70
  114. package/dist/server/sessionEvidence.js +28 -0
  115. package/dist/server/sessionToolPolicies.js +64 -0
  116. package/dist/server/toolRegistrationOptions.js +7 -0
  117. package/dist/server/toolResult.js +8 -5
  118. package/install.sh +198 -0
  119. package/package.json +19 -2
  120. package/scripts/rea.mjs +5 -1
  121. package/skills/rea-analysis/SKILL.md +77 -2
  122. /package/dist/{application/HopperToolPort.js → domain/referenceSourcePolicy.js} +0 -0
@@ -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"));
@@ -0,0 +1,285 @@
1
+ import { createHash } from "node:crypto";
2
+ import canonicalize from "canonicalize";
3
+ import { z } from "zod";
4
+ import { artifactComparisonResultSchema } from "./artifactComparison.js";
5
+ import { artifactInventoryResultSchema } from "./artifactGraph.js";
6
+ import { parseEvidence } from "./evidence.js";
7
+ import { parseEvidenceBundle } from "./evidenceBundle.js";
8
+ import { functionComparisonResultSchema } from "./functionComparison.js";
9
+ import { functionDossierSchema } from "./hopperValues.js";
10
+ import { processCaptureComparisonSchema, processCaptureSchema, } from "./processCapture.js";
11
+ import { reconstructionClaimResultSchema, reconstructionSpecificationSchema, reconstructionVerificationResultSchema, } from "./reconstructionVerificationSchemas.js";
12
+ import { reconstructionClaimUnknowns, reconstructionProbes, reconstructionUnknownHeads, } from "./reconstructionUnknowns.js";
13
+ const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u);
14
+ export { reconstructionVerificationInputSchema, reconstructionVerificationResultSchema, } from "./reconstructionVerificationSchemas.js";
15
+ const MAX_BUNDLE_RECORDS = 20_100;
16
+ const MAX_UNKNOWN_REVISIONS = 10_000;
17
+ const MAX_CANONICAL_BYTES = 32 * 1024 * 1024;
18
+ const providers = {
19
+ behavioral: {
20
+ operation: "compare_process_captures",
21
+ predicate: "rea.process-comparison/v1",
22
+ id: "rea-process",
23
+ name: "REA deterministic process harness",
24
+ },
25
+ "structural-function": {
26
+ operation: "compare_functions",
27
+ predicate: "rea.function-comparison/v1",
28
+ id: "rea-function-comparison",
29
+ name: "REA function comparison",
30
+ },
31
+ "structural-artifact": {
32
+ operation: "compare_artifacts",
33
+ predicate: "rea.artifact-comparison/v1",
34
+ id: "rea-artifact-comparison",
35
+ name: "REA artifact comparison",
36
+ },
37
+ };
38
+ /** Verify only explicitly declared claims; no global equivalence is inferred. */
39
+ export const verifyReconstruction = (specificationInput, bundleInput, offset, limit) => {
40
+ const specification = reconstructionSpecificationSchema.parse(specificationInput);
41
+ enforceInputSize(bundleInput);
42
+ const bundle = parseEvidenceBundle(bundleInput);
43
+ if (bundle.records.length > MAX_BUNDLE_RECORDS)
44
+ throw new TypeError("Reconstruction Evidence record limit exceeded");
45
+ if (bundle.unknowns.length > MAX_UNKNOWN_REVISIONS)
46
+ throw new TypeError("Reconstruction unknown revision limit exceeded");
47
+ const records = new Map(bundle.records.map((item) => [item.evidence_id, item]));
48
+ const heads = reconstructionUnknownHeads(bundle.unknowns);
49
+ const results = specification.claims
50
+ .map((claim) => evaluateClaim(claim, records, heads))
51
+ .sort((left, right) => left.claim_id.localeCompare(right.claim_id, "en"));
52
+ const page = results.slice(offset, offset + limit);
53
+ const failed = count(results, "fail");
54
+ const unresolved = count(results, "unknown");
55
+ const probes = reconstructionProbes(results, heads);
56
+ const evidenceLinks = uniqueSorted(results.flatMap(({ evidence_links: links }) => links));
57
+ if (evidenceLinks.length > 20_100)
58
+ throw new TypeError("Reconstruction Evidence closure exceeds limit");
59
+ const output = {
60
+ status: failed > 0 ? "fail" : unresolved > 0 ? "unknown" : "pass",
61
+ specification_sha256: digest({
62
+ ...specification,
63
+ claims: [...specification.claims].sort((left, right) => left.claim_id.localeCompare(right.claim_id, "en")),
64
+ }),
65
+ summary: {
66
+ total: results.length,
67
+ passed: count(results, "pass"),
68
+ failed,
69
+ unknown: unresolved,
70
+ behavioral: results.filter(({ kind }) => kind === "behavioral").length,
71
+ structural: results.filter(({ kind }) => kind !== "behavioral").length,
72
+ },
73
+ claims: {
74
+ items: page,
75
+ offset,
76
+ limit,
77
+ total: results.length,
78
+ next_offset: offset + page.length < results.length ? offset + page.length : null,
79
+ },
80
+ recommended_probes: probes.items,
81
+ evidence_links: evidenceLinks,
82
+ limitations: [
83
+ "Pass means every declared claim passed; it does not establish global implementation equivalence.",
84
+ ...(probes.truncated
85
+ ? ["Recommended probes were truncated at the 2,000-item limit."]
86
+ : []),
87
+ ],
88
+ };
89
+ return reconstructionVerificationResultSchema.parse(output);
90
+ };
91
+ const evaluateClaim = (claim, records, heads) => {
92
+ const comparison = records.get(claim.comparison_evidence_id);
93
+ if (comparison === undefined)
94
+ throw new TypeError(`Missing comparison Evidence ${claim.comparison_evidence_id}`);
95
+ validateComparisonIdentity(claim, comparison);
96
+ const sides = sourceSides(claim, comparison);
97
+ const allSourceIds = [...sides.left, ...sides.right];
98
+ if (new Set(allSourceIds).size !== allSourceIds.length)
99
+ throw new TypeError("Comparison source Evidence must be unique and two-sided");
100
+ if (!sameSet(comparison.evidence_links, allSourceIds))
101
+ throw new TypeError("Comparison Evidence closure disagrees with its parameters");
102
+ const sources = allSourceIds.map((id) => {
103
+ const source = records.get(id);
104
+ if (source === undefined)
105
+ throw new TypeError(`Missing source Evidence ${id}`);
106
+ return source;
107
+ });
108
+ validateSourceKinds(claim, sources);
109
+ const observed = observedStatus(claim, comparison);
110
+ const limitations = authorityLimitations(claim, sources, sides);
111
+ const relevantUnknowns = reconstructionClaimUnknowns(heads, comparison.evidence_id, allSourceIds);
112
+ const status = classify(observed, limitations, relevantUnknowns);
113
+ const displayedUnknowns = relevantUnknowns.slice(0, 100);
114
+ const hiddenUnknownCount = relevantUnknowns.length - displayedUnknowns.length;
115
+ return reconstructionClaimResultSchema.parse({
116
+ claim_id: claim.claim_id,
117
+ kind: claim.kind,
118
+ dimension: claim.dimension,
119
+ status,
120
+ observed_status: observed,
121
+ comparison_evidence_id: comparison.evidence_id,
122
+ left_evidence_ids: sides.left,
123
+ right_evidence_ids: sides.right,
124
+ evidence_links: uniqueSorted([comparison.evidence_id, ...allSourceIds]),
125
+ unknown_ids: displayedUnknowns.map(({ unknown_id: id }) => id),
126
+ limitations: uniqueSorted([
127
+ ...comparison.limitations,
128
+ ...limitations,
129
+ ...displayedUnknowns.map(({ question }) => `Residual unknown: ${question}`),
130
+ ...(hiddenUnknownCount > 0
131
+ ? [
132
+ `${hiddenUnknownCount} additional active residual unknowns affect this claim but are omitted by the 100-item display limit.`,
133
+ ]
134
+ : []),
135
+ ]),
136
+ });
137
+ };
138
+ const validateSourceKinds = (claim, sources) => {
139
+ const expected = claim.kind === "behavioral"
140
+ ? ["capture_process_scenario", "rea.process-capture/v2"]
141
+ : claim.kind === "structural-function"
142
+ ? ["analyze_function", "rea.analysis/v2"]
143
+ : ["inventory_artifact", "rea.analysis/v2"];
144
+ if (sources.some((source) => source.operation !== expected[0] ||
145
+ source.predicate_type !== expected[1]))
146
+ throw new TypeError("Comparison source Evidence has an unexpected observation type");
147
+ for (const source of sources) {
148
+ if (claim.kind === "behavioral")
149
+ processCaptureSchema.parse(source.normalized_result);
150
+ else if (claim.kind === "structural-function")
151
+ functionDossierSchema.parse(source.normalized_result);
152
+ else
153
+ artifactInventoryResultSchema.parse(source.normalized_result);
154
+ }
155
+ };
156
+ const validateComparisonIdentity = (claim, evidence) => {
157
+ parseEvidence(evidence);
158
+ const expected = providers[claim.kind];
159
+ if (evidence.operation !== expected.operation ||
160
+ evidence.predicate_type !== expected.predicate ||
161
+ evidence.provider.id !== expected.id ||
162
+ evidence.provider.name !== expected.name ||
163
+ evidence.provider.version !== "1" ||
164
+ evidence.confidence !== "derived" ||
165
+ evidence.authority !== "analyst-inference" ||
166
+ evidence.subject !== null)
167
+ throw new TypeError("Comparison Evidence identity or authority disagrees");
168
+ };
169
+ const sourceSides = (claim, evidence) => {
170
+ const parameters = evidence.parameters;
171
+ if (claim.kind === "behavioral") {
172
+ const parsed = z
173
+ .object({
174
+ left_evidence_id: evidenceIdSchema,
175
+ right_evidence_id: evidenceIdSchema,
176
+ })
177
+ .passthrough()
178
+ .parse(parameters);
179
+ return {
180
+ left: [parsed.left_evidence_id],
181
+ right: [parsed.right_evidence_id],
182
+ };
183
+ }
184
+ const parsed = z
185
+ .object({
186
+ left_evidence_ids: z.array(evidenceIdSchema).min(1).max(100),
187
+ right_evidence_ids: z.array(evidenceIdSchema).min(1).max(100),
188
+ })
189
+ .passthrough()
190
+ .parse(parameters);
191
+ return { left: parsed.left_evidence_ids, right: parsed.right_evidence_ids };
192
+ };
193
+ const observedStatus = (claim, evidence) => {
194
+ if (claim.kind === "behavioral") {
195
+ const result = processCaptureComparisonSchema.parse(evidence.normalized_result);
196
+ validateProcessStatus(result);
197
+ return claim.dimension === "overall"
198
+ ? result.status
199
+ : result[claim.dimension];
200
+ }
201
+ if (claim.kind === "structural-function") {
202
+ const result = functionComparisonResultSchema.parse(evidence.normalized_result);
203
+ if (claim.dimension === "overall")
204
+ return result.status;
205
+ const dimension = result.dimensions.find((item) => item.dimension === claim.dimension);
206
+ if (dimension === undefined)
207
+ throw new TypeError("Function comparison dimension missing");
208
+ if (dimension.evidence_links.some((id) => !evidence.evidence_links.includes(id)))
209
+ throw new TypeError("Function dimension cites Evidence outside comparison closure");
210
+ return dimension.status;
211
+ }
212
+ const result = artifactComparisonResultSchema.parse(evidence.normalized_result);
213
+ if (result.changes.offset !== 0 ||
214
+ result.changes.next_offset !== null ||
215
+ result.changes.items.length !== result.changes.total)
216
+ return "unknown";
217
+ if (result.changes.items.some((item) => item.evidence_links.some((id) => !evidence.evidence_links.includes(id))))
218
+ throw new TypeError("Artifact change cites Evidence outside comparison closure");
219
+ return result.status;
220
+ };
221
+ const validateProcessStatus = (result) => {
222
+ const dimensions = [
223
+ result.terminal,
224
+ result.exit,
225
+ result.filesystem,
226
+ result.protocol,
227
+ result.process,
228
+ ];
229
+ const expected = dimensions.includes("truncated")
230
+ ? "truncated"
231
+ : dimensions.some((status) => ["added", "removed", "changed"].includes(status))
232
+ ? "changed"
233
+ : dimensions.includes("unknown")
234
+ ? "unknown"
235
+ : "unchanged";
236
+ if (result.status !== expected)
237
+ throw new TypeError("Process comparison status contradicts its dimensions");
238
+ };
239
+ const authorityLimitations = (claim, sources, sides) => {
240
+ const requiredAuthority = claim.kind === "behavioral" ? "controlled-replay" : "shipped-artifact";
241
+ const limitations = [];
242
+ if (sources.some((source) => source.authority !== requiredAuthority ||
243
+ (claim.kind === "structural-function"
244
+ ? source.confidence === "inferred"
245
+ : source.confidence !== "observed")))
246
+ limitations.push(`Source Evidence lacks qualifying ${requiredAuthority} authority.`);
247
+ if (claim.kind !== "behavioral" &&
248
+ sources.some(({ subject }) => subject === null))
249
+ limitations.push("Structural source artifact identity is unavailable.");
250
+ if (claim.kind === "behavioral") {
251
+ const byId = new Map(sources.map((source) => [source.evidence_id, source]));
252
+ const left = byId.get(sides.left[0] ?? "");
253
+ const right = byId.get(sides.right[0] ?? "");
254
+ if (left?.environment === null ||
255
+ right?.environment === null ||
256
+ JSON.stringify(left?.environment) !== JSON.stringify(right?.environment))
257
+ limitations.push("Controlled replay environments are unavailable or incompatible.");
258
+ }
259
+ return limitations;
260
+ };
261
+ const classify = (observed, limitations, unknowns) => {
262
+ if (limitations.length > 0)
263
+ return "unknown";
264
+ if (["changed", "added", "removed"].includes(observed))
265
+ return "fail";
266
+ if (observed === "unknown" || observed === "truncated" || unknowns.length > 0)
267
+ return "unknown";
268
+ return "pass";
269
+ };
270
+ const digest = (value) => {
271
+ const encoded = canonicalize(value);
272
+ if (encoded === undefined)
273
+ throw new TypeError("Specification canonicalization failed");
274
+ return createHash("sha256").update(encoded).digest("hex");
275
+ };
276
+ const enforceInputSize = (value) => {
277
+ const encoded = canonicalize(value);
278
+ if (encoded === undefined ||
279
+ Buffer.byteLength(encoded, "utf8") > MAX_CANONICAL_BYTES)
280
+ throw new TypeError("Reconstruction Evidence bundle exceeds byte limit");
281
+ };
282
+ const count = (items, status) => items.filter((item) => item.status === status).length;
283
+ const sameSet = (left, right) => left.length === right.length &&
284
+ uniqueSorted(left).every((id, index) => id === uniqueSorted(right)[index]);
285
+ const uniqueSorted = (values) => [...new Set(values)].sort((left, right) => left.localeCompare(right, "en"));
@@ -0,0 +1,126 @@
1
+ import { z } from "zod";
2
+ import { evidenceBundleSchema } from "./evidenceBundle.js";
3
+ const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u);
4
+ const unknownIdSchema = z.string().regex(/^unk_[a-f0-9]{64}$/u);
5
+ const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u);
6
+ const claimIdSchema = z.string().regex(/^[A-Za-z][A-Za-z0-9._-]{0,99}$/u);
7
+ const titleSchema = z.string().trim().min(1).max(500);
8
+ const verificationStatusSchema = z.enum(["pass", "fail", "unknown"]);
9
+ const commonClaim = {
10
+ claim_id: claimIdSchema,
11
+ title: titleSchema,
12
+ comparison_evidence_id: evidenceIdSchema,
13
+ };
14
+ const behavioralClaimSchema = z.object({
15
+ ...commonClaim,
16
+ kind: z.literal("behavioral"),
17
+ dimension: z.enum([
18
+ "overall",
19
+ "terminal",
20
+ "exit",
21
+ "filesystem",
22
+ "protocol",
23
+ "process",
24
+ ]),
25
+ });
26
+ const functionClaimSchema = z.object({
27
+ ...commonClaim,
28
+ kind: z.literal("structural-function"),
29
+ dimension: z.enum([
30
+ "overall",
31
+ "identity",
32
+ "pseudocode",
33
+ "assembly",
34
+ "comments",
35
+ "calls",
36
+ "references",
37
+ "strings_names",
38
+ "cfg",
39
+ ]),
40
+ });
41
+ const artifactClaimSchema = z.object({
42
+ ...commonClaim,
43
+ kind: z.literal("structural-artifact"),
44
+ dimension: z.literal("overall"),
45
+ });
46
+ const reconstructionClaimSchema = z.discriminatedUnion("kind", [
47
+ behavioralClaimSchema,
48
+ functionClaimSchema,
49
+ artifactClaimSchema,
50
+ ]);
51
+ /** Finite, typed behavioral and structural specification. */
52
+ export const reconstructionSpecificationSchema = z
53
+ .object({
54
+ schema_version: z.literal(1),
55
+ name: z.string().trim().min(1).max(200),
56
+ claims: z.array(reconstructionClaimSchema).min(1).max(100),
57
+ })
58
+ .superRefine((value, context) => {
59
+ const ids = value.claims.map(({ claim_id: id }) => id);
60
+ if (new Set(ids).size !== ids.length)
61
+ context.addIssue({ code: "custom", message: "Claim IDs must be unique" });
62
+ const selectors = value.claims.map(({ kind, comparison_evidence_id: id, dimension }) => `${kind}:${id}:${dimension}`);
63
+ if (new Set(selectors).size !== selectors.length)
64
+ context.addIssue({
65
+ code: "custom",
66
+ message: "Claim comparison selectors must be unique",
67
+ });
68
+ });
69
+ /** Bounded reconstruction-verification input. */
70
+ export const reconstructionVerificationInputSchema = z.object({
71
+ specification: reconstructionSpecificationSchema,
72
+ evidence_bundle: evidenceBundleSchema,
73
+ offset: z.number().int().min(0).default(0),
74
+ limit: z.number().int().min(1).max(100).default(100),
75
+ unknown_registry_approved: z.literal(true).optional(),
76
+ });
77
+ export const reconstructionClaimResultSchema = z.object({
78
+ claim_id: claimIdSchema,
79
+ kind: z.enum(["behavioral", "structural-function", "structural-artifact"]),
80
+ dimension: z.string().min(1),
81
+ status: verificationStatusSchema,
82
+ observed_status: z.enum([
83
+ "unchanged",
84
+ "added",
85
+ "removed",
86
+ "changed",
87
+ "truncated",
88
+ "unknown",
89
+ ]),
90
+ comparison_evidence_id: evidenceIdSchema,
91
+ left_evidence_ids: z.array(evidenceIdSchema).min(1).max(100),
92
+ right_evidence_ids: z.array(evidenceIdSchema).min(1).max(100),
93
+ evidence_links: z.array(evidenceIdSchema).min(3).max(201),
94
+ unknown_ids: z.array(unknownIdSchema).max(100),
95
+ limitations: z.array(z.string()),
96
+ });
97
+ /** Evidence-cited result over every declared claim, independently paged. */
98
+ export const reconstructionVerificationResultSchema = z.object({
99
+ status: verificationStatusSchema,
100
+ specification_sha256: digestSchema,
101
+ summary: z.object({
102
+ total: z.number().int().min(1).max(100),
103
+ passed: z.number().int().min(0),
104
+ failed: z.number().int().min(0),
105
+ unknown: z.number().int().min(0),
106
+ behavioral: z.number().int().min(0),
107
+ structural: z.number().int().min(0),
108
+ }),
109
+ claims: z.object({
110
+ items: z.array(reconstructionClaimResultSchema).max(100),
111
+ offset: z.number().int().min(0),
112
+ limit: z.number().int().min(1).max(100),
113
+ total: z.number().int().min(1).max(100),
114
+ next_offset: z.number().int().min(0).nullable(),
115
+ }),
116
+ recommended_probes: z
117
+ .array(z.object({
118
+ operation: z.string().min(1).max(200),
119
+ rationale: z.string().min(1).max(1_000),
120
+ claim_ids: z.array(claimIdSchema).min(1).max(100),
121
+ unknown_ids: z.array(unknownIdSchema).max(100),
122
+ }))
123
+ .max(2_000),
124
+ evidence_links: z.array(evidenceIdSchema).min(3).max(20_100),
125
+ limitations: z.array(z.string()),
126
+ });