Repository navigation
ciris-persist v60.0.0
[60.0.0] - 2026-10-26
An architecture cut (docs/V60_TASK_PLAN.md): signed attestations are the only
state (CEWPOS). The cirisnode classes and Goals become scores rows, the
organisation planes become affiliation rows, the key-record, consent and
sweep doors decide in one shared plan each, and the in-process memory backend
is gone. No feature outside the issues named here. Sections 1–14 are in
slice order (S1–S6, then S0's fixes and tooling) and §15 is the advisories;
§16 is the whole adoption list for CIRISEdge, CIRISServer and
CIRISAgent, derived from git diff v59.1.0.. over the .pyi, src/ffi,
the error enums and the migrations. Migrations V202–V205; witnesses from
I1161; supersets.rs EXPECTED 313 → 342.
Changed — 1. the cirisnode classes are scores rows (S1; CIRISPersist#988 item 1, #803, #854)
Signed attestations are the only state (CEWPOS). Every cirisnode class is a
dimension CC 3.1.9 registers, so each is now a hybrid-signed scores row on
the attestation plane, signed by its contributor's federation key, admitted by
the plane's own door and replicated like any row. The cirisnode.* typed
tables that held self-signed, Ed25519-only, never-replicated copies are gone.
| class | row | admission |
|---|---|---|
| Contribution (CC 2.1) | contribution:{subject_kind}:v1 |
the plane's door |
| vote | vote:{contribution_id}:v1, attested to the Contribution's author |
the plane's door |
| moderation event | moderation:{allegation_type}:v1, attested to and naming the accused |
the moderation gate (moderate duty-holders) |
| slashing | slashing:{outcome}:v1, naming the accused as subject |
the plane's door |
| reconsideration filing / ruling | reconsideration:{grounds}:v1 (a ruling carries outcome) |
the v59 subject-standing rule |
| credits / expertise balance | credits:{domain}:{language}:{subject}:v1 / expertise:{domain}:{language}:v1, this node → the contributor |
the plane's door; PROJECTED into the ledgers |
| takedown notice | takedown_notice:{basis}:v1, subject_kind: takedown_notice, score -1 |
NEW: the CC 4.5.5 duty-holder gate |
- The ledgers are projections (CLM-silo-projection-rebuildable,
federation::ledger_projection).credits_ledger/expertise_ledgerhold
the fold of THIS node's balance rows (a peer's balance about the same
contributor is held, never projected), the latest(asserted_at, attestation_id)per key, written by the put door's executor in the row's
transaction on sqlite and postgres. Each ledger row is a pure
function of one row:last_updated_atandcreated_atare both its signed
instant, and it carries the row'sattestation_id(new column). New
SiloProjection::CreditsLedger/ExpertiseLedger; a pre-V203 balance
(noattestation_id) is a red incheck_projection_rebuilduntil the
rehome (Engine::cirisnode_rehome_ledgers, run first by every balance
write) re-emits it as this node's row. - The takedown notice is a row (#803, CLM-takedown-notice-row). The put
door admits atakedown_notice:{basis}:v1row only from a duty-holder over
the content as this node holds it (takedown_authority, the decision the
tombstone door re-asks); thesteward_purgeleaf stays held shut (its
cosigned door is not built).REGISTERED_FAMILIES_HELD_SHUTnarrows from
takedown_notice:totakedown_notice:steward_purge:. The tombstone door
takes the notice row (TakedownNotice::of_row), verifies its hybrid
signature against the signer's registered keys (an unknown signer is the
retryablefederation_attester_key_unknown), holds the row, and its header
carries the row whole;TakedownReport.notice_attestation_idnames it. A
notice row is about its content, never a binding of it:content_binding
skips the family.LegalBasistakes CC 3.3.2's wire strings:dmca512,
dsa_article16(dmca_512/dsa_article_16no longer decode); the
pinned-divergence test is deleted. - Compat read, one release (v60.x, removed in v61): a v59 peer's purge
bundle still carries its notice as the retiredtakedown_notice
CONTRIBUTION.apply_replicated_takedownstill applies it (Ed25519
self-signature, as v59 verified it; basis read in CC's spelling; CC 4.5.5
re-asked here). The LOCAL door refuses the Contribution
(takedown_unauthorised, "retired"). - #854, one subject gate.
SubjectGateand
validate_subject_key_ids_atare removed;validate_subject_key_idsis the
rule at every door, a replicated row included, so an uppercase subject id
(the raw base64 pubkey the retired Contributions carried) is refused at
ingest. The canonical-spelling clauses run before the general uppercase one
so each refusal names its defect. - Retired, refused by name: a
key_grantContribution
(cirisnode_key_grant_retired; the key-grant plane,key_grant:{axis}:v1,
is the one door for a wrapped key), and a v59 typed envelope offered at a
cirisnode door (it carriessignature/ a contributor id:
cirisnode_typed_object_retired). Newcirisnode::Error::Retired(its
kind()is the token) andcirisnode::Error::Federation(a plane refusal,
itskind()the plane's). - V203 (sqlite + postgres twins): drops
contributions,votes,
moderation_events,slashing_attestations,reconsideration_requests,
reconsideration_attestationsand V056's two watch tables
(cirisnode_consent_sla_watch,cirisnode_revocation_promotion_watch,
nothing read them): 8 tables. Keeps the ledgers (withattestation_id),
scheduled_takedown_actions,federation_delivery_attestationsand
promotion_attestations; their foreign keys into the dropped table go
(sqlite: staged and re-created under the final name; postgres:DROP … CASCADEdrops only the constraints) and the row-naming columns become
TEXTon postgres. Rewritespurge_executions.legal_basisto CC's
spelling. The dropped rows were self-signed by keys persist cannot re-sign;
nothing outside persist wrote these tables. - Kept, with a row rule: a promotion record's targets are rows held here
on the target kind's family (all or none; theis_canonicalflip is gone
with the tables); a delivery attestation's announcement is a held
contribution:federation_announcement:v1row.
Python surface (method names kept; each write plans the row and signs it
with the engine's own composed hybrid signer):
| method | v59 | v60 |
|---|---|---|
cirisnode_put_contribution |
(envelope_json) -> None |
(body_json) -> str (the row id; a takedown_notice subject kind files the notice row) |
cirisnode_cast_vote |
(envelope_json) -> None |
(body_json) -> str ({contribution_id, score, detail?, rationale?}) |
cirisnode_update_credits_ledger |
(update_json) -> None |
(update_json) -> str |
cirisnode_update_expertise_ledger |
(update_json) -> None |
(update_json) -> str |
cirisnode_put_moderation_event |
(event_json) -> None |
(body_json) -> str ({target_contributor, allegation_type, community_id, payload}) |
cirisnode_put_slashing_attestation |
(att_json) -> None |
(body_json) -> str ({moderation_id, outcome, payload}) |
cirisnode_put_reconsideration_request |
(req_json) -> None |
(body_json) -> str ({slashing_id, grounds, payload}) |
cirisnode_put_reconsideration_attestation |
(att_json) -> None |
(body_json) -> str ({request_id, outcome, payload}) |
cirisnode_list_contributions |
(filter_json, cursor_json, limit) |
(author_id, filter_json=None, cursor_json=None, limit=100); items are rows, the cursor an AttestationCursor |
cirisnode_list_votes |
(filter_json, cursor_json, limit) |
(contribution_id, voter_id=None, cursor_json=None, limit=100) |
cirisnode_list_takedowns_for_filtered_json |
filter optional | filter_json must name claimant_key_id; items are notice rows |
cirisnode_process_takedown_admission_json |
(contribution_json) |
(notice_attestation_id); the report adds notice_attestation_id |
cirisnode_get_credits_ledger / _expertise_ledger |
— | the entry adds attestation_id; created_at is the row's instant |
cirisnode_put_promotion_attestation |
— | target_ids are held row ids |
cirisnode_put_delivery_attestation |
— | the announcement must be a held announcement row |
cirisnode_rehome_ledgers |
— | NEW () -> int |
cirisnode_check_ledger_projection |
— | NEW (projection) -> str (credits_ledger / expertise_ledger) |
Removed: cirisnode_list_takedowns_for_json, cirisnode_list_key_grants_for_json,
cirisnode_list_key_grants_for_content_json,
cirisnode_list_key_grants_for_filtered_json,
cirisnode_list_key_grants_for_scope_epoch_json,
cirisnode_put_key_grant_json, cirisnode_retire_key_grants_json.
Unchanged: cirisnode_routable_contributors, cirisnode_read_vote_weight,
cirisnode_list_delivery_attestations, cirisnode_count_delivery_attestations.
Rust surface: NodeCoreService keeps the ledger reads, the promotion
record and the delivery attestations, and adds legacy_ledger_rows; the
typed writes and list reads, put_key_grant, retire_key_grants and the
media reads are gone, with ContributionEnvelope, VoteEnvelope,
ModerationEvent, SlashingAttestation, ReconsiderationRequest,
ReconsiderationAttestation, the old ContributionsFilter, VotesFilter,
ListCursor, the list pages, TakedownFilter, KeyGrantFilter,
KeyGrantPayload and its types, RetireKeyGrantsOutcome,
takedown_notice_from_contribution, check_moderation_or_reject,
check_takedown_or_reject, payload_target_descriptor,
Engine::list_takedowns_for / list_key_grants_for and the sqlite
backend's set_admission_gate (the plane's gate is the gate). New:
cirisnode::rows (the planners, decode_input, the reads) and the
Engine::cirisnode_* doors. TakedownNoticePayload drops claimant_key_id
and asserted_at (the row's) and refuses unknown fields;
takedown_tombstone::takedown_terms is async (directory, notice, origin).
What Edge, Server and Agent adapt: §16.
- Witnesses: I1166 (sqlite, postgres) the ledgers are this node's
rows, arrival-independent, a peer's balance never projected, byte-equal
rebuilds; I1167 (sqlite, postgres) each class admits as a row
and the moderation / reconsideration gates refuse the stranger;
I1168 the retired objects refused by name; I1169 (sqlite, postgres)
a legacy balance is red until superseded; I1170 (sqlite and postgres
engines; and sqlite, postgres for the binding) the notice row:
admitted for the subject, refused for a stranger,steward_purgeheld
shut, drives the tombstone door, never binds its content; the balance
doors, the rehome and the delivery attestation's row rule; I1171
(sqlite, postgres) the one-release compat read; I1172 one
subject gate at ingest; I1173 (sqlite, postgres) V203 over an
upgrading node's populated store. The takedown witnesses (I786–I799, I772,
I778, I801) now build notice rows. Python:
tests/python/test_cirisnode_rows_surface.py(replaces the retired
test_transit_key_grant_surface.py; the transit grant is the key-grant
plane's,test_key_grant_rows_surface.py). - Mutation scope
scripts/mutants/scope-s1-cirisnode.txt: 215 mutants in four
shards, 175 caught, 40 unviable, 0 missed, nothing allow-listed (two rounds:
the first left 43 survivors, each a check no witness distinguished; the
witnesses above kill them all). I1107 and I1173 are not in itstests:
(their sites, the held-shut table and V203's SQL, are not mutation sites). - Evidence: CLM-takedown-notice-row (3.1.1) at the notice row's admission,
the door's row reader and the planner; CLM-silo-projection-rebuildable
(2.4.2) at the ledger projection.
Changed — 2. a Goal is a signed goal:{scale}:v1 row (S2; CIRISPersist#988 item 4, CC 3.1.9.7)
Signed attestations are the only state. A Goal was a typed value written by
put_goal into goals, never signed by its declarer and never replicated by
persist. It is now the declarer's scores row on goal:{scale}:v1, and its
lifecycle is the structural verbs:
| act | row |
|---|---|
| declare | scores on goal:{scale}:v1; goal_id is its attestation_id |
| amend | the declarer's supersedes naming the goal's current head, carrying the whole new body |
| retire | the declarer's withdraws naming the declaration; retired_at is its signed instant |
| "never mine" | the declarer's recants naming the declaration; the goal leaves the projection |
- The scale. CC 3.1.9.7's open
{scale}vocabulary: the v59GoalScope
mapssingle_declarer→self,cohort→cohort(the cohort id as the
envelope membercohort_id),federation→federation; any other
lowercase vocabulary token admits as the registry admits it. - M-1 is the family's admission gate (
goal::check_goal_row_admission,
run by the plane's put door for local and replicated rows): a goal
declaration or amendment whose envelope lacks a well-formed
meta_goal_alignment(a dimension in the seven, a non-empty rationale, an
optional well-formeddeliberation_ref, nothing else) is
federation_malformed_rowwith reasongoal_meta_alignment_malformed;
any other body defect (dimension not a cleangoal:{scale}:v1leaf, empty
goal_text,cohort_idpresent iff notcohort, score outside[-1, 1])
isgoal_body_malformed. Asupersedesnaming a held goal row must be
its declarer's and carry the body (federation_invalid_argument
otherwise). Three reasons appended tomalformed_row::ALL:
goal_meta_alignment_malformed,goal_body_malformed,
goal_typed_object_retired. - A negative score is the declarer's stance: positive pursues the stated
state of affairs, negative declares it a state the declarer works against
(an avoidance goal); the magnitude is the commitment. It is still a
declaration; it never retires or disowns anything. goalsis a projection (goal_projection,SiloProjection::Goals): one
row per declaration held (any declarer's), the head's body, written by the
attestation executor in the row's transaction on every backend and
byte-equal to its from-rows rebuild. Rows arriving out of order fold the
same (each row of a chain refolds its declaration). V204 (both
dialects) rebuilds the table:scope_kind→scale(single_declarer
rewrittenself; the closed scope CHECK dropped, the cohort discriminant
kept),goal_idTEXT on postgres, newattestation_id(the head row,
CASCADE) andscore,inserted_atdropped (a write clock is not a
function of the rows). A pre-V204 goal keepsattestation_idNULL, is not
returned by the reads, and is red in the rebuild until
Engine::rehome_legacy_goalsmoves it: this node's own goal is re-signed as
a row (its retirement re-signed as awithdraws); another declarer's is
removed (persist cannot sign for it, and the typed row never carried its
signature); one the gate would refuse is removed.- No replication compat read. Persist never replicated a Goal (no
replication kind carried one); CIRISEdge carried its ownGoalDeclaration/
GoalRetirementmessages wrapping persist'sGoal, which Edge retires
with this release. A goal now crosses as the row.
Python surface:
| method | v59 | v60 |
|---|---|---|
cirisnode_put_goal_json |
(goal_json) -> None (a typed Goal) |
(body_json) -> str ({goal_text, scale, cohort_id?, meta_goal_alignment, score?}; returns the goal id; a v59 Goal raises goal_typed_object_retired) |
cirisnode_retire_goal_json |
(goal_id, retired_at_rfc3339) -> None |
(goal_id) -> str (the withdraws row id; the instant is the row's) |
cirisnode_get_goal_json |
goal_id a UUID |
goal_id the declaration's row id; the JSON carries scale / cohort_id (not scope), score, attestation_id |
cirisnode_list_goals_json |
filter scope_kind |
filter scale; unknown keys refused |
cirisnode_amend_goal_json |
— | NEW (goal_id, body_json) -> str |
cirisnode_recant_goal_json |
— | NEW (goal_id) -> str |
cirisnode_rehome_goals |
— | NEW () -> str ({"rehomed": n, "dropped": n}) |
cirisnode_check_goal_projection |
— | NEW () -> str ({"live": n, "rebuilt": n}) |
Rust surface (clean break): FederationDirectory::put_goal and
retire_goal are gone; get_goal takes &str; legacy_goals /
delete_legacy_goal are added. Goal::new is gone and Goal is the
projection's read type (goal_id: String, scope, score,
attestation_id); GoalScope is a validated {scale, cohort_id}
(GoalScope::new); GoalsFilter::scope_kind is scale. New: GoalInput,
LegacyGoal, GoalRehome, goal::{parse_goal_body, check_goal_row_admission, plan_goal_row, plan_goal_amendment_row, plan_goal_retraction_row, decode_goal_input}, goal_projection, and Engine::{put_goal, amend_goal, retire_goal, recant_goal, rehome_legacy_goals}. What Edge adapts: §16.
- Witnesses: I1174–I1176 (unit) the scales, the M-1 rule, the four
planners and the retired typed body refused by name; I1177 (unit) the
fold; I1178 (sqlite, postgres) the projection through the put
door, out of order included; I1179 (sqlite, postgres) the M-1
gate at the door; I1180 (sqlite and postgres engines) the four Engine
doors, the declarer rule and the rehome; I1181 (sqlite, postgres) V204
over an upgrading store; I1182 (sqlite, postgres)
approach:{goal_id}binds the goal's row id. Python:
tests/python/test_goal_rows_surface.py. - Mutation scope
scripts/mutants/scope-s2-goals.txt: 155 mutants in three
shards, 127 caught, 28 unviable, 0 missed, nothing allow-listed (two rounds:
the first left six survivors; four were witness gaps, now witnessed, and two
were equivalent forms of a redundant test, now removed). Weekly, three
shards, inmutants.yml. - Evidence: CLM-nsproc-meta-goal-alignment-goal-scale (3.1.9.7) moves from
the retiredGoal::newtocheck_goal_row_admission;
CLM-silo-projection-rebuildable (2.4.2) atgoal_projection::fold_goal.
Changed — 3. the key-record write doors decide in one plan (S3; CIRISPersist#934 step 3)
Every door that writes federation_keys (and with it the key's
key:record:v1 row) used to run its own copy of its decision on each of the
three backends. The decision now lives once, in
key_plane::plan_key_record_write, one planner per door kind
(KeyRecordWrite: Put, SeedGenesis, ScrubUpgrade,
SupersedeCanonical, Rebind, InvalidReplace, Reanchor, AttachPqc,
RehydrateScrubs); it calls the gates that were already shared
(admit_key_record, the four conferral gates, check_node_identity_*,
prepare_rebind / prepare_invalid_replace, verify_canonical_supersede,
verify_bundle_quorum, check_key_pqc_attachment) in the order the copies
ran them. Each backend keeps ONE executor,
KeyPlaneStore::store_planned_key_record(PlannedKeyRecord): the SQL, the row
mapping and the transaction, the write's atomic re-assertion as its WHERE,
and the key's row derived from the record as stored through the shared
key_record_row_step. The replicated apply is one shared body too
(apply_replicated_key_record_planned).
The parity scan (store::parity) now reports put_public_key,
adopt_scrub_upgrade, store_rebound_key_record,
store_invalid_replaced_key_record, adopt_genesis_reanchor,
attach_key_pqc_signature and apply_replicated_key_record as shared
(they were identical x3 or, for the two that delegated to inherent methods,
invisible to it), and SHARED_DOORS holds them there.
Behaviour that changed (each was a backend divergence):
- The genesis seed stores the whole scrub set: both SQL backends' seed
INSERT leftadditional_scrubsout (the dropRehydrateScrubsrepairs,
#995 row 2). consent_roleis normalized to its wire form before the hash on every
backend: a record offered with the stored token
'unregistered'now hashes as theNoneit reads back as.- A sqlite insert that loses a race to an identical row is settled, and to a
differing row isConflict(it was a primary-keyBackenderror); a
rewrite whose guard matched nothing now rolls back on every backend (the
re-anchor committed its row step before refusing). - The genesis re-anchor's anti-rollback compares at the substrate's
resolution. Postgres holds the anchoredvalid_fromin microseconds, so
a bundle record with a nanosecond instant (the v2 ceremony's canonical is
…22.147317128Z) read as 128 ns newer than itself, and re-offering it to
adopt_genesis_reanchorre-wrote the row instead of refusing the
rollback. Both sides are now compared at the substrate's signed-instant
resolution (truncate_to_substrate_resolution; I1189). The bake door's
own pre-check (bake_assembled_genesis, serve nodes) had the same shape
and compares the same way: a held anchor carrying the bundle record's
nanosecond instant was read as older on postgres (I1191, sqlite and
postgres, red on postgres before the fix). - The canonical supersede settles a byte-identical re-offer before any
verify (the #672 rule), instead of after the policy check, where an
identical record could never arrive (verify_canonical_supersederefuses
it first).
Rust surface (clean break): the inherent SqliteBackend /
PostgresBackend::{adopt_scrub_upgrade, supersede_canonical_record, apply_replicated_key_record} are gone; the FederationDirectory methods of the same names are the doors
(bring the trait into scope). No FederationDirectory method changed, no
Python signature changed, no migration.
- Witnesses: I1183 (the insert door), I1184 (the genesis seed),
I1185 (the scrub upgrade), I1186 (the replicated apply), I1187
(each executor's atomic re-assertion, every write shape), all on sqlite
and postgres; I1189 (sqlite, postgres) the re-anchor's
own refusals over the real v2 ceremony; I1190 (sqlite,
postgres) the supersede's settle, pubkey-change and policy refusals;
I1191 (sqlite, postgres) the bake door's anti-rollback at resolution;
I1188 (unit) the row step and the lost-race wording. - Mutation scope
scripts/mutants/scope-s3-key-plane.txt: 129 mutants in
three shards, 101 caught, 28 unviable, 0 missed, nothing allow-listed (two
rounds: the first, 136 mutants, left twelve survivors; ten were witness
gaps, now I1187's anchored-version and rehydrate legs, I1189 (the
re-anchor's predicate and both SQL doors), I1190 (the supersede's three),
and the role-bearing leg of I1183; two were the row step's self-scrub guard on
sqlite and postgres, an equivalent branch, now removed on all three
backends). Weekly, three shards, inmutants.yml. - Evidence: CLM-planes-as-rows (3.1.3) at
key_plane::plan_key_record_write.
Changed — 4. the consent and sweep planes decide in one function each (S4; CIRISPersist#934 step 4, #1105)
Fourteen doors were written once per backend: the two consent writes
(set_consent_role, grant_trust), the five consent reads
(list_consent_peers, list_consent_peers_for,
list_live_consent_grants_by / _for, list_consent_revocations) and the
sweeps' selections (the PQC sweep's list_hybrid_pending_keys /
_attestations, the reaper's list_expired_attestation_ids,
evict_known_wire_hashes, the room retention leg
stream_dek_evict_room_chunks, and the two observation prunes). Each now
decides in one place:
- Writes —
consent_admit::plan_set_consent_role(the CC 3.4.7.2 token
gate, the wire normal form) andconsent_admit::plan_grant_trust(the #721
authority gate first, then the shape, then the row), run by
set_consent_role_planned/grant_trust_planned, which also word the
no-row refusal.validate_trust_grantmoved fromstore::memoryto
consent_admit. - Reads — one fold each over a typed read (
ConsentRead::{Node, For}):
the fold sorts and deduplicates the peers, filters the live grants to
consent:replication:v1and orders them, applies the subject-side
revocation rule (is_subject_side_revocation); the executor returns the
projection rows or a candidate superset and decides nothing. - Sweeps — one selection each in
sweep_plan
(HybridPendingSelection,ExpiredSelection,known_hash_eviction,
RoomChunkEviction::selects) andretention::plan_observation_prune;
each backend keeps the delete (or the read) as its executor. The room leg's
executor runs the shared predicate inside its delete transaction, so the
selection and the delete stay one atomic step. - The executors are three new crate traits, compared by the parity scan like
any door:ConsentStore,SweepStore,RoomEvictionStore. The key-grant
doors (key_grant_held_owner,project_key_grant_row) already ran one
shared body; they now read assharedtoo. - The parity scan reports all of them
shared(they wereidentical x3or
gateless) andSHARED_DOORSholds them; a newSHARED_SELECTIONSlist
holds the read folds and sweep selections to the opposite rule from a plan:
a selection runs NO gate, and some door runs it.
#1105 — the family-scoped key-plane holdings read (Edge ask).
FederationDirectory::list_key_plane_wire_hashes_since(family, after, limit):
the Attestation wire hashes of the scores rows whose dimension is one of
key_plane::HOLDINGS_FAMILIES (key:record:v1, key:revocation:v1,
identity:occurrence:v1, identity:occurrence_revocation:v1), paged and
cursored on the hash exactly like list_wire_hashes_since. One shared read
plan (key_plane::plan_key_plane_holdings, the family gate refusing any
other dimension as InvalidArgument, never an empty set) and one thin
executor per backend (KeyPlaneStore::read_key_plane_wire_hashes). Routed
through the directory capsule as DirectoryOp::ListKeyPlaneWireHashesSince
(appended; DIRECTORY_ABI_VERSION unchanged; a refusal crosses the capsule
as Backend text, like every op). Not on the Python surface.
Behaviour that changed (each was a backend divergence or an unordered
tie):
list_hybrid_pending_keys/_attestationswith a zero or negative
limitreturn nothing on every backend; sqlite returned EVERY pending
row for-1(LIMIT -1is no limit) and postgres refused it. Ties on the
order instant come by id.list_expired_attestation_idsorders ties onexpires_atby id, so the
reaper's OFFSET paging is a total order (it could skip or repeat rows that
expired together); an offset pasti64::MAXreads nothing (sqlite and
postgres paged from the start).list_live_consent_grants_by/_forandlist_consent_revocationsorder
ties onasserted_atby id, descending.grant_truststampstrusted_atwith the door's one clock read on every
backend (postgres used the database'sNOW()).- Postgres's room leg takes the minter's epoch rows
FOR UPDATEin its
transaction, so two concurrent sweeps of one minter do not both read an
epoch as not yet evicted. - A postgres
grant_trustwhose row trips a V020 CHECK now reads as
Backend(it was re-wordedInvalidArgument); the plan refuses both
shapes the CHECKs guard before the write, so no admitted grant reaches it. - The live-grant read counters are recorded by the shared fold, once per
read, under the executing backend's label (I551 learns the shape).
Rust surface: FederationDirectory::list_key_plane_wire_hashes_since
(new, default Unsupported); the directory capsule op above. No other
FederationDirectory method changed, no Python signature changed, no
migration.
- Witnesses: I1192 (the consent-role door), I1193 (the trust-grant
door), I1194 (the consent-peer and live-grant folds), I1195 (the
consent-revocation fold), I1196 (the PQC sweep's selection), I1197
(the reaper's selection), I1198 (the known-hash eviction), I1201
(the #1105 holdings read), all on sqlite and postgres; I1199
(unit) the room leg's predicate; I1200 (unit, and postgres) the
observation prune plan and its executor; I1196 / I1197 also unit. - Mutation scope
scripts/mutants/scope-s4-consent-sweep.txt: 302 mutants in
six shards, 256 caught, 46 unviable, 0 missed, nothing allow-listed. Three
rounds: the first (304 mutants) left nineteen survivors. Fifteen were
witness gaps: a subject's rule-2withdraws(I1195), the strict expiry
boundary (I1197), the attestation plane of the PQC sweep (I1196), and the
key-grant doors, whose witnesses the filter did not name (now I591 / I596,
I61, I800, I743). Two were the postgres trust executor's CHECK-name
mapping, unreachable behind the plan's shape validation, now removed. The
second round left the projection door (I743 added); the third, shard 1
again, was clean. Weekly, six shards, inmutants.yml. - Evidence: CLM-planes-as-rows (3.1.3) at
key_plane::list_key_plane_wire_hashes_planned; CLM-nsproc-tier-consent
(3.3.1) atconsent_admit::consent_revocations_planned. - Size: the backend files did not shrink at S4 (sqlite +31, postgres +34
lines): what each backend kept is the SQL and row mapping, which is
the executor's, and what left (the decisions) was small. The decisions
are now written once.
Removed — 5. the in-process memory backend; every genesis door runs on each backend alone (S5; CIRISPersist#1002)
Every test, bench and witness that ran on MemoryBackend runs on SQLite. The
memory backend answered with its own in-process re-implementation of every
door: real SQL, a real schema and real transactions are where the reds it
could not produce come from, and each door it carried was a third copy to keep
in parity.
- Removed (Rust, clean break):
store::MemoryBackendandsrc/store/memory.rs
(25,604 lines at v59.1.0; 11,949 when deleted, its tests moved first). No alias. - Added (Rust):
store::test_backend::test_backend()(featuresqlite) —
a fresh, fully migrated in-memorySqliteBackend, cloned from one migrated
template per process with the SQLite backup API (7.1 ms a clone against
248 ms foropen_in_memory()+ every migration;MemoryBackend::newwas
81 µs). Three doc-hidden fixtures stay public for the integration tests and
benches (add_public_key,add_pqc_public_key,snapshot_events); every
other below-the-door seam iscfg(test), so no consumer build links a
writer that skips the doors. - The genesis scratch is per backend (steward ruling 2026-10-25: parity
even for genesis). The doors that replayed a bundle on a memory node
(verify_ceremony_outputs, the import door,CeremonyState::finish,
AmendState::{plan, finish}) take a&dyn genesis::scratch::GenesisScratch
and run on what the backend provisions: SQLite a migrated in-memory clone,
PostgreSQL a throwaway database in the live cluster created from the
configured DSN (ciris_genesis_scratch_{unix_secs}_{pid}_{hex}), migrated, and dropped
WITH (FORCE)when the replay ends — on success, on a refusal, and on drop
(a panic). The configured role needsCREATEDBfor these doors. A rolled-back
transaction was not used: the replay runs a dozen doors, each on its own
connection. A scratch a killed process left behind (its name carries its creation
instant,ciris_genesis_scratch_{unix_secs}_{pid}_{hex}) is dropped by the
next provisioning once older than an hour
(GENESIS_SCRATCH_REAP_AFTER_SECS); a name the reaper cannot read is never
dropped. Every door compiles and runs withpostgresalone and with
sqlitealone. Signature changes (clean break):
verify_ceremony_outputs(bundle_json, scratch),
install_genesis_bundle_roster(dir, scratch, bundle),
CeremonyState::finish(scratch),AmendState::plan(pinned, rows, scratch),
AmendState::finish(scratch);Engine::install_genesis_bundle_rosterand
PyEngine.install_genesis_bundle_roster_jsonpass their own backend. New
refusalCeremonyOutputsRefusal::ScratchDirectory
(ceremony_scratch_directory): the scratch could not be provisioned or
released. Newstore::postgres::GENESIS_SCRATCH_DB_PREFIX. Witnesses
I1324–I1331. - Python: no Python surface named or constructed the memory backend, so
nothing is renamed.PyEngine.install_genesis_bundle_roster_jsonkeeps its
signature and runs on every backend the build carries. No.pyichange. - Telemetry: the
backendlabel set is{sqlite, postgres}
(observe::StoreBackend::Memoryand the registry member are gone;
telemetry/catalog.jsonre-rendered). A dashboard keyed on
backend="memory"sees no series. - Benches:
ingest_pipelineandqueuerun on the test backend and need
sqlite(they declaredpostgresand never used it). Their numbers now
include SQL writes; the baselines move. - Tests: the memory arm of every witness is gone (its duplicates of a
sqlite leg dropped; the tests only memory carried moved to
src/store/backend_suite.rs);store::paritycompares two backends; the
from-disk gates andrules/read two backend files. A build with no backend
feature allows dead code crate-wide (every caller of the shared plans is a
backend). The server and server-pipeline unit tests needsqlite. - Folded: #720 — the lifecycle filter is one plan-emitted predicate
(attestation_query::lifecycle_predicate, I1320) instead of four SQL
copies and a memory fold; #691 —store::tree_readbinds the parity and
schema-parity verdicts to the tree they read (I1321); #787 — every
DirectoryOphas anOpsDirectoryproxy or a stated reason (I1322).
Changed — 6. CC v1.0-rc9 vendored at the tag; the policy table is runtime data; the corridor runs at admission (S6v)
CIRISConstitution tagged v1.0-rc9 on 2026-10-21 (tag object a1c2bea5,
commit 5d50c6bb160ecbee99a66e1700597c029846e6b6, CIRISConstitution#182).
Persist vendors it at the tag: scripts/check_vendored_cc.sh v1.0-rc9 passes
for all twelve vendored files.
For Edge: the namespace registry diff (scripts/manifest_diff.py 32512f52, rc8.1 → rc9; registry _meta.registry_sha256 22b029cf… →
feeb06a0…, source 73aa281f… → afd1948e…):
| families | 174 → 176 (net +2) |
| added | identity:occurrence_revocation (CC 3.1.3, -1 only, reserved: signed by the identity or a live occurrence of it), transport:canonical_rebind (CC 3.1.3, positive-only, reserved: substrate-self-report) |
| removed | none |
| changed | objection:{state}: the {state} segment is now an OPEN vocabulary with the listed values proposal, assent, raised (it named no values); description prose |
_meta |
cc_version 1.0-rc8.1 → 1.0-rc9; n_families 174 → 176; per_component.persist 22 → 24 |
Both added families are persist's own v59 mints (CIRISPersist#1104, the
legacy occurrence revocation's carrier, and #1106, the holders' quorum
re-bind), now registered under CC 3.1.7 R2(a); objection:proposal:v1 /
objection:assent:v1 (the self-roster door's rows, #1099) are now in CC's
listed vocabulary. No admission behaviour changed for a conformant row: the
two carriers already carried the scores their polarity classes now state
(-1, +1). The match vectors grow 1147 → 1159 (six per new family; the
objection sample is proposal). The binds oracle is regenerated at the tag
commit.
pin (registry.rs) |
rc8.1 | rc9 |
|---|---|---|
VENDORED_CC_VERSION |
1.0-rc8.1 |
1.0-rc9 |
VENDORED_CC_COMMIT |
157039d3… |
5d50c6bb… |
VENDORED_SOURCE_SHA256 |
73aa281f… |
afd1948e… |
VENDORED_REGISTRY_SHA256 |
22b029cf… |
feeb06a0… |
VENDORED_N_FAMILIES |
174 | 176 |
VENDORED_REGISTRY_FILE_SHA256 |
44d4d480… |
be0b1c86… |
VENDORED_VECTORS_FILE_SHA256 |
c1a989a7… |
a1a6ce5f… |
namespace_match_binds.json |
(rc8.1 oracle) | 86a9980b… (1159 vectors) |
- Four fixtures are runtime manifests now (
src/federation/namespace/,
beside the registry, pinned in the newnamespace::cc_manifests):
governance_policy.json(CC 4.5.13,policy_sha256 b7d2370d…),
governance_policy_vectors.json(732 vectors),csd4_chain.json,
topology_vocabulary.json, each byte-identical togit show v1.0-rc9:manifests/<file>(they were copied from the untaggedrc9branch
in v59 / v60 S0; the bytes did not change). Readers repointed:
scripts/csd4_coverage.py, thetwo_pathreplay, thereportsworkflow.
CIRISServer's catalogue (traces.json,requirements.yaml,PIN.md,
topology_vocabulary.pin) stays undertests/fixtures/csd4/: no CC tag
carries it. The sixformal/self_rostertraces stay test fixtures, pinned
at the tag. - The policy table is read at runtime (
federation::governance_policy):
PolicyTableparses the compiled-in table once;policy_ofbuilds a row as
aTwoPathPolicy(ENACT strictly over its fraction; CANCEL / FLOOR at
least; the self rows' "strict majority of the others" and "one_voter";
cancel_over;at_window; the corridor's count minimum, checked against the
table'smin_count; the window checked against its class), refusing any form
it does not read;check_corridoris CC's own table check
(tools/build_governance_policy.pyat the tag) over
TwoPathPolicy::check_corridor: a reverse-path community row at bases 6, 7,
10 and the expected-active base 344, a forward-only row's enact and window
floors at 3, 6, 7, 10, 344, a self row in its own corridor at three keys.
admit(family, door)is a door's call: the row, built, on a built base
(standing), inside its corridor. - The corridor runs at admission (the piece v59 left for the tag): the
self-roster door's policy is the table'sself_roster_add/
self_roster_evictrows (self_roster::policy()now returnsResult), read
at every roster change (gate), every execute and every standing read. A row
outside its corridor, unreadable, or two self rows that disagree refuse the
change before anything is held: newError::GovernancePolicyRefused { family, reason },kind()federation_governance_policy_refused, Python
ValueError("federation_governance_policy_refused: {family}: {reason}"),
reasonaCorridorRefusaltoken or one ofpolicy_row_missing,
policy_not_two_path,policy_row_unreadable,policy_base_not_built,
policy_rows_disagree. With the vendored table it does not arise (every
two-path row is in its corridor, I1303).TwoPathPolicy::self_roster(window)
is removed (clean break): the table is the one source; the test-anchor
W_selfseam (#1111) still shortens the window of the table's policy. - Re-run at the tag's pins: the 732 CC governance vectors replay through
the RUNTIME reader (PolicyTable::policy_of, each family row also checked
in its corridor); the six self_roster traces replay under the table's
self-roster policy. - Refusal tokens at the pin (I1308): rc9 names
federation_set_semantics_unsorted,
federation_malformed_rowand the reasonsaggregate_source_duplicate,
envelope_member_missing,typed_diverges_from_envelope, all spelled as
persist emits them, andenvelope_member_null(CC 2.6.1.1.1 finding F4),
which persist does not emit: that is CIRISPersist#1098, kept out of v60 by
the task plan (§2). No CC text at rc9 (or on the rc10 branch at9f6ff75)
nameskey_id_fingerprint_mismatchorcarrier_without_roster_quorum; the
witness latches both lists. - Witnesses: I1300 (every vendored CC file hashes to its pin: the registry, its
vectors, the four manifests, the six traces), I1301 (CIRISServer's catalogue
pin names the vendored vocabulary), I1302 (the pins aregit show v1.0-rc9:bytes and the tag isVENDORED_CC_COMMIT, when a
CIRISConstitution checkout is present:$CIRIS_CC_REPOor a sibling
directory; it skips, saying so, without one), I1303–I1306 (the table: every
two-path row in its corridor, the self-roster policy is the table's at every
roster size 1..=12, each corridor clause refused by its token, an unreadable
row refused not guessed), I1307 (sqlite, postgres: under a table
whose self row is outside its corridor every roster change is refused and
nothing is held; under the vendored table the same changes land or are
held), I1308.scripts/check_vendored_cc.shcompares all twelve files. - Censuses moved by the re-vendor:
PINNING_FAMILIESgains
identity:occurrence_revocation(-1 only, 16); the contradiction-count
contrast is 150 / 16 of 176. - Evidence:
CLM-governance-policy-table(4.5.13) at
governance_policy.rs#PolicyTable::admit;CLM-policy-corridor(4.2.1.1)
atgovernance_policy.rs#PolicyTable::check_corridorand
two_path.rs#TwoPathPolicy::check_corridor. - Mutation:
scripts/mutants/scope-s6v-vendor.txt(with #1114's three
functions), 108 mutants, three weekly shards inmutants.yml. - No migration. No store-trait method added or removed.
Changed — 7. three signed-object refusals carry their own malformed_row reasons (S6v; CIRISPersist#1114, CC 2.6.1.7 rc10)
The steward's ruling on CC rc10 99bf187 (2026-10-21) appends three reasons
to the closed federation_malformed_row set (appended, never reordered: the
set is a hashed vocabulary). Each refusal keeps the Python type of the arm it
replaces (ValueError throughout); only the kind() token and the reason move.
| reason | condition | before |
|---|---|---|
content_hash_mismatch |
original_content_hash ≠ SHA-256 of JCS(envelope) (tier_ingest::check_content_hash) |
federation_tier_unverified, free-text reason |
envelope_row_member_unknown |
the signed row mirror carries an eighth member (admission::check_row_mirror_members_closed, ahead of the typed read) |
federation_malformed_row / envelope_member_invalid |
subject_key_id_malformed |
a subject_key_ids element is neither a canonical lowercase key_id nor canonical:sha256:<hex64> (validate_subject_key_ids, every door that asks it: the attestation door and emit_attestation) |
federation_invalid_argument |
- Edge: a sender that bounded
federation_tier_unverifiedor
federation_invalid_argumentas retryable now sees the TERMINAL
federation_malformed_rowfor these three; a mirror that is missing a
member is stillenvelope_member_invalid. The key-registration plane's own
registration original_content_hash mismatchis unchanged (not in #1114). - The three are rc10 tokens, after the
v1.0-rc9tag persist vendors: I1308
latches them as "named by no CC text at the pin" until the next re-pin. - Witnesses I1309–I1311 (sqlite, postgres) through
put_attestation. - Evidence:
CLM-signed-object(2.6.1.7) at the three symbols. - Mutation: the three functions join
scope-s6v-vendor.txt.
Changed — 8. an organisation is an affiliation; the three operational planes retire (S6; CIRISPersist#1036 items 1–5, #1049, #1062, #1048; CC rc9)
CC 3.3.9 moves organization, org_membership and partner_record onto the
rows every community already uses (CIRISConstitution#161, #162, #164, #167,
#168, #163). An organisation is a community record at
cohort_scope: affiliations, named as an authority by its
community_key_id; a member is a membership:{stage} seat plus a hierarchy
delegates_to; a partner is a licensure:{community_key_id}:v1 row carrying
the typed licence payload. No Portal data existed to migrate (operator
ruling).
- Item 1, the authority quorum (CLM-licence-authority-quorum,
CLM-authority-id-is-org-id). Under a quorumconsensus_protocola licence
is a COSIGNED row: its full scrub set (base scrub +additional_scrubs[])
intersected with the authority community's roster at the instant, each
survivor re-verified byreverse_quorum::count_distinct_roster_scrubs, is
evaluated under the community's own protocol. One member's signature is
testimony; a stranger's cosignature counts for nothing. Thelicense
chain roots at an edge the quorum cosigned that names the affiliation
(community_id); a lone member's appointment roots nothing; onward hops
needsub_delegation; a root edge is retracted only by a cosigned
withdraws(rule 0).licensure_delegator_not_authorityadmits this case.
admission::licence_authority_setreads the set. - Item 2 and #1049, judged at the signed instant with W in front
(CLM-licence-judged-at-issuance, CLM-admission-freshness-window). A
licence, a consent row'sgrantchain and akey_grantrow's sealer chain
(and the epoch-axis membership) are judged at the row's signed
asserted_at(grant_chain_instantis now the row's instant, not receipt).
Signed more than W = 72 h ago (self_roster::outside_past_freshness_window,
one reading of W for every door), the standing must ALSO hold at receipt,
elseasserted_at_stale(Error::AssertedAtStale, planelicensure/
consent/key_grant/owner_binding). This is the CC 3.3.6.3
2026-10-19 reading (#1099: W binds a signer that has lost standing), so a
new node syncing history admits its legitimate old rows. The stream read
path judges a sealer's chain at the stored grant rows' instants
(key_grant::stream_grant_row_instants; no migration), so a read never
refuses a grant admission accepted. A withdrawal stops future issuance and
does not reclassify a licence already issued. - Item 3, the audience (CLM-licence-audience). A licence has no public
default: for a licence under an affiliation, the charter's new
affiliation_config.licence_audiences({license_type: cohort_scope},
validated at the community door,licence_audience_invalid) is the
declared scope per class; a class with no declaration may take any scope
but a commons one. A non-public licence reaches other readers by
RE-EMISSION: a new row (memberreemission_of= the held licence's id),
signed and cosigned again at the same scope, naming its readers in
subject_key_ids, payload and status unchanged. Refusal
federation_invalid_argumentprefixedlicence_audience_refused:with
reasonsclass_not_declared_public,scope_not_the_declared_choice,
re_emission_names_no_reader,re_emission_target_unknown,
re_emission_diverges. The authority head a verifier needs (roster,
protocol,licenseedges) travels on its own planes. - Item 4, the kinds' move.
operational::PartnerRecordis the typed
licence payload (envelope memberpartner_recordon alicensure:scores
row). Ported: the field set (malformed →federation_malformed_row
envelope_member_invalid); therevisionanti-rollback against the
AUTHORITY's own held licences only (federation_partner_record_rollback;
a stranger's testimony neither counts nor is counted); the ±5 min skew
bound; payment-processor rejection over the whole envelope. The four
set-semantics arrays must be sorted by JCS string order (UTF-16 code
units) AND unique: a repeat, which v59 let through, is
federation_set_semantics_unsorted; producers call
PartnerRecord::canonicalize_setsbefore signing. The promotion
chokepoint re-runs the payload, revision, audience, owner-binding and
term gates. V205 (both dialects) dropsfederation_organizations,
federation_org_memberships,federation_partner_recordsand their
signed_wire_index/known_wire_hashesrows. The replication kinds
Organization,OrgMembership,PartnerRecordjoinRETIRED_FRAMEDat
their hashed positions inEnvelopeKind::ALL(nothing renumbered; the
manifest hash Edge and Server pin does not move): an inbound object is
refusedfederation_envelope_kind_retired. CLM-merge (5.3.2.3) moves from
the retiredSubjectKind::merge_intentto the licence's revision gate. - Item 5, contest (CLM-licence-contest). A subject's
withdrawsagainst
alicensure:*orrevocation:*row about itself is refused
(SUBJECT_WITHDRAWS_CARVE_OUT=capacity:,detection:,licensure:,
revocation:; the v59-deferred widening in
resolve_withdraws_admission_rule_at). The contest is
reconsideration:{grounds}: the licensee files; for alicensure:{A}
target A's authority set (plus anyreview-scoped chain from it) rules. - #1062, the organisation on the bond (CLM-org-on-the-bond). An
owner-binding that names a community is admitted only when that community
is an affiliation this node holds and the row is cosigned under its
protocol at its signed instant; refusalfederation_invalid_argument
prefixedowner_binding_affiliation_refused:(affiliation_not_held,
not_an_affiliation,not_cosigned_under_protocol). New fold
admission::affiliation_of_binding(key). A binding naming no community is
a personal steward's binding, unchanged. - #1048, delegation terms (CLM-delegation-term). A
delegates_to
carryinglicense,grant,moderate,takedown,revieworslash,
orsub_delegation, or naming an affiliation (its hierarchy), MUST carry
delegation_valid_untilat most 365 days afterdelegation_valid_from
(absent = the edge'sasserted_at), and within the affiliation's
role_term; otherwisefederation_delegation_term_required
(Error::DelegationTermRequired, rulesterm_missing,term_unreadable,
over_ceiling,over_role_term). Owner-bindings and the trust plane are
not gated; an edge that needs no term stays open-ended. The licence and
moderation walks read the affiliation'srole_term. Grandfathered: an edge
signed beforeDELEGATION_TERM_ADOPTED_AT(2026-10-23) keeps the v54
reading. From the parkedv54.1-rc8-term-gate, rebuilt on the v60 doors.
Python surface (clean break):
| method | v59 | v60 |
|---|---|---|
put_organization, put_org_membership, put_partner_record |
(signed_json) -> None |
REMOVED |
list_organizations_for, list_org_memberships_for, list_partner_records_for |
(id) -> str |
REMOVED |
list_organizations_since, list_org_memberships_since, list_partner_records_since, list_signed_partner_records_since |
(since_rfc3339, since_id, limit) -> str |
REMOVED |
grant_delegation |
(delegate_key_id, scopes, sub_delegation, delegation_purpose=None) |
+ delegation_valid_from=None, delegation_valid_until=None (RFC 3339); a term-bearing edge without a bounded _until raises ValueError (delegation_term_required) |
add_moderator |
(community_id, moderator_key_id, duty) |
+ delegation_valid_from=None, delegation_valid_until=None; an appointment without a term raises ValueError |
Error kinds gone with the planes: federation_operational_authority,
federation_operational_envelope_unbound.
Rust surface (clean break): FederationDirectory::{put_organization, put_org_membership, put_partner_record, list_organizations_for, list_org_memberships_for, list_partner_records_for, list_organizations_since, list_org_memberships_since, list_partner_records_since, list_signed_partner_records_since} removed (102 trait methods delegate in
the directory double, from 112). Gone from operational: SubjectKind,
MergeIntent, Organization, OrgMembership, the signed and Served*
wrappers, the role-authority / steward-quorum / LWW / monotonic-quorum
helpers; Error::{OperationalAuthority, OperationalEnvelopeUnbound}. New:
operational::{PartnerRecord (the payload), check_partner_record_sets, partner_record_of, check_licence_payload};
admission::{check_partner_record_revision, check_licence_audience, check_owner_binding_affiliation_admission, affiliation_of_binding, licence_authority_set, check_delegation_term_admission, DelegationTermBounds, is_term_bearing_edge, TERM_BEARING_SCOPES, SUBJECT_WITHDRAWS_CARVE_OUT};
affiliation_config::role_term_seconds; Error::DelegationTermRequired;
Engine::grant_delegation / add_moderator take a DelegationTermBounds.
DIRECTORY_ABI_VERSION 10 → 11: DirectoryOp::{ListOrganizationsSince, ListOrgMembershipsSince} and DirectoryOpResult::{Organizations, OrgMemberships} removed.
What Edge, Server and Agent adapt: §16.
- Witnesses (sqlite, postgres unless noted): I1210 the payload gate;
I1211 the revision anti-rollback; I1212 the skew bound; I1213
the retired kinds and V205; I1214–I1215 item 1; I1216–I1217 item
3; I1218 item 5; I1219 #1062; I1220–I1221 #1049 (consent,
key_grant); I1222 (unit) UTF-16 set order; I1223 the promotion
chokepoint re-runs the payload gate; I1230, I1234 (unit) the term
lens and the grandfather ruling; I1231 the term gate; I1232 the
walks readrole_term; I1233 (sqlite engine) the producer doors;
I1235 the promotion chokepoint re-runs the term gate; I1225 (unit)
W's boundary. Extended: I972 (a licensee files), I313 (the stream read
keeps a wrap only from a sealer with standing at its row's instant), I798
(its duty edge carries a term). Python:
tests/python/test_affiliation_surface.py. - Mutation scope
scripts/mutants/scope-s6-affiliations.txt: 212 mutants in
five shards, 186 caught, 26 unviable, 0 missed, nothing allow-listed (three
rounds: 28, then 9 survivors, all witnessed or removed as equivalent).
Weekly, five shards, inmutants.yml. resolve_withdraws_admission_rule_at: rule 3 no longer accumulates
beyond_delegation_depth_cap(rule 4 walks the same graph over a superset
of targets; the refusal reports the same cut).- Evidence (manifest round
v60-s6): CLM-authority-id-is-org-id (3.3.9),
CLM-licence-authority-quorum ×2, CLM-licence-judged-at-issuance,
CLM-licence-audience (2.4.1.2.1), CLM-licence-contest ×2 (2.4.1.1),
CLM-org-on-the-bond ×2 (3.2), CLM-delegation-term (4.4.3.2.8),
CLM-admission-freshness-window ×3 (2.6.7), CLM-canonicalization-array
(2.6.1.1.1); CLM-merge repointed.
Changed — 9. hard_case rows replicate (S0; CIRISConstitution#170 closed)
The steward ruled through CC (2026-10-21, CIRISConstitution#170 closed, rc9
6ce1674): hard_case:* joins CC 2.4.1.1's unpurgeable accountability set,
and no peer capability advertisement is needed (conformance is the guard).
Persist already refused a hard_case purge (ACCOUNTABILITY_RECORD_PREFIXES,
both purge doors and the replicated purge apply), so the v57 hold is lifted:
a hard_case row crosses like any local row this node attests (the node signs
as the actor) and a peer admits it at the put door and the replicated apply.
- Removed (clean break):
Error::HardCaseReplicationHeld, its kind
federation_hard_case_replication_held(hard_case::refusal::REPLICATION_HELD),
hard_case::check_hard_case_replication_held, and its Python arm (a
RuntimeError); no call site returns it any more. Nothing in the.pyi
named it. - Changed:
hard_case::project_hard_case_rowis tier-agnostic (the
detection projection's rule), and the put door (also the replicated apply)
projects ahard_case:*row intohard_case_eventsin the row's
transaction on every backend. Without both, a crossed row (moved to the
federation tier in place) and a peer's replicated row would each leave
hard_case_eventsdifferent from its from-rows rebuild. A peer's events
are therefore listed bylist_hard_case_eventson the node that holds them. - Added: the listing now includes replicated peers' events, each
carryingHardCaseEvent::emitter_key_id(the key that signed its row,
neverdetail); filter byHardCaseFilter::emitter_key_idfor a
node-local view. V202 (both dialects) adds the
hard_case_events.emitter_key_idcolumn, backfilled from each projected
row's signer. - Witness I1165 (sqlite, postgres): node A records a
hard_case:consent_sla_breach:v1row and crosses it; node B receives it
only through A's advertised wire hashes and point read, applies it
(Inserted), holds it at the federation tier, lists it, serves it, and
both nodes'hard_case_eventsequal their rebuilds; B's purge doors refuse
it as the accountability record (the dry run names it inadmissible; local
execution and a quorum-cosigned replicated bundle are both
purge_retraction_inadmissible, nothing written; the row survives). The
#607 conferral witness now expects the self-attested and conferred
hard_case rows ADMITTED where it expected the hold. - Evidence:
CLM-hard-case-on-base's row at the retired hold moves to the
replicated apply door (attestation_apply.rs#apply_planned);
CLM-accountability-record-unpurgeable(2.4.1.1) gains
steward_purge.rs#accountability_record_of.
Fixed — 10. nodes_owned_by returns the nodes a roster key announced (S0; CIRISPersist#1109)
admission::nodes_owned_by(fedID) sought its candidates only among the
delegates_to owner-bindings the fedID's own key signed. A device joined by
approval announces its node with its roster key (<fedID>-<fp>), and since
v59.0.0 owner_of credits that binding to the fedID (self_of_at), so the
forward walk named the self as owner while the reverse walk never placed the
node. The seek now also reads the bindings every key bound as an occurrence
of the fedID signed; the verdict is still owner_of, so n ∈ nodes_owned_by(U) ⟺ owner_of(n) == U holds by construction and a key of
another self, or a key that bound a node after its eviction, adds nothing.
nodes_owned_by now returns roster-key-announced nodes: CIRISServer can
delete its announced_nodes_of workaround. Witness I1161 (sqlite,
postgres).
Added — 11. a test-anchor seam on W_self (S0; CIRISPersist#1111, CIRISServer#794 T11)
A test-anchor harness binary could not watch the self roster's lone path
land: W_SELF_SECS is 86 400 s and the node clock's seam is cfg(test)-only.
self_roster::policy() now reads self_roster::w_self_secs(), which is
W_SELF_SECS on every production build. Under feature = "test-anchor"
ONLY, and only while the test-anchor override is live (the existing fence:
the feature, CIRIS_TESTING_MODE, a decodable CIRIS_TEST_TRUST_ROOT, and
verify's anti-production tripwire), CIRIS_TEST_W_SELF_SECS (whole seconds
in 1..=86400, read once per process; it can shorten the window, never
lengthen it) replaces it. The window still runs on the node's clock from the
node's receipt. Witness I1162: armed with the variable set, an unanswered
addition lands after the short window on sqlite and postgres; with the
variable set and no override it does not
(tests/self_roster_test_window_1111.rs, its own process: #738); the lib
half pins the override condition and the parse, and a from-disk check pins
every read of the variable inside the #[cfg(feature = "test-anchor")]
module self_roster::test_window.
Added — 12. one mutation discipline: a scope per slice, checked from disk (S0; docs/V60_TASK_PLAN.md §3)
Every slice since v53 ran its mutation round as an ad-hoc script outside the
repo; nobody could re-run one, and the nightly matrix never learned the sites.
From v60 each slice ships scripts/mutants/scope-<slice>.txt and runs it
through scripts/mutants.sh; its matrix is the handoff's mutation table, and
green is exit 0 with an empty unvalidated.txt (docs/MUTATION_TESTING.md
"Per-slice scopes (v60)": the rule, a scope-file template, the brief
paragraph agents copy).
scripts/mutants_scopes_check.py(stdlib, from disk): every scope resolves
throughmutants_matrix.py resolve, itstests:filter is a well-formed
nextest filterset whosetest(...)leaves match afnormodname under
src/ortests/, itsfeatures:exist inCargo.toml, and every
allowed_survivors.txtline names an issue;--self-testshows it fails on
each. A certify fast gate (mutscopes, besidemutmatrix), and a step in
ci.ymland the nightlymutants.yml.- The scope grammar gains
features:(read byscripts/mutants.sh;
MUTANTS_FEATURESstill wins): a site undercfg(feature = "test-anchor")
mutated in a build without the feature is never compiled, and every mutant
in it would read MISSED. scripts/mutants/scope-s0-roster-seam.txt: #1109 and #1111's doors
against I1161/I1162, the first scope withfeatures: …,test-anchor.reports (non-gating)runsscripts/mutants_diff.sh origin/mainon every
release-*PR (it ran only on arun-mutantslabel), capped by
MUTANTS_DIFF_BUDGET_SECS(7200 s) inside the job's 150 minutes; the
summary lands in the step summary and the artifact.
Added — 13. the CSD/4 coverage report (S0; docs/V60_TASK_PLAN.md §3b, report-only)
tests/fixtures/csd4/: CIRISClient'spackaging/csd4/(traces.json,
requirements.yaml,PIN.md,topology_vocabulary.pin) byte for byte,
from CIRISClient mainab134ef2; the README records the commit and the
shas.topology_vocabulary.jsonmoved to the runtime manifests at the rc9
tag (§6).csd4_chain.json: CC's chain as data, now a runtime manifest at
src/federation/namespace/csd4_chain.json, byte-identical togit show v1.0-rc9:manifests/csd4_chain.json(§6): per atom its STATE_TABLE states, the invariants
and claims over them (by_invariant|by_section), each claim's evidence
pointers by repo, and the text's refusal tokens with their confidence.scripts/csd4_coverage.py(stdlib, from disk) READS the join from the chain:
atom → claim → CIRISPersistpath#symbol→ theevidence/cc_impl.tsvrow →
the*_invariants.rsfiles that name the symbol and the mutation scopes that
cover it. Gap lists: (a) chain refs with no row (or under another claim), and
rows no atom reaches; (b) atoms with persist claims but no witness or no
scope; (c) persistkind()tokens the text does not name, and text tokens
persist never emits, withbold/after_refusalconfidence.negatives.held
andvalues.readyare listed as harness-level, not gaps. Thetraces.json
refused map and CEWPOS's "stops at rel[k]" relations (with an owner guess)
stay.docs/CSD4_COVERAGE_v60_S0.mdis its run at the slice head. A
reports (non-gating)job runs it on every PR and weekly.
Fixed — 14. the nightly mutants job is green again (S0; CIRISPersist#1112)
The scheduled mutants and powerset job had been red every day since
2026-10-14 with seven unkilled mutants in src/observe, and nobody read it.
count_read's walk over the active-fold mask is bounded by the mask's
popcount instead ofwhile mask != 0. Each of the three operator mutants on
the clear-lowest-bit step made the old loop infinite; seventeen witnesses
hung to nextest's 60 s leash and the run outlived cargo-mutants' 300 s
timeout, which credits no witness (three TIMEOUTs). A witness cannot end an
infinite loop, so the bound makes the same mutants return at once with the
wrong fold credited, and I1164 (a read inside three nested folds at
mask bits 0, 4 and 7 credits each exactly once) kills them.- I1163:
CatalogEntry::metrics_unitrefuses every malformed brace form
({},ab},abc,},x},{ab,{x,{,x, empty). The four
MISSED guard mutants had no witness because the catalogue holds only
well-formed units.
Security — 15. three advisories published after v59.1.0 (deny.toml)
- RUSTSEC-2026-0386 (tokio
AsyncFd::newI/O safety): cleared, tokio 1.52.3
→ 1.54.1 inCargo.lock. - RUSTSEC-2026-0385 (ctutils non-constant-time masks): cleared, ctutils 0.4.2
→ 0.4.6 inCargo.lock. - RUSTSEC-2026-0387 (crypto-bigint 0.5 secret-dependent branches): ignored
with its reason. The fix is 0.7.6, which elliptic-curve 0.13 cannot take;
the crate is transitive only via p256 0.13 in CIRISVerify v20.2.0's
ciris-crypto / ciris-keyring, and persist's federation identity is Ed25519- ML-DSA-65 (a P-256 platform signer is refused as the federation key).
Re-evaluate on the next ciris-verify bump.
- ML-DSA-65 (a P-256 platform signer is refused as the federation key).
Adoption — 16. what CIRISEdge, CIRISServer and CIRISAgent must handle
Every break in v60.0.0, from git diff v59.1.0.. (python .pyi, src/ffi,
the exhaustive enums, the migrations). Edge adapts once, to this head.
Replication kinds (wire).
EnvelopeKind::{Organization, OrgMembership, PartnerRecord}join
RETIRED_FRAMED(now 7: the four key-plane kinds from v59 and these
three). An inbound object of these kinds is refused
federation_envelope_kind_retired; send nothing on them. Their positions
inEnvelopeKind::ALL(19) are kept, so the manifest hash Edge and Server
pin does not move. Live kinds: 12. (§8)- The cirisnode classes (Contribution, vote, moderation, slashing,
reconsideration, ledger balances, takedown notice) and Goals never had a
replication kind; they now cross asscoresrows on the attestation
plane. Edge retires its ownGoalDeclaration/GoalRetirementmessages,
which wrapped persist'sGoal. (§1, §2) - A v59 peer's purge bundle carrying its takedown notice as a
takedown_notice
Contribution is still applied for one release (v60.x; removed in v61). (§1)
Python methods removed (.pyi and src/ffi/pyo3.rs):
cirisnode_list_takedowns_for_json,cirisnode_list_key_grants_for_json,
cirisnode_list_key_grants_for_content_json,
cirisnode_list_key_grants_for_filtered_json,
cirisnode_list_key_grants_for_scope_epoch_json,
cirisnode_put_key_grant_json,cirisnode_retire_key_grants_json(§1; the
key-grant plane,key_grant:{axis}:v1, is the one door for a wrapped key).put_organization,put_org_membership,put_partner_record,
list_organizations_for,list_org_memberships_for,
list_partner_records_for,list_organizations_since,
list_org_memberships_since,list_partner_records_since,
list_signed_partner_records_since(§8).
Python methods re-signatured (each now returns the row id it wrote,
signed by the engine's own hybrid signer, where it returned None):
cirisnode_put_contribution(body_json) -> str,
cirisnode_cast_vote(body_json) -> str,
cirisnode_update_credits_ledger(update_json) -> str,
cirisnode_update_expertise_ledger(update_json) -> str,
cirisnode_put_moderation_event(body_json) -> str,
cirisnode_put_slashing_attestation(body_json) -> str,
cirisnode_put_reconsideration_request(body_json) -> str,
cirisnode_put_reconsideration_attestation(body_json) -> str(bodies in
the §1 table, not signed envelopes).cirisnode_list_contributions(author_id, filter_json=None, cursor_json=None, limit=100),cirisnode_list_votes(contribution_id, voter_id=None, cursor_json=None, limit=100): items are rows, the cursor an
AttestationCursor.cirisnode_list_takedowns_for_filtered_json:filter_jsonmust name
claimant_key_id; items are notice rows.cirisnode_process_takedown_admission_json(notice_attestation_id)(was the
Contribution JSON); the report addsnotice_attestation_id.cirisnode_get_credits_ledger/cirisnode_get_expertise_ledger: the entry
addsattestation_id;created_atis the row's instant.cirisnode_put_promotion_attestation:target_idsare held row ids (no
is_canonicalflip).cirisnode_put_delivery_attestation: the
announcement must be a heldcontribution:federation_announcement:v1row.cirisnode_put_goal_json(body_json) -> str(a v59 typedGoalraises
goal_typed_object_retired),cirisnode_retire_goal_json(goal_id) -> str
(noretired_atargument),cirisnode_get_goal_json(goal_id)(goal_id
is the declaration's row id, not a UUID; the JSON carriesscale/
cohort_id,score,attestation_id),cirisnode_list_goals_json(filter
scale, notscope_kind; unknown keys refused). (§2)grant_delegation(..., delegation_valid_from=None, delegation_valid_until=None)andadd_moderator(..., delegation_valid_from=None, delegation_valid_until=None)(RFC 3339): a term-bearing edge (license,
grant,moderate,takedown,review,slash,sub_delegation, or one
naming an affiliation) without a bounded_untilraisesValueError
(federation_delegation_term_required). (§8)
Python methods added: cirisnode_rehome_ledgers() -> int,
cirisnode_check_ledger_projection(projection) -> str,
cirisnode_amend_goal_json(goal_id, body_json) -> str,
cirisnode_recant_goal_json(goal_id) -> str, cirisnode_rehome_goals() -> str,
cirisnode_check_goal_projection() -> str. Run the two rehomes once after
upgrading a node that held v59 ledgers or goals (§1, §2).
Refusal tokens and error kinds.
- New
kind()tokens:federation_governance_policy_refused
(Error::GovernancePolicyRefused { family, reason },ValueError; reasons
aCorridorRefusaltoken orpolicy_row_missing,policy_not_two_path,
policy_row_unreadable,policy_base_not_built,policy_rows_disagree;
§6),federation_delegation_term_required(Error::DelegationTermRequired,
ValueError; rulesterm_missing,term_unreadable,over_ceiling,
over_role_term; §8),cirisnode_key_grant_retiredand
cirisnode_typed_object_retired(cirisnode::Error::Retired; §1),
ceremony_scratch_directory(CeremonyOutputsRefusal::ScratchDirectory;
§5). - New
federation_malformed_rowreasons, appended to the hashed
malformed_row::ALL(nothing reordered):goal_meta_alignment_malformed,
goal_body_malformed,goal_typed_object_retired(§2),
content_hash_mismatch,envelope_row_member_unknown,
subject_key_id_malformed(§7). - Moved token (#1114, §7,
ValueErrorthroughout): a content-hash mismatch
wasfederation_tier_unverifiedand a malformedsubject_key_idselement
wasfederation_invalid_argument; both are now the TERMINAL
federation_malformed_row. A sender that retried either must stop. - New
federation_invalid_argumentprefixes (ValueError):
licence_audience_refused:(class_not_declared_public,
scope_not_the_declared_choice,re_emission_names_no_reader,
re_emission_target_unknown,re_emission_diverges),
owner_binding_affiliation_refused:(affiliation_not_held,
not_an_affiliation,not_cosigned_under_protocol). New
Error::AffiliationConfigInvalidrulelicence_audience_invalidat the
community door. (§8) asserted_at_stale(Error::AssertedAtStale, kind still
federation_invalid_argument) now also fires on thelicensure,
consent,key_grantandowner_bindingplanes; the variant's
stewardfield is renamedstanding_forand it gainsplane. A
repeated licence set entry is nowfederation_set_semantics_unsorted.
(§8)- Retired, no longer returned:
federation_hard_case_replication_held
(Error::HardCaseReplicationHeld, was aRuntimeError; §9),
federation_operational_authority,federation_operational_envelope_unbound
(Error::{OperationalAuthority, OperationalEnvelopeUnbound}; §8). - A postgres
grant_trustrow that trips a V020 CHECK readsBackend, not
InvalidArgument(the plan refuses both shapes first; §4).
Exhaustive enums that changed (a match without a wildcard breaks):
federation::Error (+GovernancePolicyRefused, +DelegationTermRequired,
−HardCaseReplicationHeld, −OperationalAuthority,
−OperationalEnvelopeUnbound; AssertedAtStale's fields),
cirisnode::Error (+Retired, +Federation), CeremonyOutputsRefusal
(+ScratchDirectory), SiloProjection (+CreditsLedger,
+ExpertiseLedger, +Goals), DirectoryOp (+ListKeyPlaneWireHashesSince,
−ListOrganizationsSince, −ListOrgMembershipsSince),
DirectoryOpResult (−Organizations, −OrgMemberships),
observe::StoreBackend (−Memory). Removed public enums:
operational::{SubjectKind, MergeIntent}, SubjectGate,
cirisnode::{RetireKeyGrantsOutcome, RetireFailureStage, KeyGrantScope, WrapAlgorithm}; GoalScope is a validated struct, not an enum. New:
LegacyLedgerRow, ledger_projection::{Ledger, LedgerBody},
takedown_tombstone::NoticeOrigin. LegalBasis decodes CC's spellings
dmca512 / dsa_article16 only.
Exhaustive structs that gained a field: HardCaseEvent.emitter_key_id,
HardCaseFilter.emitter_key_id (Default), CreditsLedgerEntry /
ExpertiseLedgerEntry.attestation_id, AffiliationConfig.licence_audiences,
TakedownReport.notice_attestation_id, TakedownTerms.row.
The directory capsule. DIRECTORY_ABI_VERSION 10 → 11 (the two org ops
and their results removed; ListKeyPlaneWireHashesSince appended, §4, §8).
A consumer's capsule-receive check (abi_version == DIRECTORY_ABI_VERSION)
built against 10 refuses the v60 capsule: rebuild against 11.
Rust surface (clean break, no aliases).
FederationDirectory: removedput_organization,put_org_membership,
put_partner_record,list_organizations_for,list_org_memberships_for,
list_partner_records_for,list_organizations_since,
list_org_memberships_since,list_partner_records_since,
list_signed_partner_records_since,put_goal,retire_goal; changed
get_goal(&str); addedlegacy_goals,delete_legacy_goal,
list_key_plane_wire_hashes_since(defaultUnsupported).NodeCoreServiceand the cirisnode types per §1;Goal,GoalScope,
GoalsFilterper §2; the inherentSqliteBackend/PostgresBackend
key-record methods per §3;operationalper §8;
Engine::grant_delegation/add_moderatortake aDelegationTermBounds;
TwoPathPolicy::self_roster(window)removed andself_roster::policy()
returnsResult(§6);hard_case::check_hard_case_replication_held
removed (§9).store::MemoryBackendis gone (§5). Tests and embedders use
store::test_backend::test_backend()(featuresqlite).- Genesis doors take a scratch (§5):
verify_ceremony_outputs(bundle_json, scratch),install_genesis_bundle_roster(dir, scratch, bundle),
CeremonyState::finish(scratch),AmendState::plan(pinned, rows, scratch),
AmendState::finish(scratch).Engine::install_genesis_bundle_rosterand
the Pythoninstall_genesis_bundle_roster_jsonkeep their signatures. On
postgres the configured role needsCREATEDBfor these doors (a
throwawayciris_genesis_scratch_*database per replay, reaped after an
hour if a process died holding it). takedown_tombstone::takedown_termsisasync (directory, notice, origin);
TakedownNoticePayloaddropsclaimant_key_id/asserted_atand refuses
unknown fields (§1).
Producers. Sign a licence as the licensure:{community_key_id}:v1 row,
cosigned under the authority's protocol in additional_scrubs[]; call
PartnerRecord::canonicalize_sets (sorted AND unique) before signing; pass a
term on every term-bearing delegates_to (§8). Sign a takedown notice as a
takedown_notice:{basis}:v1 row (§1).
Migrations (sqlite + postgres twins, run at open): V202
(hard_case_events.emitter_key_id, backfilled), V203 (drops eight
cirisnode tables; ledgers gain attestation_id; purge_executions.legal_basis
rewritten to CC's spelling), V204 (goals rebuilt as a projection:
scope_kind → scale), V205 (drops federation_organizations,
federation_org_memberships, federation_partner_records and their index
rows). Nothing outside persist wrote the dropped tables.
Telemetry. The backend label set is {sqlite, postgres}; a dashboard
keyed on backend="memory" sees no series (§5).
Per consumer.
- CIRISEdge: everything under replication kinds, the capsule ABI, the
exhaustive enums, the moved #1114 tokens, the Rust surface, producers. Its
own test sites named by the slices:tests/federation_announcement.rs
(announcement written through the removed typedput_contribution), the
durability test's local takedown (built a Contribution notice), its
GoalDeclaration(wrapsGoal, built with the removedGoal::new). - CIRISServer: the Python methods above (cirisnode, goals, organisation,
grant_delegation/add_moderator), the refusal tokens,CREATEDBfor
the genesis doors on postgres, the rehomes after upgrade; it can delete its
announced_nodes_ofworkaround (§10). - CIRISAgent: the Python breaks above are the whole Python break; an Agent
that calls none of the removed or re-signatured methods needs no change.
The.pyiis the contract.