docs/schema.md in ubiquex is, in its own words, the most load-bearing
document in the project. Everything the trust chain
depends on — a proposal’s hash matching exactly what was signed, a stale
proposal being detected rather than silently applied — only holds if the
rules for computing that hash are exact, stable, and never quietly
changed. This page mirrors the ratified subset of those rules. If it
disagrees with docs/schema.md itself, the source doc is correct and
this page is stale — see the note at the bottom.
For the wire shapes these rules apply to, see Proposal
and IR. This page is specifically about how a proposal’s
id gets computed, and why the rule for that can’t drift.
Canonical hashing — ratified v1
Ratified 2026-07-10. Any further change to any rule below requires aschema_version bump and an explicit migration path for existing ledger
entries — never an edit in place. That constraint is the whole point:
once a real ledger exists, these rules are effectively unfixable, since
changing them retroactively would change the hash of every proposal
already signed against the old rule.
- Hash function: SHA-256.
- Domain separation: the literal prefix bytes
ubx:proposal:v1\nare prepended to the canonical serialization before hashing. This scopes the hash to “ubx proposal, schema v1” specifically, so it can never collide with a hash of the same bytes computed for a different purpose — a different object kind, a future incompatible v2 encoding, another tool entirely reusing the same canonical-JSON bytes. - Hashed content: the whole proposal object, excluding exactly three
fields —
id,acceptance,status. Nothing else is excluded, and in particular everyresolution.*field, includingresolved_at, is included, since it’s part of what was actually reviewed at acceptance time.idis excluded because it IS the hash (a self-reference would be circular);acceptanceandstatusare excluded because both get recorded after the hash already exists and must not be able to perturb it. - Serialization: canonical JSON, RFC 8785 (JCS) style — UTF-8, sorted object keys, no insignificant whitespace.
- Number encoding: every number in hashed content is either a JSON
integer that fits exactly in a signed 64-bit int, or a decimal value
encoded as a JSON string (
cost_delta.monthly_usdbecomes"59.00", never a bare59.00). A JSON float literal anywhere in hashed content is a hard failure at propose time, not a silent coercion — this sidesteps float serialization ambiguity (trailing zeros, exponent form,-0, NaN/Inf) entirely rather than trying to canonicalize it away after the fact. - Array ordering:
delta.creates,delta.modifies, anddelta.destroysare sorted lexicographically by(stack, type, name)— never by dependency or topological order. Apply-sequence ordering is a resolver/executor concern; deriving the hash-significant order from a topo sort would make the hash sensitive to internal graph-algorithm choices, exactly the nondeterminism this rule exists to rule out. Any other array with hash-significant order needs its own explicit, documented sort key — none may rely on insertion or map-iteration order. - Determinism upstream: no map-iteration ordering anywhere feeding the hash, no environment or clock leakage except fields explicitly recorded for that purpose. Any evaluator producing hashed content runs twice, byte-for-byte identical output required, hard failure on mismatch — the double-run rule.
Versioning
Every persisted object carries an integerschema_version. The ledger
is permanent, so a reader has to support every prior version it might
ever encounter, or provide a real migration — there’s no “just delete
the old ones,” because deleting a ledger entry is exactly the kind of
silent, unrecoverable loss the whole system exists to prevent.
The amendment discipline
docs/schema.md grows by dated, UBI-numbered amendment sections, never
by rewriting a section in place once it’s shipped. A typical amendment
names the date, the ticket, what changed, and — critically — why the
original draft was wrong or incomplete, so a reader encountering an old
proposal on disk can still make sense of which rule applied when it was
written. This is the same discipline the ratified hashing rules above
make load-bearing: once something is real and signed against a rule,
that rule’s history has to stay legible forever, not just its current
state.
This page was last reviewed against
ubiquex commit
b203594
of docs/schema.md — the same commit sync-state.json records.
sync-drift-watch flags this page for review the moment a new commit
touches that file; check the source doc
directly for anything more recent than that.
