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