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