zenkey_fleet/lib.rs
1//! Fleet engine for keyspace-v2 tooling (issue #15).
2//!
3//! The shared core of `zenctl` and `zengui`: everything a bus explorer needs
4//! that is not presentation, in five layers — see **The map** below. The
5//! RFC 05 §2.1 fan-in discipline lives in exactly one place
6//! ([`bus::query::fleet_get`], moved verbatim from zenctl — target `All`,
7//! consolidation `None`, attribution by the reply's own key); the liveliness
8//! roster, registry-slice sets, and the schema-aware decode seam build on it.
9//!
10//! Sessions opened here are deliberately **un-namespaced** (RFC 09 §5): an
11//! explorer sees the wire as it really is, full keys included — that is what
12//! lets it spot a leak. Do not "fix" this by setting a namespace.
13//!
14//! # The map
15//!
16//! Five strata, and the arrow between them only ever points one way. A
17//! sample enters at the top and leaves at the bottom as something a frontend
18//! can draw:
19//!
20//! ```text
21//! bus/ holds a session → observations
22//! model/ holds values → meaning
23//! judge/ holds meaning → verdicts
24//! report/ the serialized shapes every layer above hands out
25//! tape/ traffic as a thing: captured, replayed, manufactured, timed
26//! ```
27//!
28//! * **[`bus`]** — everything whose job needs a live session. `session`,
29//! `query`, `monitor`, `write`, `serve`, `admin`, `scout`, `seed`, `blob`,
30//! `roster`, `discover`, `producer`, `body`. The RFC 05 §2.1 fan-in
31//! discipline lives here exactly once, in [`bus::query::fleet_get`] (moved
32//! verbatim from zenctl — target `All`, consolidation `None`, attribution
33//! by the reply's own key), and everything in the layer that asks the
34//! fleet a question goes through it. This layer returns observations and
35//! never a verdict about one.
36//!
37//! Its event sources are **`Stream`s** (#343), not only `recv` loops:
38//! [`EventStream::into_stream`], [`SeededSubscriber`] (a direct impl),
39//! [`RosterWatch::changes`], and a `stream()` on
40//! [`ScoutStream`], [`Responder`], [`MockResponder`] and
41//! [`MatchingEvents`]. The last four borrow rather than consume, because
42//! the `&self` receiver is what lets a query be answered while its stream
43//! is held and a scout be stopped after one; a blanket `impl Stream` would
44//! have taken `&mut self` and spent that. The `recv`/`next` methods stay —
45//! a loop is still the clearer shape for a drain that also selects on
46//! something else, and every consumer in this workspace does.
47//!
48//! [`watchdog`] is the one that is not a `Stream` but a
49//! [`Straw`] (#397): it yields transitions *and* returns a
50//! [`WatchdogSummary`], with the acknowledged monitor teardown between the
51//! two. That is a shape `Stream` has no room for, and the only reason this
52//! crate speaks a second streaming vocabulary at all.
53//!
54//! * **[`model`]** — everything that can do its job from values already in
55//! hand. `facts`, `registry`, `project`, `stats`, `tree`, `skeleton`,
56//! `diff`, `decode`, `retain`, plus the two mechanisms every long-running
57//! projection shares (`bounded`, `examples`). Nothing here takes a
58//! session, and that is load-bearing: it is what lets a frontend replay a
59//! `.zrec` through the same projections it runs live.
60//!
61//! * **[`judge`]** — everything that takes a position. `doctor`, `expect`,
62//! `condition`, `field`, `why`, `cutover`, `retired`, `budget`, and
63//! [`judge::common`] for the vocabulary they share. The honesty rules
64//! (RFC 13, v1.24) bite hardest here, so the layer states them once.
65//!
66//! * **[`report`]** — every serde-pinned wire shape in the crate, split by
67//! domain. Its module doc carries the placement rule, which is the answer
68//! to "where does this struct go?" whenever the struct has a `Serialize`
69//! on it.
70//!
71//! * **[`tape`]** — traffic as a thing rather than an event. `record`,
72//! `ingest`, `generate`, `synth`, `bench`. It sits beside the others
73//! rather than under them because it both reads from the bus and writes
74//! back to it.
75//!
76//! **Placing a new module.** Ask, in order: does it need a session
77//! (`bus/`), can it answer from values in hand (`model/`), does it say
78//! whether something is *wrong* (`judge/`), does it turn a stream into a
79//! recording or back (`tape/`)? A new serde-pinned struct is not a module
80//! question at all — it goes to [`report`], by the rule stated there.
81//!
82//! What is deliberately **not** here: configuration. Where the operator
83//! keeps their connection contexts is `zenkey-explorer-config`'s job; this
84//! crate has no stratum for `~/.config`, and forcing `dirs` and `toml` on a
85//! library consumer so two binaries could read a TOML file was the tell.
86
87// docs.rs builds on nightly with `--cfg docsrs` (see Cargo.toml), which is
88// what lets each feature-gated item carry the feature that gates it. Inert
89// everywhere else — a stable `cargo doc` never sets the cfg (#325).
90//
91// **This is inferred, not annotated.** `doc_auto_cfg` was removed in Rust
92// 1.92 (rust-lang/rust#138907) by being folded into `doc_cfg`, so enabling
93// the feature here labels *every* `#[cfg(feature = "…")]` item, nested
94// modules included — verified against the nightly docs.rs uses by rendering
95// `judge::doctor`, `bus::body`, `model::decode` and `tape::generate` and
96// finding the badge on each. A hand-written
97// `#[cfg_attr(docsrs, doc(cfg(…)))]` beside a `#[cfg(…)]` is therefore
98// redundant, and a *wrong* one would render a lie; the ones still on the
99// re-exports below predate the merge and are harmless.
100#![cfg_attr(docsrs, feature(doc_cfg))]
101
102pub mod bus;
103pub mod error;
104pub mod judge;
105pub mod model;
106pub mod report;
107pub mod tape;
108// ─── the supported surface ──────────────────────────────────────────────────
109//
110// **The rule: the crate root is the whole supported surface.** Every type and
111// function a frontend is meant to use is re-exported here, and a path through
112// a module (`zenkey_fleet::model::decode::decode_sample`) is a spelling of the
113// same item, never the only way to reach one. The modules stay `pub` because
114// their docs are where the reasoning lives and because a reader browsing by
115// module should not hit a wall — but nothing supported is *only* there.
116//
117// Why it matters: both frontends had drifted into a mix of the two
118// (`zenkey_fleet::SliceSet` beside `zenkey_fleet::model::decode::SchemaStore`),
119// and which spelling a call site used said nothing about how supported the
120// item was. With the rule, "is this ours to use?" is answered by looking at
121// this block, and adding a public item without adding it here is the omission
122// that stands out.
123//
124// What is deliberately *not* here: `report`'s fifty-odd row and cell types,
125// which are the rendering vocabulary rather than the engine's — a frontend
126// reaches those through `zenkey_fleet::report::*`, and only the reports the
127// verbs below actually **return** are lifted to the root.
128
129#[cfg(feature = "decode")]
130#[cfg_attr(docsrs, doc(cfg(feature = "decode")))]
131pub use bus::body::{
132 BodySource, PrepareMode, PrepareSpec, PreparedBody, encode_encoding, prepare_publish,
133 prepare_request,
134};
135#[cfg(feature = "decode")]
136#[cfg_attr(docsrs, doc(cfg(feature = "decode")))]
137pub use judge::condition::{
138 CondWindow, Condition, DoctorWatch, Eval, RuleState, WatchdogSpec, watchdog,
139};
140#[cfg(feature = "decode")]
141#[cfg_attr(docsrs, doc(cfg(feature = "decode")))]
142pub use judge::doctor::{DoctorSpec, run_doctor};
143#[cfg(feature = "decode")]
144#[cfg_attr(docsrs, doc(cfg(feature = "decode")))]
145pub use judge::expect::{ExpectSpec, QosCheck, run_expect};
146#[cfg(feature = "decode")]
147#[cfg_attr(docsrs, doc(cfg(feature = "decode")))]
148pub use judge::field::{
149 DeclaredPaths, FieldObservation, FieldSpec, KeyFieldContext, KeyFields, PathStats, run_field,
150};
151#[cfg(feature = "decode")]
152#[cfg_attr(docsrs, doc(cfg(feature = "decode")))]
153pub use model::decode::{
154 DEFAULT_MAX_PRODUCERS, DecodedSample, DescribedSchema, Rendering, SchemaStore, Sealed,
155 StoreBounds, decode_sample, prewarm, schema_drift, schema_dump, schemas_for_type,
156 totality_gaps,
157};
158/// The traits [`watchdog`] is driven through (#397), re-exported so a
159/// consumer needs them in scope without taking a direct dependency on
160/// `sipper` — and so the version this engine speaks is the one it hands out.
161pub use sipper::{Sender, Sipper, Straw};
162#[cfg(feature = "decode")]
163#[cfg_attr(docsrs, doc(cfg(feature = "decode")))]
164pub use tape::generate::{
165 GenPattern, GenSpec, MockProducer, build_plan, run_gen, serve_describe, synthetic_marker,
166};
167#[cfg(feature = "decode")]
168#[cfg_attr(docsrs, doc(cfg(feature = "decode")))]
169pub use tape::synth::Synth;
170/// The #159 conformance verdict, re-exported so frontends never reach around
171/// the engine for it.
172#[cfg(feature = "decode")]
173#[cfg_attr(docsrs, doc(cfg(feature = "decode")))]
174pub use zenkey::schema::validate::{NotValidated, Verdict};
175
176pub use bus::admin::{
177 AdminEntry, admin_doc_omits_loopback, admin_get, admin_get_within, declared_entities,
178 mesh_links, origin_attachments, render_dot, routers, state_coverage, storages, topology,
179};
180#[cfg(feature = "blob")]
181#[cfg_attr(docsrs, doc(cfg(feature = "blob")))]
182pub use bus::blob::{BlobFetchSpec, FETCH_PRIORITY, blob_fetch, blob_probe, blob_tree_index};
183pub use bus::blob::{BlobTarget, blob_list, declared_by};
184pub use bus::discover::{AliveToken, discover_bases};
185pub use bus::monitor::{
186 EventStream, FleetEvent, Monitor, MonitorCore, MonitorSpec, SampleSource, SampleView,
187 StampProvenance, StreamItem, WatchId,
188};
189pub use bus::producer::{BringUp, LiveProducer, ReservedError, Responder};
190pub use bus::query::{
191 Answer, DEFAULT_MAX_REPLIES, FetchOutcome, FetchSpec, FetchedValue, FleetAnswer, GetOpts,
192 RepeatingQuery, RepeatingRegistry, ServedSlice, StateSample, declare_repeating,
193 declare_repeating_any, fetch_stored, fetch_value, fleet_get, fleet_registry,
194 fleet_registry_by_origin, fleet_registry_raw, state_snapshot,
195};
196pub use bus::roster::{
197 BridgeMatch, RosterChange, RosterWatch, apply_token, bridge_resolve, node_info, node_rows,
198 roster, token_identity,
199};
200pub use bus::scout::{ScoutStream, scout};
201pub use bus::seed::{SeedItem, SeedPolicy, SeededSubscriber, seed_subscribe};
202pub use bus::serve::{MockResponder, ServedQuery, declare_responder};
203pub use bus::session::{
204 Fleet, OPEN_TIMEOUT, OpenFailure, open, open_reporting, open_reporting_within, open_with_config,
205};
206pub use bus::write::{
207 CallSpec, CallTarget, MatchingEvents, Publication, RetireClass, call, check_retire,
208 declare_publication,
209};
210pub use judge::budget::{BudgetObservation, join_budget};
211pub use judge::common::{EXPANSION_CAP, data_plane_scopes, new_prefix};
212// Types reachable *through* root-exported ones — a caller that matches on
213// `KeyShape::V1` or walks a `Skeleton` needs these, and had to spell a module
214// path to name them (#350).
215pub use model::bounded::DEFAULT_MAX_KEYS;
216pub use model::facts::{ClassKind, OriginKind, SubjectFacts, V1Facts};
217pub use model::registry::{SliceSource, UnionOutcome};
218pub use model::skeleton::{
219 DeclRef, Evidence, NodeStats, SkeletonChunk, SkeletonCoverage, SkeletonNode, merge,
220};
221pub use model::tree::{TreeNode, TreeRow, TreeRows};
222// The rest of what the frontends actually reach for.
223#[cfg(feature = "decode")]
224#[cfg_attr(docsrs, doc(cfg(feature = "decode")))]
225pub use model::decode::{OBSERVE_LIMIT, structural, structural_value};
226pub use tape::record::rfc3339_now;
227// The judging vocabulary a caller can drive directly (#349's evidence
228// structs among them).
229#[cfg(feature = "decode")]
230#[cfg_attr(docsrs, doc(cfg(feature = "decode")))]
231pub use judge::condition::{SilenceEvidence, TickEvidence, judge_doctor_check, judge_origin_down};
232pub use judge::retired::EntryEvidence;
233// The remaining items a frontend actually calls. Every one of these was
234// reachable only by module path (#350) — which said nothing about whether it
235// was ours to use.
236pub use bus::teardown::DECLARE_TIMEOUT;
237pub use error::{BoxedCause, Error, Result, one_line};
238#[cfg(feature = "decode")]
239#[cfg_attr(docsrs, doc(cfg(feature = "decode")))]
240pub use judge::field::DEFAULT_MAX_PATHS;
241#[cfg(feature = "decode")]
242#[cfg_attr(docsrs, doc(cfg(feature = "decode")))]
243pub use judge::why::is_cause;
244// The two scope notes keep their own names rather than one: they are two
245// different O5 statements about two different windows, which is why
246// `judge/common.rs` declined to merge them. A name collision is not a reason
247// for an item to be unreachable from the root, though (#350).
248pub use judge::cutover::run_cutover;
249pub use judge::cutover::scope_note as cutover_scope_note;
250pub use judge::retired::run_retired;
251pub use judge::retired::scope_note as retired_scope_note;
252pub use judge::why::{StoredLookup, StoredValue, WhyInputs, WhySpec, WireWatch, run_why};
253// `diff` is `value_diff` at the root: a bare `diff` beside `byte_diff` in a
254// crate that also has `schema_drift` and `slice::diff` reads as *the* diff.
255pub use model::diff::{ByteDiff, Change, ValueDiff, byte_diff, diff as value_diff};
256pub use model::facts::{
257 FactsCache, KeyDescription, KeyFacts, KeyShape, Registration, describe_key,
258};
259pub use model::registry::SliceSet;
260pub use model::retain::{RetentionBudget, RetentionStats};
261pub use model::skeleton::{MergedNode, NodeStatus, Skeleton};
262pub use model::stats::{KeyStats, StampClass, StatsTable};
263pub use model::tree::KeyTreeSnapshot;
264/// The documents the verbs above **return**, at the root beside the verbs
265/// themselves — a caller that can spell `run_doctor` can spell what it hands
266/// back. The rest of `report` (rows, cells, verdict enums) stays behind
267/// `zenkey_fleet::report::*`: it is the rendering vocabulary, and lifting all
268/// of it here would make this block a second copy of that module.
269pub use report::{
270 BenchReport, CallReport, CollapsedProducer, Coverage, CoverageRow, CutoverReport,
271 DeclaredEntities, DeclaredEntity, DiscoveredBase, DoctorReport, DriftVerdict, EntityKind,
272 ExpectReport, Fault, FieldReport, Freshness, GenPlanEntry, GenReport, HelloView, Judgement,
273 LatencyReport, LatencySummary, MeshLink, NodeInfo, OriginAttachment, ProducerInfo,
274 RecordReport, ReplayReport, RetiredReport, RouterInfo, Rung, RungAnswer, SampleRow,
275 SchemaDrift, SchemaServer, SeedCoverage, StorageInfo, TopologyEdge, TopologyNode,
276 TopologyReport, TotalityGap, ValueSource, WhyReport, WhyVerdict, ZrecHeader,
277 judgement_exit_code,
278};
279#[cfg(feature = "decode")]
280#[cfg_attr(docsrs, doc(cfg(feature = "decode")))]
281pub use report::{CondState, Transition, WatchdogSummary};
282pub use tape::bench::{BenchSpec, run_bench};
283pub use tape::ingest::{IngestRow, StreamLine, parse_row, parse_stream_line};
284pub use tape::record::{
285 RecordBounds, ReplayEvent, ReplaySpec, ReplayTarget, ZREC_VERSION, ZrecItem, ZrecReader,
286 ZrecSink, ZrecSource, ZrecWriter, record, replay,
287};
288/// The RFC 07 reference client, re-exported so a frontend, an example or a
289/// test cannot end up on a different version of it than the engine.
290#[cfg(feature = "blob")]
291#[cfg_attr(docsrs, doc(cfg(feature = "blob")))]
292pub use zblob;
293
294/// `Send` on the public futures, asserted at compile time (#346).
295///
296/// Every bus-facing entry point in this crate is awaited from a `tokio::spawn`
297/// or an `iced::Task`, both of which require `Send`. Nothing said so: the
298/// property held because `zengui` happens to use iced, and would have broken
299/// on the first `Rc` or non-`Send` guard held across an `.await` — at a call
300/// site in *another* crate, with the error pointing anywhere but here.
301///
302/// A `const` block, so it costs nothing at runtime and fails the build here.
303#[cfg(all(test, feature = "decode"))]
304const _: () = {
305 const fn assert_send<T: Send>() {}
306
307 #[allow(dead_code)]
308 fn engine_futures_are_send() {
309 // One per layer, chosen because each holds something across an await
310 // that a careless change would make non-`Send`: a session, a lock
311 // guard, a decoder registry.
312 assert_send::<crate::Fleet<'_>>();
313 assert_send::<crate::SliceSet>();
314 assert_send::<crate::SchemaStore>();
315 assert_send::<crate::Monitor>();
316 assert_send::<crate::Error>();
317 }
318};