Skip to content

Agentic-emission schema versioning — additive-only evolution + frozen canonical projection as integrity anchor


title: Agentic-emission schema versioning — additive-only evolution + frozen canonical projection as integrity anchor date: 2026-05-18 status: accepted outranks: [] mr_compliance: [MR-4, MR-7] relates_to:

  • docs/specs/2026-05-18-sp2-cairnet-production-erid-design.md
  • docs/plans/2026-05-18-sp2-cairnet-production-erid-implementation.md
  • docs/decisions/2026-05-18-cairnet-bridge-principal-binding.md

Agentic-emission schema versioning — additive-only evolution + frozen canonical projection as integrity anchor

Section titled “Agentic-emission schema versioning — additive-only evolution + frozen canonical projection as integrity anchor”

SP-2 introduces an append-only integrity ledger over CAIRNET agentic-emission stones. The ledger’s value depends on a hash chain that stays valid as the emission schema evolves. Without a stated evolution rule, any future field addition to a stone risks silently invalidating every historical hash, destroying the chain’s audit value. This ADR fixes the schema-evolution contract and names the exact version-stability anchor that enforces it.

  1. 2026-05-18 — agentic-emission schema evolution is additive-only.

    • New fields are additive-only. A change that adds a field is a minor-version bump.
    • No field may be removed or repurposed within a major version. Repurposing (same key, changed meaning/type) is a breaking change, not additive.
    • Readers MUST tolerate unknown fields (forward-compatibility): an older reader encountering a newer minor-version stone ignores fields it does not recognise rather than failing.
  2. 2026-05-18 — the SP-2 ledger canonical projection is the version-stability anchor. The anchor is the fixed projection in pebble/src/pebble/domain/stone_canonical.py (_PROJECTION, in the devarno-cloud/pebble repo). Its frozen canonical (right-side) key set is exactly, and only:

    • stone_id
    • agent_id
    • stoneType
    • createdAt
    • payload

    content_hash hashes ONLY this projection (sorted-key, compact, UTF-8 canonical JSON). The mixed casing — snake_case stone_id/agent_id alongside camelCase stoneType/createdAt — is the deliberately frozen schema, not an oversight: the right-side names are byte-frozen by SP-2 spec §3.3 and this ADR. The persisted-stone source keys (left side of _PROJECTION) may be re-mapped, but only as a code change to the ledger normalisation layer, which is itself part of the frozen contract boundary — not a free or independent change. Concretely: the real _stone_to_dict emits agentId/content (camelCase); the ledger normalisation layer maps agentId→agent_id and content→payload before the projection runs, and stone_canonical.canonical_projection hard-raises if any required source key is missing. The canonical right-side names may change only via a superseding ADR (MR-7) that also issues a new projection version.

  3. 2026-05-18 — the unverified-legacy boundary is by deliberate design. Pre-SP-2 history is forward-only verifiable via lazy legacy-genesis: the ledger anchors from the first SP-2 emission forward, and pre-SP-2 stones are admitted as an unverified-legacy genesis boundary rather than retro-hashed. This is a deliberate design choice, NOT a defect — pre-SP-2 stones predate the canonical projection contract and cannot be hashed under a contract that did not exist when they were written.

  • A v2 ADDITIVE field cannot invalidate v1 chains. Because content_hash covers only the frozen 5-key projection, a new emission field is invisible to the hash. This is exactly what the Pebble T1 canonical projection plus its additive-field-immunity unit test guarantee.
  • A BREAKING change (field removal, repurpose, or projection key rename/casing change) requires: a major version bump, a migration ADR, and a NEW projection version — i.e. a superseding ADR per MR-7. Concretely, a new projection version means a new named projection constant plus a parallel hash function in stone_canonical.py (e.g. _PROJECTION_V2 / canonical_content_hash_v2), not an in-place edit of the existing _PROJECTION / canonical_content_hash (so v1 chains stay independently verifiable). It is never a silent in-place edit of stone_canonical.py.
  • The canonical projection and content_hash are computed only in Pebble (stone_canonical.py canonical_content_hash). The CLI does not independently re-derive content_hash from raw stone fields: cli/src/cairnet/chain.ts rowHash computes the ledger ROW hash sha256(content_hash ‖ prev_hash ‖ op ‖ ts ‖ actor), and recomputeAndVerify (SP-2 T7) checks chain linkage and recomputes the row hash over the committed content_hash as received from Pebble’s /events — it trusts that content_hash as authoritative. The cross-repo byte-identity invariant SP-2 actually depends on is the ROW hash construction (the sha256(content_hash ‖ prev_hash ‖ op ‖ ts ‖ actor) join — U+2016 separator, exact field order) being identical between Pebble stone_ledger._row_hash and CLI chain.ts rowHash, not a duplicated canonical projection. Independent CLI re-derivation of content_hash from raw stone payloads is deliberately out of scope for SP-2 (consistent with the chain.ts scope comment); it is not an unfulfilled obligation. The additive-only rule keeps the Pebble-side projection stable so this committed-content_hash trust remains sound without coordinated lock-step releases.
  • Readers tolerating unknown fields (Decision §1) means the concrete readers — the dashboard, the rocky-status Tier-1 head-check, and the cairnet-verify Tier-2 deep verifier — degrade gracefully across minor versions; only a major version requires a coordinated upgrade.

Accepted. The frozen canonical projection lives in pebble/src/pebble/domain/stone_canonical.py (_PROJECTION, in the devarno-cloud/pebble repo); the SP-2 cross-repo seam the CLI shares with Pebble is the row-hash construction in cli/src/cairnet/chain.ts (rowHash, SP-2 T7), not the projection itself. Cross-references in frontmatter relates_to.