Context
Two pieces of the publication model (spec 09) are still placeholders.
1. scope → namespace mapping
MST2 resolve currently derives the namespace it reports a sequence for by
normalising the request scope:
// src/api/router/snapshot_router.rs (resolve)
let namespace = state.storage.mono_storage().normalize_namespace(&req.scope);
let durable = state.storage.mono_storage().publication_sequence(&namespace).await?;
normalize_namespace (src/jupiter/storage/mono_storage.rs) is just
"leading slash, no trailing slash". That is correct only while a scope names
one native namespace, which is true for the default scope and for deployments
without composite bindings — but not once a scope is composed from several
sources (import bindings, aggregates, subdirectory checkouts). A composed scope
has to report a publication identity that reflects the set of sources it was
resolved from, otherwise two different composed views can share a sequence
number.
2. writer epoch
mst2_publication.writer_epoch exists and the coordinator fences on it
(PublicationCoordinator::prepare rejects a writer whose epoch is behind the
namespace's), but nothing assigns or advances an epoch: the column is
written as a constant 1. Spec 09 §9 requires that once a deployment starts
publishing, an old binary that does not participate in the plan must be fenced
out rather than allowed to write refs directly.
Scope
-
Namespace map. Introduce an explicit mapping from a resolved view to the
publication namespace(s) it must be checked against:
- native scope → its own namespace (today's behaviour);
- composed scope → a deterministic identity derived from the ordered set of
source identities and their publication sequences, stored so a later
resolve can compare like with like;
- resolve must return
publication_sequence such that an identical
composition yields an identical value and a different composition cannot
collide.
The mapping belongs next to the descriptor construction
(src/ceres/snapshot/descriptor.rs), not in the router.
-
Writer epoch storage and fencing.
- Persist the current epoch per namespace (the
mst2_namespace_seq.epoch
column already exists).
- Accept an epoch from each writer (config value read at startup is the
simplest honest source) and record it on the receipt.
- When an epoch is advanced, writers below it must fail their next prepare
with CONFLICT, and the failure must be visible in logs/metrics rather
than looking like an ordinary CAS race.
- Document the cutover procedure (stop old writers → advance epoch → start
new writers) in the spec 09 §9 rollout runbook.
Acceptance
- A test where a composed scope built from sources A+B and one built from A+C
report different publication identities even when A's sequence is equal.
- A test where epoch is advanced and a stale-epoch writer is refused with no
ref movement, plus a test that a stale reader is unaffected.
resolve with mst2.publication_enabled=false must keep reporting the
provisional per-tip sequence (no behaviour change for deployments that have
not opted in).
References
- Spec 09 §2 (canonical plan, expected head/epoch), §7 (data structures), §9
(enablement gates)
src/ceres/snapshot/publication.rs, src/api/router/snapshot_router.rs,
src/jupiter/storage/mono_storage.rs
Context
Two pieces of the publication model (spec 09) are still placeholders.
1. scope → namespace mapping
MST2 resolvecurrently derives the namespace it reports a sequence for bynormalising the request scope:
normalize_namespace(src/jupiter/storage/mono_storage.rs) is just"leading slash, no trailing slash". That is correct only while a scope names
one native namespace, which is true for the default scope and for deployments
without composite bindings — but not once a scope is composed from several
sources (import bindings, aggregates, subdirectory checkouts). A composed scope
has to report a publication identity that reflects the set of sources it was
resolved from, otherwise two different composed views can share a sequence
number.
2. writer epoch
mst2_publication.writer_epochexists and the coordinator fences on it(
PublicationCoordinator::preparerejects a writer whose epoch is behind thenamespace's), but nothing assigns or advances an epoch: the column is
written as a constant
1. Spec 09 §9 requires that once a deployment startspublishing, an old binary that does not participate in the plan must be fenced
out rather than allowed to write refs directly.
Scope
Namespace map. Introduce an explicit mapping from a resolved view to the
publication namespace(s) it must be checked against:
source identities and their publication sequences, stored so a later
resolve can compare like with like;
publication_sequencesuch that an identicalcomposition yields an identical value and a different composition cannot
collide.
The mapping belongs next to the descriptor construction
(
src/ceres/snapshot/descriptor.rs), not in the router.Writer epoch storage and fencing.
mst2_namespace_seq.epochcolumn already exists).
simplest honest source) and record it on the receipt.
with
CONFLICT, and the failure must be visible in logs/metrics ratherthan looking like an ordinary CAS race.
new writers) in the spec 09 §9 rollout runbook.
Acceptance
report different publication identities even when A's sequence is equal.
ref movement, plus a test that a stale reader is unaffected.
resolvewithmst2.publication_enabled=falsemust keep reporting theprovisional per-tip sequence (no behaviour change for deployments that have
not opted in).
References
(enablement gates)
src/ceres/snapshot/publication.rs,src/api/router/snapshot_router.rs,src/jupiter/storage/mono_storage.rs