Where every receipt member goes
The input is one receipt object under receipt. Every member maps into exactly one place in each envelope; nothing is invented, defaulted or dropped.
| Receipt member | Lineage RunEvent member | Event envelope member |
|---|---|---|
execution_hash | run.runId (derived ; see the derivation table below) | id |
tool_id | job.name | data.tool_id |
namespace | job.namespace (and every input dataset's namespace) | none (the caller source lands on the envelope instead) |
parent_hashes[i] | inputs[i] : one dataset named by the hash, {namespace, name, type: "table"}, the estate lineage-card facet dialect | data.parent_hashes (unchanged) |
parent_tool_ids[i] | none (paired to the parent hashes by position, never emitted) | data.parent_tool_ids (unchanged) |
chain_depth | none | data.chain_depth (unchanged) |
policy_parameters_hash | none | data.policy_parameters_hash (unchanged) |
event_time (caller-declared) | eventTime | time |
source (caller-declared URI-reference) | none | source |
| none (fixed by this node) | eventType: "COMPLETE", outputs: [], producer and schemaURL pinned to the vendored 1.55.0 schema | specversion: "1.0", type: "co.ainumbers.ocg.receipt.v1", datacontenttype: "application/json", data = the receipt fields unchanged |
The required-field surface each envelope must carry is pinned: the RunEvent mapping validates against the vendored JSON Schema (every emitted event is schema-validated in the node's fixture test), and the envelope always carries the four required context attributes of its core spec, id, source, specversion, type , plus the structured-mode members time, datacontenttype and data.
A pure function of the receipt
The RunEvent needs a run identifier. This node does not mint one at export time, because that would make the identifier a function of the moment, not of the receipt. Instead the runId is derived from the receipt's own execution_hash in three deterministic steps:
| Step | Operation | Result |
|---|---|---|
| 1 | Take the first 32 hex characters of execution_hash (its first 16 bytes) | 16 raw bytes |
| 2 | Set byte 6's version nibble to 8: b[6] = (b[6] & 0x0f) | 0x80 | version bits 1000 in the high nibble; the receipt's own low nibble preserved |
| 3 | Set byte 8's two variant bits to 10: b[8] = (b[8] & 0x3f) | 0x80 | variant bits 10x in the two high positions; the receipt's own six low bits preserved |
| 4 | Format the 16 bytes as canonical lowercase 8-4-4-4-12 hex | the runId |
Every other byte of the runId is byte-identical to the first 16 bytes of the receipt's hash. That makes the runId a commitment device: whoever reads the envelope can re-derive it from the receipt and detect a substituted receipt immediately. Example: hash 656e6fd6… yields runId 656e6fd6-3469-85d1-a93a-6dcffbebdb8d (bytes 6 and 8 carry the two bit-sets; nothing else moves).
Refused exports carry no envelopes
Any missing or malformed member refuses the export whole. A refusal emits no envelopes at all (both envelope members are null), so a downstream stack can never ingest a half-mapped receipt.
| Refusal trigger | Refused field |
|---|---|
| Any of the nine required members absent | the missing member's name |
execution_hash or policy_parameters_hash not 64 lowercase hex characters | execution_hash / policy_parameters_hash |
event_time not a well-formed timestamp (calendar-checked: month, day, leap-year February, hour ≤ 23, minute ≤ 59, second ≤ 60, offset present) | event_time |
source empty, non-ASCII, whitespace or control characters, a forbidden raw delimiter, or a percent sign not followed by two hex digits | source |
parent_hashes entries not 64-hex, or parent_tool_ids length differing from parent_hashes length | parent_hashes / parent_tool_ids |
chain_depth not a non-negative integer | chain_depth |
Hard fence: the receipt is caller-supplied and caller-asserted. This node performs no verification of the hash values themselves; this node is an exporter, not a verifier. Correctness of the underlying execution is scored by the estate's recompute nodes, never by this receipt's envelopes.
Export envelopes from a pasted receipt
Paste one receipt (the receipt object itself, or a wrapper {"receipt": ...}; both accepted). The page runs the same mapping the kernel runs, locally, with no network.
Contract surface
Kernel: chaingraph/kernels/art-831-receipt-export-envelopes.kernel.mjs, a pure total mapping from a validated receipt to two JSON envelopes; no hashing inside the mapping path, no iteration bound from inputs beyond the parent arrays. Deferred proving (a new gpu:false node, §18 steady-state rule). Conformance fixtures carry pinned golden hashes; every emitted RunEvent validates against the vendored 1.55.0 schema in the property test. Adjacency: none (a terminal export view; the node consumes nothing and feeds nothing). the node consumes nothing and feeds nothing.
input : {receipt: {tool_id, execution_hash, parent_hashes[], parent_tool_ids[], chain_depth,
policy_parameters_hash, event_time, source, namespace}}
output : {status: exported|refused, event_time, lineage_event, event_envelope,
refused_field?, reason? <- refusal carries no envelopes (both null)}
flags : RECEIPT_EXPORTED | RECEIPT_EXPORT_REFUSED (+ root/multi-parent mirror flags)
Clause authority: the two wire specs are pinned by the row (snapshots under the workspace research/clause-snapshots/receipt-export/ dir; excerpts registered in chaingraph/standard/clause-snapshot-registry.json). The RunEvent schema is vendored as test data with its Apache-2.0 license at chaingraph/kernels/fixtures/vendor/openlineage-1.55.0/.