5.1

The Evidence record

Every successful rea observation is one strict record of sixteen fields, and its id is a SHA-256 over the semantic content with the file path left outside.

An agent tells you that _percent_change returns 0 when before is 0. You want to know what rea observed, which engine said it, and whether the same claim from another machine would carry the same id. The capture holds the answer in one Evidence record. When you finish this section, you can read all sixteen fields of an Evidence record and say which three carry the trust decision. You can also recompute an ev_ id from a captured record.

One record for every successful observation

Evidence record: the one record type that rea returns for a successful observation. The schema is one strict object with sixteen fields. A record with an extra field is rejected, and so is a record that omits a field. An operation without caller or callee sets still writes relationship_coverage, with the value null. The MCP server returns the full canonical record twice, as structuredContent and as a text copy.

the sixteen fields of the _percent_change dossier recordjsonFull log
"evidence_id": "ev_b492c85eb9570bd5f8e866f94523122c8600ea0106603d90e9ea5445cf757b2a",
"subject": {
  "name": "ledger-arm64-O1",
  "digest": {
    "sha256": "01b5d08b08fb94678be5919d0e753d44b505a53107c95b94634f0e5d41c9e375"
  },
  "format": "mach-o",
  "architecture": "arm64",
  "local_path": "$CAPTURE/fixtures/ledger-arm64-O1"
},
"provider": {
  "id": "ghidra",
  "name": "Ghidra",
  "version": "12.1.4"
},
"analysis_profile": {
  …
  "digest": "3fc72c9b4b78498fb992b938f8f3389dc670244a293c1d40abf5e1d10162ed3b"
},
"predicate_type": "rea.analysis",
"operation": "analyze_function",
"parameters": {
  "procedure": "_percent_change"
},
"raw_result": {
  …
},
"normalized_result": {
  …
},
"confidence": "observed",
"authority": "shipped-artifact",
"environment": null,
"limitations": [
  "The session serves operations only after Ghidra reports default auto-analysis complete; incomplete analysis does not expose partial results.",
  …
],
"relationship_coverage": {
  "status": "complete",
  …
},
"locations": [],
"evidence_links": []
The profile parameters, the raw result, the normalized result, nine of the ten limitations, and the coverage diagnostics are cut. The kit replaced the text copy of the record with a marker.
FieldIn this recordWhat it holds
evidence_idev_b492c85eb957 and 52 more hex digitsev_ and a SHA-256
subjectledger-arm64-O1, sha256 01b5d08b08fb, mach-o, arm64, a paththe analysed file, or null
providerghidra, Ghidra, 12.1.4the engine that produced the fact
analysis_profiledigest 3fc72c9b4b7the committed engine settings, or null
predicate_typerea.analysisthe kind of claim
operationanalyze_functionthe tool that produced the record
parameters{"procedure": "_percent_change"}the caller input, as given
raw_resultthe Ghidra reportthe provider form, or null
normalized_resultthe dossier, 16 keysthe canonical result the agent reads
confidenceobservedobserved, derived, or inferred
authorityshipped-artifactone of five source kinds
environmentnullplatform and isolation of a run, or null
limitations10 stringswhat the fact does not cover
relationship_coveragecompletewhether the caller and callee sets are complete, partial, or unknown, or null for other operations
locations[]addresses, ranges, paths, or file offsets
evidence_links[]the ev_ ids this record derives from

The subject format is one of 28 container names, from mach-o and elf to javascript and directory. The architecture is one of ten families: x86, x86_64, arm, arm64, mips, 6502, riscv, powerpc, m68k, and hexagon. Only the raw-binary format can have the architecture 6502 or m68k. A location has one of five shapes: address, address-range, artifact-path, file-offset, or file-offset-range.

The MCP handler can run a preflight check before the engine starts. It builds the record after the engine answers. It calls createEvidence with the active target, the provider, and the operation. The same call passes the parameters, the result, the profile, the raw result, the limitations, the locations, and the links.

Some tools give a reduced parameter set to the record. The handler records any source records first, then this record, then returns it. Inside a Ghidra session, the binary session makes the same call.

Three defaults fill in what a producer leaves out. When the producer names no confidence or authority, the record gets observed and shipped-artifact. The subject name is the last segment of the path. When there is no target at all, the record gets the limitation "Artifact identity is unavailable for this observation.".

Confidence and authority

Confidence: one of three words, observed, derived, or inferred. It is a category, never a number. Authority: the kind of source behind the fact, one of shipped-artifact, controlled-replay, historical-reference, external-service, or analyst-inference. The two fields are set by the producer, and the captures show four producers.

ProducerconfidenceauthorityRecord
analyze_function, provider ghidraobservedshipped-artifact10-mcp-09-analyze-function-percent-change-O1.json
binary_overview, provider rea-workflowderivedshipped-artifact05-analyze-arm64-O1.json
compare_functions, provider rea-function-comparisonderivedanalyst-inference10-mcp-20-compare-functions-percent-change.json
inspect_analysis_view, provider rea-analysis-viewderivedshipped-artifact15-js-app-view-summary.json
record_unknown, provider rea-unknown-registryderivedanalyst-inferencenone in the capture

Read observed with care. In the dossier it means that rea recorded what Ghidra returned, with the bytes on disk as the authority. It does not mean that the pseudocode is the program's source. The provider documentation says so. The assembly is the full Ghidra Listing text, and the pseudocode is what the Ghidra decompiler produced.

The binary_overview record is derived, because a rea workflow computed it from provider calls. Its first limitation says so: "Derived by an REA composed workflow." The other eight are the Ghidra limitations it inherits. Each one starts with the provider, its version, and the operation, for example "Ghidra (ghidra 12.1.4) read_binary_inventory_summary:" (05-analyze-arm64-O1.json). Lesson 35 builds a ledger in plain Python with the same three words and the same five authorities.

limitations is an array of plain strings, and a producer appends its own to the ones it inherits. The comparison record shows both kinds. Its first entry is the automatic one for a record without a subject. The next 26 are the dossier limitations of both sides, each prefixed with Left: or Right:.

the trust fields of the compare_functions recordjsonFull log
"confidence": "derived",
"authority": "analyst-inference",
"environment": null,
"limitations": [
  "Artifact identity is unavailable for this observation.",
  "Left: Cancelling an active Ghidra request terminates its ephemeral database and discards session annotations. The selected target remains open; the next provider query imports the original artifact again.",
  …
],
"locations": [],
"evidence_links": [
  "ev_b492c85eb9570bd5f8e866f94523122c8600ea0106603d90e9ea5445cf757b2a",
  "ev_142cc943d8259d42ce5d62ad5eb0417fcde4de4ee9a20afbfe1fdbc51ed3d259"
]
Of the 27 limitations, the first two are shown.

evidence_links names the records a derived record was computed from. The comparison links the two dossiers it compared, and a view links its parent record. locations says where in the artifact the fact was seen. The JavaScript record in 15-js-app.json carries one artifact-path location, artifact-root. The Ghidra dossier carries none, because its addresses live inside normalized_result.

analysis_profile is part of the identity even when it is empty. The field is always present, as a profile object or as null, and both values count toward the identity of the record.

A profile whose provider differs from the record's provider is rejected with "Analysis profile provider does not match Evidence provider". The profile digest is a SHA-256 over the canonical JSON of its provider and parameters. The snapshot cache uses the same digest as part of its key.

The id is a hash of the semantic content

Semantic projection: the view of the record that the id hashes. It keeps subject.digest, subject.format, and subject.architecture, and it drops subject.name and subject.local_path. Every other field enters as it is, relationship_coverage too when its value is null. Release 6.5.0 left a null coverage field out of the hash. Release 6.7.0 therefore refuses the id of such a record in a 6.5.0 bundle (export and import).

The id is ev_ followed by the SHA-256 of that projection in canonical JSON. Object keys are sorted as RFC 8785 requires. parseEvidence recomputes the hash on every record it reads and rejects a mismatch with "Evidence semantic identifier does not match its record".

You can do the same check with a few lines of Node. The script below sorts keys, serializes without spaces, drops the two display fields, and hashes. Give it a bundle file and a record index to check one record of a bundle.

import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";

const canonical = (value) => {
  if (value === null || typeof value !== "object") return JSON.stringify(value);
  if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
  return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
};

const file = JSON.parse(readFileSync(process.argv[2], "utf8"));
const pick = (value, index) => {
  if (value.response) return value.response.result.structuredContent;
  if (value.records) return value.records[index];
  return value;
};
const record = pick(file, Number(process.argv[3] ?? 0));
const { evidence_id, subject, ...rest } = record;
const projection = {
  subject: subject === null ? null : { digest: subject.digest, format: subject.format, architecture: subject.architecture },
  ...rest,
};
const digest = createHash("sha256").update(canonical(projection)).digest("hex");
console.log(evidence_id, evidence_id === `ev_${digest}` ? "match" : "mismatch");

Run on 05-analyze-arm64-O1.json, it prints ev_91bddcdbd53ddd56996c6009458495bbd91fdc8c885238b33432e67691cf1f06 match, the id on line 2 of that file. Run on 10-mcp-09-analyze-function-percent-change-O1.json, it matches ev_b492c85eb9570bd5f8e866f94523122c8600ea0106603d90e9ea5445cf757b2a. It also matches the comparison record in 10-mcp-20 and, with the index 0, the first record inside 11-bundle-O1.json. The masked $CAPTURE path in subject.local_path changes nothing, because that field is outside the hash.

Two files in the capture do not match, and both teach the same rule. 15-js-app.json fails because the kit cut graph and semantic_graph out of normalized_result with --filter-output. 15-js-app-view-summary.json fails because the kit masked the capture directory as $CAPTURE inside normalized_result.summary.input_path and inside locations. A path in subject is outside the hash. A path anywhere else is inside it, and a record that carries one gets a different id on every machine.

Fig. 5.1the sixteen fields of one Evidence recordstructure
The sixteen fields of one Evidence record A table of the Evidence record ev_b492c85eb9570bd5f8e8 for analyze_function on _percent_change in ledger-arm64-O1, one row per field with its captured value: the subject digest, format and architecture, the Ghidra 12.1.4 provider, the analysis profile digest, predicate type, operation, parameters, raw and normalized results, confidence observed, the artifact bytes as authority, a null environment, ten limitations, complete relationship coverage, and empty locations and links. Two rows are drawn dashed, subject.name and subject.local_path, because the evidence id hashes every other row and not these two. EV_B492C85EB957, ANALYZE_FUNCTION _PERCENT_CHANGE FIELD NOTES Evidence record, 16 fields, strict schema evidence_id ev_b492c85eb9570bd5f8e8 SHA-256 of the solid rows subject.name ledger-arm64-O1 display only, not hashed subject.digest.sha256 01b5d08b08fb94678be5 identity of the analysed bytes subject.format mach-o subject.architecture arm64 subject.local_path $CAPTURE/fixtures/ledger-arm64-O1 display only, not hashed provider ghidra, Ghidra, 12.1.4 the engine, with its version analysis_profile digest 3fc72c9b4b78498fb9 the committed Ghidra settings predicate_type rea.analysis operation analyze_function parameters {"procedure": "_percent_change"} the caller input, in full raw_result object: the Ghidra report provider form, or null normalized_result object: the dossier, 16 keys what the agent reads confidence observed observed, derived, or inferred authority the artifact bytes one of five source kinds environment null limitations 10 strings kept with the claim, in full relationship_coverage complete in the hash also when null locations [] evidence_links [] ev_ ids this record derives from Solid rows enter the id hash. Dashed rows are display fields. Digests show their first hex digits.
The id hashes every field of the record except subject.name and subject.local_path, so the same observation of the same bytes gets the same id anywhere. Read top to bottom. Solid rows enter the hash and dashed rows are display fields. Values are from the function capture, digests shown by their first hex digits.

The Inside a Decompiler machine files each stage of one decompilation in an evidence ledger. The ledger uses the same three confidence words and adds a tray for unknowns. This link opens the run with the symbol table switched off, so you can watch which facts move from observed to unknown.

rea (rea-agents) with the Ghidra provider rea-agents 6.7.0, Ghidra 12.1.4 · 27dc42d · 2026-10-18 · Edition 2026.10