pub struct View {
pub workspaces: BTreeMap<String, ContentHash>,
pub changes: BTreeMap<String, ChangeState>,
pub refs: BTreeMap<String, ContentHash>,
pub reviews: BTreeMap<String, ReviewState>,
pub provenance: BTreeMap<String, BTreeMap<String, String>>,
pub checks: BTreeMap<String, CheckState>,
pub bindings: BTreeMap<String, KeyBinding>,
pub next_seq: u64,
pub latest_snapshot: Option<RefSnapshot>,
pub witnessed: BTreeMap<String, WitnessState>,
pub vouches: BTreeMap<String, BTreeMap<String, VouchState>>,
}Expand description
The materialized repo state: where every workspace and ref points.
A View is only ever produced by folding ops, so two replicas that
replay the same log prefix hold identical views.
Fields§
§workspaces: BTreeMap<String, ContentHash>Workspace name → head commit id.
changes: BTreeMap<String, ChangeState>Stable logical change id → owner, workspace and exact revision.
refs: BTreeMap<String, ContentHash>Ref name → target commit id.
reviews: BTreeMap<String, ReviewState>Review id → review state (fan-out and verdicts).
provenance: BTreeMap<String, BTreeMap<String, String>>Subject → record kind → latest body (D22 provenance records).
checks: BTreeMap<String, CheckState><subject-hex>:<check-name> → the latest report for it (D49).
One flat map rather than subject → name → state, because every
consumer of a view section wants the same two things: the ACL
narrows section rows one at a time, and the node’s paging bounds
them one at a time. A nested map would need both to learn a
second shape, and “all checks on commit X” is still a prefix
scan. The separator is : for the same reason refs uses it,
and it cannot collide: a hex subject contains no :.
bindings: BTreeMap<String, KeyBinding>Actor key id → its durable operator binding (D24 T1/T3 substrate).
Keyed by ContentHash::to_hex rather than by the hash itself so
it joins directly against the key_id a choir_oplog::Witness
carries, which is how an entry names its author.
next_seq: u64Number of ops folded so far, which is the log sequence the next applied op will occupy.
It is a fold counter rather than a value read off the log, because
View::apply is handed only the op — a signature this crate is
deliberately not changing. The counter is correct because every
path that builds a view applies exactly the log’s ops, in order,
once: View::at replays a prefix, and a node’s live view starts
from such a replay and then applies each entry the sequencer
admits. A caller holding both can assert view.next_seq == entry.seq before applying; nothing inside the fold can check it
on their behalf.
latest_snapshot: Option<RefSnapshot>The most recent admitted RefSnapshot, whole rather than by
id: readers ask a view “what is the latest attestation?” and the
chain check needs its identity, which RefSnapshot::id derives
from the value.
witnessed: BTreeMap<String, WitnessState>Witness name to its latest attestation (D67). Only the current
View::latest_snapshot is attestable, so every row here that
names it is a live witness of the state the view is serving, and
a row naming anything else is a witness that has fallen behind.
vouches: BTreeMap<String, BTreeMap<String, VouchState>>Vouched-for operator -> voucher -> the standing edge (D65).
Keyed by subject first because that is the question a reader brings: “who vouches for this actor”. The other direction is a scan, and it is the rarer one.
Nested rather than the flat a:b key View::checks uses,
because that key would have to join two operator names and an
operator name is free-form: any separator can occur inside one.
checks gets away with : only because a hex subject cannot
contain it, and borrowing the shape without the property is how
two edges come to share a row.
Implementations§
Source§impl View
impl View
Sourcepub fn vouch_stands(&self, voucher: &str, subject: &str) -> bool
pub fn vouch_stands(&self, voucher: &str, subject: &str) -> bool
Whether voucher’s vouch for subject currently stands (D65).
One lookup for both directions of the question, so admission and withdrawal cannot come to disagree about what “already vouches” means.
Sourcepub fn is_bound_operator(&self, operator: &str) -> bool
pub fn is_bound_operator(&self, operator: &str) -> bool
Whether operator holds at least one key binding that has not
been revoked (D65).
Revoked bindings do not count. A vouch from an operator whose every key has been withdrawn would be a statement nobody can be held to, which is the shape the floor exists to refuse.
Sourcepub fn validate(&self, op: &ViewOp) -> Result<(), ViewError>
pub fn validate(&self, op: &ViewOp) -> Result<(), ViewError>
Whether op would apply cleanly, without changing anything.
Every precondition in this model is a read — a CAS comparison, a
key lookup, or a non-empty check — so admission can be decided
against a shared &View rather than against a private copy of it.
That is what lets the single-writer admission path stop cloning the
whole view per submission.
View::apply calls this first and mutates only on Ok, so the
two can never disagree about what is admissible. Keeping them as
one code path is the point: a separate fast-path predicate that
drifts from the real one is how “checked in check()” turns into a
panic on the writer thread.
§Errors
The same failures View::apply would return for op.
Sourcepub fn check_key(subject: &ContentHash, name: &str) -> String
pub fn check_key(subject: &ContentHash, name: &str) -> String
The View::checks key for one (subject, check name) pair.
Sourcepub fn checks_for(&self, subject: &ContentHash) -> Vec<(&str, &CheckState)>
pub fn checks_for(&self, subject: &ContentHash) -> Vec<(&str, &CheckState)>
Every check reported against subject, as (name, state) in
name order.
Sourcepub fn checks_verdict(&self, subject: &ContentHash) -> Option<CheckStatus>
pub fn checks_verdict(&self, subject: &ContentHash) -> Option<CheckStatus>
The one answer for subject, or None when nothing reported.
Ranked by what the caller must do about it, worst first:
Failed, then Errored, then Running, then Passed.
A failure outranks a run still in flight. Both are “not green”,
but only one of them can still become green, and a caller
deciding whether to wait needs that distinction to point the
right way: told Running while a sibling check has already
failed, it waits for an outcome that cannot arrive.
Errored outranks Running for exactly that reason and no
other. A check that could not run will not become green by being
waited on – somebody has to re-run it – so reporting Running
beside it sends the caller to wait for an outcome that has
already failed to arrive once. It sits below Failed because it
is not evidence about the commit, and a summary that hid a real
red build behind our own outage would be the D18 conflation
again, pointed the other way.
Sourcepub fn apply(&mut self, op: &ViewOp) -> Result<(), ViewError>
pub fn apply(&mut self, op: &ViewOp) -> Result<(), ViewError>
Applies one op, enforcing its CAS precondition. A rejected op leaves the view unchanged.
§Errors
Returns ViewError::StaleHead when prev does not match.
Sourcepub fn snapshot(&self) -> RefSnapshot
pub fn snapshot(&self) -> RefSnapshot
The snapshot attesting this view’s current ref-state, chained to
the latest admitted one — the value OpKind::RecordRefSnapshot
admits as long as nothing lands in between (its at_seq is the
CAS: any interleaved op moves the fold position and the emitter
re-takes rather than attesting a state it did not read).
Sourcepub fn bound_actor_at(
&self,
channel: &str,
at: u64,
) -> Result<ContentHash, String>
pub fn bound_actor_at( &self, channel: &str, at: u64, ) -> Result<ContentHash, String>
The actor key the log binds to reviewer channel channel.
The inverse of KeyBinding::channel, and the join that lets an
authorization record name approvers as actor ids when a review can
only name channels (see Authorization::approvers).
Resolved as of fold position at (D44), which for an approver
is the position of the verdict being credited
(VerdictState::at) rather than the position of the landing.
Asking “now” is the bug this replaced. A channel’s key rotates:
BindKey tells the holder of a revoked key to bind a fresh one,
and the fresh one necessarily claims the same channel, because the
channel is the operator’s own name. So resolving at landing time
had two failures at once — with the withdrawn row still counted it
left the channel permanently ambiguous and refused every later
landing citing that reviewer, and with the withdrawn row ignored it
would name the fresh key as the approver of a verdict that key
never cast. Neither is a record worth keeping. The verdict’s own
position has one answer and it is the true one.
Contrast View::operator_of, which answers for a revoked key
unconditionally: that is attribution, and attribution must not go
blind the moment a key is withdrawn.
The liveness half is exact; the channel half is the latest one.
bound_at and Revocation::at are both recorded, so whether a
key was live at at replays exactly. Which channel it claimed is
read from the current binding, because a re-binding overwrites
KeyBinding::channel in place and the fold keeps no history of
it. A re-binding cannot change the operator (that is refused) and
the channel must read as the operator, so the drift this permits is
ana → ana/laptop within one operator, never across two. An
exact answer would need View::at re-folded to at, which this
method deliberately does not do: it is called from validate,
which has no log.
§Errors
Returns a caller-facing reason when the log binds no key to
channel, none that was live at at, or more than one that was.
Ambiguity is refused, not resolved. Two keys may legitimately
be live on one channel at once, and nothing in a review says which
of them cast the verdict, so picking one would put a specific key
in a durable record on the strength of a tiebreak. A record that
says nothing is recoverable; one that says the wrong thing
confidently is not.
“Nothing live at at” is its own message rather than folding into
“no binding”: the repairs differ. One needs the operator to bind a
key; the other needs a fresh verdict from a fresh key, because the
approval on file was cast by a key nobody trusts now.
Sourcepub fn operator_of(&self, key: &ContentHash) -> Option<&str>
pub fn operator_of(&self, key: &ContentHash) -> Option<&str>
The operator key is bound to, if the log ever bound it.
Deliberately still answers for a revoked key. Revocation
withdraws authority going forward; it does not un-attribute what
the key already did, and an audit that lost the operator the
moment a key was revoked would go blind exactly when it matters.
Authorization must therefore also consult
KeyBinding::is_revoked; attribution must not.
Sourcepub fn ops_since_binding(&self, key: &ContentHash) -> Option<u64>
pub fn ops_since_binding(&self, key: &ContentHash) -> Option<u64>
Ops sequenced since key was first bound, measured at this
view’s fold position.
Named for what it counts. This is an ordering and activity
primitive: it says a key has been bound across N sequenced ops,
and it is monotonic, replayable and unresettable. It is not a
wall clock, and it must not be relabelled as one — ops are not
uniformly spaced in time, so a question phrased in days or weeks
(D24 T1’s <2 weeks branch) still needs a durable timestamp this
crate does not have. A pure fold has no clock, the same reason
OpKind::ArchiveReview’s lapse decision is made node-side and
merely recorded here.
Sourcepub fn operator_keys<'a>(
&'a self,
operator: &'a str,
) -> impl Iterator<Item = (&'a str, &'a KeyBinding)> + 'a
pub fn operator_keys<'a>( &'a self, operator: &'a str, ) -> impl Iterator<Item = (&'a str, &'a KeyBinding)> + 'a
Every key currently bound to operator, revoked ones included, in
key-id order.
Revoked keys stay in the count because the question T3 asks — how
concentrated is control — is not answered by a number an operator
can lower by revoking keys it no longer needs. Callers wanting
only live keys filter on KeyBinding::is_revoked.
Sourcepub fn materialize(log: &dyn OpLog) -> Result<Self, ViewError>
pub fn materialize(log: &dyn OpLog) -> Result<Self, ViewError>
Sourcepub fn at(log: &dyn OpLog, upto: u64) -> Result<Self, ViewError>
pub fn at(log: &dyn OpLog, upto: u64) -> Result<Self, ViewError>
Folds only the first upto entries — the view as it was after op
upto - 1. This is undo/time-travel: restore by writing a new op
that sets heads back to this view (history itself is append-only).
§Errors
Same failure modes as View::materialize.