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 bus::describe::{DescribeSweep, describe_sweep};
138#[cfg(feature = "decode")]
139#[cfg_attr(docsrs, doc(cfg(feature = "decode")))]
140pub use judge::condition::{
141 CondWindow, Condition, DoctorWatch, Eval, RuleSet, RuleState, SweepOutcome, WatchdogSpec,
142 watchdog,
143};
144#[cfg(feature = "decode")]
145#[cfg_attr(docsrs, doc(cfg(feature = "decode")))]
146pub use judge::doctor::{DoctorSpec, run_doctor};
147#[cfg(feature = "decode")]
148#[cfg_attr(docsrs, doc(cfg(feature = "decode")))]
149pub use judge::expect::{ExpectSpec, QosCheck, run_expect};
150#[cfg(feature = "decode")]
151#[cfg_attr(docsrs, doc(cfg(feature = "decode")))]
152pub use judge::field::{
153 DeclaredPaths, FieldObservation, FieldSpec, KeyFieldContext, KeyFields, PathStats, run_field,
154};
155#[cfg(feature = "decode")]
156#[cfg_attr(docsrs, doc(cfg(feature = "decode")))]
157pub use judge::kind::{KeyKind, KindObservation, judge_kind};
158#[cfg(feature = "decode")]
159#[cfg_attr(docsrs, doc(cfg(feature = "decode")))]
160pub use model::decode::{
161 DEFAULT_MAX_PRODUCERS, DecodedSample, DescribedSchema, Rendering, SchemaStore, Sealed,
162 StoreBounds, decode_sample, prewarm, schema_drift, schema_dump, schema_rows_for_type,
163 totality_gaps,
164};
165/// The traits [`watchdog`] is driven through (#397), re-exported so a
166/// consumer needs them in scope without taking a direct dependency on
167/// `sipper` — and so the version this engine speaks is the one it hands out.
168pub use sipper::{Sender, Sipper, Straw};
169#[cfg(feature = "decode")]
170#[cfg_attr(docsrs, doc(cfg(feature = "decode")))]
171pub use tape::generate::{
172 GenPattern, GenSpec, MockProducer, build_plan, run_gen, serve_describe, synthetic_marker,
173};
174#[cfg(feature = "decode")]
175#[cfg_attr(docsrs, doc(cfg(feature = "decode")))]
176pub use tape::synth::Synth;
177#[cfg(feature = "decode")]
178#[cfg_attr(docsrs, doc(cfg(feature = "decode")))]
179pub use tape::trigger::{TriggerEvent, TriggerSpec, record_on, state_projection};
180/// The #159 conformance verdict, re-exported so frontends never reach around
181/// the engine for it.
182#[cfg(feature = "decode")]
183#[cfg_attr(docsrs, doc(cfg(feature = "decode")))]
184pub use zenkey::schema::validate::{NotValidated, Verdict};
185
186pub use bus::admin::{
187 AdminEntry, admin_doc_omits_loopback, admin_get, admin_get_within, attach_tokens, consumers,
188 declared_entities, declared_entities_within, declared_entity_selectors, mesh_links,
189 origin_attachments, render_dot, routers, state_coverage, storages, subject_impact, topology,
190};
191#[cfg(feature = "blob")]
192#[cfg_attr(docsrs, doc(cfg(feature = "blob")))]
193pub use bus::blob::{BlobFetchSpec, FETCH_PRIORITY, blob_fetch, blob_probe, blob_tree_index};
194pub use bus::blob::{BlobTarget, blob_list, declared_by};
195pub use bus::discover::{AliveToken, discover_bases};
196pub use bus::monitor::{
197 EventStream, FleetEvent, Monitor, MonitorCore, MonitorSpec, SampleSource, SampleView,
198 StampProvenance, StreamItem, WatchId,
199};
200pub use bus::producer::{BringUp, LiveProducer, ReservedError, Responder};
201pub use bus::query::{
202 Answer, DEFAULT_MAX_REPLIES, FetchOutcome, FetchSpec, FetchedValue, FleetAnswer, GetOpts,
203 RepeatingQuery, RepeatingRegistry, ServedSlice, SnapshotReplies, StateSample,
204 declare_repeating, declare_repeating_any, fetch_stored, fetch_value, fleet_get, fleet_registry,
205 fleet_registry_by_origin, fleet_registry_raw, snapshot_get, state_snapshot,
206};
207pub use bus::roster::{
208 BridgeMatch, RosterChange, RosterWatch, apply_token, bridge_resolve, node_info, node_rows,
209 roster, token_identity,
210};
211pub use bus::scout::{ScoutStream, scout};
212pub use bus::seed::{SeedItem, SeedPolicy, SeededSubscriber, seed_subscribe};
213pub use bus::serve::{MockResponder, ServedQuery, declare_responder};
214pub use bus::session::{
215 Fleet, OPEN_TIMEOUT, OpenFailure, open, open_reporting, open_reporting_within, open_with_config,
216};
217pub use bus::write::{
218 CallSpec, CallTarget, MatchingEvents, Publication, RetireClass, TraceSpec, call, call_traced,
219 check_retire, declare_publication,
220};
221pub use judge::budget::{BudgetObservation, join_budget};
222pub use judge::common::{EXPANSION_CAP, data_plane_scopes, new_prefix};
223pub use judge::doctor_delta::doctor_delta;
224pub use judge::self_stats::{SelfStats, TableStats, judge_self_stats, read_self_stats};
225// Types reachable *through* root-exported ones — a caller that matches on
226// `KeyShape::V1` or walks a `Skeleton` needs these, and had to spell a module
227// path to name them (#350).
228pub use model::bounded::DEFAULT_MAX_KEYS;
229pub use model::facts::{ClassKind, OriginKind, SubjectFacts, V1Facts};
230pub use model::registry::{SliceSource, UnionOutcome};
231pub use model::skeleton::{
232 DeclRef, Evidence, NodeStats, SkeletonChunk, SkeletonCoverage, SkeletonNode, merge,
233};
234pub use model::tree::{TreeNode, TreeRow, TreeRows};
235// The rest of what the frontends actually reach for.
236#[cfg(feature = "decode")]
237#[cfg_attr(docsrs, doc(cfg(feature = "decode")))]
238pub use model::decode::{OBSERVE_LIMIT, structural, structural_value};
239// Registry inference (#225, RFC 08 §6.1): the observation, the inference,
240// and the draft emitter a frontend writes files from.
241#[cfg(feature = "decode")]
242#[cfg_attr(docsrs, doc(cfg(feature = "decode")))]
243pub use model::infer::{
244 InferObservation, Provenance, draft_file_names, draft_schema_files, infer, to_draft_toml,
245 to_draft_types_toml,
246};
247pub use tape::record::{rfc3339_from_unix, rfc3339_now};
248// The judging vocabulary a caller can drive directly (#349's evidence
249// structs among them).
250#[cfg(feature = "decode")]
251#[cfg_attr(docsrs, doc(cfg(feature = "decode")))]
252pub use judge::condition::{SilenceEvidence, TickEvidence, judge_doctor_check, judge_origin_down};
253pub use judge::retired::EntryEvidence;
254// The remaining items a frontend actually calls. Every one of these was
255// reachable only by module path (#350) — which said nothing about whether it
256// was ours to use.
257pub use bus::teardown::DECLARE_TIMEOUT;
258pub use error::{BoxedCause, Error, Result, one_line};
259#[cfg(feature = "decode")]
260#[cfg_attr(docsrs, doc(cfg(feature = "decode")))]
261pub use judge::field::DEFAULT_MAX_PATHS;
262#[cfg(feature = "decode")]
263#[cfg_attr(docsrs, doc(cfg(feature = "decode")))]
264pub use judge::why::is_cause;
265// The two scope notes keep their own names rather than one: they are two
266// different O5 statements about two different windows, which is why
267// `judge/common.rs` declined to merge them. A name collision is not a reason
268// for an item to be unreachable from the root, though (#350).
269pub use judge::cutover::run_cutover;
270pub use judge::cutover::scope_note as cutover_scope_note;
271pub use judge::retired::run_retired;
272pub use judge::retired::scope_note as retired_scope_note;
273pub use judge::why::{StoredLookup, StoredValue, WhyInputs, WhySpec, WireWatch, run_why};
274// `diff` is `value_diff` at the root: a bare `diff` beside `byte_diff` in a
275// crate that also has `schema_drift` and `slice::diff` reads as *the* diff.
276pub use model::acl::{AclOptions, check_acl, explain_acl, plan_acl, to_json5 as acl_plan_json5};
277pub use model::alert::alert_transition;
278pub use model::consumers::{SubjectTarget, declaring_sessions, join_consumers, subject_target};
279pub use model::diff::{ByteDiff, Change, ValueDiff, byte_diff, diff as value_diff};
280pub use model::export::{
281 DEFAULT_MAX_SERIES, DoctorRun, ExportLedger, FIELD_CAP, FoldInputs, Observed, PayloadVerdict,
282 WILDCARD_EXCLUDES, excluded_by,
283};
284pub use model::facts::{
285 FactsCache, KeyDescription, KeyFacts, KeyShape, Registration, describe_key,
286};
287pub use model::impact::{ImpactInputs, MAX_DEPTH_CAP, attribute, entity_of};
288pub use model::origin_map::{Label, MapError, MapPlan, OriginProfile, origin_profiles, plan_map};
289pub use model::prom::{exposition, metric_name};
290pub use model::registry::SliceSet;
291pub use model::retain::{RetentionBudget, RetentionStats};
292pub use model::skeleton::{MergedNode, NodeStatus, Skeleton};
293pub use model::snapshot::{fold_latest, holder_of, registration_of, stamper_of};
294pub use model::snapshot_diff::{DiffOpts, diff_normalized, diff_snapshots};
295pub use model::stats::{KeyStats, StampClass, StatsTable};
296pub use model::storage::{
297 check_storages, explain as explain_storage, plan_storages, to_json5 as storage_plan_json5,
298};
299pub use model::timeline::{
300 ArrivalAxis, ArrivalOrdering, Break, HlcAxis, HlcOrdering, HlcStamp, Ingested, Order, Placed,
301 PlacedBreak, SnLane, TimelineRow, Unstamped, Window, timeline,
302};
303pub use model::tree::KeyTreeSnapshot;
304/// The documents the verbs above **return**, at the root beside the verbs
305/// themselves — a caller that can spell `run_doctor` can spell what it hands
306/// back. The rest of `report` (rows, cells, verdict enums) stays behind
307/// `zenkey_fleet::report::*`: it is the rendering vocabulary, and lifting all
308/// of it here would make this block a second copy of that module.
309pub use report::{
310 AdminAnswer, AlertState, AlertTransition, AliasDoc, BenchReport, CallReport, CollapsedProducer,
311 ConsumerRow, ConsumersReport, Coverage, CoverageRow, CutoverReport, DeclaredEntities,
312 DeclaredEntity, DiscoveredBase, DoctorDelta, DoctorReport, DriftVerdict, EdgeDoc, EdgeEnd,
313 EdgeKind, EntityDoc, EntityKind, ExpectReport, ExportSnapshot, Fault, FieldReport, Freshness,
314 GenPlanEntry, GenReport, HelloView, ImpactReport, InferReport, InferredProducer,
315 InferredSubject, InferredType, Judgement, LatencyReport, LatencySummary, MeshLink, NodeInfo,
316 OriginAttachment, ProducerInfo, RecordReport, RenderSource, ReplayReport, RetiredReport,
317 RouterInfo, Rung, RungAnswer, SampleRow, SchemaDrift, SchemaServer, SeedCoverage, Snapshot,
318 SnapshotDiff, SnapshotReport, SnapshotRow, StorageInfo, SubjectImpact, TimelineReport,
319 TopologyEdge, TopologyNode, TopologyReport, TotalityGap, TraceReport, ValueSource, WhyReport,
320 WhyVerdict, ZrecHeader, ZsnapHeader, judgement_exit_code,
321};
322// `CondState` and `Transition` are unconditional since v1.34: a version-2
323// `.zrec` carries the trigger record, and the reader is not decode-gated.
324#[cfg(feature = "decode")]
325#[cfg_attr(docsrs, doc(cfg(feature = "decode")))]
326pub use report::WatchdogSummary;
327pub use report::{CondState, PreRollInfo, PreambleInfo, PreambleSemantics, Transition};
328pub use tape::bench::{BenchSpec, run_bench};
329pub use tape::ingest::{IngestRow, StreamLine, parse_row, parse_stream_line};
330pub use tape::record::{
331 PREAMBLE_SKIP_REASON, RecordBounds, ReplayEvent, ReplaySpec, ReplayTarget, SinkCounts,
332 ZREC_READS, ZREC_VERSION, ZrecItem, ZrecReader, ZrecSink, ZrecSource, ZrecWriter, record,
333 replay,
334};
335#[cfg(feature = "decode")]
336#[cfg_attr(docsrs, doc(cfg(feature = "decode")))]
337pub use tape::snapshot::{SnapshotSpec, Taken, take_snapshot};
338pub use tape::snapshot::{ZSNAP_VERSION, ZsnapReader, ZsnapWriter, report_of as snapshot_report};
339/// The RFC 07 reference client, re-exported so a frontend, an example or a
340/// test cannot end up on a different version of it than the engine.
341#[cfg(feature = "blob")]
342#[cfg_attr(docsrs, doc(cfg(feature = "blob")))]
343pub use zblob;
344
345/// `Send` on the public futures, asserted at compile time (#346).
346///
347/// Every bus-facing entry point in this crate is awaited from a `tokio::spawn`
348/// or an `iced::Task`, both of which require `Send`. Nothing said so: the
349/// property held because `zengui` happens to use iced, and would have broken
350/// on the first `Rc` or non-`Send` guard held across an `.await` — at a call
351/// site in *another* crate, with the error pointing anywhere but here.
352///
353/// A `const` block, so it costs nothing at runtime and fails the build here.
354#[cfg(all(test, feature = "decode"))]
355const _: () = {
356 const fn assert_send<T: Send>() {}
357
358 #[allow(dead_code)]
359 fn engine_futures_are_send() {
360 // One per layer, chosen because each holds something across an await
361 // that a careless change would make non-`Send`: a session, a lock
362 // guard, a decoder registry.
363 assert_send::<crate::Fleet<'_>>();
364 assert_send::<crate::SliceSet>();
365 assert_send::<crate::SchemaStore>();
366 assert_send::<crate::Monitor>();
367 assert_send::<crate::Error>();
368 }
369};