shenas floor partition
Changing this page. Any edit to a custody row, to a syncable row, or to
the evictable declaration in section 5 requires review by shenas’s security
owner before it merges. Section 11 says what else a change requires.
1. What this page is
Section titled “1. What this page is”SSP/1 section 15.1 requires a conforming mesh to define a floor: a partition of its tables into three categories, fixed by the implementation rather than by configuration, identical on every node, with the never-syncable category structurally unrepresentable in policy rather than merely denied by default. Registry section 55.1 item 3 makes documenting that partition a conformance requirement.
SSP/1 deliberately does not enumerate which schemas fall where, because that is a property of the application rather than of the protocol. This page is that enumeration for shenas. Section 17 puts a second declaration in the same place for the same reason (which categories may have their history evicted), and section 5 is that one.
It is normative, not descriptive. Two implementations enforce it, one in Python and one in Rust, and each is bound to this page by its own test that fails in both directions, so the tables below are the source of truth and the code is the enforcement. Section 8 records why there are two hand-written expressions rather than one generated one.
The floor belongs to the application’s persistence layer rather than to a replication layer because it is a partition over the DuckDB schemas the application declares: a property of this application’s storage, which outlives any particular protocol that carries rows off the device. Section 6 says what that means while no such protocol is registered.
Relationship to the v1 scoping decision. shenas’s own architecture decision on mesh sync scoping (its ADR 0008, not published) fixed a v1 floor in which only first-party user content and canonical metrics may replicate, and credentials, raw upstream data and node bookkeeping may not. This page does not widen that floor. It restates it at table granularity in the SSP three-category shape and closes two gaps the decision stated but the code did not enforce (section 7).
2. The three categories
Section titled “2. The three categories”| Category | Policy may reach it | Meaning in shenas | Cost of moving a table out |
|---|---|---|---|
syncable |
Yes | May replicate, subject to a policy row. Membership is permission to write a policy row, never replication itself: resolution still fails closed with no matching row. | Narrowing. Normal change, security review. |
never-syncable |
No | Structurally excluded. A policy row naming the schema is refused at write time, the producer raises, and the receiver drops and logs. | Widening. Floor revision (section 11). |
node-local-derived |
No | Reconstructible on any node from data that node already holds. Replicating it is waste, and for index-shaped tables it is wrong: the pointers dangle on a receiver holding a different set of rows. | Widening. Floor revision (section 11). |
The partition is total: every table is in exactly one category. Because
only one category is policy-reachable, a table that is “not syncable yet”
resolves to never-syncable unless it is genuinely locally reconstructible.
That makes the word never carry two different weights, so every row also
declares a band recording why it is where it is:
| Band | Applies to | Meaning |
|---|---|---|
custody |
never-syncable |
Credential, key, session, or trust material. A single mistake here is unrecoverable. Reclassification is never on the table. |
node-local |
never-syncable |
Describes this node (install state, run ledgers, transport bookkeeping, telemetry), not the user’s data. Replicating it would be meaningless or actively wrong. |
deferred |
never-syncable |
Replicable in principle; excluded by the v1 floor until a named condition is met. Structurally excluded today exactly so that a misconfiguration cannot pre-empt that decision. |
derived |
node-local-derived |
Rebuildable locally. Rebuild may cost time or a paid third-party call; that is a cost argument, not a replication argument. |
user-data |
syncable |
The user’s own content, canonical or user-authored. |
The enforcement layer is binary (a table either may cross the mesh or may not),
so the code does not carry the band. The band is a decision record: it tells
the next reader what kind of argument would have to be won to move a row, and
it stops ui.hotkeys from being read as key material because it shares a
category with auth.*.
3. Schema defaults
Section titled “3. Schema defaults”Every table in a schema takes the schema’s category unless section 4 names it. Schemas are the DuckDB names the application declares.
Every declared schema MUST have a row here. Section 2 claims the partition is total; this is what makes that a rule about the table below rather than a property assumed of it. A declared schema with no row here is undocumented, which fails both that claim and registry section 55.1 item 3, even though it fails closed at runtime, because the floor denies what it does not recognise. A stated constraint that nothing asserts is a comment (section 7), so the rule is asserted rather than asked for: adding a schema without adding a row fails the Python binding test.
Every row here MUST name a declared schema. This table and the schema declarations are a bijection, asserted in both directions, with no exception. The converse direction went unasserted for a long time, and a row naming a schema nothing declared passed CI until it was noticed by reading. A first fix let a row name an undeclared schema provided it also named the work item that would declare it. That escape hatch is gone, because its invariant is not one a test can reach: whether the named work is still open, and whether it will ever create the schema, are facts outside the source tree, and the hatch rotted twice for that reason. A schema is either declared or it has no row here.
Deciding a category ahead of the code is still legitimate, and the SOVM note
below still does that for the sovm.* tables that do not exist yet. It is
prose, which claims nothing about the enforcement layer, as against a table
row, which looks enforced whether or not it is.
| Schema | Category | Band | Rationale |
|---|---|---|---|
analysis |
never-syncable | deferred | Hypotheses, findings and promoted-metric lineage are user-authored intent and would reasonably follow the user. Excluded in v1: findings bind to inputs that may not exist on the receiver, so a replicated finding can be unfalsifiable there. |
auth |
never-syncable | custody | Per-source OAuth tokens and API credentials. The archetype of the category. |
cache |
node-local-derived | derived | Geocode, reverse-geocode and the generic transformer key-value store. Rebuildable by re-issuing the query. Rebuild costs a paid API call; that is a reason to build cache sharing, not a reason to replicate. A table that cannot be rebuilt must not live in this schema. |
catalog |
syncable | user-data | User-authored category sets, values, geofences and resource annotations. Classification is meaningless on a peer without them. |
config |
never-syncable | node-local | Per-plugin configuration singletons, the dlt ingestion ledger and landing-zone staging. Describes this node’s plugin installation and its run history. |
datasets |
syncable | user-data | Canonical, normalized metrics: the query floor every device renders from. Table-level exceptions in section 4. |
entities |
syncable | user-data | User-authored entity graph and the reference data every other synced row links to. Table-level exceptions in section 4. |
inputs |
syncable | user-data | First-party user-authored content; source of truth; small. Named surfaces until the schema was renamed for what it holds rather than for the plugin kind that declares it. |
media |
never-syncable | deferred | Opaque media bytes, on-device only by construction. Bulk binary belongs on the object plane (SOP/1), not in the change-event stream. |
plugins |
never-syncable | node-local | Installed-plugin registry and per-plugin sync history. A replicated install row asserts an install the receiver does not have. |
shenas |
never-syncable | custody | Local users (password hashes, remote tokens), sessions, and device-wide settings. |
sources |
never-syncable | deferred | Raw dlt-loaded upstream data. See section 9 Q3: this is deferred, not derived – raw tables are re-ingestable at the origin API with that node’s own credentials, which is not the same as reconstructible from local holdings. |
sovm |
never-syncable | custody | The app’s SOVM bookkeeping, today only sovm.projection_checkpoint. custody by the standing rule below, which fixes the band for the namespace rather than for its present contents: the checkpoint alone reads as node-local, but a schema default governs every sovm.* table added after it, and the rule was written for the ones that would carry trust configuration. Both bands enforce identically, so the choice records intent and costs nothing. Replicating a checkpoint would close a feedback loop: this node’s projection progress applied by a peer and projected back. The node’s own durable state is not in DuckDB at all; see below. |
streaming |
never-syncable | node-local | Change-feed log, per-consumer cursors, per-dataset retention floors and subscription audit. All of it describes this node’s consumption of its own datasets. |
telemetry |
never-syncable | node-local | OpenTelemetry spans and logs. This node’s operational record, and an exfiltration channel if it ever crossed the mesh: span attributes carry query text and error payloads that no schema constrains. |
transforms |
never-syncable | deferred | Transform instances (user-supplied SQL, reasonably portable) and step-level run history (per-node). Excluded as a schema in v1 because splitting the two is a widening decision, not a documentation one. |
ui |
never-syncable | deferred | Hotkeys and workspace layout. Would reasonably follow the user; excluded in v1 pending a product decision on cross-device UI state. |
The SOVM layer, and how little the sovm row covers
Section titled “The SOVM layer, and how little the sovm row covers”The row above governs one table and must not be read as governing the SOVM layer. Almost none of that layer is in DuckDB.
sovm once carried a row on the expectation that the SOVM layer would keep its
identity, key and transport bookkeeping in DuckDB alongside everything else. It
does not. The node’s durable stores persist to files by atomic rename: the HLC
high-water mark, the retained event log (under authenticated encryption), and
peer cursors and applied watermarks. Device identity and the keyring take the
same file-backed shape. None of it is a DuckDB namespace, so none of it is
reachable by the floor, by the write path, or by anything else this page
governs.
File-backed is the better outcome and not merely what happened. Keeping the keyring, the HLC mark, the retained log and transport state out of the application’s DuckDB file puts the replication layer’s own trust configuration outside the namespace the write path scans, out of reach of a mis-registered sink, and out of the blast radius of a DuckDB-level compromise. That makes never-syncable structural here rather than enforced, which is the stronger form and the one SSP/1 section 15.1 is reaching for.
What did come back into DuckDB is the one piece that is not the node’s state
but the app’s: sovm.projection_checkpoint, an opaque token recording how
far this database has been projected into. It belongs beside the rows it
describes because it describes them: copy or restore the file and the
checkpoint arrives at the position those rows are actually at, which a file
written next to the node’s own state would not. It is a single row, opaque to
the app, and deliberately not decomposed into the HLC’s parts: the app never
compares HLCs, and a column pair would invite exactly that.
Its exclusion is structural, not an exception. The floor’s set of syncable
schemas does not name sovm, and an unrecognised schema resolves to
never-syncable, so the table is out of the floor without any rule naming it:
the same shape as the file-backed stores above, one layer up. A test pins that
it is out for that reason, and fails loudly if someone widens the set to
include sovm.
The decision the original row recorded still stands, and now covers the tables
that do not exist yet rather than the one that does: if the SOVM layer ever
moves its identity, key or transport bookkeeping into DuckDB, those tables are
never-syncable / custody, and adding them is a documentation change rather
than a decision. SSP/1 section 15.1 names transport state, and anything
leaking the replication layer’s own trust configuration, as the structural core
of that category; replicating the policy store would additionally let a
receiver widen its own feed, which the sender-side design exists to prevent.
That rule is why the schema default above is custody even though the
checkpoint alone would not have earned it.
4. Table-level exceptions
Section titled “4. Table-level exceptions”| Table | Category | Band | Rationale |
|---|---|---|---|
datasets.entity_state |
never-syncable | custody | Inferred per-entity state. A stated negative constraint from the security review of the state subsystem: the substrate ships no egress path of any kind, and the mesh is an egress path. |
datasets.entity_state_consent |
never-syncable | custody | Per-dimension explicit opt-in flags. Consent is granted on a device, about that device’s inferences; replicating the grant would broaden it silently. |
datasets.entity_state_deletion_log |
never-syncable | custody | Deletion audit for the state substrate. Replicating the record of a deletion re-exports what the deletion removed. |
datasets.lineage |
never-syncable | deferred | Row-level lineage carries (input_table, input_row_keys) pointers into sources.* (never-syncable/deferred, Q3 of the open questions). A peer without those source rows cannot resolve the references. The lineage code itself declares the invariant (“lineage is local metadata only”) and ships no egress path. Security ruling, 2026-08-18. Widening is gated on Q3. |
datasets.user_state_suggestion_event |
never-syncable | custody | Suggested state values awaiting user acceptance; carries the same values as entity_state. |
entities.entity_index |
node-local-derived | derived | Per-node uuid -> (db, table, row_id) resolver over whatever tables this node holds. Replicating it ships dangling pointers to rows the receiver may not have. Each node rebuilds it. |
entities.places_wide |
node-local-derived | derived | A denormalized view over places and their properties, recreated by ensure_places_wide_view. It has no independent rows to replicate, and an inbound event naming it would ask the materializer to write to a view. |
analysis.recipe_cache |
node-local-derived | derived | Content-hash-keyed cache of recipe execution results. Recorded for completeness: its schema is already never-syncable, but the honest category is derived, which is what a future decision to open analysis would need to know. |
Schemas whose tables are created dynamically (auth.*, one table per source;
config.*, one per plugin; sources.*, managed by dlt; and the per-plugin
metric tables in datasets.*) are covered by their schema default. That is
deliberate: a partition that had to enumerate dynamically-created tables would
fail open the first time a plugin created one.
5. Evictable categories
Section titled “5. Evictable categories”SSP/1 section 17 lets a storage-constrained node discard cold history below a per-table HLC horizon, and requires that only categories the implementation declares evictable may be evicted, that the declaration be documented, and that the declaration state that resurrection on a declared category may come back partial. This section is that declaration for shenas.
Evictability is a different axis from the floor, and nothing here widens or
narrows section 3. The floor answers whether a table’s rows may leave this
device. Evictability answers whether this device may drop its own copy of their
history. A syncable table may be retained in full, and a never-syncable
table is not thereby evictable.
Declaring a category evictable accepts partial resurrection on the
application’s behalf. Two nodes holding the same events under different
horizons materialize different rows. A row that resurrects rebuilds only from
the history this node still holds, so a row whose
covering set reached below the horizon
comes back missing exactly those columns whose last write was not the row’s
last write: the failure mode is a silently incomplete row, not an absent one. A
category whose rows must resurrect exactly MUST NOT appear as yes below. That
is how a table opts into full retention, per table rather than mesh-wide.
| Schema | Evictable | Why |
|---|---|---|
datasets.* |
yes | Canonical normalized metrics: high volume, append-mostly, and the one category where retaining every event forever is the constraint that bites first. A partially-resurrected metric row is recoverable – the dataset plugin recomputes it from sources.* at the origin – so the cost of the trade is bounded and local. The table-level exceptions in section 4 are never-syncable and therefore out of this table’s reach. |
catalog |
no | User-authored category sets, values, geofences and resource annotations. A geofence that resurrects missing a coordinate column is a geofence that silently stops matching, and nothing recomputes it. |
entities |
no | The entity graph every other synced row links to. A partially-resurrected entity is a dangling reference held by rows that resurrect correctly, so the damage is not contained to the row that lost columns. |
inputs |
no | First-party user-authored content and the source of truth for it. Small enough that eviction buys nothing, and irreplaceable if it comes back short. |
The table covers the syncable schemas and nothing else, because eviction acts
on the retained event log and only a replicating table has one. A
never-syncable or node-local-derived table has no log to evict and no
horizon to raise; its local retention is an ordinary storage question, decided
by whatever owns that table, and stating it here would imply an SSP/1
mechanism that does not reach it.
Nothing asserts this yet, and that is stated rather than left to be
noticed. No node evicts today, for the same reason no replication layer is
registered (section 6). When one exists, the declaration is expressed in Rust
as an implementation of the SSP/1 host crate’s EvictableCategories port,
alongside the SyncableFloor implementation section 8 describes, and it is
bound to this table by the same both-directions test. Until then this section
binds in advance, exactly as the SOVM note in section 3 does. Eviction refuses
a category this declaration does not name, so the gap fails closed against
eviction rather than open.
6. Enforcement
Section titled “6. Enforcement”The floor is dual-sided and fail-closed. Every site below reads from the Python expression of sections 3 and 4; a replication layer delegates to it rather than restating the partition. Section 8 records the second expression, in Rust, and why there are two.
No replication layer is registered today. The earlier implementation that wired these sites was removed ahead of its SOVM replacement, so the floor currently gates nothing at runtime: with no sink registered, every table resolves as not syncable and every write takes the no-sink path. The table below is therefore the contract the next layer wires, not a description of live code. It stays on this page, and the partition stays tested against it, because re-deriving a security floor from prose once there is a caller again is exactly how G1 in section 7 happened the first time.
| Site | What it does | Where it binds |
|---|---|---|
| Write path | Every table save, insert and delete consults the registered sink’s is_syncable before building a payload, so an out-of-floor table mints no change event and nothing enters the change log in the first place. |
The plugin table base class, through the change-data-capture seam, to the registered sink |
| Producer scan | The outbound query filters on the floor’s SQL predicate, so the floor is applied in SQL and the sync cursor cannot stall on a run of out-of-floor events. | The layer’s change-log scan |
| Producer assertion | assert_syncable raises OutOfUniverseError. A local producer emitting out-of-floor data is a bug, and silence would hide it. |
The Python floor |
| Send-side policy | Above the floor, a per-(peer, table) verdict decides which of the in-floor candidates this peer receives, denying when no row matches. The floor answers whether a table’s rows may cross the mesh at all; this row answers to whom. It is sender-side and never appears on the wire. |
SSP/1 section 15.2: a sync-policy table in Python; the PolicyStore port in Rust |
| Policy write | assert_universe_schema refuses a policy row naming a schema outside the floor. This is what makes exclusion structural rather than deny-by-default, as SSP/1 section 15.1 requires. |
The Python floor, called by the layer’s policy store |
| Receiver | Scope gating on the envelope’s keyed scope tags runs before decryption; surviving events are re-checked per row and dropped-and-logged, never raised, so a hostile peer cannot wedge the apply loop. | The layer’s receive pipeline |
Table-level exclusion applies at every one of these sites, including the SQL predicate. Excluding a table only on the write path would leave events already in the change log selectable by the producer scan.
The send-side policy row is unfilled in the Rust stack. Unlike the rows
above it, this one is not merely awaiting a replication layer: the shenas node
is that layer, and it does not consult a PolicyStore. It selects what to
offer on the floor category alone, so the second conjunct is absent and a peer
holding any grant is offered every syncable table this node holds. The
consequence is that the floor is currently the whole of outbound enforcement
in Rust, and an operator’s per-table grant names the peer’s write set only.
This is recorded as a conformance finding against SSP/1 section 15.2 and is
open. The row stays in this table because it is the contract, and a table that
omitted the one site nobody wired is how the gap stayed invisible.
7. Gaps this page closes
Section titled “7. Gaps this page closes”G1: the entity-state substrate was stated never-syncable and was in fact
syncable. The state subsystem declared its tables never-syncable as a stated
negative constraint from its security review, but the floor had no table-level
exclusion for datasets.*, so all four tables were in-floor. Before this
partition was written (2026-08-16), is_syncable("datasets", "entity_state")
returned True, and an insert on that table therefore appended a replicable
event. The change that introduced the partition added the exclusion at all four
enforcement sites, and the binding test pins the subsystem’s declaration
against the floor so the two cannot drift again.
G2: entities.places_wide was in-floor. It is a view, so no local write
path emits for it, but an inbound event naming it passed the receiver’s floor
check. Now excluded as node-local-derived.
Both are narrowings. Neither requires a floor revision.
8. Where the partition is expressed
Section titled “8. Where the partition is expressed”There are two expressions of sections 3 and 4, one per language, and there is no generator. Each is hand-written, and each is bound to this page by its own test that fails in both directions: a row here with no counterpart in the code fails, and an entry in the code with no row here fails.
| Expression | What it is | Bound by |
|---|---|---|
| Python | The application’s floor module, which the write path and the registered sink consult | A test that parses the three tables above by their markers |
| Rust | The shenas-floor crate, an implementation of the SSP/1 SyncableFloor port |
A crate test that parses the same tables the same way |
The Rust expression exists because SyncableFloor is what a SOVM node consults
on its receive path, and it is a port precisely because which table is
node-local is an application fact the protocol crates have no standing to
decide. This section is the contract that crate is built to.
Why not one expression, read as data. A partition a node loads at startup is configuration, and section 15.1 requires the floor “fixed by the implementation rather than by configuration”. It also fails the structural half of the same requirement, whose argument is that “a deny-by-default rule can be overridden by a misconfiguration or a widening operation. Structural exclusion cannot.” A file on disk is exactly what a misconfiguration overrides. Shipping the partition as data does not conform.
Why not a generator. Generating both expressions from the tables above is the more attractive option on paper, and it is one this project has already retired once, for this site’s own derived pages. Two costs beyond that precedent:
- It removes the reviewed artifact. This page routes any change to a
custodyor asyncablerow through security review. Today a narrowing arrives as a diff to constant sets a reviewer reads alongside the prose. Under codegen the reviewed artifact is markdown and the enforcement artifact is machine output nobody reads: strictly worse for the control the band vocabulary exists to serve. - STRIDE, Tampering. A generator is a build-time writer onto a security-critical constant set. A test is not: it can only fail the build, never write the artifact. On a partition whose whole job is structural exclusion, adding a build-time writer to the excluding code is the wrong direction.
Why two hand-written expressions do not drift. Two hand-maintained expressions of one normative document is the drift this page exists to prevent, which is the objection, and section 7 answers it. G1 was not two expressions disagreeing. G1 was one expression nothing asserted: the state subsystem declared its tables never-syncable and no test tied that to the floor. The fix was not a generator; it was a test that fails in both directions. Drift needs an unchecked expression, and two expressions each bound both ways to this page have none.
The accepted cost. A floor change edits three files: this page and both expressions. Both tests name the file to edit in their failure message, and CI fails on any two of the three. That is the price, and it is smaller than a build-time code generator on the enforcement path.
Why the Rust expression is not in the protocol workspace. The SOVM crates
are meant to ship as a general-purpose workspace of their own, and a binding
test inside that workspace would lose its path to this page the moment the
workspace was extracted. The floor is the artifact where a test that only
passes in one checkout is least acceptable, so shenas-floor sits in shenas’s
own node workspace, and one application’s storage layout stays out of a
general-purpose mesh workspace.
9. Open questions
Section titled “9. Open questions”Q1: datasets.lineage and datasets.weather. Ruled by the security owner,
2026-08-18.
datasets.lineage is never-syncable / deferred. Lineage rows carry
(input_table, input_row_keys) pointers into sources.* (never-syncable, Q3
below). A peer receiving lineage rows cannot resolve those references; the
records are partial and misleading on the receiver. The state-isolation
invariant does not govern lineage directly (it carries no entity-state values),
but structural referential integrity does. Widening is gated on Q3. STRIDE:
Information Disclosure and Tampering.
datasets.weather is syncable / user-data, confirmed. Place binding is a
third-party export concern. In mesh sync the peer already has entities.*
(including places) under the fleet key; weather reveals nothing additional.
Weather is publicly rebuildable for known places. No narrowing is required.
Why there is no third “optionally syncable” verdict. syncable already is
optional, so confirming weather is not a decision to replicate it. Membership
grants permission to write a policy row and nothing else (section 2), and
SSP/1 section 15.2 denies when no row
matches, so weather stays on the device until someone writes an explicit
allow. Policy is keyed (peer_site_id, table_schema, table_name, layer) and
an exact table beats *, so datasets.weather can be denied on its own while
the rest of datasets.* is allowed: a per-table opt-out that needs no floor
change. A fourth category would move that choice out of policy and into the
partition, which section 15.1 forbids: the floor is “fixed by the
implementation rather than by configuration” and identical on every node, so a
per-node opt-in is exactly what it excludes. The two verdicts above are
therefore “no policy row can ever reach it” (lineage) and “reachable, and off
until the user opts in” (weather).
Correction, 2026-09-18: the paragraph above describes a mechanism that is not implemented, and the weather verdict does not rest on it. The conformance finding in section 6 means the Rust node never fills the SSP/1 section 15.2 port. Every clause of that paragraph is therefore false in the Rust stack today: there is no policy row, no deny-on-no-match, no exact-beats-
*resolution, and no per-table opt-out.The ruling stands; its stated ground does not. The security owner re-confirmed
datasets.weatherassyncable / user-dataon the first paragraph’s grounds, which are content-based and do not depend on section 15.2 existing: weather is co-disclosed withentities.*(including places), which is itselfsyncable / user-dataand reaches the same peer regardless of per-table grants, and weather is independently publicly rebuildable for a known place. Both hold at zero section 15.2 enforcement. That confirmation is scoped to the content-based argument and does not extend past the point real user data crosses a link.The generalization does not survive, and must not be cited for any other table. While section 15.2 is unenforced,
syncableis a two-category partition in practice rather than three.datasets.*(excluding weather),catalog.*,inputs.*andentities.*itself have no analog to weather’s two-part escape hatch: each is fully exposed to any peer holding any grant, with no operator-expressible narrowing. The third category is real again when the send-side policy port is filled, not before.
Q2: publication. Resolved 2026-09-30. SSP/1 conformance requires the partition to be documented, and a floor is only checkable by a peer operator who can read it. This page is that publication. It sits among the implementation entries rather than in the specification because section 15.1 puts the partition in the application, not the protocol; an implementation page is a published page about an application, held outside the specification.
Q3: reclassifying sources.*. Gated on the SSP/1 migration. Raw source data
is the user’s data and the mission argues for it being portable. Two things must
be answered before it could move to syncable: the row-key contract (dlt’s
_dlt_* surrogate and load-lineage columns are per-node, so cross-node row
identity is undefined) and embedded credentials (raw payloads are unmodelled
upstream JSON and may carry tokens no source plugin ever declared). The
supported path today is the documented one: promote into datasets.* through a
dataset plugin that owns the canonical shape.
10. Lenses applied
Section titled “10. Lenses applied”- Failure-closed by default. Every category boundary denies on ambiguity; the receiver drops rather than raises so a peer cannot wedge the apply loop.
- Blast radius.
custodyexists as a band because the recovery cost of a leak there is unbounded, and structural exclusion is the only control that survives a misconfiguration. - Reversibility. Narrowing is a two-way door and ships normally; widening is a one-way door that changes a mesh-wide invariant and is gated in section 11.
- Least privilege. Floor membership grants the ability to write a policy row, never replication. Nothing replicates without an explicit allow.
11. Changing this page
Section titled “11. Changing this page”Narrowing (any category to never-syncable or node-local-derived): a
normal change with security review. Peers converge safely because a narrower
node simply sends less, and the receiver’s own floor was never the thing that
admitted the data.
Declaring a category evictable (section 5, no to yes): security review,
and treated as one-way. Un-declaring stops future eviction but restores
nothing: history evicted under the old declaration is gone wherever every node
evicted it, and the rows that resurrect short stay short. The reversible
direction is yes to no, which is a normal change.
Widening (to syncable): a floor revision. SSP/1 requires the partition to
be identical on every node, so a widening is only safe once every node agrees.
It requires a revision of the v1 scoping decision, a version bump that peers can
detect, and a backfill plan for the history
that was withheld. It is never a configuration change, and it is never done by
editing a policy row.