Skip to main content

shipshape_core/release/adapters/
cargo.rs

1//! Rust ecosystem adapter: `cargo-publish` / `cargo-publish-ci` (crates.io) and
2//! `cargo-dist`.
3//!
4//! `cargo-publish` publishes a crate to crates.io via `cargo publish`.
5//! `cargo-dist` plans and builds distributable binaries locally (`dist`), but
6//! its *upload* is the CI release workflow — so its publish body is
7//! [`AdapterError::Unsupported`] from this host rather than a fabricated receipt
8//! for a build-only command. `verify` (for `cargo-publish`) reconciles against
9//! crates.io through [`RegistryQuery`](crate::ports::RegistryQuery) via the
10//! adapter's default path.
11//!
12//! ## `cargo-publish-ci` — the crates.io publish runs in CI, not here
13//!
14//! `cargo-publish-ci` is `cargo-publish`'s **CI-delegated** identity, for a repo
15//! whose release model is "push the version tag; a tag-triggered workflow runs
16//! `cargo publish` with the repo's registry secret" (glasspad's
17//! `publish-crates.yml`, the common publish-from-CI-not-a-laptop pattern). Such a
18//! repo deliberately forbids the local publish: the maintainer's
19//! `~/.cargo/credentials.toml` may be stale (403) or absent, and the CI token is
20//! the source of truth — so an engine cut that ran `cargo publish` here would
21//! either fail or race the workflow into a double-publish.
22//!
23//! It differs from `cargo-publish` in **exactly one** respect: the publish. Its
24//! `dry_run` and `build` run the identical local gates (`cargo check`, `cargo
25//! package --no-verify`), because those are read-only preflights whose whole value
26//! is catching an unpublishable manifest *before* the irreversible tag push — and a
27//! repo that publishes from CI still wants them. Its `publish` is
28//! [`AdapterError::Unsupported`], and it declares
29//! [`is_ci_delegated`](ReleaseAdapter::is_ci_delegated), so the coordinator journals
30//! `target_delegated` and skips it in publish-all (never publishing, never failing).
31//! It does **not** own the GitHub Release (it uploads to crates.io), so a plan
32//! carrying only this delegated identity still gets an engine-created Release.
33//!
34//! The result is a cut whose terminal *actionable* phase is the tag push, followed
35//! by the mandatory verify barrier — which polls the crates.io index until CI's
36//! publish is observed (see the coordinator's delegated-verify wait). "Delegated"
37//! never means "assumed": an unobserved target still fails the cut.
38//!
39//! ## One plan target = one publish unit (ADR-0004)
40//!
41//! Each plan target publishes **exactly its own package** — one target ⇒ one
42//! `cargo publish -p <package>`. The [coordinator](super::super::coordinator) owns
43//! all cross-target ordering: it cuts same-ecosystem targets in dependency order
44//! (a dependency's target before its dependents'), so the adapter never re-orders
45//! or re-publishes another target's crate. This removes the earlier
46//! closure-per-target model, where two authorities (coordinator + adapter) each
47//! computed overlapping publish orderings and the crates.io publish→index lag
48//! between them could trigger a *duplicate* `cargo publish` of a shared dependency
49//! → a partial-publish trap.
50//!
51//! A workspace whose crates depend on one another still cannot publish in one
52//! shot: crates.io rejects a crate whose sibling dependency is not yet indexed
53//! (`no matching package named … found`). So before publishing its own package,
54//! the adapter discovers that package's publishable intra-workspace dependencies
55//! (read-only `cargo metadata`) and **waits for each to be crates.io-index-visible**
56//! (polling the injected [`RegistryQuery`](crate::ports::RegistryQuery), bounded by
57//! a timeout) — the dependency's own target, cut earlier, already published it, so
58//! this only closes the index-lag window. A crate with no publishable workspace
59//! dependencies publishes immediately with no wait.
60//!
61//! ## Deferred packaging for a `=`-pinned dependent (cargo-interleave, ADR-0002)
62//!
63//! A dependent crate that pins its workspace dependency by exact version
64//! (`dep = "=X.Y.Z"`, the shape `/shipshape-init` emits) **cannot be `cargo package`d
65//! before that dependency is published** — not even with `--no-verify`. `cargo
66//! package` resolves the `=`-pinned dependency against the crates.io *index* while
67//! preparing the upload (a published `.crate` cannot reference a `path` dep), and
68//! that version only lands later, in publish-all. `--no-verify` skips the isolated
69//! verify *compile*, but not this index resolution. So a strict `build-all` that
70//! packaged every crate up front could never package such a dependent
71//! (`release-cut-build-phase-dep-ordering`).
72//!
73//! The fix scopes the ADR-0002 phase barrier narrowly for cargo. `dry_run` /
74//! `build` read the workspace graph and probe the registry (see
75//! `unpublished_workspace_deps`), then branch on whether the target depends on a
76//! workspace crate **not yet on the crates.io index**:
77//!
78//! - **No unpublished workspace dep** (a leaf, or a dependent whose workspace deps
79//!   are already published — a re-cut): fully packaged pre-publish — `cargo check`
80//!   (compile safety net) then `cargo package --no-verify` (produces the `.crate`,
81//!   validating the manifest). It CAN be packaged: `cargo package` resolves the dep
82//!   against the index it is already on.
83//! - **Depends on a not-yet-published workspace crate**: its **packaging is deferred**
84//!   to `cargo publish` in publish-all, which packages and publishes as one unit
85//!   *after* the dependency is published and index-visible (`build` runs only the
86//!   index-independent `cargo check`, which resolves the sibling via its `path`).
87//!   This is the "build interleaves with publish" exception the coordinator relies
88//!   on: the dependent's package step is intrinsic to its dep-ordered publish, not a
89//!   premature global-build step. The pre-publish compile safety net (`cargo check`
90//!   over every target) still runs as a global build-all barrier, so a compile error
91//!   in the default host build fails before **any** irreversible publish.
92//!
93//! The registry probe is **fail-closed**: a dep the registry cannot confirm as
94//! published defers, so a registry outage never risks a build-all `cargo package`
95//! that resolves a `=`-pinned dep against an index it cannot reach.
96//!
97//! The consequence is a target model where **each publishable crate is its own
98//! declared target** (which is what `/shipshape-init` emits). A multi-crate workspace
99//! that wants every crate on crates.io declares every crate as a target; a target
100//! whose package depends on a workspace crate that is *not* itself a declared
101//! target — and whose required version is not already on the index — times out
102//! waiting for that crate, the signal that it must be declared. (If that
103//! dependency's version happens to already be published, the wait clears and the
104//! publish proceeds; the coverage check that would catch an under-declared plan up
105//! front is tracked separately, not owned by the adapter.)
106
107use std::collections::BTreeMap;
108use std::collections::HashSet;
109use std::time::Duration;
110
111use serde::Deserialize;
112
113use crate::contract::schema::{Adapter, Ecosystem, Registry};
114use crate::protocol::release::{BuildArtifacts, DryRunReport, PlannedCommand, PublishReceipt};
115
116use super::{
117    hash_file, make_receipt, run_all, AdapterError, AdapterTarget, EffectCtx, ReleaseAdapter,
118};
119
120/// Wall-clock ceiling for a single crate's crates.io index-wait, in seconds.
121///
122/// crates.io's sparse index is usually visible within seconds of a publish, but
123/// the publish→index pipeline can lag under load; a generous per-crate ceiling
124/// avoids a spurious failure while still bounding a hung wait so it can never
125/// wedge a run.
126const INDEX_WAIT_TIMEOUT_SECS: u64 = 300;
127
128/// Interval between crates.io index polls while waiting for a just-published
129/// version to appear.
130const INDEX_POLL_INTERVAL: Duration = Duration::from_secs(3);
131
132/// Cargo's registry alias for crates.io. Used two ways: as the value of every
133/// `cargo publish/package --registry <alias>` this adapter emits (so the publish
134/// destination is pinned and never resolved from ambient registry config —
135/// `registry.default`, `.cargo/config.toml`, `CARGO_REGISTRY_DEFAULT`), and as the
136/// token a manifest's `publish` allow-list must contain to be crates.io-publishable
137/// (a member restricted to a *different* registry is excluded).
138const CRATES_IO_ALIAS: &str = "crates-io";
139
140/// The rust release adapter, operating as `cargo-publish`, `cargo-publish-ci`, or
141/// `cargo-dist`.
142pub struct CargoAdapter {
143    adapter: Adapter,
144}
145
146impl CargoAdapter {
147    /// Construct for a resolved rust adapter identity (`cargo-publish` /
148    /// `cargo-publish-ci` / `cargo-dist`).
149    #[must_use]
150    pub fn new(adapter: Adapter) -> Self {
151        debug_assert!(matches!(
152            adapter,
153            Adapter::CargoPublish | Adapter::CargoPublishCi | Adapter::CargoDist
154        ));
155        Self { adapter }
156    }
157
158    /// The cargo `--registry` alias to pin this target's `cargo publish`/`package`
159    /// invocations to, derived from the target's declared registry.
160    ///
161    /// crates.io is the only rust registry shipshape supports today, so any other
162    /// declared registry is a misconfiguration that must fail **fast, before any
163    /// external action** — never a silent publish to an unexpected destination.
164    /// Returns [`CRATES_IO_ALIAS`] for [`Registry::CratesIo`], else
165    /// [`AdapterError::UnsupportedRegistry`] tagged with **this** adapter's identity
166    /// (`self.adapter`, not a hard-coded value, so a `cargo-dist` caller could never
167    /// misreport itself). Threading the flag value through here (rather than
168    /// hard-coding it at each call site) keeps the destination tied to the contract's
169    /// `registry` field and the rejection in one place.
170    fn crates_io_registry(&self, t: &AdapterTarget) -> Result<&'static str, AdapterError> {
171        match t.target.registry {
172            Registry::CratesIo => Ok(CRATES_IO_ALIAS),
173            registry => Err(AdapterError::UnsupportedRegistry {
174                adapter: self.adapter,
175                registry,
176            }),
177        }
178    }
179}
180
181impl ReleaseAdapter for CargoAdapter {
182    fn adapter(&self) -> Adapter {
183        self.adapter
184    }
185
186    fn is_ci_delegated(&self) -> bool {
187        // `cargo-dist` builds distributables locally but its *upload* is the
188        // tag-triggered `release.yml`; `cargo-publish-ci` declares that this repo's
189        // crates.io publish is likewise a tag-triggered CI job holding the registry
190        // secret. For both, the engine cannot (and must not) publish from this host.
191        // `cargo-publish` is a real host publish and is not delegated. Consistent
192        // with `publish` returning `Unsupported` for exactly these two identities.
193        matches!(self.adapter, Adapter::CargoDist | Adapter::CargoPublishCi)
194    }
195
196    fn ci_owns_github_release(&self) -> bool {
197        // `cargo-dist`'s generated `release.yml` runs `gh release create <tag> …
198        // artifacts/*` — it creates AND finalizes the shared GitHub Release and
199        // uploads the cross-platform binaries. So the coordinator must not create
200        // the Release itself (a pre-existing Release makes `gh release create`
201        // error). Neither `cargo-publish` nor `cargo-publish-ci` owns a GitHub
202        // Release — the latter is CI-delegated, but to crates.io, not GitHub, which
203        // is exactly why this capability is narrower than `is_ci_delegated`. See
204        // `coordinator-release-vs-cargo-dist-ownership`.
205        matches!(self.adapter, Adapter::CargoDist)
206    }
207
208    fn dry_run(
209        &self,
210        ctx: &EffectCtx<'_>,
211        t: &AdapterTarget,
212    ) -> Result<DryRunReport, AdapterError> {
213        if matches!(self.adapter, Adapter::CargoDist) {
214            return Ok(DryRunReport {
215                adapter: self.adapter,
216                planned_commands: vec![PlannedCommand::new(
217                    "dist",
218                    &["plan", "--output-format=json"],
219                )],
220                notes: vec![],
221            });
222        }
223        // Reject a non-crates.io target before doing anything — the dry-run
224        // preflight must exercise the exact registry-pinned build a real cut runs, and
225        // a misconfigured registry is a fail-fast error, not a plannable command.
226        let registry = self.crates_io_registry(t)?;
227        // FAITHFUL PREFLIGHT: actually run the SAME index-independent build gate the
228        // build phase runs, so a plan that cannot compile (or, for a leaf, cannot
229        // package) fails HERE at dry-run-all, before any external effect, rather than
230        // passing dry-run and failing mid-cut in build-all. (The old dry-run only
231        // *described* a `cargo publish --dry-run` without running it, so a build-phase
232        // failure slipped past the preflight.)
233        //
234        // `target_workspace_deps` runs read-only `cargo metadata` first, validating
235        // the target is a publishable member and listing its publishable workspace
236        // dependencies. `unpublished_workspace_deps` then probes the registry for each
237        // (fail-closed) to decide the gate ([`cargo_build_gate`]): a target with **no
238        // workspace dep still absent from the index** is packaged now (`cargo package
239        // --no-verify`, validating the manifest); a dependent on a **not-yet-published**
240        // workspace crate DEFERS packaging to its own `cargo publish` — it cannot be
241        // `cargo package`d until that dep is on the index — so the preflight is the
242        // index-independent `cargo check` compile gate alone (the check resolves the
243        // sibling via its on-disk `path`, never the index), and never false-fails on
244        // the unpublished dep. The real end-to-end verify happens in publish-all's
245        // `cargo publish`, after the dep is index-visible.
246        //
247        // The gate COMMANDS are local + self-overwriting (a package writes
248        // `target/package/`, the same artifact build-all produces; `check` only warms
249        // `target/`), so dry-run stays re-runnable and free of any *external* side
250        // effect (ADR-0002). One plan target = one publish unit: exactly this target's
251        // own package.
252        let deps = target_workspace_deps(ctx, t)?;
253        // PRE-TAG BASELINE for a CI-delegated publish. The engine's own publish path
254        // probes the registry before uploading (`is_published` → skip / digest-
255        // authenticate), which also means a version that is ALREADY on crates.io can
256        // never be silently re-published by the engine. A delegated target has no such
257        // probe: publish-all skips it, so nothing checks the version until verify —
258        // and verify observes *presence*, which a pre-existing upload satisfies. The
259        // reachable failure that closes: cutting a version that is already published
260        // (a re-cut, or a manifest that was never bumped). CI's `cargo publish` fails
261        // with "crate version already uploaded", the engine observes the OLD upload,
262        // and the run goes green over a publish that never happened — silently.
263        //
264        // So establish the baseline here, in dry-run-all: pre-tag, side-effect-free,
265        // and before anything irreversible. Fail-closed on an unreachable registry
266        // (`is_published`'s own discipline): if we cannot prove the version is absent
267        // now, a later "present" observation proves nothing about this cut.
268        if self.adapter == Adapter::CargoPublishCi
269            && is_published(ctx, t.ecosystem(), &t.package, &t.version)?
270        {
271            return Err(AdapterError::DelegatedVersionAlreadyPublished {
272                package: t.package.clone(),
273                version: t.version.clone(),
274            });
275        }
276        let deferred = unpublished_workspace_deps(ctx, t.ecosystem(), &deps);
277        let defer_packaging = !deferred.is_empty();
278        let (planned_commands, _artifacts) =
279            cargo_build_gate(registry, &t.package, &t.version, defer_packaging);
280        run_all(ctx, &planned_commands)?;
281        // The note states who publishes, which differs by identity: the engine's own
282        // publish-all for `cargo-publish`, the tag-triggered workflow for the
283        // CI-delegated `cargo-publish-ci` (whose publish-all entry is a journalled
284        // skip). An approver reading the dry-run must not be told the engine will run
285        // a publish it will never run.
286        //
287        // Keyed on the IDENTITY, not on `is_ci_delegated()`: that capability is also
288        // true for `cargo-dist`, whose CI runs no `cargo publish` and is verified on
289        // its GitHub Release. `cargo-dist` returns from its own arm above and never
290        // reaches here, so the broad flag reads correctly today — but only by accident
291        // of control flow, and this note is what an approver trusts.
292        let mut notes = vec![if self.adapter == Adapter::CargoPublishCi {
293            format!(
294                "publish is CI-delegated: the tag push triggers the workflow that runs \
295                 `cargo publish` for `{}`; the engine skips it in publish-all and observes \
296                 the crates.io index in verify",
297                t.package
298            )
299        } else {
300            format!(
301                "publishes with `cargo publish --registry {registry} -p {}` in publish-all",
302                t.package
303            )
304        }];
305        if defer_packaging {
306            let chain = deferred
307                .iter()
308                .map(|m| format!("{}@{}", m.name, m.version))
309                .collect::<Vec<_>>()
310                .join(", ");
311            notes.push(format!(
312                "packaging of `{}` is deferred to that publish: it depends on workspace \
313                 crate(s) not yet on the crates.io index, and a dependent cannot be \
314                 `cargo package`d until they are published",
315                t.package
316            ));
317            // The index-wait is the ENGINE's between-publishes wait; a CI-delegated
318            // target never runs it (its whole publish happens in the workflow, which
319            // owns its own ordering), so promising it would misdescribe the cut.
320            notes.push(if self.adapter == Adapter::CargoPublishCi {
321                format!(
322                    "the CI publish workflow must publish these workspace dependencies of \
323                     `{}` first — the engine does not order a delegated publish: {chain}",
324                    t.package
325                )
326            } else {
327                format!(
328                    "waits for these workspace dependencies to be crates.io-index-visible \
329                     before publishing `{}`: {chain}",
330                    t.package
331                )
332            });
333        }
334        Ok(DryRunReport {
335            adapter: self.adapter,
336            planned_commands,
337            notes,
338        })
339    }
340
341    fn build(
342        &self,
343        ctx: &EffectCtx<'_>,
344        t: &AdapterTarget,
345    ) -> Result<BuildArtifacts, AdapterError> {
346        // `dist build` emits per-platform tarballs/installers, not a `.crate`;
347        // name the artifact set to match what each identity actually produces.
348        let (cmds, artifacts, notes) = if matches!(self.adapter, Adapter::CargoDist) {
349            (
350                vec![PlannedCommand::new("dist", &["build"])],
351                vec!["dist/".to_string()],
352                vec![],
353            )
354        } else {
355            // Pin `cargo package` to crates.io too (rejecting a non-crates.io target
356            // up front) so the build phase can never verify-package against a
357            // different registry than the publish phase will target. Reject BEFORE the
358            // read-only `cargo metadata` probe so a misconfigured registry runs no
359            // command at all.
360            let registry = self.crates_io_registry(t)?;
361            // Read the workspace graph, then probe the registry to decide the gate: a
362            // target that depends on a workspace crate NOT YET on the index cannot be
363            // packaged (packaging resolves the `=`-pinned dep against the index), so its
364            // packaging is DEFERRED to `cargo publish` and build runs only the
365            // index-independent `cargo check`; a target whose workspace deps are already
366            // published (or has none) is packaged now. Fail-closed: an unreachable
367            // registry defers (see [`unpublished_workspace_deps`]). See
368            // [`cargo_build_gate`] and `release-cut-build-phase-dep-ordering`.
369            let deps = target_workspace_deps(ctx, t)?;
370            let deferred = unpublished_workspace_deps(ctx, t.ecosystem(), &deps);
371            let defer_packaging = !deferred.is_empty();
372            let (cmds, artifacts) =
373                cargo_build_gate(registry, &t.package, &t.version, defer_packaging);
374            let notes = if defer_packaging {
375                let chain = deferred
376                    .iter()
377                    .map(|m| format!("{}@{}", m.name, m.version))
378                    .collect::<Vec<_>>()
379                    .join(", ");
380                vec![format!(
381                    "packaging of `{}` deferred to `cargo publish` in publish-all (it \
382                     depends on workspace crate(s) not yet on the crates.io index: {chain})",
383                    t.package
384                )]
385            } else {
386                vec![]
387            };
388            (cmds, artifacts, notes)
389        };
390        run_all(ctx, &cmds)?;
391        // SKELETON: a production build parses the exact packaged `.crate` /
392        // `dist-manifest.json` paths out of the command output; here we name the
393        // expected artifact set deterministically.
394        Ok(BuildArtifacts {
395            adapter: self.adapter,
396            artifacts,
397            notes,
398        })
399    }
400
401    fn publish(
402        &self,
403        ctx: &EffectCtx<'_>,
404        t: &AdapterTarget,
405    ) -> Result<PublishReceipt, AdapterError> {
406        // cargo-dist uploads via the CI release workflow, not from this host —
407        // `dist build` only builds. `cargo-publish-ci` is the same story for
408        // crates.io: the tag-triggered workflow holds the registry token and runs
409        // the publish. Report that honestly rather than returning a receipt for a
410        // publish that did not happen. Both identities declare `is_ci_delegated`, so
411        // the coordinator skips them in publish-all and never reaches this arm; it
412        // is the honest answer for any other caller (and the invariant the
413        // capability documents: delegated ⇒ `Unsupported`).
414        if matches!(self.adapter, Adapter::CargoDist | Adapter::CargoPublishCi) {
415            return Err(AdapterError::Unsupported {
416                adapter: self.adapter,
417                operation: "publish",
418            });
419        }
420        // Reject a non-crates.io target BEFORE the idempotency probe or any publish —
421        // the whole publish path (probe, index-wait, receipt URL) assumes crates.io,
422        // so a mismatched registry must fail closed here, never reach `cargo publish`.
423        let registry = self.crates_io_registry(t)?;
424        // PER-TARGET IRREVERSIBLE — drives the real `cargo publish` through the
425        // injected runner (the port is the safety seam under test). ADR-0004: one
426        // plan target = one publish unit, so this publishes ONLY `t.package`; the
427        // coordinator cut every dependency's target before this one. No
428        // `--no-verify`: a resume that enters publish without re-running build must
429        // still let cargo verify the package before it lands.
430        //
431        // IDEMPOTENT re-entry with TRI-STATE probing. On resume the coordinator
432        // re-enters this method from the top, so probe the registry first and skip
433        // an already-landed publish (a second `cargo publish` of an uploaded version
434        // hard-fails and would wedge every resume). Crucially, a probe that cannot
435        // reach the registry is NOT read as "not published" — that would permit a
436        // duplicate upload of a crate that in fact landed. It fails the publish
437        // closed ([`AdapterError::RegistryUnavailable`]), mirroring the reconcile
438        // layer's outage ⇒ `Unknown` ⇒ never-`Missing` discipline.
439        //
440        // DIGEST-AUTHENTICATE THE SKIP (`is-published-digest-authenticate`). "Already
441        // published" by name + version is NOT proof the crate on the registry is the
442        // one this cut intended — a *different* artifact could occupy the version. So
443        // before trusting the skip, [`authenticate_skip`] proves the registry's crate
444        // is byte-identical to what this cut would upload (its `.crate` sha256 vs the
445        // sparse-index `cksum`); only a match skips, a mismatch fails CLOSED, and an
446        // outage keeps the same never-guess discipline. This closes the last
447        // "receipt without a fresh upload" path (the self-visibility no-op's mirror).
448        let ecosystem = t.ecosystem();
449        if is_published(ctx, ecosystem, &t.package, &t.version)? {
450            return authenticate_skip(ctx, registry, ecosystem, t);
451        }
452        // crates.io rejects a crate whose sibling dependency is not yet indexed, so
453        // wait for this package's own publishable workspace dependencies to be
454        // index-visible before publishing it. Each dependency's target was cut
455        // earlier by the coordinator; this only closes the publish→index lag window.
456        for dep in &target_workspace_deps(ctx, t)? {
457            wait_for_index(ctx, ecosystem, &dep.name, &dep.version)?;
458        }
459        run_all(
460            ctx,
461            &[PlannedCommand::new(
462                "cargo",
463                &["publish", "--registry", registry, "-p", &t.package],
464            )],
465        )?;
466        // SELF-VISIBILITY CONFIRM (`cut-noop-self-visibility-check`). `cargo publish`
467        // exiting 0 is NOT proof the crate landed: a registry-alias/credential/env
468        // difference (or an under-declared target) can make it a silent no-op that
469        // ships nothing. Before journaling a receipt, probe the index for this
470        // target's OWN `{package, version}` — reusing the bounded index-wait so
471        // normal propagation lag is tolerated (only a genuine never-appears no-op
472        // fails), and failing closed on a registry outage rather than fabricating a
473        // receipt for a publish that may not have happened.
474        confirm_self_published(ctx, ecosystem, &t.package, &t.version)?;
475        // SKELETON: a production publish parses the crates.io checksum from the
476        // `cargo publish` output for `digest`; the canonical URL is well-known. One
477        // target publishes exactly one crate, so the journal records exactly one
478        // receipt for this crate — `resume`/`verify` track it precisely.
479        Ok(make_receipt(ctx, t, None, Some(remote_url(t))))
480    }
481
482    fn timeout(&self) -> Duration {
483        Duration::from_secs(600)
484    }
485}
486
487/// The build/preflight gate for a `cargo-publish` target — the commands `dry_run`
488/// and `build` both run — plus the build artifacts it produces.
489///
490/// A pure function of the caller's `defer_packaging` decision (computed identically
491/// by `dry_run` and `build` from [`unpublished_workspace_deps`], so the two stay in
492/// lockstep — a faithful preflight):
493///
494/// - **`defer_packaging == false`** (a leaf, or a dependent whose workspace deps are
495///   already on the index — so it CAN be packaged): `cargo check -p <pkg>` (compile
496///   safety net) then `cargo package --registry <r> -p <pkg> --no-verify`. The
497///   package validates the manifest and produces the `.crate`, the single build
498///   artifact. `--no-verify` skips only the isolated verify *compile* (redundant with
499///   the `cargo check` above and re-run for real by `cargo publish` in publish-all).
500/// - **`defer_packaging == true`** (a dependent on a workspace crate NOT yet on the
501///   index): `cargo check -p <pkg>` **alone** — an index-independent compile (the
502///   sibling resolves via its on-disk `path`, never the index). It is the pre-publish
503///   safety net that fails a genuine compile error (type/trait/API mismatch, missing
504///   item) before any irreversible publish — the partial-publish trap ADR-0004 exists
505///   to prevent. Packaging is **deferred** to `cargo publish` in publish-all, which
506///   packages+publishes as one unit *after* the dependency is published and
507///   index-visible: `cargo package` (even `--no-verify`) resolves the `=X.Y.Z` dep
508///   against the crates.io *index* when preparing the upload, so it cannot run until
509///   that dependency is published (`release-cut-build-phase-dep-ordering`). No
510///   `.crate` is produced here, so the artifact set is empty.
511///
512/// The gate commands are local + per-target, so no build-time cross-target ordering
513/// leaks into the adapter (ADR-0002/0004 preserved); the coordinator alone orders
514/// the publishes.
515fn cargo_build_gate(
516    registry: &str,
517    package: &str,
518    version: &str,
519    defer_packaging: bool,
520) -> (Vec<PlannedCommand>, Vec<String>) {
521    let mut cmds = vec![PlannedCommand::new("cargo", &["check", "-p", package])];
522    if defer_packaging {
523        // Deferred packaging: only the index-independent compile gate runs now.
524        return (cmds, Vec::new());
525    }
526    cmds.push(PlannedCommand::new(
527        "cargo",
528        &[
529            "package",
530            "--registry",
531            registry,
532            "-p",
533            package,
534            "--no-verify",
535        ],
536    ));
537    (cmds, vec![format!("{package}-{version}.crate")])
538}
539
540/// The canonical crates.io URL for a target's own package at its version — the
541/// receipt's `remote_url`. Correct because the publish paths call this only after
542/// [`crates_io_registry`] has confirmed the target's registry is crates.io.
543fn remote_url(t: &AdapterTarget) -> String {
544    format!("https://crates.io/crates/{}/{}", t.package, t.version)
545}
546
547/// The publishable intra-workspace dependencies of the target's own package — the
548/// crates that must be crates.io-index-visible before `t.package` can publish
549/// (ADR-0004). Each has its own plan target, cut earlier by the coordinator.
550///
551/// Runs read-only `cargo metadata`, keeps only members publishable to crates.io
552/// (dropping `publish = false` and members restricted to another registry), and
553/// returns the direct workspace dependencies of `t.package` among them. Errors if
554/// `t.package` is not itself a publishable member (the plan approved a package this
555/// workspace cannot publish to crates.io). A crate with no publishable workspace
556/// dependencies resolves to an empty list — it publishes with no wait.
557fn target_workspace_deps(
558    ctx: &EffectCtx<'_>,
559    t: &AdapterTarget,
560) -> Result<Vec<Member>, AdapterError> {
561    let meta = load_metadata(ctx)?;
562    let members = publishable_members(&meta);
563    let by_name: BTreeMap<&str, &Member> = members.iter().map(|m| (m.name.as_str(), m)).collect();
564    let Some(target) = by_name.get(t.package.as_str()) else {
565        let available: Vec<&str> = members.iter().map(|m| m.name.as_str()).collect();
566        return Err(AdapterError::Command {
567            command: "cargo metadata".to_string(),
568            code: None,
569            stderr: format!(
570                "target package `{}` is not a crates.io-publishable member of this workspace \
571                 (publishable members: {available:?}); declare each publishable crate as its own \
572                 target and check the contract `package` and each crate's `publish` setting",
573                t.package
574            ),
575        });
576    };
577    // `publishable_members` already restricted each member's `deps` to *other kept
578    // members*, so every dep name here resolves to a publishable member.
579    Ok(target
580        .deps
581        .iter()
582        .filter_map(|d| by_name.get(d.as_str()).map(|m| (*m).clone()))
583        .collect())
584}
585
586/// Run `cargo metadata` and parse the workspace graph. Errors on a command
587/// failure, on empty output (a real `cargo metadata` never succeeds with empty
588/// stdout — empty means a broken host/runner, which must not silently degrade the
589/// publish set), or on unparseable output.
590fn load_metadata(ctx: &EffectCtx<'_>) -> Result<CargoMetadata, AdapterError> {
591    let cmd = PlannedCommand::new("cargo", &["metadata", "--no-deps", "--format-version", "1"]);
592    let outputs = run_all(ctx, std::slice::from_ref(&cmd))?;
593    let stdout = outputs[0].stdout.trim();
594    if stdout.is_empty() {
595        return Err(AdapterError::Command {
596            command: cmd.rendered(),
597            code: None,
598            stderr: "`cargo metadata` succeeded but emitted no output — cannot resolve the \
599                     workspace publish set"
600                .to_string(),
601        });
602    }
603    serde_json::from_str(stdout).map_err(|e| AdapterError::Command {
604        command: cmd.rendered(),
605        code: None,
606        stderr: format!("could not parse `cargo metadata` output: {e}"),
607    })
608}
609
610/// Project the metadata onto the crates.io-publishable members and their
611/// intra-workspace (non-dev) dependency edges.
612///
613/// A member is kept unless its manifest sets `publish = false` (which
614/// `cargo metadata` reports as an empty `publish` array) or restricts publishing
615/// to a registry set that does not include crates.io. Only edges to *other kept
616/// members* gate order; dev-dependencies are excluded (they never gate publish
617/// order and can form legitimate cycles, e.g. a lib crate that dev-depends on the
618/// CLI crate for integration tests).
619fn publishable_members(meta: &CargoMetadata) -> Vec<Member> {
620    let member_ids: HashSet<&str> = meta.workspace_members.iter().map(String::as_str).collect();
621    let pkgs: Vec<&MetaPackage> = meta
622        .packages
623        .iter()
624        .filter(|p| member_ids.contains(p.id.as_str()))
625        .filter(|p| publishable_to_crates_io(p.publish.as_deref()))
626        .collect();
627    let names: HashSet<&str> = pkgs.iter().map(|p| p.name.as_str()).collect();
628    pkgs.iter()
629        .map(|p| {
630            let mut deps: Vec<String> = p
631                .dependencies
632                .iter()
633                // Allow-list the ordering-relevant kinds (normal + build); a future
634                // dep kind is excluded rather than accidentally treated as ordering.
635                .filter(|d| matches!(d.kind.as_deref(), None | Some("build")))
636                .filter(|d| d.name != p.name && names.contains(d.name.as_str()))
637                .map(|d| d.name.clone())
638                .collect();
639            deps.sort();
640            deps.dedup();
641            Member {
642                name: p.name.clone(),
643                version: p.version.clone(),
644                deps,
645            }
646        })
647        .collect()
648}
649
650/// Whether a member's `publish` field permits crates.io. `None`/absent ⇒ any
651/// registry (yes); `Some([])` ⇒ `publish = false` (no); `Some([regs…])` ⇒ only if
652/// the list names crates.io.
653fn publishable_to_crates_io(publish: Option<&[String]>) -> bool {
654    match publish {
655        None => true,
656        Some(regs) => regs.iter().any(|r| r == CRATES_IO_ALIAS),
657    }
658}
659
660/// The subset of the target's publishable workspace dependencies whose **exact
661/// release version is not yet visible on the crates.io index** — the dependencies
662/// that make the target unpackageable *now*, so its packaging must defer to
663/// `cargo publish` (which packages after those deps publish + index).
664///
665/// **Fail-closed:** a dependency the registry cannot confirm as published — absent
666/// (`Ok(false)`) **or** a registry error (`Err`) — counts as not-yet-published, so
667/// packaging defers rather than risk a build-all `cargo package` that resolves a
668/// `=`-pinned dep against an index that is missing it or cannot be reached. A
669/// dependency **already on the index** (`Ok(true)`) is dropped, so a re-cut whose
670/// dependency was published by an earlier release still packages — and manifest-
671/// validates — the dependent in build-all (it can: `cargo package` resolves that
672/// dep against the index it is already on). This is the precise predicate: "defer
673/// iff a workspace dep is not yet on the index", not the coarser "has any workspace
674/// dep".
675fn unpublished_workspace_deps(
676    ctx: &EffectCtx<'_>,
677    ecosystem: Ecosystem,
678    deps: &[Member],
679) -> Vec<Member> {
680    deps.iter()
681        .filter(|d| !matches!(is_published(ctx, ecosystem, &d.name, &d.version), Ok(true)))
682        .cloned()
683        .collect()
684}
685
686/// Whether `package@version` is already visible on crates.io — the idempotency
687/// probe run before `cargo publish` so a resumed cut skips a crate that already
688/// landed instead of hard-failing on a duplicate upload.
689///
690/// **Tri-state, fail-closed.** `Ok(true)` ⇒ already published (skip); `Ok(false)`
691/// ⇒ the registry answered and the version is definitively absent (safe to
692/// publish); `Err(RegistryUnavailable)` ⇒ the registry could not be reached, so
693/// the probe cannot prove the crate has *not* landed. A registry error is **never**
694/// read as "not published" (which would permit a duplicate, irreversible upload);
695/// the caller fails closed, mirroring the reconcile layer's outage ⇒ `Unknown`
696/// discipline.
697fn is_published(
698    ctx: &EffectCtx<'_>,
699    ecosystem: Ecosystem,
700    package: &str,
701    version: &str,
702) -> Result<bool, AdapterError> {
703    match ctx.registry.published_versions(ecosystem.as_str(), package) {
704        Ok(versions) => Ok(versions.iter().any(|v| v == version)),
705        Err(e) => Err(AdapterError::RegistryUnavailable {
706            package: package.to_string(),
707            version: version.to_string(),
708            source: e.to_string(),
709        }),
710    }
711}
712
713/// Authenticate an idempotency skip: `t.package@t.version` is already on the
714/// registry, so prove the crate published there is **byte-identical** to what this
715/// cut would upload before trusting the skip (`is-published-digest-authenticate`).
716///
717/// Name + version existence alone is not enough — a *different* artifact could
718/// occupy the version (a re-used version from another source, or a supply-chain
719/// substitution). So this compares two digests:
720///
721/// - the **intended** digest: the sha256 of the `.crate` this cut would upload,
722///   (re)produced deterministically by [`intended_crate_digest`] (the target's
723///   published dependencies are on the index, so it packages cleanly);
724/// - the **published** digest: the registry-recorded checksum (crates.io
725///   sparse-index `cksum`) from [`RegistryQuery::published_checksum`].
726///
727/// Only a match trusts the skip and journals a receipt carrying the verified
728/// digest (no `cargo publish` runs — the crate is already there). A **mismatch**
729/// fails **closed** with [`AdapterError::DigestMismatch`] (the registry holds a
730/// different artifact than intended). An **outage** — the checksum cannot be read
731/// — fails closed with [`AdapterError::RegistryUnavailable`], never trusting a skip
732/// it cannot authenticate (the same never-guess discipline as the idempotency
733/// probe and self-visibility confirm).
734fn authenticate_skip(
735    ctx: &EffectCtx<'_>,
736    registry: &str,
737    ecosystem: Ecosystem,
738    t: &AdapterTarget,
739) -> Result<PublishReceipt, AdapterError> {
740    let local = intended_crate_digest(ctx, registry, &t.package, &t.version)?;
741    let remote = ctx
742        .registry
743        .published_checksum(ecosystem.as_str(), &t.package, &t.version)
744        .map_err(|e| AdapterError::RegistryUnavailable {
745            package: t.package.clone(),
746            version: t.version.clone(),
747            source: e.to_string(),
748        })?;
749    // Re-validate the registry digest at this domain boundary rather than trusting
750    // the [`RegistryQuery`] contract blindly: a faulty/future backend returning a
751    // non-hex `Ok(..)` must fail CLOSED as unavailable, never be reported as a
752    // `DigestMismatch` (which would misattribute a backend bug to a conflicting
753    // artifact). The real crates.io impl already validates; this guards the port.
754    if !is_sha256_hex(&remote) {
755        return Err(AdapterError::RegistryUnavailable {
756            package: t.package.clone(),
757            version: t.version.clone(),
758            source: format!("registry returned a malformed checksum: {remote:?}"),
759        });
760    }
761    // Both digests are lowercase hex; compare case-insensitively for robustness.
762    if !local.eq_ignore_ascii_case(&remote) {
763        return Err(AdapterError::DigestMismatch {
764            package: t.package.clone(),
765            version: t.version.clone(),
766            local,
767            remote,
768        });
769    }
770    // Authenticated: the registry's crate matches what this cut intended, so the
771    // publish is safely skipped and the receipt records the verified digest.
772    Ok(make_receipt(ctx, t, Some(local), Some(remote_url(t))))
773}
774
775/// The sha256 (lowercase hex) of the `.crate` this cut would upload for
776/// `package@version` — the *intended* digest an idempotency skip is authenticated
777/// against.
778///
779/// (Re)produces the `.crate` with `cargo package --no-verify` (local, idempotent,
780/// self-overwriting — the same artifact build-all produces) so the digest is of the
781/// exact bytes `cargo publish` would upload, independent of whether an earlier build
782/// phase left the file on disk. The target is already published, so its `=`-pinned
783/// workspace dependencies are on the crates.io index and packaging resolves them.
784///
785/// The packaged `.crate` lands at `<target_directory>/package/<pkg>-<version>.crate`,
786/// where `target_directory` is read from `cargo metadata` (honoring
787/// `CARGO_TARGET_DIR` / `[build] target-dir` / workspace config — never a hard-coded
788/// `<repo>/target`); [`hash_file`] then hashes it cross-platform.
789///
790/// **Caveat (tracked for the receipt-provenance cluster).** `cargo package` is
791/// deterministic only under a fixed toolchain + source tree + index state; a resume
792/// under a *different* cargo version can produce different `.crate` bytes for the same
793/// sealed commit, so the digest is a faithful "what this toolchain would upload now",
794/// not a durable record of the original upload. The regression-free source of the
795/// intended digest is a value journaled at the original publish — see
796/// `cargo-publish-receipt-provenance-resume-safety`.
797fn intended_crate_digest(
798    ctx: &EffectCtx<'_>,
799    registry: &str,
800    package: &str,
801    version: &str,
802) -> Result<String, AdapterError> {
803    // Resolve the real target directory BEFORE packaging so the hash reads the file
804    // cargo actually writes, not a hard-coded path a custom target-dir would miss.
805    let target_dir = load_metadata(ctx)?.target_directory;
806    run_all(
807        ctx,
808        &[PlannedCommand::new(
809            "cargo",
810            &[
811                "package",
812                "--registry",
813                registry,
814                "-p",
815                package,
816                "--no-verify",
817            ],
818        )],
819    )?;
820    let crate_path = format!("{target_dir}/package/{package}-{version}.crate");
821    hash_file(ctx, &crate_path).map_err(|source| AdapterError::Command {
822        command: format!("sha256 of {crate_path}"),
823        code: None,
824        stderr: source,
825    })
826}
827
828/// Whether `s` is a well-formed lowercase-or-mixed-case 64-char hex SHA-256 — the
829/// shape both a crates.io `cksum` and a local `.crate` hash must have. Used to
830/// validate a registry-supplied digest at the [`authenticate_skip`] boundary.
831fn is_sha256_hex(s: &str) -> bool {
832    s.len() == 64 && s.bytes().all(|b| b.is_ascii_hexdigit())
833}
834
835/// Why a bounded index-wait gave up without ever observing `package@version` — the
836/// honest distinction the caller maps onto its own [`AdapterError`] variant.
837enum WaitFailure {
838    /// The registry *answered at least once* over the window and the version was
839    /// definitively absent every time it did — a genuine "did not appear" (the
840    /// dependency never indexed, or the crate's own publish shipped nothing).
841    Absent {
842        /// How long the wait actually lasted before giving up, in seconds.
843        waited_secs: u64,
844    },
845    /// The registry was **never** reached with a definitive answer over the whole
846    /// window — every poll errored — so absence could not be established: an outage,
847    /// not a proven absence. Carries the last underlying error so it is surfaced,
848    /// never masked.
849    Unreachable {
850        /// The underlying registry lookup error, rendered as text.
851        source: String,
852    },
853}
854
855/// Poll the crates.io index (through the injected [`RegistryQuery`]) until
856/// `package@version` is visible, or the per-crate timeout elapses.
857///
858/// Between polls it waits [`INDEX_POLL_INTERVAL`] through the injected
859/// [`Clock::sleep`](crate::ports::Clock::sleep) — real time in production, a
860/// virtual advance under test — so the loop is bounded, never busy, and
861/// deterministic in tests. A transient lookup error is retried (waiting is
862/// reversible), and the outcome on timeout is classified over the **whole window,
863/// not just the final poll**: if *any* poll reached the registry and observed the
864/// version absent, that is [`WaitFailure::Absent`]; if **no** poll ever got a
865/// definitive answer (every one errored), that is [`WaitFailure::Unreachable`]
866/// carrying the last error — a sustained outage is never masked as "did not index"
867/// just because the last poll happened (or failed) to answer. This fails closed:
868/// an all-outage window is `Unreachable`, never a false absence. The two callers
869/// ([`wait_for_index`] for a dependency, [`confirm_self_published`] for the crate
870/// just published) map these to their own [`AdapterError`] variants, so the same
871/// bounded, propagation-lag-tolerant wait backs both.
872fn poll_for_index(
873    ctx: &EffectCtx<'_>,
874    ecosystem: Ecosystem,
875    package: &str,
876    version: &str,
877) -> Result<(), WaitFailure> {
878    let start = ctx.clock.now_unix();
879    // Whether ANY poll reached the registry and got a definitive answer (a version
880    // list, in which the version was absent). Drives the fail-closed classification:
881    // a window that never once saw the registry answer is an outage
882    // (`Unreachable`), never a proven absence — even if the final poll erred or
883    // answered. Only a window that DID observe a clean absence classifies as
884    // `Absent`.
885    let mut observed_absent = false;
886    // The most recent registry error, surfaced when the window was a pure outage.
887    let mut last_err: Option<String> = None;
888    loop {
889        match ctx.registry.published_versions(ecosystem.as_str(), package) {
890            Ok(versions) => {
891                if versions.iter().any(|v| v == version) {
892                    return Ok(());
893                }
894                // A definitive answer: the registry was reached and the version is
895                // absent. `last_err` is intentionally NOT cleared — it is only read on
896                // the `Unreachable` path, which is taken solely when `observed_absent`
897                // is false (no clean answer ever occurred), so a stale error string
898                // can never leak into an `Absent` classification.
899                observed_absent = true;
900            }
901            Err(e) => last_err = Some(e.to_string()),
902        }
903        let waited = ctx.clock.now_unix().saturating_sub(start);
904        if waited >= INDEX_WAIT_TIMEOUT_SECS {
905            return Err(if observed_absent {
906                WaitFailure::Absent {
907                    waited_secs: waited,
908                }
909            } else {
910                WaitFailure::Unreachable {
911                    source: last_err.unwrap_or_else(|| {
912                        "the registry never returned a definitive answer".to_string()
913                    }),
914                }
915            });
916        }
917        ctx.clock.sleep(INDEX_POLL_INTERVAL);
918    }
919}
920
921/// Wait for a **workspace dependency** to be crates.io-index-visible before the
922/// dependent's `cargo publish` (crates.io rejects a crate whose sibling dependency
923/// is not yet indexed). An absence-after-wait is [`AdapterError::IndexTimeout`] (the
924/// dependency never indexed — likely under-declared); an outage is
925/// [`AdapterError::RegistryUnavailable`] (fail-closed, never masked as "did not
926/// index").
927fn wait_for_index(
928    ctx: &EffectCtx<'_>,
929    ecosystem: Ecosystem,
930    package: &str,
931    version: &str,
932) -> Result<(), AdapterError> {
933    poll_for_index(ctx, ecosystem, package, version).map_err(|f| match f {
934        WaitFailure::Absent { waited_secs } => AdapterError::IndexTimeout {
935            package: package.to_string(),
936            version: version.to_string(),
937            waited_secs,
938        },
939        WaitFailure::Unreachable { source } => AdapterError::RegistryUnavailable {
940            package: package.to_string(),
941            version: version.to_string(),
942            source,
943        },
944    })
945}
946
947/// Confirm the crate the adapter **just published** is visible on the index before a
948/// receipt is journaled — the self-visibility check that turns an unconfirmed upload
949/// into a fail-closed refusal rather than a fabricated success
950/// (`cut-noop-self-visibility-check`).
951///
952/// A `cargo publish` that exits 0 but shipped nothing (a registry-alias/credential/
953/// env difference, an under-declared target) would otherwise fabricate a
954/// [`PublishReceipt`](crate::protocol::release::PublishReceipt) and report the cut a
955/// success while nothing reached crates.io. So after the irreversible upload the
956/// publish path probes the registry for the target's *own* `{package, version}`,
957/// reusing the same bounded [`poll_for_index`] wait as the dependency index-wait so
958/// normal sparse-index propagation lag is tolerated — only a version that never
959/// appears within the window fails. That failure is
960/// [`AdapterError::PublishNotVisible`] (naming the crate + version): the cut fails
961/// **closed** rather than record a receipt it cannot substantiate — the upload may
962/// have landed on a slow index (resume/verify) or shipped nothing (a genuine no-op).
963/// A registry outage (never reachable across the window) is
964/// [`AdapterError::RegistryUnavailable`] instead (fail-closed too, mirroring the
965/// reconcile layer's outage discipline).
966fn confirm_self_published(
967    ctx: &EffectCtx<'_>,
968    ecosystem: Ecosystem,
969    package: &str,
970    version: &str,
971) -> Result<(), AdapterError> {
972    poll_for_index(ctx, ecosystem, package, version).map_err(|f| match f {
973        WaitFailure::Absent { waited_secs } => AdapterError::PublishNotVisible {
974            package: package.to_string(),
975            version: version.to_string(),
976            waited_secs,
977        },
978        WaitFailure::Unreachable { source } => AdapterError::RegistryUnavailable {
979            package: package.to_string(),
980            version: version.to_string(),
981            source,
982        },
983    })
984}
985
986/// A publishable workspace member with its version and its intra-workspace
987/// (non-dev) dependency names.
988#[derive(Clone)]
989struct Member {
990    name: String,
991    version: String,
992    deps: Vec<String>,
993}
994
995/// The subset of `cargo metadata --format-version 1 --no-deps` output the
996/// publish-order discovery reads.
997#[derive(Deserialize)]
998struct CargoMetadata {
999    /// Every package in the metadata; with `--no-deps` these are the workspace
1000    /// members only.
1001    packages: Vec<MetaPackage>,
1002    /// The package ids that are workspace members (matched against
1003    /// [`MetaPackage::id`] to be exact regardless of the id string format).
1004    workspace_members: Vec<String>,
1005    /// The absolute build target directory cargo resolved for this invocation —
1006    /// honoring `CARGO_TARGET_DIR`, `[build] target-dir`, and workspace config, so a
1007    /// consumer never hard-codes `<repo>/target`. `cargo metadata` always emits it;
1008    /// defaulted only so the many callers that read only [`Self::packages`] /
1009    /// [`Self::workspace_members`] can deserialize fixtures without it. The
1010    /// packaged `.crate` lands under `<target_directory>/package/`.
1011    #[serde(default)]
1012    target_directory: String,
1013}
1014
1015/// One package entry from `cargo metadata`.
1016#[derive(Deserialize)]
1017struct MetaPackage {
1018    name: String,
1019    version: String,
1020    id: String,
1021    #[serde(default)]
1022    dependencies: Vec<MetaDep>,
1023    /// `null`/absent ⇒ publishable to any registry; `[]` ⇒ `publish = false`;
1024    /// `["<registry>",…]` ⇒ publishable to a restricted set (still publishable).
1025    #[serde(default)]
1026    publish: Option<Vec<String>>,
1027}
1028
1029/// One dependency entry from `cargo metadata`.
1030#[derive(Deserialize)]
1031struct MetaDep {
1032    name: String,
1033    /// `null` (normal), `"dev"`, or `"build"`. Only normal/build deps gate
1034    /// publish order; dev-deps never do.
1035    #[serde(default)]
1036    kind: Option<String>,
1037}