Skip to main content

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