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