Skip to main content

stow_types/
api.rs

1//! HTTP wire types exchanged between the CLI, the edge worker, the
2//! scheduler Durable Object, and trusted CI.
3//!
4//! Field docs describe the wire meaning of each payload; the validated
5//! identity newtypes from [`crate::identity`] carry the invariants.
6
7use std::collections::BTreeMap;
8
9use serde::{Deserialize, Serialize};
10use utoipa::ToSchema;
11
12use crate::artifact::{ArtifactKind, RustCrateType};
13use crate::identity::{
14    CMetadata, CrateName, CrateVersion, DependencyCMetadataJson, FeaturesJson, TargetTriple,
15    WireRustcVersion,
16};
17use crate::index::ArtifactIndexRow;
18use crate::platform::Profile;
19
20/// The compilation target triples the trusted CI build fleet covers.
21///
22/// The runner map in `build-crate.yml` builds for exactly this set, so
23/// `POST /api/v1/requests` expands every requested crate onto each of
24/// them. The set is `WaterUI`'s shipping matrix (water-rs/stow#90);
25/// the ESP32 `*-espidf` triples stay out because they need a forked
26/// toolchain. Discontinued platforms stay out too — Intel Macs
27/// (`x86_64-apple-darwin`, dropped by macOS 27), x86 Android, and
28/// 32-bit ARM Android.
29pub const CI_TARGET_TRIPLES: &[&str] = &[
30    "aarch64-apple-darwin",
31    "aarch64-apple-ios",
32    "aarch64-apple-ios-sim",
33    "aarch64-linux-android",
34    "x86_64-unknown-linux-gnu",
35    "aarch64-unknown-linux-gnu",
36    "x86_64-pc-windows-msvc",
37    "aarch64-pc-windows-msvc",
38    "wasm32-unknown-unknown",
39];
40
41/// Whether trusted CI has a runner that can build this target.
42///
43/// A target outside the set resolves to an empty `runs-on` in
44/// `build-crate.yml`, and the dispatched run then dies before any job
45/// starts — no job, no log, no completion report, and the queue slot spent
46/// until the stale-dispatch sweep reclaims it. Every path that can put a
47/// task in the queue checks this first.
48#[must_use]
49pub fn is_ci_target(target: &str) -> bool {
50    CI_TARGET_TRIPLES.contains(&target)
51}
52
53/// The GitHub Actions runner pool a CI target builds on.
54///
55/// Mirrors the `runs-on` map in `build-crate.yml`: `macos-14` for the
56/// Apple targets, `windows-latest` for the MSVC targets, `ubuntu-latest`
57/// for the rest. The scheduler caps dispatches per family because the
58/// pools are sized very differently — the org's macOS pool is the
59/// smallest — and starts the slow Windows legs of a wave first.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
61pub enum RunnerFamily {
62    /// `ubuntu-latest` — also hosts the Android and wasm builds.
63    Linux,
64    /// `macos-14` — the smallest pool in the org's runner fleet.
65    MacOs,
66    /// `windows-latest` — the slowest legs of a full wave.
67    Windows,
68}
69
70impl RunnerFamily {
71    /// Every family, for iteration.
72    pub const ALL: [Self; 3] = [Self::Linux, Self::MacOs, Self::Windows];
73
74    /// The [`CI_TARGET_TRIPLES`] members that build on this family's
75    /// runner.
76    #[must_use]
77    pub const fn targets(self) -> &'static [&'static str] {
78        match self {
79            Self::Linux => &[
80                "aarch64-linux-android",
81                "x86_64-unknown-linux-gnu",
82                "aarch64-unknown-linux-gnu",
83                "wasm32-unknown-unknown",
84            ],
85            Self::MacOs => &[
86                "aarch64-apple-darwin",
87                "aarch64-apple-ios",
88                "aarch64-apple-ios-sim",
89            ],
90            Self::Windows => &["x86_64-pc-windows-msvc", "aarch64-pc-windows-msvc"],
91        }
92    }
93}
94
95/// Which runner family `build-crate.yml` dispatches this target to, or
96/// `None` for a target its `runs-on` map does not name.
97#[must_use]
98pub fn runner_family(target: &str) -> Option<RunnerFamily> {
99    RunnerFamily::ALL
100        .into_iter()
101        .find(|family| family.targets().contains(&target))
102}
103
104/// The task the scheduler dispatches to `stow-build`, carried verbatim as the
105/// `workflow_dispatch` input of the trusted build workflow.
106///
107/// Simple: just crate + target. CI figures out features/deps via `cargo metadata`.
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
109pub struct BuildTaskPayload {
110    /// Opaque scheduler task identifier (blake3 of identity tuple).
111    pub task_id: String,
112    /// Which queue attempt this dispatch carries. The scheduler bumps a
113    /// row's attempt every time a re-request resurrects it out of
114    /// failed/completed, and `complete` only applies a report whose attempt
115    /// matches the row's live one — a stale or duplicate report is a
116    /// conflict, never a silent overwrite of a newer attempt's state.
117    /// Defaults to 0 so a payload serialized before the field existed still
118    /// decodes; attempt 0 matches no row (attempts start at 1), so such a
119    /// report is rejected rather than applied blindly.
120    #[serde(default)]
121    pub attempt: u32,
122    /// Crate name as known to crates.io.
123    pub crate_name: CrateName,
124    /// Exact crate version to build.
125    pub version: CrateVersion,
126    /// Canonicalized features list (sorted, deduplicated).
127    pub features_json: FeaturesJson,
128    /// Compilation target triple.
129    pub target: TargetTriple,
130    /// Stable rustc version (e.g. `"1.83.0"`).
131    pub rustc_version: WireRustcVersion,
132    /// When true, the trusted build runner keeps the bundled `Cargo.lock` from
133    /// the crates.io tarball instead of removing it. Used by the binary-derived
134    /// overlay (`stow-admin preheat-binary-overlay`) so transitive `c_metadata`
135    /// matches what `cargo install --locked <bin>` would produce on the user's
136    /// machine. Defaults to false to preserve the historical "build against
137    /// latest semver-compatible deps" behavior for library preheats.
138    #[serde(default)]
139    pub preserve_lockfile: bool,
140    /// When set, the trusted build compiles a git checkout — the source a
141    /// real project ships — instead of a crates.io tarball. The checkout's
142    /// own `Cargo.lock` resolves the graph, so captured artifacts carry the
143    /// `dependency_c_metadata` chain that project's consumers compute. This
144    /// is the lockfile-seeding mode `stow-admin preheat-binary-overlay
145    /// --manifest-path` submits: building the project's real workspace makes
146    /// every cached artifact the one the project's graph actually asks for.
147    #[serde(default)]
148    pub project_source: Option<ProjectSource>,
149}
150
151impl BuildTaskPayload {
152    /// Whether the build resolves against the lockfile the source ships.
153    ///
154    /// Always true for project-source tasks: the project's own `Cargo.lock`
155    /// is the entire point of the mode, so a task that somehow arrived with
156    /// `preserve_lockfile` unset still builds `--locked` rather than
157    /// silently drifting to latest-semver resolution.
158    #[must_use]
159    pub const fn uses_source_lockfile(&self) -> bool {
160        self.preserve_lockfile || self.project_source.is_some()
161    }
162}
163
164/// A git checkout the trusted build compiles instead of a crates.io
165/// tarball, resolved by the checkout's own `Cargo.lock`.
166#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
167pub struct ProjectSource {
168    /// Git URL the runner clones (https in production; a local path is
169    /// accepted so the dev dispatch server can clone a worktree).
170    pub url: String,
171    /// Full commit SHA the checkout is pinned to. An immutable ref keeps a
172    /// moving branch from swapping the compiled code between enqueue and
173    /// build.
174    pub commit: String,
175    /// Manifest path relative to the repository root (`Cargo.toml` for a
176    /// workspace-root manifest).
177    pub manifest_path: String,
178}
179
180/// Artifact record CI POSTs to the edge's register endpoint after a build.
181///
182/// Sent to `/api/v1/admin/artifacts/register` once the build, sign, and OCI
183/// push have all succeeded. The edge worker authenticates the caller's
184/// GitHub identity (Actions OIDC for CI, push-user token otherwise) and
185/// persists the row in D1.
186#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
187pub struct ArtifactRecord {
188    /// Stable hash of the trusted build's exact rustc invocation identity.
189    pub compile_key: String,
190    /// Cargo's `-C metadata` value — part of the composite cache lookup key.
191    pub c_metadata: CMetadata,
192    /// Cargo's `-C extra-filename` suffix from the trusted build.
193    pub extra_filename: String,
194    /// Compilation target triple.
195    pub target: TargetTriple,
196    /// Rustc version string (e.g., "1.83.0").
197    pub rustc_version: WireRustcVersion,
198    /// Exact profile observed from the captured rustc invocation.
199    pub profile: Profile,
200    /// Exact `--emit` modes observed from the captured rustc invocation.
201    /// Must be strictly sorted and deduplicated.
202    pub emit: Vec<String>,
203    /// Crate name (for analytics/display).
204    pub crate_name: CrateName,
205    /// Crate version.
206    pub version: CrateVersion,
207    /// JSON-encoded feature set; canonicalized (sorted + deduplicated).
208    pub features_json: FeaturesJson,
209    /// JSON-encoded dependency `c_metadata` identities captured from rustc
210    /// --extern inputs; sorted by `(crate_name, c_metadata)`.
211    pub dependency_c_metadata_json: DependencyCMetadataJson,
212    /// OCI reference (e.g., "ghcr.io/water-rs/stow-cache:serde.1.0.0-...").
213    pub oci_reference: String,
214    /// OCI manifest digest (e.g., "sha256:...").
215    pub oci_digest: String,
216    /// Whether this artifact has native (C/C++) components.
217    pub has_native: bool,
218    /// Primary artifact kind for OCI naming and analytics.
219    pub artifact_kind: ArtifactKind,
220    /// Declared Rust crate types from cargo metadata / rustc args.
221    pub crate_types: Vec<RustCrateType>,
222    /// Artifact size in bytes: the sum of the uncompressed output files.
223    pub artifact_size: u64,
224    /// Digest (`sha256:…`) of the assembled bundle tar the trusted publish
225    /// stage pushed as the `<tag>.bundle` layer. The edge streams exactly
226    /// this blob to CLIs; it is what the byte path is keyed and fetched by.
227    pub bundle_digest: String,
228    /// Size in bytes of the bundle tar — the exact `content-length` of a
229    /// bundle GET and the input to the Cache API size gate.
230    pub bundle_size: u64,
231    /// Wall-clock milliseconds the captured rustc invocation took — what a
232    /// served hit on this artifact is credited as CPU time saved.
233    pub compile_millis: u64,
234}
235
236/// Request body for `POST /api/v1/admin/artifacts/register`.
237///
238/// `task_id` binds the record set to the scheduler task the calling run
239/// was dispatched for: the edge requires the task to be in flight and
240/// every record to belong to the task's dependency closure before it
241/// writes a row. The Actions OIDC identity must name a task; a repo-push
242/// caller (the operator/backfill path) may omit it.
243#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
244pub struct RegisterArtifactsRequest {
245    /// Scheduler task id the registering run was dispatched for
246    /// (`BuildTaskPayload::task_id`). Required from the Actions OIDC
247    /// identity; optional for repo-push callers.
248    #[serde(default)]
249    pub task_id: Option<String>,
250    /// Artifact records to upsert into the catalog.
251    pub records: Vec<ArtifactRecord>,
252}
253
254/// Request body for scheduler task submission.
255#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
256pub struct EnqueueRequest {
257    /// Crate name to build.
258    pub crate_name: CrateName,
259    /// Crate version to build.
260    pub version: CrateVersion,
261    /// Canonical features list.
262    pub features_json: FeaturesJson,
263    /// Compilation target triple.
264    pub target: TargetTriple,
265    /// Stable rustc version.
266    pub rustc_version: WireRustcVersion,
267    /// Total download count from crates.io (used for priority calculation).
268    pub downloads: u64,
269    /// Source of the enqueue request.
270    pub source: EnqueueSource,
271    /// Task-level dependencies that must be completed before this task can dispatch.
272    #[serde(default)]
273    pub depends_on: Vec<EnqueueDependency>,
274    /// Mirrors `BuildTaskPayload::preserve_lockfile`. Set to true for binary-
275    /// derived overlay enqueues so the trusted build resolves transitive deps
276    /// against the binary's published `Cargo.lock`.
277    #[serde(default)]
278    pub preserve_lockfile: bool,
279    /// Mirrors `BuildTaskPayload::project_source`: when set, the task builds
280    /// the named git checkout with its own `Cargo.lock` rather than a
281    /// crates.io tarball. Part of the queue identity — a project task and a
282    /// crate tarball task for the same `crate_name`/`version` never
283    /// deduplicate.
284    #[serde(default)]
285    pub project_source: Option<ProjectSource>,
286}
287
288impl EnqueueRequest {
289    /// Whether the build resolves against the lockfile the source ships —
290    /// the same rule [`BuildTaskPayload::uses_source_lockfile`] applies once
291    /// the task reaches the runner.
292    #[must_use]
293    pub const fn uses_source_lockfile(&self) -> bool {
294        self.preserve_lockfile || self.project_source.is_some()
295    }
296}
297
298/// One task-level dependency that must complete before its parent dispatches.
299#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
300pub struct EnqueueDependency {
301    /// Dependency crate name.
302    pub crate_name: CrateName,
303    /// Dependency crate version.
304    pub version: CrateVersion,
305    /// Canonical features list.
306    pub features_json: FeaturesJson,
307    /// Compilation target triple.
308    pub target: TargetTriple,
309    /// Stable rustc version.
310    pub rustc_version: WireRustcVersion,
311}
312
313/// Where an enqueue request originated.
314#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
315pub enum EnqueueSource {
316    /// Watcher detected a crate version update.
317    CrateUpdate,
318    /// Watcher detected a new rustc stable release.
319    RustcUpdate,
320    /// Edge reported a cache miss.
321    CacheMiss,
322    /// A Turnstile-verified human submitted `POST /api/v1/requests`. Tasks
323    /// enqueued with this source land in the scheduler's human lane.
324    HumanRequest,
325}
326
327/// Admission ticket the edge mints for one canonical enqueue task when a
328/// public request misses the cache.
329///
330/// Returned inside miss responses — as the 404 body of
331/// `POST /api/v1/artifacts/semantic` and in
332/// `DependencyGraphResponse::miss_admissions` — so the fetch path stays
333/// cheap. The client redeems the ticket by solving its proof-of-work and
334/// posting an [`EnqueueTicket`] to `POST /api/v1/enqueue`.
335#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
336pub struct EnqueueAdmission {
337    /// Canonical scheduler task id (blake3-derived identity string).
338    pub task_id: String,
339    /// Server-issued challenge — the hex HMAC-SHA256 over
340    /// `task_id ‖ canonical request JSON ‖ issue_minute` under the edge's
341    /// `STOW_POW_CHALLENGE_SECRET`. Opaque to clients; accepted during its
342    /// issue minute and the minute after it.
343    pub challenge: String,
344    /// Leading zero bits the client's
345    /// `blake3(task_id ‖ challenge ‖ nonce)` digest must show for
346    /// `/enqueue` to accept the ticket. `0` means the queue is shallow
347    /// enough that admission is free.
348    pub difficulty: u32,
349    /// The canonical enqueue request this admission authorizes. The edge is
350    /// stateless: the client echoes `request` back in its ticket and
351    /// `/api/v1/enqueue` forwards it to the scheduler after verifying the
352    /// challenge binds it.
353    pub request: EnqueueRequest,
354}
355
356/// Request body for `POST /api/v1/enqueue`: the redemption of an
357/// [`EnqueueAdmission`].
358#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
359pub struct EnqueueTicket {
360    /// Canonical task id carried by the admission.
361    pub task_id: String,
362    /// The admission's server-issued challenge.
363    pub challenge: String,
364    /// Client-computed nonce such that
365    /// `blake3(task_id ‖ challenge ‖ nonce)` has at least the required
366    /// number of leading zero bits.
367    pub nonce: u64,
368    /// The canonical enqueue request from the admission. The edge
369    /// recomputes the challenge HMAC over this payload and forwards it to
370    /// the scheduler — no server-side request lookup.
371    pub request: EnqueueRequest,
372}
373
374/// Request body for `POST /api/v1/admissions`: the misses a client's
375/// local index resolution found, plus the resolved graph the edge needs
376/// to re-derive the enqueue set with dominator pruning.
377///
378/// The client's dependency graph never leaves the machine in raw form for
379/// *coverage* — this call happens only when the local resolver already
380/// decided entries are uncovered, and the edge re-checks coverage against
381/// the catalog before minting anything.
382#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
383pub struct AdmissionRequest {
384    /// Compilation target triple.
385    pub target: TargetTriple,
386    /// Stable rustc version.
387    pub rustc_version: WireRustcVersion,
388    /// Direct-dep entries the local resolver found uncovered.
389    pub entries: Vec<DependencyGraphEntry>,
390    /// The client's resolved transitive graph.
391    #[serde(default)]
392    pub expanded_entries: Vec<ResolvedDependencyGraphEntry>,
393}
394
395/// CI reports job completion to the scheduler DO.
396#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
397pub struct BuildCompleteReport {
398    /// Scheduler task identifier, echoing `BuildTaskPayload::task_id`.
399    pub task_id: String,
400    /// Queue attempt this report belongs to, echoing
401    /// `BuildTaskPayload::attempt`. The scheduler applies the report only
402    /// when it matches the row's live attempt in a dispatched/running
403    /// state; anything else is a stale or duplicate report and conflicts.
404    /// Defaults to 0, which matches no row (attempts start at 1).
405    #[serde(default)]
406    pub attempt: u32,
407    /// Whether the build, sign, push, and registration all succeeded.
408    pub success: bool,
409    /// Failure description when `success` is false.
410    pub error: Option<String>,
411    /// Number of artifacts uploaded (including transitive deps).
412    pub artifacts_uploaded: u32,
413    /// GitHub Actions run id the report came from. CI leaves it `None`;
414    /// the edge overwrites it with the OIDC token's `run_id` claim before
415    /// forwarding, so the queue row carries the run that produced it.
416    #[serde(default)]
417    pub github_run_id: Option<String>,
418}
419
420/// A normalized dependency entry from a resolved Cargo dependency graph.
421#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, ToSchema)]
422pub struct DependencyGraphEntry {
423    /// Crate name.
424    pub crate_name: CrateName,
425    /// Crate version.
426    #[schema(value_type = String)]
427    pub version: semver::Version,
428    /// Sorted, deduplicated features (raw list — wire form is JSON array).
429    pub features: Vec<String>,
430}
431
432/// One exact dependency edge in a client-resolved Cargo graph.
433#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, ToSchema)]
434pub struct ResolvedDependencyGraphDependency {
435    /// Dependency crate name.
436    pub crate_name: CrateName,
437    /// Dependency crate version.
438    #[schema(value_type = String)]
439    pub version: semver::Version,
440}
441
442/// One exact crates.io package node resolved from the client's current lockfile graph.
443#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
444pub struct ResolvedDependencyGraphEntry {
445    /// Package crate name.
446    pub crate_name: CrateName,
447    /// Package crate version.
448    #[schema(value_type = String)]
449    pub version: semver::Version,
450    /// Sorted, deduplicated features (raw list — wire form is JSON array).
451    pub features: Vec<String>,
452    /// Direct dependencies of this package.
453    pub dependencies: Vec<ResolvedDependencyGraphDependency>,
454}
455
456/// The anonymous-traffic circuit breaker ("panic switch").
457///
458/// Held by the scheduler Durable Object. `enabled: true` makes every
459/// anonymous edge route answer `503 Service Unavailable` while the trusted
460/// `/api/v1/admin/*` and `/api/v1/scheduler/*` routes keep working. Wire
461/// shape of `GET`/`POST /api/v1/admin/panic` and of the scheduler object's
462/// `/panic` routes.
463#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
464pub struct PanicSwitch {
465    /// Whether anonymous traffic is being shed.
466    pub enabled: bool,
467}
468
469/// Scheduler DO queue status for monitoring.
470#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
471pub struct SchedulerStatus {
472    /// Tasks waiting to become dispatchable.
473    pub pending: u32,
474    /// Pending tasks in the human lane — a subset of `pending` counted
475    /// separately so operators can see human-requested work.
476    pub human_pending: u32,
477    /// Tasks whose `workflow_dispatch` was sent but not yet picked up.
478    pub dispatched: u32,
479    /// Tasks a CI run has claimed but not yet reported complete.
480    pub running: u32,
481    /// Tasks that completed successfully.
482    pub completed: u32,
483    /// Tasks whose CI run reported failure.
484    pub failed: u32,
485}
486
487/// Request body for `POST /api/v1/requests`: a human asking for one crate
488/// to be built into the public cache.
489///
490/// The endpoint is the submission path behind the request form on
491/// `stow.waterui.dev`; the Turnstile token is the admission check and every
492/// accepted request lands in the scheduler's human lane ahead of the
493/// cache-miss queue.
494#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
495pub struct CrateRequest {
496    /// Crate name as published on crates.io.
497    pub crate_name: CrateName,
498    /// Exact version to request. When `None` the edge resolves the newest
499    /// non-prerelease, non-yanked crates.io version.
500    #[serde(default)]
501    pub version: Option<CrateVersion>,
502    /// Features to enable for the requested crate, in the canonical
503    /// [`FeaturesJson`] representation. The list is taken literally:
504    /// `["default", ...]` builds with default features on, and an empty
505    /// list is `--no-default-features` with nothing added.
506    pub features_json: FeaturesJson,
507    /// Cloudflare Turnstile token produced by the invisible widget. Verified
508    /// against siteverify before the edge does any resolution work.
509    pub turnstile_token: String,
510}
511
512/// One crate in a `GET /api/v1/crates/search` result.
513#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
514pub struct CrateSearchHit {
515    /// Crate name as published on crates.io.
516    pub crate_name: CrateName,
517    /// The crate's one-line description, when it has one.
518    #[serde(default)]
519    pub description: Option<String>,
520    /// Newest version crates.io lists for the crate.
521    pub max_version: CrateVersion,
522    /// All-time download count, the ordering crates.io search returns.
523    pub downloads: u64,
524}
525
526/// Response body for `GET /api/v1/crates/search`.
527#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
528pub struct CrateSearchResponse {
529    /// Matching crates, most downloaded first, capped by the `limit` query
530    /// parameter.
531    pub crates: Vec<CrateSearchHit>,
532}
533
534/// Response body for `GET /api/v1/crates/{crate_name}/versions`.
535#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
536pub struct CrateVersionsResponse {
537    /// Published, non-yanked versions, newest first.
538    pub versions: Vec<CrateVersion>,
539}
540
541/// One feature a crate version declares.
542#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
543pub struct CrateFeature {
544    /// The feature name, as written in the crate's `[features]` table or
545    /// implied by an optional dependency.
546    pub name: String,
547    /// The features and optional dependencies this one turns on. Empty for
548    /// an implicit optional-dependency feature.
549    pub implies: Vec<String>,
550    /// Whether the crate's `default` feature set enables this feature,
551    /// directly or transitively.
552    pub default: bool,
553}
554
555/// Response body for `GET /api/v1/crates/{crate_name}/versions/{version}/features`.
556#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
557pub struct CrateFeaturesResponse {
558    /// Every selectable feature of the version, `default` first and the
559    /// rest alphabetical.
560    pub features: Vec<CrateFeature>,
561}
562
563/// Which scheduler lane a queue row belongs to.
564#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
565#[serde(rename_all = "snake_case")]
566pub enum TaskLane {
567    /// Filled by the cache-miss path and watchers; subject to
568    /// `STOW_DISPATCH_MIN_AGE_MINUTES` before dispatch.
569    Miss,
570    /// Filled by the Turnstile-verified human request API. Dispatches ahead
571    /// of the miss lane and bypasses the minimum-age hold.
572    Human,
573}
574
575impl TaskLane {
576    /// The stable string persisted in the scheduler's `lane` column.
577    #[must_use]
578    pub const fn as_str(self) -> &'static str {
579        match self {
580            Self::Miss => "miss",
581            Self::Human => "human",
582        }
583    }
584
585    /// Inverse of [`Self::as_str`] for values read back from queue rows.
586    #[must_use]
587    pub fn parse(value: &str) -> Option<Self> {
588        match value {
589            "miss" => Some(Self::Miss),
590            "human" => Some(Self::Human),
591            _ => None,
592        }
593    }
594}
595
596/// Lifecycle of one scheduler queue row.
597#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
598#[serde(rename_all = "snake_case")]
599pub enum QueueTaskStatus {
600    /// Waiting for dependencies or dispatch eligibility.
601    Pending,
602    /// `workflow_dispatch` sent, awaiting the CI job to claim it.
603    Dispatched,
604    /// A CI run claimed the task but has not reported completion.
605    Running,
606    /// Build, signing, push, and registration all succeeded.
607    Completed,
608    /// The CI run reported failure.
609    Failed,
610}
611
612impl QueueTaskStatus {
613    /// The stable string persisted in the scheduler's `status` column.
614    #[must_use]
615    pub const fn as_str(self) -> &'static str {
616        match self {
617            Self::Pending => "pending",
618            Self::Dispatched => "dispatched",
619            Self::Running => "running",
620            Self::Completed => "completed",
621            Self::Failed => "failed",
622        }
623    }
624
625    /// Inverse of [`Self::as_str`] for values read back from queue rows.
626    #[must_use]
627    pub fn parse(value: &str) -> Option<Self> {
628        match value {
629            "pending" => Some(Self::Pending),
630            "dispatched" => Some(Self::Dispatched),
631            "running" => Some(Self::Running),
632            "completed" => Some(Self::Completed),
633            "failed" => Some(Self::Failed),
634            _ => None,
635        }
636    }
637}
638
639/// Per-target state reported inside [`CrateRequestTarget`].
640#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
641#[serde(rename_all = "snake_case")]
642pub enum CrateRequestState {
643    /// The artifact already exists in the public cache for this target.
644    Cached,
645    /// This request enqueued the task into the human lane.
646    Queued,
647    /// The task was already in the queue (re-requesting promoted or
648    /// refreshed it) when this request arrived.
649    AlreadyQueued,
650    /// The task has been dispatched or is actively building.
651    Building,
652}
653
654/// Per-target outcome inside a [`CrateRequestOutcome`].
655#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
656pub struct CrateRequestTarget {
657    /// Compilation target triple — one of [`CI_TARGET_TRIPLES`].
658    pub target: TargetTriple,
659    /// Where this target's task stands after the request.
660    pub state: CrateRequestState,
661    /// Scheduler task id for the requested crate on this target. Absent
662    /// when `state` is [`CrateRequestState::Cached`].
663    #[serde(default)]
664    pub task_id: Option<String>,
665    /// 1-based position among pending human-lane tasks in dispatch order;
666    /// `None` unless the task is still pending in the human lane.
667    #[serde(default)]
668    pub human_lane_position: Option<u32>,
669}
670
671/// Response of `POST /api/v1/requests`: what the edge resolved and where
672/// each supported target stands.
673#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
674pub struct CrateRequestOutcome {
675    /// Echoed crate name.
676    pub crate_name: CrateName,
677    /// The resolved version — the requested one, or the newest
678    /// non-prerelease, non-yanked crates.io release.
679    pub version: CrateVersion,
680    /// Current stable rustc version the enqueued tasks target.
681    pub rustc_version: WireRustcVersion,
682    /// Per-target outcomes in [`CI_TARGET_TRIPLES`] order.
683    pub targets: Vec<CrateRequestTarget>,
684}
685
686/// Point-in-time view of one scheduler task, returned by
687/// `GET /api/v1/requests/{task_id}`.
688#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
689pub struct RequestStatus {
690    /// Canonical scheduler task id (blake3-derived identity string).
691    pub task_id: String,
692    /// Crate name.
693    pub crate_name: CrateName,
694    /// Exact crate version.
695    pub version: CrateVersion,
696    /// Canonical features list.
697    pub features_json: FeaturesJson,
698    /// Compilation target triple.
699    pub target: TargetTriple,
700    /// Stable rustc version.
701    pub rustc_version: WireRustcVersion,
702    /// Which scheduler lane the task is queued in.
703    pub lane: TaskLane,
704    /// Current task lifecycle state.
705    pub status: QueueTaskStatus,
706    /// 1-based position among pending human-lane tasks in dispatch order;
707    /// `None` unless the task is a pending human-lane task.
708    #[serde(default)]
709    pub human_lane_position: Option<u32>,
710    /// Whether the task builds against the bundled `Cargo.lock`
711    /// (`EnqueueRequest::preserve_lockfile`). Combined with
712    /// `project_source` it tells whether the task's dependency closure is
713    /// reproducible from crates.io metadata.
714    #[serde(default)]
715    pub preserve_lockfile: bool,
716    /// Project checkout the task builds instead of a crates.io tarball.
717    #[serde(default)]
718    pub project_source: Option<ProjectSource>,
719}
720
721impl RequestStatus {
722    /// Whether the task resolves dependencies from a lockfile the edge
723    /// cannot reproduce — a `preserve_lockfile` overlay resolves the
724    /// tarball's bundled lockfile, and a project-source task resolves the
725    /// checkout's own one. Mirrors
726    /// [`BuildTaskPayload::uses_source_lockfile`].
727    #[must_use]
728    pub const fn uses_source_lockfile(&self) -> bool {
729        self.preserve_lockfile || self.project_source.is_some()
730    }
731}
732
733/// Response body for `GET /api/v1/admin/index/{target}/{rustc_version}`.
734///
735/// One keyset page of the slice's servable artifact rows — the data
736/// `stow-admin index export` assembles into the published
737/// [`crate::index::ArtifactIndex`].
738#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
739pub struct ArtifactIndexPage {
740    /// Rows ordered by `c_metadata`, every one strictly after the
741    /// request's `after` cursor and carrying a published bundle.
742    pub rows: Vec<ArtifactIndexRow>,
743    /// The `after` cursor for the next page — the last row's `c_metadata`
744    /// when this page was full, `None` once the slice is exhausted.
745    pub next_after: Option<String>,
746}
747
748// ===== Operations API (`stow-admin` under `/api/v1/admin/*`) =====
749
750/// Row filter for `GET /api/v1/admin/queue` and the `filter` half of a
751/// [`QueueSelector`].
752///
753/// Every field is optional; a fully empty filter selects every row (and
754/// is rejected for mutations — an operator mutation must name either
755/// explicit task ids or at least one predicate).
756#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
757pub struct QueueFilter {
758    /// Lifecycle status to match.
759    #[serde(default)]
760    pub status: Option<QueueTaskStatus>,
761    /// Compilation target to match.
762    #[serde(default)]
763    pub target: Option<TargetTriple>,
764    /// Crate name to match (`crate` on the wire).
765    #[serde(default, rename = "crate", alias = "crate_name")]
766    pub crate_name: Option<CrateName>,
767    /// Only rows whose `updated_at` is at least this many seconds old
768    /// (`older_than` on the wire).
769    #[serde(default, rename = "older_than", alias = "older_than_secs")]
770    pub older_than_secs: Option<u64>,
771    /// Most rows a listing returns (bounded server-side); mutations
772    /// ignore it — a mutation selector either matches everything its
773    /// predicates describe or is rejected.
774    #[serde(default)]
775    pub limit: Option<u32>,
776}
777
778/// Selector for `POST /api/v1/admin/queue/{retry,cancel,promote,purge}`.
779///
780/// Flattened into the query string it also serves `GET
781/// /api/v1/admin/queue`, so the mutation preview lists exactly the rows
782/// a selector names.
783///
784/// The `filter` fields flatten into the selector's own keys, so both the
785/// JSON body `{"task_ids": […], "status": "failed"}` and the query string
786/// `?task_ids=a&task_ids=b&status=failed` decode the same shape.
787///
788/// A non-empty `task_ids` selects exactly those rows and the filter is
789/// ignored; otherwise the filter selects. Either way the verb's own
790/// status/lane predicates still apply — a mutation never touches a row
791/// outside its transition domain.
792#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
793pub struct QueueSelector {
794    /// Explicit task ids.
795    #[serde(default)]
796    pub task_ids: Vec<String>,
797    /// Row filter used when `task_ids` is empty.
798    #[serde(default, flatten)]
799    pub filter: QueueFilter,
800}
801
802/// Response of the `queue retry|cancel|promote|purge` endpoints: how many
803/// rows the transition touched.
804#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
805pub struct QueueMutationResult {
806    /// Rows the transition affected.
807    pub affected: u32,
808}
809
810/// One queue row as `GET /api/v1/admin/queue` reports it.
811#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
812pub struct QueueTask {
813    /// Scheduler task identifier.
814    pub task_id: String,
815    /// Crate name to build.
816    pub crate_name: CrateName,
817    /// Crate version to build.
818    pub version: CrateVersion,
819    /// Canonical features list.
820    pub features_json: FeaturesJson,
821    /// Compilation target.
822    pub target: TargetTriple,
823    /// Rustc version the row builds for.
824    pub rustc_version: WireRustcVersion,
825    /// Dispatch lane the row is queued in.
826    pub lane: TaskLane,
827    /// Lifecycle status.
828    pub status: QueueTaskStatus,
829    /// Live enqueue epoch — completion reports only apply to this attempt.
830    pub attempt: u32,
831    /// Last dispatch/build error, empty when none.
832    pub error: String,
833    /// crates.io download count captured at enqueue time.
834    pub downloads: u64,
835    /// Cache misses this row is responsible for.
836    pub miss_count: u32,
837    /// Cache-hit requests this row has served demand for.
838    pub request_count: u32,
839    /// Dispatches attempted against this row.
840    pub dispatch_attempts: u32,
841    /// Whether the row builds against its checked-in lockfile.
842    pub preserve_lockfile: bool,
843    /// Project-checkout source when the row is not a crates.io build.
844    #[serde(default)]
845    pub project_source: Option<ProjectSource>,
846    /// GitHub Actions run id the dispatched build reported back through
847    /// its OIDC-claimed register/complete calls.
848    #[serde(default)]
849    pub github_run_id: Option<String>,
850    /// First request timestamp (`YYYY-MM-DD HH:MM:SS` UTC).
851    pub first_requested_at: String,
852    /// Row creation timestamp.
853    pub created_at: String,
854    /// Last state-transition timestamp.
855    pub updated_at: String,
856}
857
858/// One in-flight (dispatched/running) queue row in [`AdminStatus`].
859#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
860pub struct AdminInFlight {
861    /// Scheduler task identifier.
862    pub task_id: String,
863    /// Crate name to build.
864    pub crate_name: CrateName,
865    /// Crate version to build.
866    pub version: CrateVersion,
867    /// Compilation target.
868    pub target: TargetTriple,
869    /// Rustc version the row builds for.
870    pub rustc_version: WireRustcVersion,
871    /// Lifecycle status (`dispatched` or `running`).
872    pub status: QueueTaskStatus,
873    /// Live enqueue epoch.
874    pub attempt: u32,
875    /// Dispatches attempted against this row.
876    pub dispatch_attempts: u32,
877    /// Last state-transition timestamp — the stale-dispatch lease clock.
878    pub updated_at: String,
879    /// GitHub Actions run id, once the run has reported back.
880    #[serde(default)]
881    pub github_run_id: Option<String>,
882}
883
884/// Per-target completion tallies over the trailing 24 hours.
885#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
886pub struct AdminTargetStats {
887    /// Compilation target.
888    pub target: TargetTriple,
889    /// Rows that completed in the window.
890    pub completed_24h: u32,
891    /// Rows that failed in the window.
892    pub failed_24h: u32,
893}
894
895/// Response of `GET /api/v1/admin/status` — the scheduler's operator view.
896#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
897pub struct AdminStatus {
898    /// Pending rows in the cache-miss lane.
899    pub pending_miss: u32,
900    /// Pending rows in the human lane.
901    pub pending_human: u32,
902    /// Age in seconds of the oldest pending row (`first_requested_at`).
903    #[serde(default)]
904    pub oldest_pending_seconds: Option<u64>,
905    /// Dispatched/running rows, oldest transition first.
906    pub in_flight: Vec<AdminInFlight>,
907    /// Per-target completion tallies over the trailing 24 hours.
908    pub targets: Vec<AdminTargetStats>,
909    /// Whether the anonymous-traffic circuit breaker is engaged.
910    pub panic_enabled: bool,
911}
912
913/// Response of `POST /api/v1/scheduler/tasks/submit` — what a request batch
914/// became after canonicalization and enqueue.
915#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
916pub struct SchedulerSubmitResponse {
917    /// Requests that canonicalized and were handed to the scheduler.
918    pub submitted: u32,
919    /// Brand-new queue rows inserted; the rest of `submitted` updated or
920    /// merged into existing rows.
921    pub inserted: u32,
922    /// Requests dropped during canonicalization (unpublished version or
923    /// unresolvable `depends_on`).
924    pub dropped: u32,
925}
926
927/// Response of `GET /api/v1/admin/coverage/{crate}` — per-CI-target
928/// servable identities for one crate (one version when `version` was given).
929#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
930pub struct CrateCoverage {
931    /// Crate the coverage describes.
932    pub crate_name: CrateName,
933    /// Version the coverage is scoped to (`None` = every published version).
934    #[serde(default)]
935    pub version: Option<CrateVersion>,
936    /// One entry per [`CI_TARGET_TRIPLES`] target — or just the requested
937    /// target — in canonical order; an empty `artifacts` list means the
938    /// target has nothing servable.
939    pub targets: Vec<CoverageTarget>,
940}
941
942/// One target's servable identities inside [`CrateCoverage`].
943#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
944pub struct CoverageTarget {
945    /// Compilation target.
946    pub target: TargetTriple,
947    /// Servable identities (published bundle rows) for this target.
948    pub artifacts: Vec<CoverageArtifact>,
949}
950
951/// One servable artifact identity inside [`CoverageTarget`].
952#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
953pub struct CoverageArtifact {
954    /// Crate version the artifact serves.
955    pub version: CrateVersion,
956    /// Canonical features list the artifact was built with.
957    pub features_json: FeaturesJson,
958    /// Rustc version the artifact was built by.
959    pub rustc_version: WireRustcVersion,
960    /// Cargo `-C metadata` identity.
961    pub c_metadata: CMetadata,
962    /// Bundle tar size in bytes.
963    pub bundle_size: u64,
964}
965
966/// Request body for `POST /api/v1/admin/preheat/plan` — a dry run of the
967/// resolver's closure expansion and dominance pruning for one crate
968/// request. Nothing is enqueued.
969#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
970pub struct PreheatPlanRequest {
971    /// Crate name to plan for.
972    pub crate_name: CrateName,
973    /// Exact version; absent resolves the newest non-prerelease,
974    /// non-yanked published release.
975    #[serde(default)]
976    pub version: Option<CrateVersion>,
977    /// Seed features (`[]` = `--no-default-features` semantics).
978    pub features_json: FeaturesJson,
979    /// One compilation target, or absent for every [`CI_TARGET_TRIPLES`]
980    /// target.
981    #[serde(default)]
982    pub target: Option<TargetTriple>,
983    /// Rustc version; absent resolves the scheduler's stable channel
984    /// version.
985    #[serde(default)]
986    pub rustc_version: Option<WireRustcVersion>,
987}
988
989/// Response of `POST /api/v1/admin/preheat/plan`.
990#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
991pub struct PreheatPlanResponse {
992    /// Crate the plan was computed for.
993    pub crate_name: CrateName,
994    /// Version the request resolved to.
995    pub version: CrateVersion,
996    /// Rustc version the plan was computed for.
997    pub rustc_version: WireRustcVersion,
998    /// One plan per resolved target, in [`CI_TARGET_TRIPLES`] order.
999    pub targets: Vec<PreheatPlanTarget>,
1000}
1001
1002/// One target's enqueue plan inside [`PreheatPlanResponse`].
1003#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
1004pub struct PreheatPlanTarget {
1005    /// Compilation target.
1006    pub target: TargetTriple,
1007    /// Whether the requested crate already has a servable artifact here.
1008    pub root_cached: bool,
1009    /// Tasks the dispatch wave would enqueue — dominance-pruned, so
1010    /// `depends_on` edges hold dominated rows until their dominator
1011    /// resolves. Tasks with an empty `depends_on` are the wave's roots.
1012    pub tasks: Vec<EnqueueRequest>,
1013}
1014
1015/// Response of `GET /api/v1/admin/artifacts/{target}/{rustc_version}/{c_metadata}`:
1016/// the D1 catalog row plus the bundle image's OCI manifest read from GHCR.
1017#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
1018pub struct ArtifactInspection {
1019    /// The D1 catalog row.
1020    pub record: ArtifactRecord,
1021    /// The bundle image's OCI manifest (`manifests/<tag>.bundle`).
1022    pub manifest: OciManifest,
1023}
1024
1025/// OCI image manifest — the document a `manifests/<reference>` GET serves.
1026#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
1027pub struct OciManifest {
1028    /// Manifest schema version (`2` for every served document).
1029    pub schema_version: u32,
1030    /// Manifest media type.
1031    #[serde(default)]
1032    pub media_type: Option<String>,
1033    /// Config blob descriptor.
1034    pub config: OciDescriptor,
1035    /// Layer blob descriptors.
1036    #[serde(default)]
1037    pub layers: Vec<OciDescriptor>,
1038    /// Manifest annotations.
1039    #[serde(default)]
1040    pub annotations: BTreeMap<String, String>,
1041}
1042
1043/// One blob descriptor inside an [`OciManifest`].
1044#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
1045pub struct OciDescriptor {
1046    /// Blob media type.
1047    pub media_type: String,
1048    /// `sha256:…` content digest.
1049    pub digest: String,
1050    /// Blob size in bytes.
1051    pub size: u64,
1052    /// Descriptor annotations.
1053    #[serde(default)]
1054    pub annotations: BTreeMap<String, String>,
1055}
1056
1057/// Query for `GET /api/v1/admin/artifacts` — a bounded catalog listing
1058/// used to preview a prune and for ad-hoc inspection.
1059#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
1060pub struct ArtifactListQuery {
1061    /// Only rows built by this rustc version.
1062    #[serde(default)]
1063    pub rustc_version: Option<WireRustcVersion>,
1064    /// Only rows for this compilation target.
1065    #[serde(default)]
1066    pub target: Option<TargetTriple>,
1067    /// Only rows for this crate (`crate` on the wire).
1068    #[serde(default, rename = "crate", alias = "crate_name")]
1069    pub crate_name: Option<CrateName>,
1070    /// Most rows to return (bounded server-side).
1071    #[serde(default)]
1072    pub limit: Option<u32>,
1073}
1074
1075/// Request body for `POST /api/v1/admin/artifacts/prune`.
1076///
1077/// Deletes every catalog row built by `rustc_version` — the retired
1078/// toolchain — and invalidates its lookup-cache entries. GHCR image tags
1079/// are not deleted; they age out under the package's own retention.
1080#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
1081pub struct ArtifactPruneRequest {
1082    /// Retired toolchain whose rows are pruned.
1083    pub rustc_version: WireRustcVersion,
1084}
1085
1086/// Response of `POST /api/v1/admin/artifacts/prune`.
1087#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
1088pub struct ArtifactPruneResponse {
1089    /// Catalog rows deleted.
1090    pub deleted: u32,
1091}
1092
1093/// Body for the scheduler DO's `/tasks/observe-run`.
1094///
1095/// Stamps the GitHub Actions run id onto an in-flight queue row so
1096/// `status` can surface a run URL. Stamping does not touch `updated_at`
1097/// — the stale-dispatch lease clock only moves on real state
1098/// transitions.
1099#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
1100pub struct ObserveRun {
1101    /// Task id the run was dispatched for.
1102    pub task_id: String,
1103    /// GitHub Actions run id from the OIDC token's `run_id` claim.
1104    pub github_run_id: String,
1105}
1106
1107#[cfg(test)]
1108mod tests {
1109    use super::{CI_TARGET_TRIPLES, RunnerFamily, runner_family};
1110
1111    /// Every CI target must land in exactly one family, and the families'
1112    /// `targets()` lists together must be exactly `CI_TARGET_TRIPLES` —
1113    /// drift between this mapping and the `runs-on` map in
1114    /// `build-crate.yml` would let the scheduler cap and order the wrong
1115    /// rows.
1116    #[test]
1117    fn runner_families_partition_ci_target_triples() {
1118        let mut mapped: Vec<&str> = Vec::new();
1119        for family in RunnerFamily::ALL {
1120            mapped.extend_from_slice(family.targets());
1121        }
1122        mapped.sort_unstable();
1123        let mut all = CI_TARGET_TRIPLES.to_vec();
1124        all.sort_unstable();
1125        assert_eq!(mapped, all);
1126        for target in CI_TARGET_TRIPLES {
1127            assert!(
1128                runner_family(target).is_some(),
1129                "{target} maps to no runner family"
1130            );
1131        }
1132        assert_eq!(runner_family("aarch64-unknown-linux-musl"), None);
1133    }
1134}
1135/// Public aggregate usage statistics served by `GET /api/v1/stats`.
1136///
1137/// Every count is derived from anonymized Analytics Engine events: hit
1138/// events are sampled at one in ten and their published counts are scaled
1139/// by the stored sample weight, so the numbers are approximate by design.
1140/// Fields that could publish a dangerously small count are suppressed
1141/// (`None`) rather than reported.
1142#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
1143pub struct UsageStats {
1144    /// Average number of distinct installs served a cache hit per day over
1145    /// the last 7 days, counted by daily-salted unlinkable install hash (a
1146    /// hash cannot be joined across days, so the figure is per-day, and
1147    /// hits are sampled, so it is a lower bound). `None` below the minimum
1148    /// publication threshold — stow never reports small counts.
1149    pub daily_active_installs_7d: Option<u64>,
1150    /// Cache hits served in the last 24 hours (sample-scaled estimate).
1151    pub hits_24h: u64,
1152    /// Cache misses served in the last 24 hours.
1153    pub misses_24h: u64,
1154    /// `hits_24h / (hits_24h + misses_24h)`; `0.0` when nothing was served.
1155    pub hit_rate_24h: f64,
1156    /// CPU-hours of rustc compilation saved in the last 30 days: the
1157    /// recorded compile time of every artifact served, sample-scaled.
1158    pub cpu_hours_saved_30d: f64,
1159    /// Most-served crates over the last 30 days.
1160    pub top_crates_30d: Vec<UsageStatEntry>,
1161    /// Hits per compilation target over the last 30 days.
1162    pub targets_30d: Vec<UsageStatEntry>,
1163    /// Hits per CLI version over the last 30 days; requests that sent no
1164    /// `stow-cli` user agent are not counted under any version.
1165    pub cli_versions_30d: Vec<UsageStatEntry>,
1166}
1167
1168/// One `(name, hits)` bucket of a [`UsageStats`] leaderboard.
1169#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
1170pub struct UsageStatEntry {
1171    /// Bucket label: crate name, target triple, or CLI version.
1172    pub name: String,
1173    /// Sample-scaled hit count for the bucket.
1174    pub hits: u64,
1175}