From 5b237abf3ecfa4f78aa0c43dd775384a198f9c53 Mon Sep 17 00:00:00 2001 From: Austin Gregg-Smith Date: Sat, 29 Aug 2026 19:07:29 +0100 Subject: [PATCH 1/3] Claim a promised type's canonical rows for the promise file `cargo public-api` renders inherent methods and trait impls at a type's canonical path only, never at the path it is re-exported under, so `api::Launch::run` is rendered `flows::launch::Launch::run` and a filter matching `devlaunch_core::api\b` could not see it. `public-api.api.txt` was 126 rows and not one of them was a `::new(` or a `::run(`: renaming `Launch::run` left the file that exists to catch exactly that byte-identical. The classifier now reads the `api` module's own `pub use crate::...` statements, resolves each to the canonical path it names, and claims that item's rows too. A row's subject is what is claimed, never a mention: promised types are arguments in half the signatures in this crate, and a substring match would drag the binary surface into the promise file. 395 rows move from `public-api.rest.txt` to `public-api.api.txt` and none appear from nowhere. Both were regenerated with nightly and the pinned cargo-public-api 0.52.0, after reproducing the base's three files byte-identically. The rule is reachable now without the toolchain, as `--classify api|rest` over rows on stdin, so the Python guard exercises the real one on rows chosen to be awkward and the Rust guard holds the two files to a fixed point of it rather than restating it in Rust. The assertion that every promised row names `devlaunch_core::api` stopped being true and is what the fixed point replaces. Closes #352 --- .github/workflows/ci.yml | 5 +- CHANGELOG.md | 17 + docs/development.md | 43 +- rust/devlaunch-core/public-api.api.txt | 395 ++++++++++++++++++ rust/devlaunch-core/public-api.rest.txt | 395 ------------------ rust/devlaunch-core/src/lib.rs | 39 +- .../tests/public_api_snapshots.rs | 136 ++++-- scripts/public-api-snapshots.sh | 211 ++++++++-- test/test_public_api_snapshots_doc.py | 112 ++++- 9 files changed, 848 insertions(+), 505 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9dc8b732..cd3e29e1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -459,8 +459,9 @@ jobs: "scripts/public-api-snapshots.sh (needs a nightly toolchain and the" \ "cargo-public-api the script pins; see 'The public-API snapshots' in" \ "docs/development.md). A diff in devlaunch-core/public-api.api.txt is a change" \ - "to the promised API — say which, and note that a promised type's" \ - "methods and impls diff in the rest file instead (#352)." + "to the promised API — say which. A promised type's methods and impls are" \ + "in there too, under the canonical path they are rendered at, so a diff" \ + "that names a flows:: or domain:: path in that file is still a promise." exit 1 fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ba0f899..510fbf6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **The frozen-API snapshot now covers the promised types' behaviour, not just their + names (#352).** `cargo public-api` renders inherent methods and trait impls at a + type's canonical path only, never at the path it is re-exported under, so + `api::Launch::run` was rendered `flows::launch::Launch::run` and a filter matching + the `api` path could not see it. `public-api.api.txt` held 126 rows and not one + `::new(` or `::run(`, which means renaming `Launch::run` left the file that exists + to catch exactly that byte-identical. Measured, not inferred: the rename was done + and the file did not move. + + The classifier now reads the `api` module's own re-exports, resolves each back to + the path it names, and claims that item's rows too. 395 rows moved from + `public-api.rest.txt` to `public-api.api.txt`, none appeared from nowhere, and the + same rename now diffs the promise file. Two things follow for a reader of these + files. Most of the promise file is written at `flows::` and `domain::` paths now, + and that is the promise rather than strays. And moving a promised type between + modules churns it, which is the price of the guard working at all. + - **`devlaunch_core::api` can now build a launcher, not just name one.** The two implementations that decide whether a launch can go cold at all lived in the `dl` binary: the one that opens devlaunch's records (config, `metadata.json`, the cache diff --git a/docs/development.md b/docs/development.md index c66f9823..1e31bcb5 100644 --- a/docs/development.md +++ b/docs/development.md @@ -49,19 +49,30 @@ because they are not one promise: | File | What a diff means | | --- | --- | -| `devlaunch-core/public-api.api.txt` | **A change to the promised contract.** A removal or a changed signature breaks a consumer, an addition is a deliberate widening. Holds the 126 declarations written *at* the `devlaunch_core::api` path, and only those. | -| `devlaunch-core/public-api.rest.txt` | Mostly routine. The binary API (`flows::`, `domain::`, `clients::`) is reachable but never promised, so read it for the accidental `pub`. **But** the promised types' methods and impls are in here too (see below), and a diff touching one of those is a contract change. | +| `devlaunch-core/public-api.api.txt` | **A change to the promised contract.** A removal or a changed signature breaks a consumer, an addition is a deliberate widening. Holds the 521 rows that declare what `devlaunch_core::api` re-exports, wherever `cargo public-api` chose to render them. | +| `devlaunch-core/public-api.rest.txt` | Mostly routine. The binary API (`flows::`, `domain::`, `clients::`) is reachable but never promised, so read it for the accidental `pub`. **But** see the limit below before reading a diff as routine. | | `devlaunch-runner/public-api.txt` | The process seam an external `Runner` implementer writes against. | -**The promise file holds declarations, not behaviour.** `cargo public-api` renders inherent methods -and trait impls only at a type's *canonical* path, never at the path it is re-exported under, so -the classifier cannot see them. `api::Launch`'s only constructor and only method are rendered -`flows::launch::Launch::{new, run}` and land in the rest file, along with `CommandContext::new`, -`DevcontainerPath::as_str` and every derived `Clone`/`Debug`/`PartialEq` on the promised types: 133 -of the 259 rows the generator emits for the `api` section. Measured consequence: renaming -`api::Launch::run` leaves `public-api.api.txt` byte-identical. The guard is therefore one-way. A -diff in the promise file is a change to the promise, but not every change to the promise diffs it. -Widening the classifier is [#352](https://github.com/blooop/devlaunch/issues/352). +**Why the promise file is full of `flows::` and `domain::` paths.** `cargo public-api` renders an +item's own declaration at every path it is reachable by, so `api::Launch` gets a row of its own. It +renders inherent methods and trait impls at the type's *canonical* path only, never at the path it +is re-exported under, so `api::Launch::run` is rendered `flows::launch::Launch::run`. A classifier +that matched the `api` path alone therefore kept the promised types' names and dropped all of their +behaviour, and renaming `api::Launch::run` left the file byte-identical. So the classifier resolves +each `api` re-export back to the path it names and claims that item's rows too +([#352](https://github.com/blooop/devlaunch/issues/352)), which moved 395 rows across: `Launch::new` +and `Launch::run`, `CommandContext::new`, `DevcontainerPath::as_str`, and every derived +`Clone`/`Debug`/`PartialEq` on a promised type. Those paths are the promise and not strays. The +cost is real and worth knowing before you see it: moving a promised type between modules now churns +the file where churn is expensive. Some rows appear twice, because the generator emits them twice, +once under the `api` section and once under the module that owns them. + +**What it still does not reach.** A type `api` never re-exports but a promised signature hands back +is reachable from outside and classified as binary surface. `Launch::run` returns +`flows::launch::Launched`, so renaming one of that type's fields breaks a consumer and diffs +`public-api.rest.txt` alone. The `-ss` flag also omits blanket and auto-trait impls from both files, +deliberately: those rows move when rustdoc moves, and a tripwire that fires on toolchain drift +teaches people to update snapshots unread. The runner had no snapshot of its own until #338: its whole API entered core's as the single unexpanded row `pub use devlaunch_core::runner::<>`, so removing a trait @@ -89,10 +100,12 @@ cargo install cargo-public-api --locked --version "$(scripts/public-api-snapshot ``` Committing a regenerated `public-api.api.txt` is committing a change to the promised contract, so -say which one in the pull request. If the change was to a promised type's methods or impls, -the diff to point at is in `public-api.rest.txt`. `rust/devlaunch-core/tests/public_api_snapshots.rs` -holds the two core files to the split itself, every promised row an `api` declaration and none -of the others one, so a hand-edited snapshot fails in the Rust suite rather than in review. +say which one in the pull request. `rust/devlaunch-core/tests/public_api_snapshots.rs` holds the two +core files to the split itself: feed it both and the classifier has to hand back the same two files, +each row on the side it is already on. So a hand-edited snapshot, or a pair regenerated by different +rules, fails in the Rust suite rather than in review. That test reaches the classifier through +`scripts/public-api-snapshots.sh --classify`, which filters rows on stdin and needs no toolchain at +all, so the rule has one definition rather than a Rust copy of a shell one. **What a failed run leaves behind**, precisely, because "nothing" would be a claim rather than a fact. The script checks every destination is writable before it generates anything, then writes diff --git a/rust/devlaunch-core/public-api.api.txt b/rust/devlaunch-core/public-api.api.txt index 301b3594..342efa77 100644 --- a/rust/devlaunch-core/public-api.api.txt +++ b/rust/devlaunch-core/public-api.api.txt @@ -2,6 +2,14 @@ pub mod devlaunch_core::api pub enum devlaunch_core::api::ColdRefused pub devlaunch_core::api::ColdRefused::NoColdPath pub devlaunch_core::api::ColdRefused::Startup(devlaunch_core::flows::records::StartupError) +impl core::clone::Clone for devlaunch_core::flows::launch::ColdRefused +pub fn devlaunch_core::flows::launch::ColdRefused::clone(&self) -> devlaunch_core::flows::launch::ColdRefused +impl core::cmp::Eq for devlaunch_core::flows::launch::ColdRefused +impl core::cmp::PartialEq for devlaunch_core::flows::launch::ColdRefused +pub fn devlaunch_core::flows::launch::ColdRefused::eq(&self, &devlaunch_core::flows::launch::ColdRefused) -> bool +impl core::fmt::Debug for devlaunch_core::flows::launch::ColdRefused +pub fn devlaunch_core::flows::launch::ColdRefused::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::launch::ColdRefused pub enum devlaunch_core::api::LaunchNotice pub devlaunch_core::api::LaunchNotice::AlreadyRunning pub devlaunch_core::api::LaunchNotice::AlreadyRunning::workspace_id: alloc::string::String @@ -43,6 +51,14 @@ pub devlaunch_core::api::LaunchNotice::TokenNotStaged pub devlaunch_core::api::LaunchNotice::TokenNotStaged::reason: alloc::string::String pub devlaunch_core::api::LaunchNotice::WaitingForSiblingLaunch pub devlaunch_core::api::LaunchNotice::WaitingForSiblingLaunch::workspace_id: alloc::string::String +impl core::clone::Clone for devlaunch_core::flows::launch::LaunchNotice +pub fn devlaunch_core::flows::launch::LaunchNotice::clone(&self) -> devlaunch_core::flows::launch::LaunchNotice +impl core::cmp::Eq for devlaunch_core::flows::launch::LaunchNotice +impl core::cmp::PartialEq for devlaunch_core::flows::launch::LaunchNotice +pub fn devlaunch_core::flows::launch::LaunchNotice::eq(&self, &devlaunch_core::flows::launch::LaunchNotice) -> bool +impl core::fmt::Debug for devlaunch_core::flows::launch::LaunchNotice +pub fn devlaunch_core::flows::launch::LaunchNotice::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::launch::LaunchNotice pub enum devlaunch_core::api::LaunchVerb pub devlaunch_core::api::LaunchVerb::Attach pub devlaunch_core::api::LaunchVerb::Attach::command: core::option::Option @@ -52,6 +68,14 @@ pub devlaunch_core::api::LaunchVerb::Recreate pub devlaunch_core::api::LaunchVerb::Reset pub devlaunch_core::api::LaunchVerb::Restart pub devlaunch_core::api::LaunchVerb::Up +impl core::clone::Clone for devlaunch_core::flows::launch::LaunchVerb +pub fn devlaunch_core::flows::launch::LaunchVerb::clone(&self) -> devlaunch_core::flows::launch::LaunchVerb +impl core::cmp::Eq for devlaunch_core::flows::launch::LaunchVerb +impl core::cmp::PartialEq for devlaunch_core::flows::launch::LaunchVerb +pub fn devlaunch_core::flows::launch::LaunchVerb::eq(&self, &devlaunch_core::flows::launch::LaunchVerb) -> bool +impl core::fmt::Debug for devlaunch_core::flows::launch::LaunchVerb +pub fn devlaunch_core::flows::launch::LaunchVerb::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::launch::LaunchVerb pub enum devlaunch_core::api::ProvisionEvent pub devlaunch_core::api::ProvisionEvent::NotInstalled pub devlaunch_core::api::ProvisionEvent::NotInstalled::exit: devlaunch_runner::Exit @@ -73,20 +97,58 @@ pub devlaunch_core::api::ProvisionEvent::StageNotReported::workspace: alloc::str pub devlaunch_core::api::ProvisionEvent::TripRefused pub devlaunch_core::api::ProvisionEvent::TripRefused::refusal: devlaunch_core::clients::devpod::NotRun pub devlaunch_core::api::ProvisionEvent::TripRefused::workspace: alloc::string::String +impl core::clone::Clone for devlaunch_core::flows::provision::ProvisionEvent +pub fn devlaunch_core::flows::provision::ProvisionEvent::clone(&self) -> devlaunch_core::flows::provision::ProvisionEvent +impl core::cmp::Eq for devlaunch_core::flows::provision::ProvisionEvent +impl core::cmp::PartialEq for devlaunch_core::flows::provision::ProvisionEvent +pub fn devlaunch_core::flows::provision::ProvisionEvent::eq(&self, &devlaunch_core::flows::provision::ProvisionEvent) -> bool +impl core::fmt::Debug for devlaunch_core::flows::provision::ProvisionEvent +pub fn devlaunch_core::flows::provision::ProvisionEvent::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::provision::ProvisionEvent pub enum devlaunch_core::api::RecordsNotice pub devlaunch_core::api::RecordsNotice::Metadata(devlaunch_core::domain::metadata::Notice) pub devlaunch_core::api::RecordsNotice::Migrated(devlaunch_core::flows::migration::MigrationReport) pub devlaunch_core::api::RecordsNotice::MigrationRefused(devlaunch_core::domain::metadata::MetadataError) pub devlaunch_core::api::RecordsNotice::RetiredKey(devlaunch_core::domain::config::RetiredKey) +impl core::clone::Clone for devlaunch_core::flows::records::RecordsNotice +pub fn devlaunch_core::flows::records::RecordsNotice::clone(&self) -> devlaunch_core::flows::records::RecordsNotice +impl core::cmp::Eq for devlaunch_core::flows::records::RecordsNotice +impl core::cmp::PartialEq for devlaunch_core::flows::records::RecordsNotice +pub fn devlaunch_core::flows::records::RecordsNotice::eq(&self, &devlaunch_core::flows::records::RecordsNotice) -> bool +impl core::fmt::Debug for devlaunch_core::flows::records::RecordsNotice +pub fn devlaunch_core::flows::records::RecordsNotice::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::records::RecordsNotice pub enum devlaunch_core::api::SpecIdentity<'a> pub devlaunch_core::api::SpecIdentity::ExistingName(&'a str) pub devlaunch_core::api::SpecIdentity::PathLeaf(&'a str) pub devlaunch_core::api::SpecIdentity::RepoLabel(alloc::string::String) pub devlaunch_core::api::SpecIdentity::Workspace(alloc::string::String) +impl<'a> core::clone::Clone for devlaunch_core::domain::spec::SpecIdentity<'a> +pub fn devlaunch_core::domain::spec::SpecIdentity<'a>::clone(&self) -> devlaunch_core::domain::spec::SpecIdentity<'a> +impl<'a> core::cmp::Eq for devlaunch_core::domain::spec::SpecIdentity<'a> +impl<'a> core::cmp::PartialEq for devlaunch_core::domain::spec::SpecIdentity<'a> +pub fn devlaunch_core::domain::spec::SpecIdentity<'a>::eq(&self, &devlaunch_core::domain::spec::SpecIdentity<'a>) -> bool +impl<'a> core::fmt::Debug for devlaunch_core::domain::spec::SpecIdentity<'a> +pub fn devlaunch_core::domain::spec::SpecIdentity<'a>::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl<'a> core::marker::StructuralPartialEq for devlaunch_core::domain::spec::SpecIdentity<'a> pub enum devlaunch_core::api::StartupError pub devlaunch_core::api::StartupError::Config(devlaunch_core::domain::config::ConfigError) pub devlaunch_core::api::StartupError::Metadata(devlaunch_core::domain::metadata::MetadataError) pub devlaunch_core::api::StartupError::NoHomeDirectory +impl core::clone::Clone for devlaunch_core::flows::records::StartupError +pub fn devlaunch_core::flows::records::StartupError::clone(&self) -> devlaunch_core::flows::records::StartupError +impl core::cmp::Eq for devlaunch_core::flows::records::StartupError +impl core::cmp::PartialEq for devlaunch_core::flows::records::StartupError +pub fn devlaunch_core::flows::records::StartupError::eq(&self, &devlaunch_core::flows::records::StartupError) -> bool +impl core::convert::From for devlaunch_core::flows::records::StartupError +pub fn devlaunch_core::flows::records::StartupError::from(devlaunch_core::domain::config::ConfigError) -> Self +impl core::convert::From for devlaunch_core::flows::records::StartupError +pub fn devlaunch_core::flows::records::StartupError::from(devlaunch_core::domain::metadata::MetadataError) -> Self +impl core::convert::From for devlaunch_core::flows::records::StartupError +pub fn devlaunch_core::flows::records::StartupError::from(devlaunch_core::domain::xdg::NoHomeDirectory) -> Self +impl core::fmt::Debug for devlaunch_core::flows::records::StartupError +pub fn devlaunch_core::flows::records::StartupError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::records::StartupError pub enum devlaunch_core::api::WorkspaceSpec<'a> pub devlaunch_core::api::WorkspaceSpec::ExistingIdOrName(&'a str) pub devlaunch_core::api::WorkspaceSpec::HostPath(&'a str) @@ -97,26 +159,96 @@ pub devlaunch_core::api::WorkspaceSpec::OwnerRepo::repo: &'a str pub devlaunch_core::api::WorkspaceSpec::Path(&'a str) pub devlaunch_core::api::WorkspaceSpec::SshUrl(&'a str) pub devlaunch_core::api::WorkspaceSpec::Url(&'a str) +impl<'a> core::clone::Clone for devlaunch_core::domain::spec::WorkspaceSpec<'a> +pub fn devlaunch_core::domain::spec::WorkspaceSpec<'a>::clone(&self) -> devlaunch_core::domain::spec::WorkspaceSpec<'a> +impl<'a> core::cmp::Eq for devlaunch_core::domain::spec::WorkspaceSpec<'a> +impl<'a> core::cmp::PartialEq for devlaunch_core::domain::spec::WorkspaceSpec<'a> +pub fn devlaunch_core::domain::spec::WorkspaceSpec<'a>::eq(&self, &devlaunch_core::domain::spec::WorkspaceSpec<'a>) -> bool +impl<'a> core::fmt::Debug for devlaunch_core::domain::spec::WorkspaceSpec<'a> +pub fn devlaunch_core::domain::spec::WorkspaceSpec<'a>::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl<'a> core::marker::Copy for devlaunch_core::domain::spec::WorkspaceSpec<'a> +impl<'a> core::marker::StructuralPartialEq for devlaunch_core::domain::spec::WorkspaceSpec<'a> pub struct devlaunch_core::api::Cold<'a, 'r> pub devlaunch_core::api::Cold::clones: &'a devlaunch_core::flows::workspace_clone::WorkspaceCloneManager<'r> pub devlaunch_core::api::Cold::storage: &'a mut devlaunch_core::domain::metadata::MetadataStorage pub struct devlaunch_core::api::ColdPath<'r, 'e> +impl<'r, 'e> devlaunch_core::flows::launch::ColdPath<'r, 'e> +pub fn devlaunch_core::flows::launch::ColdPath<'r, 'e>::new(&'r dyn devlaunch_runner::Runner, &'e mut dyn devlaunch_core::notices::Notices) -> Self +pub fn devlaunch_core::flows::launch::ColdPath<'r, 'e>::records(&mut self) -> core::result::Result<&mut devlaunch_core::flows::records::Records<'r>, devlaunch_core::flows::records::StartupError> +impl<'r> devlaunch_core::flows::launch::ColdMachinery<'r> for devlaunch_core::flows::launch::ColdPath<'r, '_> +pub fn devlaunch_core::flows::launch::ColdPath<'r, '_>::open(&mut self) -> core::result::Result, devlaunch_core::flows::launch::ColdRefused> pub struct devlaunch_core::api::CommandContext<'r> +impl<'r> devlaunch_core::flows::listing::CommandContext<'r> +pub fn devlaunch_core::flows::listing::CommandContext<'r>::git(&self) -> devlaunch_core::clients::git::Git<'r> +pub fn devlaunch_core::flows::listing::CommandContext<'r>::new(&'r dyn devlaunch_runner::Runner) -> Self +pub fn devlaunch_core::flows::listing::CommandContext<'r>::workspaces(&mut self) -> core::result::Result, devlaunch_core::clients::devpod::ListingUnreadable> pub struct devlaunch_core::api::DevcontainerPath(_) +impl devlaunch_core::domain::spec::DevcontainerPath +pub fn devlaunch_core::domain::spec::DevcontainerPath::as_str(&self) -> &str +impl core::clone::Clone for devlaunch_core::domain::spec::DevcontainerPath +pub fn devlaunch_core::domain::spec::DevcontainerPath::clone(&self) -> devlaunch_core::domain::spec::DevcontainerPath +impl core::cmp::Eq for devlaunch_core::domain::spec::DevcontainerPath +impl core::cmp::PartialEq for devlaunch_core::domain::spec::DevcontainerPath +pub fn devlaunch_core::domain::spec::DevcontainerPath::eq(&self, &devlaunch_core::domain::spec::DevcontainerPath) -> bool +impl core::fmt::Debug for devlaunch_core::domain::spec::DevcontainerPath +pub fn devlaunch_core::domain::spec::DevcontainerPath::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::domain::spec::DevcontainerPath pub struct devlaunch_core::api::Host +impl devlaunch_core::flows::launch::Host +pub fn devlaunch_core::flows::launch::Host::from_process(impl core::convert::Into) -> Self +impl core::clone::Clone for devlaunch_core::flows::launch::Host +pub fn devlaunch_core::flows::launch::Host::clone(&self) -> devlaunch_core::flows::launch::Host +impl core::cmp::Eq for devlaunch_core::flows::launch::Host +impl core::cmp::PartialEq for devlaunch_core::flows::launch::Host +pub fn devlaunch_core::flows::launch::Host::eq(&self, &devlaunch_core::flows::launch::Host) -> bool +impl core::default::Default for devlaunch_core::flows::launch::Host +pub fn devlaunch_core::flows::launch::Host::default() -> devlaunch_core::flows::launch::Host +impl core::fmt::Debug for devlaunch_core::flows::launch::Host +pub fn devlaunch_core::flows::launch::Host::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::launch::Host pub struct devlaunch_core::api::Launch<'a, 'r, 'l> +impl<'a, 'r, 'l> devlaunch_core::flows::launch::Launch<'a, 'r, 'l> +pub fn devlaunch_core::flows::launch::Launch<'a, 'r, 'l>::new(&'a mut devlaunch_core::flows::listing::CommandContext<'r>, &'a mut devlaunch_core::flows::lifecycle::Refresh<'l>, &'a mut dyn devlaunch_core::flows::launch::ColdMachinery<'r>, &'a dyn devlaunch_core::flows::launch::Provision, &'a devlaunch_core::flows::launch::Host, &'a mut dyn core::ops::function::FnMut(&str), &'a mut dyn devlaunch_core::notices::Notices) -> Self +pub fn devlaunch_core::flows::launch::Launch<'a, 'r, 'l>::recognised_as(self, core::option::Option) -> Self +pub fn devlaunch_core::flows::launch::Launch<'a, 'r, 'l>::run(&mut self, &str, &devlaunch_core::flows::launch::LaunchVerb, core::option::Option<&devlaunch_core::domain::spec::DevcontainerPath>) -> core::result::Result pub struct devlaunch_core::api::Refresh<'a> +impl<'a> devlaunch_core::flows::lifecycle::Refresh<'a> +pub fn devlaunch_core::flows::lifecycle::Refresh<'a>::ask(&mut self, &dyn devlaunch_runner::Runner, devlaunch_core::flows::lifecycle::RefreshReason) -> devlaunch_core::flows::lifecycle::RefreshSpawn +pub fn devlaunch_core::flows::lifecycle::Refresh<'a>::new(&'a devlaunch_core::flows::lifecycle::SelfInvocation, &'a std::path::Path) -> Self +pub fn devlaunch_core::flows::lifecycle::Refresh<'a>::rearm(&mut self) +pub fn devlaunch_core::flows::lifecycle::Refresh<'a>::spawned(&self) -> bool pub struct devlaunch_core::api::SelfInvocation +impl devlaunch_core::flows::lifecycle::SelfInvocation +pub fn devlaunch_core::flows::lifecycle::SelfInvocation::new(impl core::convert::Into) -> Self +impl core::clone::Clone for devlaunch_core::flows::lifecycle::SelfInvocation +pub fn devlaunch_core::flows::lifecycle::SelfInvocation::clone(&self) -> devlaunch_core::flows::lifecycle::SelfInvocation +impl core::cmp::Eq for devlaunch_core::flows::lifecycle::SelfInvocation +impl core::cmp::PartialEq for devlaunch_core::flows::lifecycle::SelfInvocation +pub fn devlaunch_core::flows::lifecycle::SelfInvocation::eq(&self, &devlaunch_core::flows::lifecycle::SelfInvocation) -> bool +impl core::fmt::Debug for devlaunch_core::flows::lifecycle::SelfInvocation +pub fn devlaunch_core::flows::lifecycle::SelfInvocation::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::lifecycle::SelfInvocation pub struct devlaunch_core::api::ToolProvisioning<'e> +impl<'e> devlaunch_core::flows::launch::ToolProvisioning<'e> +pub fn devlaunch_core::flows::launch::ToolProvisioning<'e>::from_env(&std::path::Path, &'e mut dyn devlaunch_core::notices::Notices) -> Self +impl devlaunch_core::flows::launch::Provision for devlaunch_core::flows::launch::ToolProvisioning<'_> +pub fn devlaunch_core::flows::launch::ToolProvisioning<'_>::provision_tools(&self, &dyn devlaunch_runner::Runner, &str, devlaunch_core::flows::provision::PassOccasion, core::option::Option<&str>) -> core::result::Result, devlaunch_core::flows::provision::DevpodMissing> +pub fn devlaunch_core::flows::launch::ToolProvisioning<'_>::remembered_claude(&self, &str) -> core::option::Option pub const devlaunch_core::api::HANDOFF_VAR: &str pub const devlaunch_core::api::PREWARM_VAR: &str pub trait devlaunch_core::api::ColdMachinery<'r> pub fn devlaunch_core::api::ColdMachinery::open(&mut self) -> core::result::Result, devlaunch_core::flows::launch::ColdRefused> +impl<'r> devlaunch_core::flows::launch::ColdMachinery<'r> for devlaunch_core::flows::launch::ColdPath<'r, '_> +pub fn devlaunch_core::flows::launch::ColdPath<'r, '_>::open(&mut self) -> core::result::Result, devlaunch_core::flows::launch::ColdRefused> pub trait devlaunch_core::api::Notices pub fn devlaunch_core::api::Notices::say(&mut self, T) +impl devlaunch_core::notices::Notices for alloc::vec::Vec pub trait devlaunch_core::api::Provision pub fn devlaunch_core::api::Provision::provision_tools(&self, &dyn devlaunch_runner::Runner, &str, devlaunch_core::flows::provision::PassOccasion, core::option::Option<&str>) -> core::result::Result, devlaunch_core::flows::provision::DevpodMissing> pub fn devlaunch_core::api::Provision::remembered_claude(&self, &str) -> core::option::Option +impl devlaunch_core::flows::launch::Provision for devlaunch_core::flows::launch::ToolProvisioning<'_> +pub fn devlaunch_core::flows::launch::ToolProvisioning<'_>::provision_tools(&self, &dyn devlaunch_runner::Runner, &str, devlaunch_core::flows::provision::PassOccasion, core::option::Option<&str>) -> core::result::Result, devlaunch_core::flows::provision::DevpodMissing> +pub fn devlaunch_core::flows::launch::ToolProvisioning<'_>::remembered_claude(&self, &str) -> core::option::Option pub fn devlaunch_core::api::enriched_listing(&mut devlaunch_core::flows::listing::CommandContext<'_>, &devlaunch_core::flows::listing::DlView<'_>, devlaunch_core::flows::listing::Sizes) -> core::result::Result, devlaunch_core::clients::devpod::ListingUnreadable> pub fn devlaunch_core::api::identity(&str) -> core::result::Result, devlaunch_core::domain::workspace_id::UnsafeName> pub fn devlaunch_core::api::json_document(&[devlaunch_core::flows::listing::ListedWorkspace]) -> serde_json::value::Value @@ -124,3 +256,266 @@ pub fn devlaunch_core::api::parse(&str) -> devlaunch_core::domain::spec::Workspa pub fn devlaunch_core::api::resolve_devcontainer_ref(&str) -> core::result::Result pub fn devlaunch_core::api::workspace_delete(&mut devlaunch_core::flows::listing::CommandContext<'_>, &mut devlaunch_core::flows::lifecycle::Refresh<'_>, &devlaunch_core::flows::workspace_clone::WorkspaceCloneManager<'_>, &mut devlaunch_core::domain::metadata::MetadataStorage, core::option::Option<&devlaunch_core::clients::devpod_home::DevpodHome>, &str, devlaunch_core::flows::lifecycle::Insistence, devlaunch_core::flows::lifecycle::Persistence, &mut dyn core::ops::function::FnMut(devlaunch_core::flows::lifecycle::DeleteStalled), &mut dyn devlaunch_core::notices::Notices) -> core::result::Result pub fn devlaunch_core::api::workspace_stop(&mut devlaunch_core::flows::listing::CommandContext<'_>, &mut devlaunch_core::flows::lifecycle::Refresh<'_>, &str) -> core::result::Result +impl core::convert::From for devlaunch_core::flows::records::StartupError +pub fn devlaunch_core::flows::records::StartupError::from(devlaunch_core::domain::config::ConfigError) -> Self +impl core::convert::From for devlaunch_core::flows::records::StartupError +pub fn devlaunch_core::flows::records::StartupError::from(devlaunch_core::domain::metadata::MetadataError) -> Self +pub enum devlaunch_core::domain::spec::SpecIdentity<'a> +pub devlaunch_core::domain::spec::SpecIdentity::ExistingName(&'a str) +pub devlaunch_core::domain::spec::SpecIdentity::PathLeaf(&'a str) +pub devlaunch_core::domain::spec::SpecIdentity::RepoLabel(alloc::string::String) +pub devlaunch_core::domain::spec::SpecIdentity::Workspace(alloc::string::String) +impl<'a> core::clone::Clone for devlaunch_core::domain::spec::SpecIdentity<'a> +pub fn devlaunch_core::domain::spec::SpecIdentity<'a>::clone(&self) -> devlaunch_core::domain::spec::SpecIdentity<'a> +impl<'a> core::cmp::Eq for devlaunch_core::domain::spec::SpecIdentity<'a> +impl<'a> core::cmp::PartialEq for devlaunch_core::domain::spec::SpecIdentity<'a> +pub fn devlaunch_core::domain::spec::SpecIdentity<'a>::eq(&self, &devlaunch_core::domain::spec::SpecIdentity<'a>) -> bool +impl<'a> core::fmt::Debug for devlaunch_core::domain::spec::SpecIdentity<'a> +pub fn devlaunch_core::domain::spec::SpecIdentity<'a>::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl<'a> core::marker::StructuralPartialEq for devlaunch_core::domain::spec::SpecIdentity<'a> +pub enum devlaunch_core::domain::spec::WorkspaceSpec<'a> +pub devlaunch_core::domain::spec::WorkspaceSpec::ExistingIdOrName(&'a str) +pub devlaunch_core::domain::spec::WorkspaceSpec::HostPath(&'a str) +pub devlaunch_core::domain::spec::WorkspaceSpec::OwnerRepo +pub devlaunch_core::domain::spec::WorkspaceSpec::OwnerRepo::branch: core::option::Option<&'a str> +pub devlaunch_core::domain::spec::WorkspaceSpec::OwnerRepo::owner: &'a str +pub devlaunch_core::domain::spec::WorkspaceSpec::OwnerRepo::repo: &'a str +pub devlaunch_core::domain::spec::WorkspaceSpec::Path(&'a str) +pub devlaunch_core::domain::spec::WorkspaceSpec::SshUrl(&'a str) +pub devlaunch_core::domain::spec::WorkspaceSpec::Url(&'a str) +impl<'a> core::clone::Clone for devlaunch_core::domain::spec::WorkspaceSpec<'a> +pub fn devlaunch_core::domain::spec::WorkspaceSpec<'a>::clone(&self) -> devlaunch_core::domain::spec::WorkspaceSpec<'a> +impl<'a> core::cmp::Eq for devlaunch_core::domain::spec::WorkspaceSpec<'a> +impl<'a> core::cmp::PartialEq for devlaunch_core::domain::spec::WorkspaceSpec<'a> +pub fn devlaunch_core::domain::spec::WorkspaceSpec<'a>::eq(&self, &devlaunch_core::domain::spec::WorkspaceSpec<'a>) -> bool +impl<'a> core::fmt::Debug for devlaunch_core::domain::spec::WorkspaceSpec<'a> +pub fn devlaunch_core::domain::spec::WorkspaceSpec<'a>::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl<'a> core::marker::Copy for devlaunch_core::domain::spec::WorkspaceSpec<'a> +impl<'a> core::marker::StructuralPartialEq for devlaunch_core::domain::spec::WorkspaceSpec<'a> +pub struct devlaunch_core::domain::spec::DevcontainerPath(_) +impl devlaunch_core::domain::spec::DevcontainerPath +pub fn devlaunch_core::domain::spec::DevcontainerPath::as_str(&self) -> &str +impl core::clone::Clone for devlaunch_core::domain::spec::DevcontainerPath +pub fn devlaunch_core::domain::spec::DevcontainerPath::clone(&self) -> devlaunch_core::domain::spec::DevcontainerPath +impl core::cmp::Eq for devlaunch_core::domain::spec::DevcontainerPath +impl core::cmp::PartialEq for devlaunch_core::domain::spec::DevcontainerPath +pub fn devlaunch_core::domain::spec::DevcontainerPath::eq(&self, &devlaunch_core::domain::spec::DevcontainerPath) -> bool +impl core::fmt::Debug for devlaunch_core::domain::spec::DevcontainerPath +pub fn devlaunch_core::domain::spec::DevcontainerPath::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::domain::spec::DevcontainerPath +pub fn devlaunch_core::domain::spec::identity(&str) -> core::result::Result, devlaunch_core::domain::workspace_id::UnsafeName> +pub fn devlaunch_core::domain::spec::parse(&str) -> devlaunch_core::domain::spec::WorkspaceSpec<'_> +pub fn devlaunch_core::domain::spec::resolve_devcontainer_ref(&str) -> core::result::Result +impl core::convert::From for devlaunch_core::flows::records::StartupError +pub fn devlaunch_core::flows::records::StartupError::from(devlaunch_core::domain::xdg::NoHomeDirectory) -> Self +pub enum devlaunch_core::flows::launch::ColdRefused +pub devlaunch_core::flows::launch::ColdRefused::NoColdPath +pub devlaunch_core::flows::launch::ColdRefused::Startup(devlaunch_core::flows::records::StartupError) +impl core::clone::Clone for devlaunch_core::flows::launch::ColdRefused +pub fn devlaunch_core::flows::launch::ColdRefused::clone(&self) -> devlaunch_core::flows::launch::ColdRefused +impl core::cmp::Eq for devlaunch_core::flows::launch::ColdRefused +impl core::cmp::PartialEq for devlaunch_core::flows::launch::ColdRefused +pub fn devlaunch_core::flows::launch::ColdRefused::eq(&self, &devlaunch_core::flows::launch::ColdRefused) -> bool +impl core::fmt::Debug for devlaunch_core::flows::launch::ColdRefused +pub fn devlaunch_core::flows::launch::ColdRefused::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::launch::ColdRefused +pub enum devlaunch_core::flows::launch::LaunchNotice +pub devlaunch_core::flows::launch::LaunchNotice::AlreadyRunning +pub devlaunch_core::flows::launch::LaunchNotice::AlreadyRunning::workspace_id: alloc::string::String +pub devlaunch_core::flows::launch::LaunchNotice::AlreadyRunningAttaching +pub devlaunch_core::flows::launch::LaunchNotice::AlreadyRunningAttaching::workspace_id: alloc::string::String +pub devlaunch_core::flows::launch::LaunchNotice::BroughtUpBySibling +pub devlaunch_core::flows::launch::LaunchNotice::BroughtUpBySibling::workspace_id: alloc::string::String +pub devlaunch_core::flows::launch::LaunchNotice::Cache(devlaunch_core::flows::repo_manager::CacheNotice) +pub devlaunch_core::flows::launch::LaunchNotice::CreateNeverFinished +pub devlaunch_core::flows::launch::LaunchNotice::CreateNeverFinished::workspace_id: alloc::string::String +pub devlaunch_core::flows::launch::LaunchNotice::DevcontainerIgnoredRunning +pub devlaunch_core::flows::launch::LaunchNotice::DevcontainerIgnoredRunning::spec: alloc::string::String +pub devlaunch_core::flows::launch::LaunchNotice::DevcontainerIgnoredRunning::workspace_id: alloc::string::String +pub devlaunch_core::flows::launch::LaunchNotice::DevpodSessionFailed +pub devlaunch_core::flows::launch::LaunchNotice::DevpodSessionFailed::exit: devlaunch_runner::Exit +pub devlaunch_core::flows::launch::LaunchNotice::LaunchLockUnavailable +pub devlaunch_core::flows::launch::LaunchNotice::LaunchLockUnavailable::reason: alloc::string::String +pub devlaunch_core::flows::launch::LaunchNotice::LaunchLockUnavailable::workspace_id: alloc::string::String +pub devlaunch_core::flows::launch::LaunchNotice::Lifecycle(devlaunch_core::flows::lifecycle::LifecycleNotice) +pub devlaunch_core::flows::launch::LaunchNotice::NoDevpodSshConfig +pub devlaunch_core::flows::launch::LaunchNotice::NoDevpodSshConfig::looked_in: std::path::PathBuf +pub devlaunch_core::flows::launch::LaunchNotice::NoDevpodSshConfig::workspace_id: alloc::string::String +pub devlaunch_core::flows::launch::LaunchNotice::NoGitHubToken(devlaunch_core::clients::gh::GhEvent) +pub devlaunch_core::flows::launch::LaunchNotice::NoTerminalAlias +pub devlaunch_core::flows::launch::LaunchNotice::NoTerminalAlias::config: std::path::PathBuf +pub devlaunch_core::flows::launch::LaunchNotice::NoTerminalAlias::workspace_id: alloc::string::String +pub devlaunch_core::flows::launch::LaunchNotice::PixiCacheNotADirectory +pub devlaunch_core::flows::launch::LaunchNotice::PixiCacheNotADirectory::source: std::path::PathBuf +pub devlaunch_core::flows::launch::LaunchNotice::PixiCacheNotCreated +pub devlaunch_core::flows::launch::LaunchNotice::PixiCacheNotCreated::reason: alloc::string::String +pub devlaunch_core::flows::launch::LaunchNotice::PixiCacheNotCreated::source: std::path::PathBuf +pub devlaunch_core::flows::launch::LaunchNotice::SshCommand +pub devlaunch_core::flows::launch::LaunchNotice::SshCommand::argv: alloc::vec::Vec +pub devlaunch_core::flows::launch::LaunchNotice::SshConfigUnlocatable +pub devlaunch_core::flows::launch::LaunchNotice::StartingForDotfiles +pub devlaunch_core::flows::launch::LaunchNotice::StartingForDotfiles::workspace_id: alloc::string::String +pub devlaunch_core::flows::launch::LaunchNotice::TerminalTitle(devlaunch_core::flows::launch::TerminalTitle) +pub devlaunch_core::flows::launch::LaunchNotice::TokenNotStaged +pub devlaunch_core::flows::launch::LaunchNotice::TokenNotStaged::reason: alloc::string::String +pub devlaunch_core::flows::launch::LaunchNotice::WaitingForSiblingLaunch +pub devlaunch_core::flows::launch::LaunchNotice::WaitingForSiblingLaunch::workspace_id: alloc::string::String +impl core::clone::Clone for devlaunch_core::flows::launch::LaunchNotice +pub fn devlaunch_core::flows::launch::LaunchNotice::clone(&self) -> devlaunch_core::flows::launch::LaunchNotice +impl core::cmp::Eq for devlaunch_core::flows::launch::LaunchNotice +impl core::cmp::PartialEq for devlaunch_core::flows::launch::LaunchNotice +pub fn devlaunch_core::flows::launch::LaunchNotice::eq(&self, &devlaunch_core::flows::launch::LaunchNotice) -> bool +impl core::fmt::Debug for devlaunch_core::flows::launch::LaunchNotice +pub fn devlaunch_core::flows::launch::LaunchNotice::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::launch::LaunchNotice +pub enum devlaunch_core::flows::launch::LaunchVerb +pub devlaunch_core::flows::launch::LaunchVerb::Attach +pub devlaunch_core::flows::launch::LaunchVerb::Attach::command: core::option::Option +pub devlaunch_core::flows::launch::LaunchVerb::Code +pub devlaunch_core::flows::launch::LaunchVerb::Dotfiles +pub devlaunch_core::flows::launch::LaunchVerb::Recreate +pub devlaunch_core::flows::launch::LaunchVerb::Reset +pub devlaunch_core::flows::launch::LaunchVerb::Restart +pub devlaunch_core::flows::launch::LaunchVerb::Up +impl core::clone::Clone for devlaunch_core::flows::launch::LaunchVerb +pub fn devlaunch_core::flows::launch::LaunchVerb::clone(&self) -> devlaunch_core::flows::launch::LaunchVerb +impl core::cmp::Eq for devlaunch_core::flows::launch::LaunchVerb +impl core::cmp::PartialEq for devlaunch_core::flows::launch::LaunchVerb +pub fn devlaunch_core::flows::launch::LaunchVerb::eq(&self, &devlaunch_core::flows::launch::LaunchVerb) -> bool +impl core::fmt::Debug for devlaunch_core::flows::launch::LaunchVerb +pub fn devlaunch_core::flows::launch::LaunchVerb::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::launch::LaunchVerb +pub struct devlaunch_core::flows::launch::Cold<'a, 'r> +pub devlaunch_core::flows::launch::Cold::clones: &'a devlaunch_core::flows::workspace_clone::WorkspaceCloneManager<'r> +pub devlaunch_core::flows::launch::Cold::storage: &'a mut devlaunch_core::domain::metadata::MetadataStorage +pub struct devlaunch_core::flows::launch::ColdPath<'r, 'e> +impl<'r, 'e> devlaunch_core::flows::launch::ColdPath<'r, 'e> +pub fn devlaunch_core::flows::launch::ColdPath<'r, 'e>::new(&'r dyn devlaunch_runner::Runner, &'e mut dyn devlaunch_core::notices::Notices) -> Self +pub fn devlaunch_core::flows::launch::ColdPath<'r, 'e>::records(&mut self) -> core::result::Result<&mut devlaunch_core::flows::records::Records<'r>, devlaunch_core::flows::records::StartupError> +impl<'r> devlaunch_core::flows::launch::ColdMachinery<'r> for devlaunch_core::flows::launch::ColdPath<'r, '_> +pub fn devlaunch_core::flows::launch::ColdPath<'r, '_>::open(&mut self) -> core::result::Result, devlaunch_core::flows::launch::ColdRefused> +pub struct devlaunch_core::flows::launch::Host +impl devlaunch_core::flows::launch::Host +pub fn devlaunch_core::flows::launch::Host::from_process(impl core::convert::Into) -> Self +impl core::clone::Clone for devlaunch_core::flows::launch::Host +pub fn devlaunch_core::flows::launch::Host::clone(&self) -> devlaunch_core::flows::launch::Host +impl core::cmp::Eq for devlaunch_core::flows::launch::Host +impl core::cmp::PartialEq for devlaunch_core::flows::launch::Host +pub fn devlaunch_core::flows::launch::Host::eq(&self, &devlaunch_core::flows::launch::Host) -> bool +impl core::default::Default for devlaunch_core::flows::launch::Host +pub fn devlaunch_core::flows::launch::Host::default() -> devlaunch_core::flows::launch::Host +impl core::fmt::Debug for devlaunch_core::flows::launch::Host +pub fn devlaunch_core::flows::launch::Host::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::launch::Host +pub struct devlaunch_core::flows::launch::Launch<'a, 'r, 'l> +impl<'a, 'r, 'l> devlaunch_core::flows::launch::Launch<'a, 'r, 'l> +pub fn devlaunch_core::flows::launch::Launch<'a, 'r, 'l>::new(&'a mut devlaunch_core::flows::listing::CommandContext<'r>, &'a mut devlaunch_core::flows::lifecycle::Refresh<'l>, &'a mut dyn devlaunch_core::flows::launch::ColdMachinery<'r>, &'a dyn devlaunch_core::flows::launch::Provision, &'a devlaunch_core::flows::launch::Host, &'a mut dyn core::ops::function::FnMut(&str), &'a mut dyn devlaunch_core::notices::Notices) -> Self +pub fn devlaunch_core::flows::launch::Launch<'a, 'r, 'l>::recognised_as(self, core::option::Option) -> Self +pub fn devlaunch_core::flows::launch::Launch<'a, 'r, 'l>::run(&mut self, &str, &devlaunch_core::flows::launch::LaunchVerb, core::option::Option<&devlaunch_core::domain::spec::DevcontainerPath>) -> core::result::Result +pub struct devlaunch_core::flows::launch::ToolProvisioning<'e> +impl<'e> devlaunch_core::flows::launch::ToolProvisioning<'e> +pub fn devlaunch_core::flows::launch::ToolProvisioning<'e>::from_env(&std::path::Path, &'e mut dyn devlaunch_core::notices::Notices) -> Self +impl devlaunch_core::flows::launch::Provision for devlaunch_core::flows::launch::ToolProvisioning<'_> +pub fn devlaunch_core::flows::launch::ToolProvisioning<'_>::provision_tools(&self, &dyn devlaunch_runner::Runner, &str, devlaunch_core::flows::provision::PassOccasion, core::option::Option<&str>) -> core::result::Result, devlaunch_core::flows::provision::DevpodMissing> +pub fn devlaunch_core::flows::launch::ToolProvisioning<'_>::remembered_claude(&self, &str) -> core::option::Option +pub trait devlaunch_core::flows::launch::ColdMachinery<'r> +pub fn devlaunch_core::flows::launch::ColdMachinery::open(&mut self) -> core::result::Result, devlaunch_core::flows::launch::ColdRefused> +impl<'r> devlaunch_core::flows::launch::ColdMachinery<'r> for devlaunch_core::flows::launch::ColdPath<'r, '_> +pub fn devlaunch_core::flows::launch::ColdPath<'r, '_>::open(&mut self) -> core::result::Result, devlaunch_core::flows::launch::ColdRefused> +pub trait devlaunch_core::flows::launch::Provision +pub fn devlaunch_core::flows::launch::Provision::provision_tools(&self, &dyn devlaunch_runner::Runner, &str, devlaunch_core::flows::provision::PassOccasion, core::option::Option<&str>) -> core::result::Result, devlaunch_core::flows::provision::DevpodMissing> +pub fn devlaunch_core::flows::launch::Provision::remembered_claude(&self, &str) -> core::option::Option +impl devlaunch_core::flows::launch::Provision for devlaunch_core::flows::launch::ToolProvisioning<'_> +pub fn devlaunch_core::flows::launch::ToolProvisioning<'_>::provision_tools(&self, &dyn devlaunch_runner::Runner, &str, devlaunch_core::flows::provision::PassOccasion, core::option::Option<&str>) -> core::result::Result, devlaunch_core::flows::provision::DevpodMissing> +pub fn devlaunch_core::flows::launch::ToolProvisioning<'_>::remembered_claude(&self, &str) -> core::option::Option +pub struct devlaunch_core::flows::lifecycle::Refresh<'a> +impl<'a> devlaunch_core::flows::lifecycle::Refresh<'a> +pub fn devlaunch_core::flows::lifecycle::Refresh<'a>::ask(&mut self, &dyn devlaunch_runner::Runner, devlaunch_core::flows::lifecycle::RefreshReason) -> devlaunch_core::flows::lifecycle::RefreshSpawn +pub fn devlaunch_core::flows::lifecycle::Refresh<'a>::new(&'a devlaunch_core::flows::lifecycle::SelfInvocation, &'a std::path::Path) -> Self +pub fn devlaunch_core::flows::lifecycle::Refresh<'a>::rearm(&mut self) +pub fn devlaunch_core::flows::lifecycle::Refresh<'a>::spawned(&self) -> bool +pub struct devlaunch_core::flows::lifecycle::SelfInvocation +impl devlaunch_core::flows::lifecycle::SelfInvocation +pub fn devlaunch_core::flows::lifecycle::SelfInvocation::new(impl core::convert::Into) -> Self +impl core::clone::Clone for devlaunch_core::flows::lifecycle::SelfInvocation +pub fn devlaunch_core::flows::lifecycle::SelfInvocation::clone(&self) -> devlaunch_core::flows::lifecycle::SelfInvocation +impl core::cmp::Eq for devlaunch_core::flows::lifecycle::SelfInvocation +impl core::cmp::PartialEq for devlaunch_core::flows::lifecycle::SelfInvocation +pub fn devlaunch_core::flows::lifecycle::SelfInvocation::eq(&self, &devlaunch_core::flows::lifecycle::SelfInvocation) -> bool +impl core::fmt::Debug for devlaunch_core::flows::lifecycle::SelfInvocation +pub fn devlaunch_core::flows::lifecycle::SelfInvocation::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::lifecycle::SelfInvocation +pub fn devlaunch_core::flows::lifecycle::workspace_delete(&mut devlaunch_core::flows::listing::CommandContext<'_>, &mut devlaunch_core::flows::lifecycle::Refresh<'_>, &devlaunch_core::flows::workspace_clone::WorkspaceCloneManager<'_>, &mut devlaunch_core::domain::metadata::MetadataStorage, core::option::Option<&devlaunch_core::clients::devpod_home::DevpodHome>, &str, devlaunch_core::flows::lifecycle::Insistence, devlaunch_core::flows::lifecycle::Persistence, &mut dyn core::ops::function::FnMut(devlaunch_core::flows::lifecycle::DeleteStalled), &mut dyn devlaunch_core::notices::Notices) -> core::result::Result +pub fn devlaunch_core::flows::lifecycle::workspace_stop(&mut devlaunch_core::flows::listing::CommandContext<'_>, &mut devlaunch_core::flows::lifecycle::Refresh<'_>, &str) -> core::result::Result +pub struct devlaunch_core::flows::listing::CommandContext<'r> +impl<'r> devlaunch_core::flows::listing::CommandContext<'r> +pub fn devlaunch_core::flows::listing::CommandContext<'r>::git(&self) -> devlaunch_core::clients::git::Git<'r> +pub fn devlaunch_core::flows::listing::CommandContext<'r>::new(&'r dyn devlaunch_runner::Runner) -> Self +pub fn devlaunch_core::flows::listing::CommandContext<'r>::workspaces(&mut self) -> core::result::Result, devlaunch_core::clients::devpod::ListingUnreadable> +pub fn devlaunch_core::flows::listing::enriched_listing(&mut devlaunch_core::flows::listing::CommandContext<'_>, &devlaunch_core::flows::listing::DlView<'_>, devlaunch_core::flows::listing::Sizes) -> core::result::Result, devlaunch_core::clients::devpod::ListingUnreadable> +pub fn devlaunch_core::flows::listing::json_document(&[devlaunch_core::flows::listing::ListedWorkspace]) -> serde_json::value::Value +pub enum devlaunch_core::flows::provision::ProvisionEvent +pub devlaunch_core::flows::provision::ProvisionEvent::NotInstalled +pub devlaunch_core::flows::provision::ProvisionEvent::NotInstalled::exit: devlaunch_runner::Exit +pub devlaunch_core::flows::provision::ProvisionEvent::NotInstalled::tools: alloc::vec::Vec<&'static str> +pub devlaunch_core::flows::provision::ProvisionEvent::NotInstalled::workspace: alloc::string::String +pub devlaunch_core::flows::provision::ProvisionEvent::PayloadNotBundled +pub devlaunch_core::flows::provision::ProvisionEvent::PayloadNotBundled::failure: devlaunch_core::flows::provision::BundleFailed +pub devlaunch_core::flows::provision::ProvisionEvent::ProvisioningDisabled +pub devlaunch_core::flows::provision::ProvisionEvent::ProvisioningDisabled::workspace: alloc::string::String +pub devlaunch_core::flows::provision::ProvisionEvent::StageFailed +pub devlaunch_core::flows::provision::ProvisionEvent::StageFailed::loudness: devlaunch_core::flows::provision::FailureLevel +pub devlaunch_core::flows::provision::ProvisionEvent::StageFailed::stage: &'static str +pub devlaunch_core::flows::provision::ProvisionEvent::StageFailed::status: i32 +pub devlaunch_core::flows::provision::ProvisionEvent::StageFailed::workspace: alloc::string::String +pub devlaunch_core::flows::provision::ProvisionEvent::StageNotReported +pub devlaunch_core::flows::provision::ProvisionEvent::StageNotReported::loudness: devlaunch_core::flows::provision::FailureLevel +pub devlaunch_core::flows::provision::ProvisionEvent::StageNotReported::stage: &'static str +pub devlaunch_core::flows::provision::ProvisionEvent::StageNotReported::workspace: alloc::string::String +pub devlaunch_core::flows::provision::ProvisionEvent::TripRefused +pub devlaunch_core::flows::provision::ProvisionEvent::TripRefused::refusal: devlaunch_core::clients::devpod::NotRun +pub devlaunch_core::flows::provision::ProvisionEvent::TripRefused::workspace: alloc::string::String +impl core::clone::Clone for devlaunch_core::flows::provision::ProvisionEvent +pub fn devlaunch_core::flows::provision::ProvisionEvent::clone(&self) -> devlaunch_core::flows::provision::ProvisionEvent +impl core::cmp::Eq for devlaunch_core::flows::provision::ProvisionEvent +impl core::cmp::PartialEq for devlaunch_core::flows::provision::ProvisionEvent +pub fn devlaunch_core::flows::provision::ProvisionEvent::eq(&self, &devlaunch_core::flows::provision::ProvisionEvent) -> bool +impl core::fmt::Debug for devlaunch_core::flows::provision::ProvisionEvent +pub fn devlaunch_core::flows::provision::ProvisionEvent::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::provision::ProvisionEvent +pub enum devlaunch_core::flows::records::RecordsNotice +pub devlaunch_core::flows::records::RecordsNotice::Metadata(devlaunch_core::domain::metadata::Notice) +pub devlaunch_core::flows::records::RecordsNotice::Migrated(devlaunch_core::flows::migration::MigrationReport) +pub devlaunch_core::flows::records::RecordsNotice::MigrationRefused(devlaunch_core::domain::metadata::MetadataError) +pub devlaunch_core::flows::records::RecordsNotice::RetiredKey(devlaunch_core::domain::config::RetiredKey) +impl core::clone::Clone for devlaunch_core::flows::records::RecordsNotice +pub fn devlaunch_core::flows::records::RecordsNotice::clone(&self) -> devlaunch_core::flows::records::RecordsNotice +impl core::cmp::Eq for devlaunch_core::flows::records::RecordsNotice +impl core::cmp::PartialEq for devlaunch_core::flows::records::RecordsNotice +pub fn devlaunch_core::flows::records::RecordsNotice::eq(&self, &devlaunch_core::flows::records::RecordsNotice) -> bool +impl core::fmt::Debug for devlaunch_core::flows::records::RecordsNotice +pub fn devlaunch_core::flows::records::RecordsNotice::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::records::RecordsNotice +pub enum devlaunch_core::flows::records::StartupError +pub devlaunch_core::flows::records::StartupError::Config(devlaunch_core::domain::config::ConfigError) +pub devlaunch_core::flows::records::StartupError::Metadata(devlaunch_core::domain::metadata::MetadataError) +pub devlaunch_core::flows::records::StartupError::NoHomeDirectory +impl core::clone::Clone for devlaunch_core::flows::records::StartupError +pub fn devlaunch_core::flows::records::StartupError::clone(&self) -> devlaunch_core::flows::records::StartupError +impl core::cmp::Eq for devlaunch_core::flows::records::StartupError +impl core::cmp::PartialEq for devlaunch_core::flows::records::StartupError +pub fn devlaunch_core::flows::records::StartupError::eq(&self, &devlaunch_core::flows::records::StartupError) -> bool +impl core::convert::From for devlaunch_core::flows::records::StartupError +pub fn devlaunch_core::flows::records::StartupError::from(devlaunch_core::domain::config::ConfigError) -> Self +impl core::convert::From for devlaunch_core::flows::records::StartupError +pub fn devlaunch_core::flows::records::StartupError::from(devlaunch_core::domain::metadata::MetadataError) -> Self +impl core::convert::From for devlaunch_core::flows::records::StartupError +pub fn devlaunch_core::flows::records::StartupError::from(devlaunch_core::domain::xdg::NoHomeDirectory) -> Self +impl core::fmt::Debug for devlaunch_core::flows::records::StartupError +pub fn devlaunch_core::flows::records::StartupError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +impl core::marker::StructuralPartialEq for devlaunch_core::flows::records::StartupError +pub trait devlaunch_core::notices::Notices +pub fn devlaunch_core::notices::Notices::say(&mut self, T) +impl devlaunch_core::notices::Notices for alloc::vec::Vec +pub const devlaunch_core::timing::HANDOFF_VAR: &str +pub const devlaunch_core::timing::PREWARM_VAR: &str diff --git a/rust/devlaunch-core/public-api.rest.txt b/rust/devlaunch-core/public-api.rest.txt index e8ab10d7..180447cd 100644 --- a/rust/devlaunch-core/public-api.rest.txt +++ b/rust/devlaunch-core/public-api.rest.txt @@ -1,137 +1,5 @@ pub mod devlaunch_core -impl core::clone::Clone for devlaunch_core::flows::launch::ColdRefused -pub fn devlaunch_core::flows::launch::ColdRefused::clone(&self) -> devlaunch_core::flows::launch::ColdRefused -impl core::cmp::Eq for devlaunch_core::flows::launch::ColdRefused -impl core::cmp::PartialEq for devlaunch_core::flows::launch::ColdRefused -pub fn devlaunch_core::flows::launch::ColdRefused::eq(&self, &devlaunch_core::flows::launch::ColdRefused) -> bool -impl core::fmt::Debug for devlaunch_core::flows::launch::ColdRefused -pub fn devlaunch_core::flows::launch::ColdRefused::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result -impl core::marker::StructuralPartialEq for devlaunch_core::flows::launch::ColdRefused -impl core::clone::Clone for devlaunch_core::flows::launch::LaunchNotice -pub fn devlaunch_core::flows::launch::LaunchNotice::clone(&self) -> devlaunch_core::flows::launch::LaunchNotice -impl core::cmp::Eq for devlaunch_core::flows::launch::LaunchNotice -impl core::cmp::PartialEq for devlaunch_core::flows::launch::LaunchNotice -pub fn devlaunch_core::flows::launch::LaunchNotice::eq(&self, &devlaunch_core::flows::launch::LaunchNotice) -> bool -impl core::fmt::Debug for devlaunch_core::flows::launch::LaunchNotice -pub fn devlaunch_core::flows::launch::LaunchNotice::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result -impl core::marker::StructuralPartialEq for devlaunch_core::flows::launch::LaunchNotice -impl core::clone::Clone for devlaunch_core::flows::launch::LaunchVerb -pub fn devlaunch_core::flows::launch::LaunchVerb::clone(&self) -> devlaunch_core::flows::launch::LaunchVerb -impl core::cmp::Eq for devlaunch_core::flows::launch::LaunchVerb -impl core::cmp::PartialEq for devlaunch_core::flows::launch::LaunchVerb -pub fn devlaunch_core::flows::launch::LaunchVerb::eq(&self, &devlaunch_core::flows::launch::LaunchVerb) -> bool -impl core::fmt::Debug for devlaunch_core::flows::launch::LaunchVerb -pub fn devlaunch_core::flows::launch::LaunchVerb::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result -impl core::marker::StructuralPartialEq for devlaunch_core::flows::launch::LaunchVerb -impl core::clone::Clone for devlaunch_core::flows::provision::ProvisionEvent -pub fn devlaunch_core::flows::provision::ProvisionEvent::clone(&self) -> devlaunch_core::flows::provision::ProvisionEvent -impl core::cmp::Eq for devlaunch_core::flows::provision::ProvisionEvent -impl core::cmp::PartialEq for devlaunch_core::flows::provision::ProvisionEvent -pub fn devlaunch_core::flows::provision::ProvisionEvent::eq(&self, &devlaunch_core::flows::provision::ProvisionEvent) -> bool -impl core::fmt::Debug for devlaunch_core::flows::provision::ProvisionEvent -pub fn devlaunch_core::flows::provision::ProvisionEvent::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result -impl core::marker::StructuralPartialEq for devlaunch_core::flows::provision::ProvisionEvent -impl core::clone::Clone for devlaunch_core::flows::records::RecordsNotice -pub fn devlaunch_core::flows::records::RecordsNotice::clone(&self) -> devlaunch_core::flows::records::RecordsNotice -impl core::cmp::Eq for devlaunch_core::flows::records::RecordsNotice -impl core::cmp::PartialEq for devlaunch_core::flows::records::RecordsNotice -pub fn devlaunch_core::flows::records::RecordsNotice::eq(&self, &devlaunch_core::flows::records::RecordsNotice) -> bool -impl core::fmt::Debug for devlaunch_core::flows::records::RecordsNotice -pub fn devlaunch_core::flows::records::RecordsNotice::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result -impl core::marker::StructuralPartialEq for devlaunch_core::flows::records::RecordsNotice -impl<'a> core::clone::Clone for devlaunch_core::domain::spec::SpecIdentity<'a> -pub fn devlaunch_core::domain::spec::SpecIdentity<'a>::clone(&self) -> devlaunch_core::domain::spec::SpecIdentity<'a> -impl<'a> core::cmp::Eq for devlaunch_core::domain::spec::SpecIdentity<'a> -impl<'a> core::cmp::PartialEq for devlaunch_core::domain::spec::SpecIdentity<'a> -pub fn devlaunch_core::domain::spec::SpecIdentity<'a>::eq(&self, &devlaunch_core::domain::spec::SpecIdentity<'a>) -> bool -impl<'a> core::fmt::Debug for devlaunch_core::domain::spec::SpecIdentity<'a> -pub fn devlaunch_core::domain::spec::SpecIdentity<'a>::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result -impl<'a> core::marker::StructuralPartialEq for devlaunch_core::domain::spec::SpecIdentity<'a> -impl core::clone::Clone for devlaunch_core::flows::records::StartupError -pub fn devlaunch_core::flows::records::StartupError::clone(&self) -> devlaunch_core::flows::records::StartupError -impl core::cmp::Eq for devlaunch_core::flows::records::StartupError -impl core::cmp::PartialEq for devlaunch_core::flows::records::StartupError -pub fn devlaunch_core::flows::records::StartupError::eq(&self, &devlaunch_core::flows::records::StartupError) -> bool -impl core::convert::From for devlaunch_core::flows::records::StartupError -pub fn devlaunch_core::flows::records::StartupError::from(devlaunch_core::domain::config::ConfigError) -> Self -impl core::convert::From for devlaunch_core::flows::records::StartupError -pub fn devlaunch_core::flows::records::StartupError::from(devlaunch_core::domain::metadata::MetadataError) -> Self -impl core::convert::From for devlaunch_core::flows::records::StartupError -pub fn devlaunch_core::flows::records::StartupError::from(devlaunch_core::domain::xdg::NoHomeDirectory) -> Self -impl core::fmt::Debug for devlaunch_core::flows::records::StartupError -pub fn devlaunch_core::flows::records::StartupError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result -impl core::marker::StructuralPartialEq for devlaunch_core::flows::records::StartupError -impl<'a> core::clone::Clone for devlaunch_core::domain::spec::WorkspaceSpec<'a> -pub fn devlaunch_core::domain::spec::WorkspaceSpec<'a>::clone(&self) -> devlaunch_core::domain::spec::WorkspaceSpec<'a> -impl<'a> core::cmp::Eq for devlaunch_core::domain::spec::WorkspaceSpec<'a> -impl<'a> core::cmp::PartialEq for devlaunch_core::domain::spec::WorkspaceSpec<'a> -pub fn devlaunch_core::domain::spec::WorkspaceSpec<'a>::eq(&self, &devlaunch_core::domain::spec::WorkspaceSpec<'a>) -> bool -impl<'a> core::fmt::Debug for devlaunch_core::domain::spec::WorkspaceSpec<'a> -pub fn devlaunch_core::domain::spec::WorkspaceSpec<'a>::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result -impl<'a> core::marker::Copy for devlaunch_core::domain::spec::WorkspaceSpec<'a> -impl<'a> core::marker::StructuralPartialEq for devlaunch_core::domain::spec::WorkspaceSpec<'a> -impl<'r, 'e> devlaunch_core::flows::launch::ColdPath<'r, 'e> -pub fn devlaunch_core::flows::launch::ColdPath<'r, 'e>::new(&'r dyn devlaunch_runner::Runner, &'e mut dyn devlaunch_core::notices::Notices) -> Self -pub fn devlaunch_core::flows::launch::ColdPath<'r, 'e>::records(&mut self) -> core::result::Result<&mut devlaunch_core::flows::records::Records<'r>, devlaunch_core::flows::records::StartupError> -impl<'r> devlaunch_core::flows::launch::ColdMachinery<'r> for devlaunch_core::flows::launch::ColdPath<'r, '_> -pub fn devlaunch_core::flows::launch::ColdPath<'r, '_>::open(&mut self) -> core::result::Result, devlaunch_core::flows::launch::ColdRefused> -impl<'r> devlaunch_core::flows::listing::CommandContext<'r> -pub fn devlaunch_core::flows::listing::CommandContext<'r>::git(&self) -> devlaunch_core::clients::git::Git<'r> -pub fn devlaunch_core::flows::listing::CommandContext<'r>::new(&'r dyn devlaunch_runner::Runner) -> Self -pub fn devlaunch_core::flows::listing::CommandContext<'r>::workspaces(&mut self) -> core::result::Result, devlaunch_core::clients::devpod::ListingUnreadable> -impl devlaunch_core::domain::spec::DevcontainerPath -pub fn devlaunch_core::domain::spec::DevcontainerPath::as_str(&self) -> &str -impl core::clone::Clone for devlaunch_core::domain::spec::DevcontainerPath -pub fn devlaunch_core::domain::spec::DevcontainerPath::clone(&self) -> devlaunch_core::domain::spec::DevcontainerPath -impl core::cmp::Eq for devlaunch_core::domain::spec::DevcontainerPath -impl core::cmp::PartialEq for devlaunch_core::domain::spec::DevcontainerPath -pub fn devlaunch_core::domain::spec::DevcontainerPath::eq(&self, &devlaunch_core::domain::spec::DevcontainerPath) -> bool -impl core::fmt::Debug for devlaunch_core::domain::spec::DevcontainerPath -pub fn devlaunch_core::domain::spec::DevcontainerPath::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result -impl core::marker::StructuralPartialEq for devlaunch_core::domain::spec::DevcontainerPath -impl devlaunch_core::flows::launch::Host -pub fn devlaunch_core::flows::launch::Host::from_process(impl core::convert::Into) -> Self -impl core::clone::Clone for devlaunch_core::flows::launch::Host -pub fn devlaunch_core::flows::launch::Host::clone(&self) -> devlaunch_core::flows::launch::Host -impl core::cmp::Eq for devlaunch_core::flows::launch::Host -impl core::cmp::PartialEq for devlaunch_core::flows::launch::Host -pub fn devlaunch_core::flows::launch::Host::eq(&self, &devlaunch_core::flows::launch::Host) -> bool -impl core::default::Default for devlaunch_core::flows::launch::Host -pub fn devlaunch_core::flows::launch::Host::default() -> devlaunch_core::flows::launch::Host -impl core::fmt::Debug for devlaunch_core::flows::launch::Host -pub fn devlaunch_core::flows::launch::Host::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result -impl core::marker::StructuralPartialEq for devlaunch_core::flows::launch::Host -impl<'a, 'r, 'l> devlaunch_core::flows::launch::Launch<'a, 'r, 'l> -pub fn devlaunch_core::flows::launch::Launch<'a, 'r, 'l>::new(&'a mut devlaunch_core::flows::listing::CommandContext<'r>, &'a mut devlaunch_core::flows::lifecycle::Refresh<'l>, &'a mut dyn devlaunch_core::flows::launch::ColdMachinery<'r>, &'a dyn devlaunch_core::flows::launch::Provision, &'a devlaunch_core::flows::launch::Host, &'a mut dyn core::ops::function::FnMut(&str), &'a mut dyn devlaunch_core::notices::Notices) -> Self -pub fn devlaunch_core::flows::launch::Launch<'a, 'r, 'l>::recognised_as(self, core::option::Option) -> Self -pub fn devlaunch_core::flows::launch::Launch<'a, 'r, 'l>::run(&mut self, &str, &devlaunch_core::flows::launch::LaunchVerb, core::option::Option<&devlaunch_core::domain::spec::DevcontainerPath>) -> core::result::Result -impl<'a> devlaunch_core::flows::lifecycle::Refresh<'a> -pub fn devlaunch_core::flows::lifecycle::Refresh<'a>::ask(&mut self, &dyn devlaunch_runner::Runner, devlaunch_core::flows::lifecycle::RefreshReason) -> devlaunch_core::flows::lifecycle::RefreshSpawn -pub fn devlaunch_core::flows::lifecycle::Refresh<'a>::new(&'a devlaunch_core::flows::lifecycle::SelfInvocation, &'a std::path::Path) -> Self -pub fn devlaunch_core::flows::lifecycle::Refresh<'a>::rearm(&mut self) -pub fn devlaunch_core::flows::lifecycle::Refresh<'a>::spawned(&self) -> bool -impl devlaunch_core::flows::lifecycle::SelfInvocation -pub fn devlaunch_core::flows::lifecycle::SelfInvocation::new(impl core::convert::Into) -> Self -impl core::clone::Clone for devlaunch_core::flows::lifecycle::SelfInvocation -pub fn devlaunch_core::flows::lifecycle::SelfInvocation::clone(&self) -> devlaunch_core::flows::lifecycle::SelfInvocation -impl core::cmp::Eq for devlaunch_core::flows::lifecycle::SelfInvocation -impl core::cmp::PartialEq for devlaunch_core::flows::lifecycle::SelfInvocation -pub fn devlaunch_core::flows::lifecycle::SelfInvocation::eq(&self, &devlaunch_core::flows::lifecycle::SelfInvocation) -> bool -impl core::fmt::Debug for devlaunch_core::flows::lifecycle::SelfInvocation -pub fn devlaunch_core::flows::lifecycle::SelfInvocation::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result -impl core::marker::StructuralPartialEq for devlaunch_core::flows::lifecycle::SelfInvocation -impl<'e> devlaunch_core::flows::launch::ToolProvisioning<'e> -pub fn devlaunch_core::flows::launch::ToolProvisioning<'e>::from_env(&std::path::Path, &'e mut dyn devlaunch_core::notices::Notices) -> Self -impl devlaunch_core::flows::launch::Provision for devlaunch_core::flows::launch::ToolProvisioning<'_> -pub fn devlaunch_core::flows::launch::ToolProvisioning<'_>::provision_tools(&self, &dyn devlaunch_runner::Runner, &str, devlaunch_core::flows::provision::PassOccasion, core::option::Option<&str>) -> core::result::Result, devlaunch_core::flows::provision::DevpodMissing> -pub fn devlaunch_core::flows::launch::ToolProvisioning<'_>::remembered_claude(&self, &str) -> core::option::Option -impl<'r> devlaunch_core::flows::launch::ColdMachinery<'r> for devlaunch_core::flows::launch::ColdPath<'r, '_> -pub fn devlaunch_core::flows::launch::ColdPath<'r, '_>::open(&mut self) -> core::result::Result, devlaunch_core::flows::launch::ColdRefused> -impl devlaunch_core::notices::Notices for alloc::vec::Vec pub fn alloc::vec::Vec::say(&mut self, T) -impl devlaunch_core::flows::launch::Provision for devlaunch_core::flows::launch::ToolProvisioning<'_> -pub fn devlaunch_core::flows::launch::ToolProvisioning<'_>::provision_tools(&self, &dyn devlaunch_runner::Runner, &str, devlaunch_core::flows::provision::PassOccasion, core::option::Option<&str>) -> core::result::Result, devlaunch_core::flows::provision::DevpodMissing> -pub fn devlaunch_core::flows::launch::ToolProvisioning<'_>::remembered_claude(&self, &str) -> core::option::Option pub mod devlaunch_core::clients pub mod devlaunch_core::clients::devpod pub enum devlaunch_core::clients::devpod::ContainerState @@ -393,8 +261,6 @@ pub fn devlaunch_core::domain::config::ConfigError::clone(&self) -> devlaunch_co impl core::cmp::Eq for devlaunch_core::domain::config::ConfigError impl core::cmp::PartialEq for devlaunch_core::domain::config::ConfigError pub fn devlaunch_core::domain::config::ConfigError::eq(&self, &devlaunch_core::domain::config::ConfigError) -> bool -impl core::convert::From for devlaunch_core::flows::records::StartupError -pub fn devlaunch_core::flows::records::StartupError::from(devlaunch_core::domain::config::ConfigError) -> Self impl core::convert::From for devlaunch_core::domain::config::ConfigError pub fn devlaunch_core::domain::config::ConfigError::from(devlaunch_core::domain::xdg::NoHomeDirectory) -> Self impl core::fmt::Debug for devlaunch_core::domain::config::ConfigError @@ -513,8 +379,6 @@ pub fn devlaunch_core::domain::metadata::MetadataError::clone(&self) -> devlaunc impl core::cmp::Eq for devlaunch_core::domain::metadata::MetadataError impl core::cmp::PartialEq for devlaunch_core::domain::metadata::MetadataError pub fn devlaunch_core::domain::metadata::MetadataError::eq(&self, &devlaunch_core::domain::metadata::MetadataError) -> bool -impl core::convert::From for devlaunch_core::flows::records::StartupError -pub fn devlaunch_core::flows::records::StartupError::from(devlaunch_core::domain::metadata::MetadataError) -> Self impl core::fmt::Debug for devlaunch_core::domain::metadata::MetadataError pub fn devlaunch_core::domain::metadata::MetadataError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result impl core::marker::StructuralPartialEq for devlaunch_core::domain::metadata::MetadataError @@ -631,52 +495,6 @@ impl core::fmt::Debug for devlaunch_core::domain::spec::DevcontainerRefError pub fn devlaunch_core::domain::spec::DevcontainerRefError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result impl core::marker::Copy for devlaunch_core::domain::spec::DevcontainerRefError impl core::marker::StructuralPartialEq for devlaunch_core::domain::spec::DevcontainerRefError -pub enum devlaunch_core::domain::spec::SpecIdentity<'a> -pub devlaunch_core::domain::spec::SpecIdentity::ExistingName(&'a str) -pub devlaunch_core::domain::spec::SpecIdentity::PathLeaf(&'a str) -pub devlaunch_core::domain::spec::SpecIdentity::RepoLabel(alloc::string::String) -pub devlaunch_core::domain::spec::SpecIdentity::Workspace(alloc::string::String) -impl<'a> core::clone::Clone for devlaunch_core::domain::spec::SpecIdentity<'a> -pub fn devlaunch_core::domain::spec::SpecIdentity<'a>::clone(&self) -> devlaunch_core::domain::spec::SpecIdentity<'a> -impl<'a> core::cmp::Eq for devlaunch_core::domain::spec::SpecIdentity<'a> -impl<'a> core::cmp::PartialEq for devlaunch_core::domain::spec::SpecIdentity<'a> -pub fn devlaunch_core::domain::spec::SpecIdentity<'a>::eq(&self, &devlaunch_core::domain::spec::SpecIdentity<'a>) -> bool -impl<'a> core::fmt::Debug for devlaunch_core::domain::spec::SpecIdentity<'a> -pub fn devlaunch_core::domain::spec::SpecIdentity<'a>::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result -impl<'a> core::marker::StructuralPartialEq for devlaunch_core::domain::spec::SpecIdentity<'a> -pub enum devlaunch_core::domain::spec::WorkspaceSpec<'a> -pub devlaunch_core::domain::spec::WorkspaceSpec::ExistingIdOrName(&'a str) -pub devlaunch_core::domain::spec::WorkspaceSpec::HostPath(&'a str) -pub devlaunch_core::domain::spec::WorkspaceSpec::OwnerRepo -pub devlaunch_core::domain::spec::WorkspaceSpec::OwnerRepo::branch: core::option::Option<&'a str> -pub devlaunch_core::domain::spec::WorkspaceSpec::OwnerRepo::owner: &'a str -pub devlaunch_core::domain::spec::WorkspaceSpec::OwnerRepo::repo: &'a str -pub devlaunch_core::domain::spec::WorkspaceSpec::Path(&'a str) -pub devlaunch_core::domain::spec::WorkspaceSpec::SshUrl(&'a str) -pub devlaunch_core::domain::spec::WorkspaceSpec::Url(&'a str) -impl<'a> core::clone::Clone for devlaunch_core::domain::spec::WorkspaceSpec<'a> -pub fn devlaunch_core::domain::spec::WorkspaceSpec<'a>::clone(&self) -> devlaunch_core::domain::spec::WorkspaceSpec<'a> -impl<'a> core::cmp::Eq for devlaunch_core::domain::spec::WorkspaceSpec<'a> -impl<'a> core::cmp::PartialEq for devlaunch_core::domain::spec::WorkspaceSpec<'a> -pub fn devlaunch_core::domain::spec::WorkspaceSpec<'a>::eq(&self, &devlaunch_core::domain::spec::WorkspaceSpec<'a>) -> bool -impl<'a> core::fmt::Debug for devlaunch_core::domain::spec::WorkspaceSpec<'a> -pub fn devlaunch_core::domain::spec::WorkspaceSpec<'a>::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result -impl<'a> core::marker::Copy for devlaunch_core::domain::spec::WorkspaceSpec<'a> -impl<'a> core::marker::StructuralPartialEq for devlaunch_core::domain::spec::WorkspaceSpec<'a> -pub struct devlaunch_core::domain::spec::DevcontainerPath(_) -impl devlaunch_core::domain::spec::DevcontainerPath -pub fn devlaunch_core::domain::spec::DevcontainerPath::as_str(&self) -> &str -impl core::clone::Clone for devlaunch_core::domain::spec::DevcontainerPath -pub fn devlaunch_core::domain::spec::DevcontainerPath::clone(&self) -> devlaunch_core::domain::spec::DevcontainerPath -impl core::cmp::Eq for devlaunch_core::domain::spec::DevcontainerPath -impl core::cmp::PartialEq for devlaunch_core::domain::spec::DevcontainerPath -pub fn devlaunch_core::domain::spec::DevcontainerPath::eq(&self, &devlaunch_core::domain::spec::DevcontainerPath) -> bool -impl core::fmt::Debug for devlaunch_core::domain::spec::DevcontainerPath -pub fn devlaunch_core::domain::spec::DevcontainerPath::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result -impl core::marker::StructuralPartialEq for devlaunch_core::domain::spec::DevcontainerPath -pub fn devlaunch_core::domain::spec::identity(&str) -> core::result::Result, devlaunch_core::domain::workspace_id::UnsafeName> -pub fn devlaunch_core::domain::spec::parse(&str) -> devlaunch_core::domain::spec::WorkspaceSpec<'_> -pub fn devlaunch_core::domain::spec::resolve_devcontainer_ref(&str) -> core::result::Result pub mod devlaunch_core::domain::workspace_id pub enum devlaunch_core::domain::workspace_id::NamePart pub devlaunch_core::domain::workspace_id::NamePart::Owner @@ -794,8 +612,6 @@ impl core::convert::From for devla pub fn devlaunch_core::domain::config::ConfigError::from(devlaunch_core::domain::xdg::NoHomeDirectory) -> Self impl core::convert::From for devlaunch_core::flows::completion::InstallError pub fn devlaunch_core::flows::completion::InstallError::from(devlaunch_core::domain::xdg::NoHomeDirectory) -> Self -impl core::convert::From for devlaunch_core::flows::records::StartupError -pub fn devlaunch_core::flows::records::StartupError::from(devlaunch_core::domain::xdg::NoHomeDirectory) -> Self impl core::fmt::Debug for devlaunch_core::domain::xdg::NoHomeDirectory pub fn devlaunch_core::domain::xdg::NoHomeDirectory::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result impl core::marker::Copy for devlaunch_core::domain::xdg::NoHomeDirectory @@ -1126,17 +942,6 @@ pub fn devlaunch_core::flows::launch::BranchNotNamed::eq(&self, &devlaunch_core: impl core::fmt::Debug for devlaunch_core::flows::launch::BranchNotNamed pub fn devlaunch_core::flows::launch::BranchNotNamed::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result impl core::marker::StructuralPartialEq for devlaunch_core::flows::launch::BranchNotNamed -pub enum devlaunch_core::flows::launch::ColdRefused -pub devlaunch_core::flows::launch::ColdRefused::NoColdPath -pub devlaunch_core::flows::launch::ColdRefused::Startup(devlaunch_core::flows::records::StartupError) -impl core::clone::Clone for devlaunch_core::flows::launch::ColdRefused -pub fn devlaunch_core::flows::launch::ColdRefused::clone(&self) -> devlaunch_core::flows::launch::ColdRefused -impl core::cmp::Eq for devlaunch_core::flows::launch::ColdRefused -impl core::cmp::PartialEq for devlaunch_core::flows::launch::ColdRefused -pub fn devlaunch_core::flows::launch::ColdRefused::eq(&self, &devlaunch_core::flows::launch::ColdRefused) -> bool -impl core::fmt::Debug for devlaunch_core::flows::launch::ColdRefused -pub fn devlaunch_core::flows::launch::ColdRefused::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result -impl core::marker::StructuralPartialEq for devlaunch_core::flows::launch::ColdRefused pub enum devlaunch_core::flows::launch::LaunchAborted pub devlaunch_core::flows::launch::LaunchAborted::DevpodNotRun(devlaunch_core::clients::devpod::NotRun) pub devlaunch_core::flows::launch::LaunchAborted::ListingUnreadable(devlaunch_core::clients::devpod::ListingUnreadable) @@ -1149,55 +954,6 @@ pub fn devlaunch_core::flows::launch::LaunchAborted::eq(&self, &devlaunch_core:: impl core::fmt::Debug for devlaunch_core::flows::launch::LaunchAborted pub fn devlaunch_core::flows::launch::LaunchAborted::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result impl core::marker::StructuralPartialEq for devlaunch_core::flows::launch::LaunchAborted -pub enum devlaunch_core::flows::launch::LaunchNotice -pub devlaunch_core::flows::launch::LaunchNotice::AlreadyRunning -pub devlaunch_core::flows::launch::LaunchNotice::AlreadyRunning::workspace_id: alloc::string::String -pub devlaunch_core::flows::launch::LaunchNotice::AlreadyRunningAttaching -pub devlaunch_core::flows::launch::LaunchNotice::AlreadyRunningAttaching::workspace_id: alloc::string::String -pub devlaunch_core::flows::launch::LaunchNotice::BroughtUpBySibling -pub devlaunch_core::flows::launch::LaunchNotice::BroughtUpBySibling::workspace_id: alloc::string::String -pub devlaunch_core::flows::launch::LaunchNotice::Cache(devlaunch_core::flows::repo_manager::CacheNotice) -pub devlaunch_core::flows::launch::LaunchNotice::CreateNeverFinished -pub devlaunch_core::flows::launch::LaunchNotice::CreateNeverFinished::workspace_id: alloc::string::String -pub devlaunch_core::flows::launch::LaunchNotice::DevcontainerIgnoredRunning -pub devlaunch_core::flows::launch::LaunchNotice::DevcontainerIgnoredRunning::spec: alloc::string::String -pub devlaunch_core::flows::launch::LaunchNotice::DevcontainerIgnoredRunning::workspace_id: alloc::string::String -pub devlaunch_core::flows::launch::LaunchNotice::DevpodSessionFailed -pub devlaunch_core::flows::launch::LaunchNotice::DevpodSessionFailed::exit: devlaunch_runner::Exit -pub devlaunch_core::flows::launch::LaunchNotice::LaunchLockUnavailable -pub devlaunch_core::flows::launch::LaunchNotice::LaunchLockUnavailable::reason: alloc::string::String -pub devlaunch_core::flows::launch::LaunchNotice::LaunchLockUnavailable::workspace_id: alloc::string::String -pub devlaunch_core::flows::launch::LaunchNotice::Lifecycle(devlaunch_core::flows::lifecycle::LifecycleNotice) -pub devlaunch_core::flows::launch::LaunchNotice::NoDevpodSshConfig -pub devlaunch_core::flows::launch::LaunchNotice::NoDevpodSshConfig::looked_in: std::path::PathBuf -pub devlaunch_core::flows::launch::LaunchNotice::NoDevpodSshConfig::workspace_id: alloc::string::String -pub devlaunch_core::flows::launch::LaunchNotice::NoGitHubToken(devlaunch_core::clients::gh::GhEvent) -pub devlaunch_core::flows::launch::LaunchNotice::NoTerminalAlias -pub devlaunch_core::flows::launch::LaunchNotice::NoTerminalAlias::config: std::path::PathBuf -pub devlaunch_core::flows::launch::LaunchNotice::NoTerminalAlias::workspace_id: alloc::string::String -pub devlaunch_core::flows::launch::LaunchNotice::PixiCacheNotADirectory -pub devlaunch_core::flows::launch::LaunchNotice::PixiCacheNotADirectory::source: std::path::PathBuf -pub devlaunch_core::flows::launch::LaunchNotice::PixiCacheNotCreated -pub devlaunch_core::flows::launch::LaunchNotice::PixiCacheNotCreated::reason: alloc::string::String -pub devlaunch_core::flows::launch::LaunchNotice::PixiCacheNotCreated::source: std::path::PathBuf -pub devlaunch_core::flows::launch::LaunchNotice::SshCommand -pub devlaunch_core::flows::launch::LaunchNotice::SshCommand::argv: alloc::vec::Vec -pub devlaunch_core::flows::launch::LaunchNotice::SshConfigUnlocatable -pub devlaunch_core::flows::launch::LaunchNotice::StartingForDotfiles -pub devlaunch_core::flows::launch::LaunchNotice::StartingForDotfiles::workspace_id: alloc::string::String -pub devlaunch_core::flows::launch::LaunchNotice::TerminalTitle(devlaunch_core::flows::launch::TerminalTitle) -pub devlaunch_core::flows::launch::LaunchNotice::TokenNotStaged -pub devlaunch_core::flows::launch::LaunchNotice::TokenNotStaged::reason: alloc::string::String -pub devlaunch_core::flows::launch::LaunchNotice::WaitingForSiblingLaunch -pub devlaunch_core::flows::launch::LaunchNotice::WaitingForSiblingLaunch::workspace_id: alloc::string::String -impl core::clone::Clone for devlaunch_core::flows::launch::LaunchNotice -pub fn devlaunch_core::flows::launch::LaunchNotice::clone(&self) -> devlaunch_core::flows::launch::LaunchNotice -impl core::cmp::Eq for devlaunch_core::flows::launch::LaunchNotice -impl core::cmp::PartialEq for devlaunch_core::flows::launch::LaunchNotice -pub fn devlaunch_core::flows::launch::LaunchNotice::eq(&self, &devlaunch_core::flows::launch::LaunchNotice) -> bool -impl core::fmt::Debug for devlaunch_core::flows::launch::LaunchNotice -pub fn devlaunch_core::flows::launch::LaunchNotice::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result -impl core::marker::StructuralPartialEq for devlaunch_core::flows::launch::LaunchNotice pub enum devlaunch_core::flows::launch::LaunchRefusal pub devlaunch_core::flows::launch::LaunchRefusal::BranchNotNamed pub devlaunch_core::flows::launch::LaunchRefusal::BranchNotNamed::error: devlaunch_core::flows::launch::BranchNotNamed @@ -1224,23 +980,6 @@ pub fn devlaunch_core::flows::launch::LaunchRefusal::eq(&self, &devlaunch_core:: impl core::fmt::Debug for devlaunch_core::flows::launch::LaunchRefusal pub fn devlaunch_core::flows::launch::LaunchRefusal::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result impl core::marker::StructuralPartialEq for devlaunch_core::flows::launch::LaunchRefusal -pub enum devlaunch_core::flows::launch::LaunchVerb -pub devlaunch_core::flows::launch::LaunchVerb::Attach -pub devlaunch_core::flows::launch::LaunchVerb::Attach::command: core::option::Option -pub devlaunch_core::flows::launch::LaunchVerb::Code -pub devlaunch_core::flows::launch::LaunchVerb::Dotfiles -pub devlaunch_core::flows::launch::LaunchVerb::Recreate -pub devlaunch_core::flows::launch::LaunchVerb::Reset -pub devlaunch_core::flows::launch::LaunchVerb::Restart -pub devlaunch_core::flows::launch::LaunchVerb::Up -impl core::clone::Clone for devlaunch_core::flows::launch::LaunchVerb -pub fn devlaunch_core::flows::launch::LaunchVerb::clone(&self) -> devlaunch_core::flows::launch::LaunchVerb -impl core::cmp::Eq for devlaunch_core::flows::launch::LaunchVerb -impl core::cmp::PartialEq for devlaunch_core::flows::launch::LaunchVerb -pub fn devlaunch_core::flows::launch::LaunchVerb::eq(&self, &devlaunch_core::flows::launch::LaunchVerb) -> bool -impl core::fmt::Debug for devlaunch_core::flows::launch::LaunchVerb -pub fn devlaunch_core::flows::launch::LaunchVerb::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result -impl core::marker::StructuralPartialEq for devlaunch_core::flows::launch::LaunchVerb pub enum devlaunch_core::flows::launch::Launched pub devlaunch_core::flows::launch::Launched::AlreadyRunning pub devlaunch_core::flows::launch::Launched::Ready @@ -1365,39 +1104,6 @@ pub fn devlaunch_core::flows::launch::TerminalTitle::eq(&self, &devlaunch_core:: impl core::fmt::Debug for devlaunch_core::flows::launch::TerminalTitle pub fn devlaunch_core::flows::launch::TerminalTitle::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result impl core::marker::StructuralPartialEq for devlaunch_core::flows::launch::TerminalTitle -pub struct devlaunch_core::flows::launch::Cold<'a, 'r> -pub devlaunch_core::flows::launch::Cold::clones: &'a devlaunch_core::flows::workspace_clone::WorkspaceCloneManager<'r> -pub devlaunch_core::flows::launch::Cold::storage: &'a mut devlaunch_core::domain::metadata::MetadataStorage -pub struct devlaunch_core::flows::launch::ColdPath<'r, 'e> -impl<'r, 'e> devlaunch_core::flows::launch::ColdPath<'r, 'e> -pub fn devlaunch_core::flows::launch::ColdPath<'r, 'e>::new(&'r dyn devlaunch_runner::Runner, &'e mut dyn devlaunch_core::notices::Notices) -> Self -pub fn devlaunch_core::flows::launch::ColdPath<'r, 'e>::records(&mut self) -> core::result::Result<&mut devlaunch_core::flows::records::Records<'r>, devlaunch_core::flows::records::StartupError> -impl<'r> devlaunch_core::flows::launch::ColdMachinery<'r> for devlaunch_core::flows::launch::ColdPath<'r, '_> -pub fn devlaunch_core::flows::launch::ColdPath<'r, '_>::open(&mut self) -> core::result::Result, devlaunch_core::flows::launch::ColdRefused> -pub struct devlaunch_core::flows::launch::Host -impl devlaunch_core::flows::launch::Host -pub fn devlaunch_core::flows::launch::Host::from_process(impl core::convert::Into) -> Self -impl core::clone::Clone for devlaunch_core::flows::launch::Host -pub fn devlaunch_core::flows::launch::Host::clone(&self) -> devlaunch_core::flows::launch::Host -impl core::cmp::Eq for devlaunch_core::flows::launch::Host -impl core::cmp::PartialEq for devlaunch_core::flows::launch::Host -pub fn devlaunch_core::flows::launch::Host::eq(&self, &devlaunch_core::flows::launch::Host) -> bool -impl core::default::Default for devlaunch_core::flows::launch::Host -pub fn devlaunch_core::flows::launch::Host::default() -> devlaunch_core::flows::launch::Host -impl core::fmt::Debug for devlaunch_core::flows::launch::Host -pub fn devlaunch_core::flows::launch::Host::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result -impl core::marker::StructuralPartialEq for devlaunch_core::flows::launch::Host -pub struct devlaunch_core::flows::launch::Launch<'a, 'r, 'l> -impl<'a, 'r, 'l> devlaunch_core::flows::launch::Launch<'a, 'r, 'l> -pub fn devlaunch_core::flows::launch::Launch<'a, 'r, 'l>::new(&'a mut devlaunch_core::flows::listing::CommandContext<'r>, &'a mut devlaunch_core::flows::lifecycle::Refresh<'l>, &'a mut dyn devlaunch_core::flows::launch::ColdMachinery<'r>, &'a dyn devlaunch_core::flows::launch::Provision, &'a devlaunch_core::flows::launch::Host, &'a mut dyn core::ops::function::FnMut(&str), &'a mut dyn devlaunch_core::notices::Notices) -> Self -pub fn devlaunch_core::flows::launch::Launch<'a, 'r, 'l>::recognised_as(self, core::option::Option) -> Self -pub fn devlaunch_core::flows::launch::Launch<'a, 'r, 'l>::run(&mut self, &str, &devlaunch_core::flows::launch::LaunchVerb, core::option::Option<&devlaunch_core::domain::spec::DevcontainerPath>) -> core::result::Result -pub struct devlaunch_core::flows::launch::ToolProvisioning<'e> -impl<'e> devlaunch_core::flows::launch::ToolProvisioning<'e> -pub fn devlaunch_core::flows::launch::ToolProvisioning<'e>::from_env(&std::path::Path, &'e mut dyn devlaunch_core::notices::Notices) -> Self -impl devlaunch_core::flows::launch::Provision for devlaunch_core::flows::launch::ToolProvisioning<'_> -pub fn devlaunch_core::flows::launch::ToolProvisioning<'_>::provision_tools(&self, &dyn devlaunch_runner::Runner, &str, devlaunch_core::flows::provision::PassOccasion, core::option::Option<&str>) -> core::result::Result, devlaunch_core::flows::provision::DevpodMissing> -pub fn devlaunch_core::flows::launch::ToolProvisioning<'_>::remembered_claude(&self, &str) -> core::option::Option pub struct devlaunch_core::flows::launch::UnquotableCommand pub devlaunch_core::flows::launch::UnquotableCommand::command: alloc::string::String impl core::clone::Clone for devlaunch_core::flows::launch::UnquotableCommand @@ -1408,16 +1114,6 @@ pub fn devlaunch_core::flows::launch::UnquotableCommand::eq(&self, &devlaunch_co impl core::fmt::Debug for devlaunch_core::flows::launch::UnquotableCommand pub fn devlaunch_core::flows::launch::UnquotableCommand::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result impl core::marker::StructuralPartialEq for devlaunch_core::flows::launch::UnquotableCommand -pub trait devlaunch_core::flows::launch::ColdMachinery<'r> -pub fn devlaunch_core::flows::launch::ColdMachinery::open(&mut self) -> core::result::Result, devlaunch_core::flows::launch::ColdRefused> -impl<'r> devlaunch_core::flows::launch::ColdMachinery<'r> for devlaunch_core::flows::launch::ColdPath<'r, '_> -pub fn devlaunch_core::flows::launch::ColdPath<'r, '_>::open(&mut self) -> core::result::Result, devlaunch_core::flows::launch::ColdRefused> -pub trait devlaunch_core::flows::launch::Provision -pub fn devlaunch_core::flows::launch::Provision::provision_tools(&self, &dyn devlaunch_runner::Runner, &str, devlaunch_core::flows::provision::PassOccasion, core::option::Option<&str>) -> core::result::Result, devlaunch_core::flows::provision::DevpodMissing> -pub fn devlaunch_core::flows::launch::Provision::remembered_claude(&self, &str) -> core::option::Option -impl devlaunch_core::flows::launch::Provision for devlaunch_core::flows::launch::ToolProvisioning<'_> -pub fn devlaunch_core::flows::launch::ToolProvisioning<'_>::provision_tools(&self, &dyn devlaunch_runner::Runner, &str, devlaunch_core::flows::provision::PassOccasion, core::option::Option<&str>) -> core::result::Result, devlaunch_core::flows::provision::DevpodMissing> -pub fn devlaunch_core::flows::launch::ToolProvisioning<'_>::remembered_claude(&self, &str) -> core::option::Option pub fn devlaunch_core::flows::launch::plan(&str) -> core::result::Result pub fn devlaunch_core::flows::launch::resolve_triple(&mut devlaunch_core::flows::listing::CommandContext<'_>, &mut dyn devlaunch_core::flows::launch::ColdMachinery<'_>, &devlaunch_core::domain::workspace_id::WorkspaceId, &mut dyn devlaunch_core::notices::Notices, devlaunch_core::clients::devpod::Patience) -> core::result::Result pub mod devlaunch_core::flows::lifecycle @@ -1862,23 +1558,6 @@ pub fn devlaunch_core::flows::lifecycle::ReconcileReport::eq(&self, &devlaunch_c impl core::fmt::Debug for devlaunch_core::flows::lifecycle::ReconcileReport pub fn devlaunch_core::flows::lifecycle::ReconcileReport::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result impl core::marker::StructuralPartialEq for devlaunch_core::flows::lifecycle::ReconcileReport -pub struct devlaunch_core::flows::lifecycle::Refresh<'a> -impl<'a> devlaunch_core::flows::lifecycle::Refresh<'a> -pub fn devlaunch_core::flows::lifecycle::Refresh<'a>::ask(&mut self, &dyn devlaunch_runner::Runner, devlaunch_core::flows::lifecycle::RefreshReason) -> devlaunch_core::flows::lifecycle::RefreshSpawn -pub fn devlaunch_core::flows::lifecycle::Refresh<'a>::new(&'a devlaunch_core::flows::lifecycle::SelfInvocation, &'a std::path::Path) -> Self -pub fn devlaunch_core::flows::lifecycle::Refresh<'a>::rearm(&mut self) -pub fn devlaunch_core::flows::lifecycle::Refresh<'a>::spawned(&self) -> bool -pub struct devlaunch_core::flows::lifecycle::SelfInvocation -impl devlaunch_core::flows::lifecycle::SelfInvocation -pub fn devlaunch_core::flows::lifecycle::SelfInvocation::new(impl core::convert::Into) -> Self -impl core::clone::Clone for devlaunch_core::flows::lifecycle::SelfInvocation -pub fn devlaunch_core::flows::lifecycle::SelfInvocation::clone(&self) -> devlaunch_core::flows::lifecycle::SelfInvocation -impl core::cmp::Eq for devlaunch_core::flows::lifecycle::SelfInvocation -impl core::cmp::PartialEq for devlaunch_core::flows::lifecycle::SelfInvocation -pub fn devlaunch_core::flows::lifecycle::SelfInvocation::eq(&self, &devlaunch_core::flows::lifecycle::SelfInvocation) -> bool -impl core::fmt::Debug for devlaunch_core::flows::lifecycle::SelfInvocation -pub fn devlaunch_core::flows::lifecycle::SelfInvocation::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result -impl core::marker::StructuralPartialEq for devlaunch_core::flows::lifecycle::SelfInvocation pub struct devlaunch_core::flows::lifecycle::SweepReport pub devlaunch_core::flows::lifecycle::SweepReport::notices: alloc::vec::Vec impl core::default::Default for devlaunch_core::flows::lifecycle::SweepReport @@ -1930,9 +1609,7 @@ pub fn devlaunch_core::flows::lifecycle::purge_plan(&mut devlaunch_core::flows:: pub fn devlaunch_core::flows::lifecycle::reconcile_plan(&devlaunch_core::flows::workspace_clone::WorkspaceCloneManager<'_>, &devlaunch_core::domain::metadata::MetadataStorage, &[devlaunch_core::clients::devpod::Workspace], &devlaunch_core::flows::lifecycle::ClonePlacement, &mut dyn devlaunch_core::notices::Notices) -> devlaunch_core::flows::lifecycle::ReconcilePlan pub fn devlaunch_core::flows::lifecycle::sweep_repo_fetches(&devlaunch_core::flows::repo_manager::RepositoryManager<'_>, &mut devlaunch_core::domain::metadata::MetadataStorage) -> devlaunch_core::flows::lifecycle::SweepReport pub fn devlaunch_core::flows::lifecycle::unsaved_work_in(&devlaunch_core::flows::workspace_clone::WorkspaceCloneManager<'_>, &devlaunch_core::domain::metadata::MetadataStorage, &devlaunch_core::clients::git::Git<'_>, &std::path::Path, &str, &mut dyn devlaunch_core::notices::Notices) -> devlaunch_core::domain::workspace_state::Unsaved -pub fn devlaunch_core::flows::lifecycle::workspace_delete(&mut devlaunch_core::flows::listing::CommandContext<'_>, &mut devlaunch_core::flows::lifecycle::Refresh<'_>, &devlaunch_core::flows::workspace_clone::WorkspaceCloneManager<'_>, &mut devlaunch_core::domain::metadata::MetadataStorage, core::option::Option<&devlaunch_core::clients::devpod_home::DevpodHome>, &str, devlaunch_core::flows::lifecycle::Insistence, devlaunch_core::flows::lifecycle::Persistence, &mut dyn core::ops::function::FnMut(devlaunch_core::flows::lifecycle::DeleteStalled), &mut dyn devlaunch_core::notices::Notices) -> core::result::Result pub fn devlaunch_core::flows::lifecycle::workspace_state(&dyn devlaunch_runner::Runner, &str, devlaunch_core::clients::devpod::Patience) -> core::result::Result -pub fn devlaunch_core::flows::lifecycle::workspace_stop(&mut devlaunch_core::flows::listing::CommandContext<'_>, &mut devlaunch_core::flows::lifecycle::Refresh<'_>, &str) -> core::result::Result pub mod devlaunch_core::flows::listing pub enum devlaunch_core::flows::listing::LastUsed pub devlaunch_core::flows::listing::LastUsed::At(alloc::string::String) @@ -1995,11 +1672,6 @@ pub fn devlaunch_core::flows::listing::WorkspaceTable::eq(&self, &devlaunch_core impl core::fmt::Debug for devlaunch_core::flows::listing::WorkspaceTable pub fn devlaunch_core::flows::listing::WorkspaceTable::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result impl core::marker::StructuralPartialEq for devlaunch_core::flows::listing::WorkspaceTable -pub struct devlaunch_core::flows::listing::CommandContext<'r> -impl<'r> devlaunch_core::flows::listing::CommandContext<'r> -pub fn devlaunch_core::flows::listing::CommandContext<'r>::git(&self) -> devlaunch_core::clients::git::Git<'r> -pub fn devlaunch_core::flows::listing::CommandContext<'r>::new(&'r dyn devlaunch_runner::Runner) -> Self -pub fn devlaunch_core::flows::listing::CommandContext<'r>::workspaces(&mut self) -> core::result::Result, devlaunch_core::clients::devpod::ListingUnreadable> pub struct devlaunch_core::flows::listing::DlView<'a> pub devlaunch_core::flows::listing::DlView::cache_dir: &'a std::path::Path pub devlaunch_core::flows::listing::DlView::clones: &'a dyn devlaunch_core::flows::listing::ClonePathResolver @@ -2080,10 +1752,8 @@ impl devlaunch_core::flows::listing::ClonePathResolver for devlaunch_core::flows pub fn devlaunch_core::flows::lifecycle::CloneDirectories<'_, '_>::clone_path(&self, &devlaunch_core::domain::model::WorktreeInfo) -> core::option::Option pub fn devlaunch_core::flows::listing::describe_source(&devlaunch_core::clients::devpod::WorkspaceSource) -> devlaunch_core::flows::listing::SourceDescription pub fn devlaunch_core::flows::listing::discover_repos_from_workspaces(&devlaunch_core::clients::git::Git<'_>, &[devlaunch_core::clients::devpod::Workspace]) -> devlaunch_core::flows::listing::RepoDiscovery -pub fn devlaunch_core::flows::listing::enriched_listing(&mut devlaunch_core::flows::listing::CommandContext<'_>, &devlaunch_core::flows::listing::DlView<'_>, devlaunch_core::flows::listing::Sizes) -> core::result::Result, devlaunch_core::clients::devpod::ListingUnreadable> pub fn devlaunch_core::flows::listing::flatten_repos(&indexmap::map::IndexMap>) -> alloc::vec::Vec pub fn devlaunch_core::flows::listing::head_branch_of(&devlaunch_core::clients::devpod::Workspace, &std::path::Path) -> core::option::Option -pub fn devlaunch_core::flows::listing::json_document(&[devlaunch_core::flows::listing::ListedWorkspace]) -> serde_json::value::Value pub fn devlaunch_core::flows::listing::owner_of(&devlaunch_core::clients::devpod::Workspace, &std::path::Path) -> core::option::Option pub fn devlaunch_core::flows::listing::repo_of(&devlaunch_core::clients::devpod::Workspace, &std::path::Path) -> core::option::Option pub fn devlaunch_core::flows::listing::workspace_table(&mut devlaunch_core::flows::listing::CommandContext<'_>, &std::path::Path, devlaunch_core::flows::listing::Sizes) -> core::result::Result @@ -2256,35 +1926,6 @@ impl core::fmt::Debug for devlaunch_core::flows::provision::PassOccasion pub fn devlaunch_core::flows::provision::PassOccasion::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result impl core::marker::Copy for devlaunch_core::flows::provision::PassOccasion impl core::marker::StructuralPartialEq for devlaunch_core::flows::provision::PassOccasion -pub enum devlaunch_core::flows::provision::ProvisionEvent -pub devlaunch_core::flows::provision::ProvisionEvent::NotInstalled -pub devlaunch_core::flows::provision::ProvisionEvent::NotInstalled::exit: devlaunch_runner::Exit -pub devlaunch_core::flows::provision::ProvisionEvent::NotInstalled::tools: alloc::vec::Vec<&'static str> -pub devlaunch_core::flows::provision::ProvisionEvent::NotInstalled::workspace: alloc::string::String -pub devlaunch_core::flows::provision::ProvisionEvent::PayloadNotBundled -pub devlaunch_core::flows::provision::ProvisionEvent::PayloadNotBundled::failure: devlaunch_core::flows::provision::BundleFailed -pub devlaunch_core::flows::provision::ProvisionEvent::ProvisioningDisabled -pub devlaunch_core::flows::provision::ProvisionEvent::ProvisioningDisabled::workspace: alloc::string::String -pub devlaunch_core::flows::provision::ProvisionEvent::StageFailed -pub devlaunch_core::flows::provision::ProvisionEvent::StageFailed::loudness: devlaunch_core::flows::provision::FailureLevel -pub devlaunch_core::flows::provision::ProvisionEvent::StageFailed::stage: &'static str -pub devlaunch_core::flows::provision::ProvisionEvent::StageFailed::status: i32 -pub devlaunch_core::flows::provision::ProvisionEvent::StageFailed::workspace: alloc::string::String -pub devlaunch_core::flows::provision::ProvisionEvent::StageNotReported -pub devlaunch_core::flows::provision::ProvisionEvent::StageNotReported::loudness: devlaunch_core::flows::provision::FailureLevel -pub devlaunch_core::flows::provision::ProvisionEvent::StageNotReported::stage: &'static str -pub devlaunch_core::flows::provision::ProvisionEvent::StageNotReported::workspace: alloc::string::String -pub devlaunch_core::flows::provision::ProvisionEvent::TripRefused -pub devlaunch_core::flows::provision::ProvisionEvent::TripRefused::refusal: devlaunch_core::clients::devpod::NotRun -pub devlaunch_core::flows::provision::ProvisionEvent::TripRefused::workspace: alloc::string::String -impl core::clone::Clone for devlaunch_core::flows::provision::ProvisionEvent -pub fn devlaunch_core::flows::provision::ProvisionEvent::clone(&self) -> devlaunch_core::flows::provision::ProvisionEvent -impl core::cmp::Eq for devlaunch_core::flows::provision::ProvisionEvent -impl core::cmp::PartialEq for devlaunch_core::flows::provision::ProvisionEvent -pub fn devlaunch_core::flows::provision::ProvisionEvent::eq(&self, &devlaunch_core::flows::provision::ProvisionEvent) -> bool -impl core::fmt::Debug for devlaunch_core::flows::provision::ProvisionEvent -pub fn devlaunch_core::flows::provision::ProvisionEvent::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result -impl core::marker::StructuralPartialEq for devlaunch_core::flows::provision::ProvisionEvent pub enum devlaunch_core::flows::provision::Provisioning pub devlaunch_core::flows::provision::Provisioning::AlreadyProvisioned pub devlaunch_core::flows::provision::Provisioning::CachedProvisioned @@ -2380,37 +2021,6 @@ impl core::marker::Copy for devlaunch_core::flows::provision::Switches impl core::marker::StructuralPartialEq for devlaunch_core::flows::provision::Switches pub fn devlaunch_core::flows::provision::provision_tools(&dyn devlaunch_runner::Runner, &str, devlaunch_core::flows::provision::PassOccasion, devlaunch_core::flows::provision::Switches, core::option::Option<&str>, core::option::Option<&devlaunch_core::flows::provision::HostLayout>, core::option::Option<&devlaunch_core::flows::provision::verdict_cache::VerdictCache>, &mut dyn devlaunch_core::notices::Notices) -> core::result::Result pub mod devlaunch_core::flows::records -pub enum devlaunch_core::flows::records::RecordsNotice -pub devlaunch_core::flows::records::RecordsNotice::Metadata(devlaunch_core::domain::metadata::Notice) -pub devlaunch_core::flows::records::RecordsNotice::Migrated(devlaunch_core::flows::migration::MigrationReport) -pub devlaunch_core::flows::records::RecordsNotice::MigrationRefused(devlaunch_core::domain::metadata::MetadataError) -pub devlaunch_core::flows::records::RecordsNotice::RetiredKey(devlaunch_core::domain::config::RetiredKey) -impl core::clone::Clone for devlaunch_core::flows::records::RecordsNotice -pub fn devlaunch_core::flows::records::RecordsNotice::clone(&self) -> devlaunch_core::flows::records::RecordsNotice -impl core::cmp::Eq for devlaunch_core::flows::records::RecordsNotice -impl core::cmp::PartialEq for devlaunch_core::flows::records::RecordsNotice -pub fn devlaunch_core::flows::records::RecordsNotice::eq(&self, &devlaunch_core::flows::records::RecordsNotice) -> bool -impl core::fmt::Debug for devlaunch_core::flows::records::RecordsNotice -pub fn devlaunch_core::flows::records::RecordsNotice::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result -impl core::marker::StructuralPartialEq for devlaunch_core::flows::records::RecordsNotice -pub enum devlaunch_core::flows::records::StartupError -pub devlaunch_core::flows::records::StartupError::Config(devlaunch_core::domain::config::ConfigError) -pub devlaunch_core::flows::records::StartupError::Metadata(devlaunch_core::domain::metadata::MetadataError) -pub devlaunch_core::flows::records::StartupError::NoHomeDirectory -impl core::clone::Clone for devlaunch_core::flows::records::StartupError -pub fn devlaunch_core::flows::records::StartupError::clone(&self) -> devlaunch_core::flows::records::StartupError -impl core::cmp::Eq for devlaunch_core::flows::records::StartupError -impl core::cmp::PartialEq for devlaunch_core::flows::records::StartupError -pub fn devlaunch_core::flows::records::StartupError::eq(&self, &devlaunch_core::flows::records::StartupError) -> bool -impl core::convert::From for devlaunch_core::flows::records::StartupError -pub fn devlaunch_core::flows::records::StartupError::from(devlaunch_core::domain::config::ConfigError) -> Self -impl core::convert::From for devlaunch_core::flows::records::StartupError -pub fn devlaunch_core::flows::records::StartupError::from(devlaunch_core::domain::metadata::MetadataError) -> Self -impl core::convert::From for devlaunch_core::flows::records::StartupError -pub fn devlaunch_core::flows::records::StartupError::from(devlaunch_core::domain::xdg::NoHomeDirectory) -> Self -impl core::fmt::Debug for devlaunch_core::flows::records::StartupError -pub fn devlaunch_core::flows::records::StartupError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result -impl core::marker::StructuralPartialEq for devlaunch_core::flows::records::StartupError pub struct devlaunch_core::flows::records::Records<'r> pub devlaunch_core::flows::records::Records::clones: devlaunch_core::flows::workspace_clone::WorkspaceCloneManager<'r> pub devlaunch_core::flows::records::Records::reported: alloc::vec::Vec @@ -2725,9 +2335,6 @@ pub fn devlaunch_core::json::JsonKind::fmt(&self, &mut core::fmt::Formatter<'_>) impl core::marker::Copy for devlaunch_core::json::JsonKind impl core::marker::StructuralPartialEq for devlaunch_core::json::JsonKind pub mod devlaunch_core::notices -pub trait devlaunch_core::notices::Notices -pub fn devlaunch_core::notices::Notices::say(&mut self, T) -impl devlaunch_core::notices::Notices for alloc::vec::Vec pub fn alloc::vec::Vec::say(&mut self, T) pub mod devlaunch_core::osext pub fn devlaunch_core::osext::env_str(&str) -> core::option::Option @@ -2749,7 +2356,5 @@ pub fn devlaunch_core::timing::Report::eq(&self, &devlaunch_core::timing::Report impl core::fmt::Debug for devlaunch_core::timing::Report pub fn devlaunch_core::timing::Report::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result impl core::marker::StructuralPartialEq for devlaunch_core::timing::Report -pub const devlaunch_core::timing::HANDOFF_VAR: &str -pub const devlaunch_core::timing::PREWARM_VAR: &str pub fn devlaunch_core::timing::begin() pub fn devlaunch_core::timing::emit() -> core::option::Option diff --git a/rust/devlaunch-core/src/lib.rs b/rust/devlaunch-core/src/lib.rs index 06fa7676..ad8d5fbe 100644 --- a/rust/devlaunch-core/src/lib.rs +++ b/rust/devlaunch-core/src/lib.rs @@ -43,23 +43,30 @@ //! item, and by two `cargo public-api` snapshots that CI diffs on every pull //! request: any change to the crate's public surface is a committed, reviewed //! diff or a red tick. The two tiers get a file each — `public-api.api.txt` -//! for declarations at the [`api`] path, `public-api.rest.txt` for the rest — -//! so that a change to the promised tier's *declarations* is a diff in a -//! 126-row file rather than one row inside two thousand. +//! for the promised tier, `public-api.rest.txt` for the rest — so that a change +//! to what [`api`] promises is a diff in a 521-row file rather than one row +//! inside two thousand. //! -//! **What that file does not cover, and it is not a small gap.** -//! `cargo public-api` renders inherent methods and trait impls only at a -//! type's canonical path, never at the path it is re-exported under. So -//! [`api::Launch`]'s only constructor and only method are rendered -//! `flows::launch::Launch::{new, run}` and land in `public-api.rest.txt`, as do -//! `CommandContext::new`, `DevcontainerPath::as_str` and every derived -//! `Clone`/`Debug`/`PartialEq` on the promised types: 133 of the 259 rows the -//! generator emits for the `api` section. Renaming `api::Launch::run` — an -//! unambiguous break — leaves `public-api.api.txt` byte-identical. So the -//! sound direction is one-way: a diff in the promise file *is* a change to the -//! promise, but a change to the promise need not diff it, and a diff in the -//! rest file that touches a promised type is a contract change too. Widening -//! the classifier is . +//! **How the promise file is filled, because the `api` path is not the whole +//! answer.** `cargo public-api` renders an item's own declaration at every path +//! it is reachable by, so [`api::Launch`] gets a row — but it renders inherent +//! methods and trait impls at the type's *canonical* path only, never at the +//! path it is re-exported under, so `api::Launch::run` is rendered +//! `flows::launch::Launch::run`. Matching the `api` path alone therefore kept +//! the promised types' names and dropped all of their behaviour, and renaming +//! `api::Launch::run` — an unambiguous break — left the file byte-identical. +//! So the classifier resolves each of [`api`]'s re-exports back to the path it +//! names and claims that item's rows too (#352), which moved 395 rows out of +//! the rest file: `Launch::{new, run}`, `CommandContext::new`, +//! `DevcontainerPath::as_str` and every derived `Clone`/`Debug`/`PartialEq` on +//! a promised type. A `flows::` or `domain::` path inside `public-api.api.txt` +//! is the promise and not a stray, and the cost of that is real: moving a +//! promised type between modules now churns the file where churn is expensive. +//! +//! What it still does not reach is a type [`api`] never re-exports but a +//! promised signature hands back — `flows::launch::Launched`, which +//! `Launch::run` returns. That is reachable from outside and classified as +//! binary surface, so a break in it diffs `public-api.rest.txt` alone. //! //! `scripts/public-api-snapshots.sh` regenerates both; see "The public-API //! snapshots" in docs/development.md. diff --git a/rust/devlaunch-core/tests/public_api_snapshots.rs b/rust/devlaunch-core/tests/public_api_snapshots.rs index 53b7b345..f77ba5fb 100644 --- a/rust/devlaunch-core/tests/public_api_snapshots.rs +++ b/rust/devlaunch-core/tests/public_api_snapshots.rs @@ -1,10 +1,10 @@ //! The two files core's public-API snapshot splits into, and the invariant that //! keeps them worth reading. //! -//! `public-api.api.txt` is the promise as a path match can see it: every row is -//! a declaration written *at* `devlaunch_core::api`, the surface an external -//! consumer is entitled to depend on, so a diff there is a change to that -//! contract. `public-api.rest.txt` is the tripwire: the binary surface, +//! `public-api.api.txt` is the promise: every row declares something +//! `devlaunch_core::api` re-exports, the surface an external consumer is +//! entitled to depend on, so a diff there is a change to that contract. +//! `public-api.rest.txt` is the tripwire: the binary surface, //! reachable but never promised, regenerated freely whenever a refactor moves //! it. //! @@ -13,21 +13,22 @@ //! removed `api` function reads as routine, and the one row that mattered goes //! through review unremarked. //! -//! The tests below hold the partition, which is not the same as holding the -//! promise. `cargo public-api` renders methods and impls only at a type's -//! canonical path, so `api::Launch::{new, run}` and every derived impl on a -//! promised type are in the rest file — 42 of the 79 rows the generator emits -//! for the `api` section — and renaming `Launch::run` diffs neither of these -//! two files in the place a reader would look. Deliberately not asserted here: -//! widens the classifier, and -//! its red is that rename, so a test pinning today's classification of those -//! rows would have to be deleted to let the fix land. +//! The promise file holds behaviour and not only declarations, which took a +//! second rule to achieve (#352). `cargo public-api` renders inherent methods +//! and trait impls at a type's *canonical* path only, never at the path it is +//! re-exported under, so `api::Launch::{new, run}` are rendered +//! `flows::launch::Launch::{new, run}` and a path match on `api` cannot see +//! them. The classifier resolves each re-export back to the path it names and +//! claims that item's rows too, which is why `public-api.api.txt` is full of +//! `flows::` and `domain::` paths: they are the promise, not strays. //! //! The classification lives in `scripts/public-api-snapshots.sh` and nowhere -//! else — the CI job runs that script rather than re-implementing its filter. -//! These tests are the other half of that: they hold the *checked-in* files to -//! the rule, so a hand-edited snapshot, or a regeneration that put the promise -//! in the file nobody reads, fails here rather than passing quietly. +//! else — the CI job runs that script rather than re-implementing its filter, +//! and so does the fixed-point test below rather than restating the rule in +//! Rust, where the restatement is what would be under test. These tests are the +//! other half of that: they hold the *checked-in* files to the rule, so a +//! hand-edited snapshot, or a regeneration that put the promise in the file +//! nobody reads, fails here rather than passing quietly. const PROMISE: &str = include_str!("../public-api.api.txt"); const REST: &str = include_str!("../public-api.rest.txt"); @@ -57,16 +58,56 @@ fn names_api(row: &str) -> bool { }) } -#[test] -fn every_promised_row_is_an_api_declaration() { - let strays: Vec<&str> = rows(PROMISE) - .into_iter() - .filter(|row| !names_api(row)) - .collect(); +/// One side of the split, as the regeneration script itself decides it. +/// +/// Shelling out rather than reimplementing: the rule is now two clauses over a +/// path list read out of `src/lib.rs`, and a copy of it here would be a second +/// definition of what is promised -- the exact thing the script exists to stop +/// there being. Needs no nightly toolchain and generates nothing; `--classify` +/// is a filter over rows on stdin. +fn classified(kind: &str, corpus: &str) -> String { + let script = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../scripts/public-api-snapshots.sh"); + let input = std::env::temp_dir().join(format!( + "devlaunch-public-api-{kind}-{}.txt", + std::process::id() + )); + std::fs::write(&input, corpus).expect("staging the corpus for the classifier"); + let done = std::process::Command::new(&script) + .args(["--classify", kind]) + .stdin(std::fs::File::open(&input).expect("reopening the staged corpus")) + .output() + .expect("running the regeneration script's classifier"); + let _ = std::fs::remove_file(&input); assert!( - strays.is_empty(), - "public-api.api.txt is the frozen promise and holds only \ - devlaunch_core::api declarations; these rows are something else: {strays:#?}" + done.status.success(), + "{} --classify {kind} failed: {}", + script.display(), + String::from_utf8_lossy(&done.stderr) + ); + String::from_utf8(done.stdout).expect("the classifier's output is the rows it was given") +} + +#[test] +fn the_checked_in_split_is_the_one_the_classifier_makes() { + // Feed the classifier everything the two files hold and it must hand back + // the same two files: each row on the side it is already on, in the order + // it is already in. That is a stronger statement than "every promised row + // names the api path", which stopped being true when the classifier learned + // to claim canonical paths -- and it covers what that one covered, since a + // hand-added row on the wrong side changes which list it comes back in. + let corpus = format!("{PROMISE}{REST}"); + assert_eq!( + rows(&classified("api", &corpus)), + rows(PROMISE), + "public-api.api.txt is not what the classifier would put there; a row \ + was hand-edited in, or the two were regenerated by different rules" + ); + assert_eq!( + rows(&classified("rest", &corpus)), + rows(REST), + "public-api.rest.txt holds a row the classifier calls promised, where a \ + breaking change to it would read as routine churn" ); } @@ -83,6 +124,49 @@ fn no_row_of_the_rest_is_an_api_declaration() { ); } +/// Whether some row of a snapshot declares `Type::method`, wherever +/// `cargo public-api` chose to render it and whatever generics it carried. +/// +/// A row is `pub fn () -> `, so the declaration is everything +/// before the first `(`; matching against that and not the whole row is what +/// keeps an argument of type `Launch` from reading as a method on one. +fn declares(snapshot: &str, ty: &str, method: &str) -> bool { + let owner = format!("::{ty}"); + let called = format!("::{method}"); + rows(snapshot).into_iter().any(|row| { + let Some(rest) = row.strip_prefix("pub fn ") else { + return false; + }; + let path = rest.split('(').next().unwrap_or_default(); + path.ends_with(&called) + && path + .match_indices(&owner) + .any(|(at, _)| matches!(path[at + owner.len()..].chars().next(), Some(':' | '<'))) + }) +} + +#[test] +fn the_promise_file_carries_the_promised_types_behaviour() { + // Every one of these is reachable only through the promised tier, and + // renaming any of them breaks a caller that has `api` and nothing else. + let missing: Vec = [ + ("Launch", "new"), + ("Launch", "run"), + ("CommandContext", "new"), + ("DevcontainerPath", "as_str"), + ] + .into_iter() + .filter(|(ty, method)| !declares(PROMISE, ty, method)) + .map(|(ty, method)| format!("{ty}::{method}")) + .collect(); + assert!( + missing.is_empty(), + "public-api.api.txt is the frozen promise and does not declare {missing:?}; \ + renaming one of those is an unambiguous break that leaves the file \ + byte-identical" + ); +} + #[test] fn the_two_files_share_no_row() { let promise = rows(PROMISE); diff --git a/scripts/public-api-snapshots.sh b/scripts/public-api-snapshots.sh index 5fefdae6..bb3b1e4c 100755 --- a/scripts/public-api-snapshots.sh +++ b/scripts/public-api-snapshots.sh @@ -5,49 +5,55 @@ # # There are three files because there are three different promises: # -# rust/devlaunch-core/public-api.api.txt the promise, as far as a path match -# can see it: every row is a declaration *at* `devlaunch_core::api`, the -# tier an external consumer is entitled to depend on. A diff here is a -# deliberate change to that tier -- a removal or a changed signature is a -# break -- and wants a reviewer who reads it that way. The converse does -# not hold; see the limit below. -# rust/devlaunch-core/public-api.rest.txt the tripwire, and today also the -# promised types' behaviour. Mostly the binary surface -- `flows::`, -# `domain::`, `clients::` -- which is reachable but never promised, so -# most of a diff here is routine and read for the accidental `pub`. But -# see the limit below before reading a diff that touches a promised type -# as routine. +# rust/devlaunch-core/public-api.api.txt the promise: every row declares +# something `devlaunch_core::api` re-exports, the tier an external +# consumer is entitled to depend on. A diff here is a deliberate change to +# that tier -- a removal or a changed signature is a break -- and wants a +# reviewer who reads it that way. Most of its rows are written at a +# `flows::` or `domain::` path rather than at `api`, which is not a bug in +# the filter; see how it is filled, below. +# rust/devlaunch-core/public-api.rest.txt the tripwire. The binary surface -- +# `flows::`, `domain::`, `clients::` -- which is reachable but never +# promised, so most of a diff here is routine and read for the accidental +# `pub`. But see the limit below before reading one as routine. # rust/devlaunch-runner/public-api.txt the process seam, as an external # `Runner` implementer sees it. It had no snapshot until the split: the # whole crate entered core's as one unexpanded glob row, so a removed # trait method moved nothing and passed CI. # -# The limit, measured rather than assumed: `cargo public-api` renders inherent -# methods and trait impls only at a type's *canonical* path, never at the path -# it is re-exported under. So `api::Launch::run` is rendered -# `devlaunch_core::flows::launch::Launch::run` and this classifier cannot see -# it. Of the 259 rows the generator emits for the `api` section, the match keeps -# 126; the other 133 -- `Launch::new`, `Launch::run`, `CommandContext::new`, -# `DevcontainerPath::as_str` and every derived `Clone`/`Debug`/`PartialEq` on -# the promised types -- land in the rest file. Renaming `Launch::run` therefore -# leaves the promise file byte-identical. Two consequences worth carrying: -# a diff in the rest file that touches a promised type is a contract change -# too, and this classifier is not the whole guard. Widening it is -# https://github.com/blooop/devlaunch/issues/352. -# -# The classification is one `grep` and it lives here, in the script CI runs, -# because the alternative is two copies of it -- one in the workflow, one in -# whatever regenerates the files -- drifting until the promise file quietly -# stops holding even what it does hold. `.github/workflows/ci.yml`'s -# `public-api` job runs this into a scratch tree and diffs the result against -# what the repo carries; a developer runs it with no argument to accept a -# deliberate change. +# How the promise file is filled, measured rather than assumed. `cargo +# public-api` renders an item's own declaration at every path it is reachable +# by, so `api::Launch` gets a row -- but it renders inherent methods and trait +# impls at the type's *canonical* path only, never at the path it is re-exported +# under, so `api::Launch::run` is rendered +# `devlaunch_core::flows::launch::Launch::run`. Matching the `api` path alone +# kept 126 rows and left 395 in the rest file, `Launch::new` and `Launch::run` +# among them, so renaming `Launch::run` left the promise file byte-identical +# (#352). So the classifier resolves each `api` re-export back to the path it +# names and claims that item's rows too, which is what `promised_row_pattern` +# below does. Some rows are in the file twice, because the generator emits them +# twice: once under the `api` section and once under the module that owns them. +# +# The limit that is left. A type `api` never re-exports but a promised signature +# hands back -- `flows::launch::Launched`, which `Launch::run` returns -- is +# reachable from outside and is classified as binary surface, so a break in it +# diffs public-api.rest.txt alone. +# +# The classification lives here, in the script CI runs, because the alternative +# is two copies of it -- one in the workflow, one in whatever regenerates the +# files -- drifting until the promise file quietly stops holding even what it +# does hold. `.github/workflows/ci.yml`'s `public-api` job runs this into a +# scratch tree and diffs the result against what the repo carries; a developer +# runs it with no argument to accept a deliberate change; and the tests over the +# checked-in files reach it through `--classify` rather than restating it. # # Usage: # scripts/public-api-snapshots.sh # rewrite the checked-in files # scripts/public-api-snapshots.sh DEST # write them under DEST instead # scripts/public-api-snapshots.sh --print-pin # scripts/public-api-snapshots.sh --print-files +# scripts/public-api-snapshots.sh --print-promised +# scripts/public-api-snapshots.sh --classify api|rest # rows on stdin # # Needs a nightly toolchain (cargo-public-api's rustdoc-JSON backend is # nightly-only; the crates themselves still build on the stable pin) and the @@ -70,7 +76,7 @@ PIN=0.52.0 # moves -- `UnsafeUnpin` appeared with a nightly, not with a crate change -- and # a tripwire that fires on toolchain drift teaches people to update snapshots # unread. Derived impls (Clone, Debug, serde) stay in: losing one is a real -# break. Note where they stay -- the rest file, per the limit above. +# break, and a promised type's are in the promise file with the rest of it. FLAGS=(-ss) # The boundary matters: `\b` is what keeps a future `devlaunch_core::apiary` @@ -87,6 +93,97 @@ REST_FILE=devlaunch-core/public-api.rest.txt RUNNER_FILE=devlaunch-runner/public-api.txt FILES=("$API_FILE" "$REST_FILE" "$RUNNER_FILE") +# Where the promised tier is declared, and so where the canonical paths behind +# it are read from. The `api` module is a wall of `pub use crate::...`, which is +# exactly the mapping the renderer throws away. +CORE_LIB=devlaunch-core/src/lib.rs + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +# The canonical path of every item the `api` module re-exports, spelled the way +# `cargo public-api` spells one. +# +# Reading Rust with awk is not free, and the alternative was worse: a list of +# paths kept here by hand is a second declaration of what is promised, and the +# day it falls behind `api` the promise file quietly stops covering whatever was +# added. This reads the one declaration there is. It understands the two forms +# the module uses -- `pub use crate::a::b::Name;` and `pub use crate::a::b::{X, +# Y};`, either wrapped across lines -- and refuses anything else rather than +# guessing, because a form it half-understands is a path it gets wrong. +promised_paths() { + awk ' + /^pub mod api \{/ { inside = 1; next } + inside && /^\}/ { inside = 0 } + inside { sub(/\/\/.*/, ""); body = body " " $0 } + END { + n = split(body, statements, ";") + for (i = 1; i <= n; i++) { + if (statements[i] !~ /pub[ \t]+use[ \t]+crate::/) continue + item = statements[i] + sub(/^.*pub[ \t]+use[ \t]+crate::/, "", item) + gsub(/[ \t]/, "", item) + if (match(item, /\{/)) { + module = substr(item, 1, RSTART - 1) + names = substr(item, RSTART + 1) + sub(/\}.*$/, "", names) + m = split(names, list, ",") + for (j = 1; j <= m; j++) if (list[j] != "") print "devlaunch_core::" module list[j] + } else { + print "devlaunch_core::" item + } + } + } + ' "$repo_root/rust/$CORE_LIB" | while read -r path; do + if [[ ! "$path" =~ ^devlaunch_core(::[A-Za-z_][A-Za-z0-9_]*)+$ ]]; then + echo "cannot read '$path' as a path: the api module uses a re-export form this script does not parse." >&2 + exit 1 + fi + echo "$path" + done +} + +# The rule that decides which file a row belongs in, as one extended regex. +# +# Two clauses, and the second is what #352 added. `cargo public-api` renders an +# item's own declaration at every path it is reachable by, so `api::Launch` gets +# a row -- but it renders inherent methods and trait impls at the type's +# *canonical* path only, so `api::Launch::run` is rendered +# `devlaunch_core::flows::launch::Launch::run` and the first clause cannot see +# it. The second clause claims those by resolving the re-export back to the path +# it names. +# +# The anchors are the whole difficulty. A promised type appears in a signature +# somewhere in most of this crate, so claiming any row that *mentions* one would +# drag the binary surface into the promise file. A row's subject is what is +# claimed: the path right after `pub` and its item keyword, or -- for an impl, +# where the subject is either side of `for` -- any path in the header that +# follows a space, which a path nested in generics (`From<...>`, `Vec<...>`) +# never does. +# +# Note what this costs, and it is not nothing: the canonical path is now part of +# the promise file, so moving a promised type between modules diffs it. That is +# churn in the file where churn is expensive. It is still the better trade, +# because the alternative is a promise file that a rename of the promise does +# not touch. +promised_row_pattern() { + local paths + paths="$(promised_paths | paste -sd'|' -)" + if [[ -z "$paths" ]]; then + echo "the api module re-exports nothing: rust/$CORE_LIB no longer declares 'pub mod api'?" >&2 + exit 1 + fi + printf '%s|^pub ([a-z]+ )?(%s)\\b|^impl.* (%s)\\b' "$API_ROW" "$paths" "$paths" +} + +# One side of the split, over rows on stdin. `grep` says "no match" with status +# 1, which is not an error for a filter -- an empty side is only wrong when a +# whole snapshot is being generated, and the generation path checks it there. +classify() { + local status=0 + grep -E "$@" || status=$? + [[ "$status" -le 1 ]] || exit "$status" +} + case "${1:-}" in --print-pin) echo "$PIN" @@ -96,9 +193,27 @@ case "${1:-}" in printf '%s\n' "${FILES[@]}" exit 0 ;; +--print-promised) + promised_paths + exit 0 + ;; +--classify) + # The classification, reachable without a nightly toolchain and without + # generating anything -- so the rule above can be exercised on rows chosen to + # be awkward. `test/test_public_api_snapshots_doc.py` is what does that. + pattern="$(promised_row_pattern)" + case "${2:-}" in + api) classify "$pattern" ;; + rest) classify -v "$pattern" ;; + *) + echo "--classify takes 'api' or 'rest'" >&2 + exit 2 + ;; + esac + exit 0 + ;; esac -repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" dest="${1:-$repo_root/rust}" for file in "${FILES[@]}"; do mkdir -p "$dest/$(dirname "$file")" @@ -155,18 +270,38 @@ done cd "$repo_root/rust" core="$(cargo public-api -p devlaunch-core "${FLAGS[@]}")" +pattern="$(promised_row_pattern)" # Two greps rather than one pass with a fallthrough, so the two files are # complements by construction. Either coming out empty means the filter no # longer matches the crate, which is a broken split rather than a small API. -if ! printf '%s\n' "$core" | grep -E "$API_ROW" >"$staging/$API_FILE"; then - echo "no rows matched $API_ROW: devlaunch-core no longer declares an 'api' module?" >&2 +if ! printf '%s\n' "$core" | grep -E "$pattern" >"$staging/$API_FILE"; then + echo "no rows matched the promise: devlaunch-core no longer declares an 'api' module?" >&2 exit 1 fi -if ! printf '%s\n' "$core" | grep -Ev "$API_ROW" >"$staging/$REST_FILE"; then - echo "every row matched $API_ROW: the split has nothing left to classify?" >&2 +if ! printf '%s\n' "$core" | grep -Ev "$pattern" >"$staging/$REST_FILE"; then + echo "every row matched the promise: the split has nothing left to classify?" >&2 exit 1 fi + +# Every promised path has to claim something. Without this the parse above can +# drift silently: `api` grows a re-export form awk reads as a shorter path, or +# a rename leaves a path nothing is rendered at, and the promise file loses the +# rows for it while every test over the two files still passes -- they check the +# split is a partition, not that it is the right one. +# +# A here-string rather than a pipe into `grep -q`: `grep -q` stops reading at +# the first match, `printf` takes a SIGPIPE for the rest, and `pipefail` reports +# that as the pipeline failing -- so every path that *did* claim a row was +# reported as one that had not. +while read -r path; do + if ! grep -qE "^pub ([a-z]+ )?$path\\b|^impl.* $path\\b" <<<"$core"; then + echo "the api module re-exports $path and no rendered row declares it." >&2 + echo "Either that item is gone, or --print-promised is reading the module wrongly." >&2 + exit 1 + fi +done < <(promised_paths) + cargo public-api -p devlaunch-runner "${FLAGS[@]}" >"$staging/$RUNNER_FILE" for file in "${FILES[@]}"; do diff --git a/test/test_public_api_snapshots_doc.py b/test/test_public_api_snapshots_doc.py index 86927e3c..b44b8281 100644 --- a/test/test_public_api_snapshots_doc.py +++ b/test/test_public_api_snapshots_doc.py @@ -40,9 +40,11 @@ CI_CHECK_STEP = "The public surface is the snapshots the repo carries" # The classification itself: the pattern that decides "promise" from "rest". API_ROW_PATTERN = "devlaunch_core::api\\b" -# The ticket that widens the classifier to cover a promised type's methods and -# impls. Named in the docs so the gap is a known follow-up rather than folklore. -WIDENING_TICKET = "352" +# The other snapshot, named in the docs because the widened classifier still does +# not reach everything a promised signature does: a type the `api` module never +# re-exports but a promised method returns is reachable from outside and lives +# only here, so a diff in it can still be a contract change. +REST_SNAPSHOT = "public-api.rest.txt" def script_files() -> list[str]: @@ -178,27 +180,111 @@ def test_the_ci_error_string_sends_a_reader_to_the_document_that_has_the_section @pytest.mark.unit -def test_the_docs_say_what_the_promise_file_does_not_cover(): +def test_the_docs_say_how_the_promise_file_is_filled_and_what_it_still_misses(): """The overclaim this section is one edit away from becoming again. `cargo public-api` renders methods and impls only at a type's canonical - path, so `api::Launch::run` is in the *rest* file and renaming it leaves the - promise file byte-identical. A guard that is trusted and silently does not - fire is worse than no guard, so the limit is documented where the guard is, - and the ticket that closes it is named. + path, so the classifier reaches `api::Launch::run` by resolving each `api` + re-export back to the path it names (#352). Two things a reader has to be + told, and both live wherever the promise file is described: that the + canonical-path rows in it are there on purpose and are not strays, and that + the rest file can still carry a contract change, because a type the `api` + module never re-exports but a promised signature hands back is reachable + from outside and is classified as binary surface. """ for path in (DEV_DOC, SCRIPT, RUST / "devlaunch-core" / "src" / "lib.rs"): text = path.read_text(encoding="utf-8") assert "canonical" in text, ( - f"{path.name} describes the promise file without the canonical-path limit " - "that decides what it can see" + f"{path.name} describes the promise file without the canonical-path " + "rendering that decides how it is filled" ) - assert WIDENING_TICKET in text, ( - f"{path.name} states the limit without naming issue #{WIDENING_TICKET}, " - "which is what turns a known gap into a tracked one" + assert REST_SNAPSHOT in text, ( + f"{path.name} describes the promise file without naming {REST_SNAPSHOT}, " + "which is where a promised signature's own types are still classified" ) +def classify(kind: str, rows: list[str]) -> list[str]: + """One side of the split, as the script itself decides it. + + The classification is a filter over rows, and the script is the only place + it exists -- so this exercises the real one on rows chosen to be awkward, + rather than restating the pattern here where the restatement is what would + be tested. + """ + done = subprocess.run( + [str(SCRIPT), "--classify", kind], + input="".join(f"{row}\n" for row in rows), + capture_output=True, + text=True, + check=True, + ) + return done.stdout.splitlines() + + +# Rows in the shape `cargo public-api` renders, naming types this crate really +# has, so the classifier is exercised against the `api` module as it stands +# rather than against a fiction. `Launch` and `Host` are re-exported; the branch +# manager is not. +PROMISED_ROWS = [ + "pub struct devlaunch_core::api::Launch<'a, 'r, 'l>", + "impl<'a, 'r, 'l> devlaunch_core::flows::launch::Launch<'a, 'r, 'l>", + "pub fn devlaunch_core::flows::launch::Launch<'a, 'r, 'l>::run(&mut self) -> ()", + "impl core::fmt::Debug for devlaunch_core::flows::launch::Host", +] +UNPROMISED_ROWS = [ + "pub mod devlaunch_core", + "pub struct devlaunch_core::flows::branch_manager::BranchManager<'a>", + # The trap the whole rule turns on: a promised type in an argument, on a + # method of something that is not promised at all. + "pub fn devlaunch_core::flows::branch_manager::BranchManager<'a>::adopt" + "(&self, &devlaunch_core::flows::launch::Host) -> ()", + # And the boundary the `\b` is for. + "pub mod devlaunch_core::apiary", +] + + +@pytest.mark.unit +def test_a_promised_types_canonical_rows_are_classified_as_promise(): + """The finding this ticket is about, at the seam that decides it. + + `Launch::run` is rendered at `flows::launch::Launch`, never at the `api` + path it is re-exported under, so a classifier that matches the `api` path + alone keeps the type's declaration and drops its only method. + """ + kept = classify("api", PROMISED_ROWS + UNPROMISED_ROWS) + assert kept == PROMISED_ROWS, ( + "the classifier does not claim a promised type's canonical-path rows, so " + "renaming Launch::run leaves the promise file byte-identical" + ) + + +@pytest.mark.unit +def test_naming_a_promised_type_in_a_signature_does_not_promise_the_signature(): + """The control, and the reason the rule is anchored rather than a substring. + + Promised types are arguments and return types all over this crate. Claiming + every row that mentions one would pull most of the binary surface into the + promise file, which is the failure the split exists to prevent, wearing the + other hat. + """ + left = classify("rest", PROMISED_ROWS + UNPROMISED_ROWS) + assert left == UNPROMISED_ROWS, ( + "the classifier claimed a row that only mentions a promised type, or " + "dropped one that mentions nothing promised at all" + ) + + +@pytest.mark.unit +def test_the_two_sides_of_the_classification_are_a_partition(): + rows = PROMISED_ROWS + UNPROMISED_ROWS + kept, left = classify("api", rows), classify("rest", rows) + assert sorted(kept + left) == sorted(rows), ( + "a row was dropped by both sides or kept by both; the two files are " + "complements or they are not a split at all" + ) + + def ci_step_script(job: str, step_name: str) -> str: """The shell of one step of a job, dedented and runnable. From e79e7529a3ab1f28668bfd9a97a6a40a998dbab3 Mon Sep 17 00:00:00 2001 From: Austin Gregg-Smith Date: Sat, 29 Aug 2026 21:49:29 +0100 Subject: [PATCH 2/3] State the residual honestly, and guard the sentence that states it Three sites described the limit on the promise file as flows::launch::Launched and nothing else. Measured on the merged tree it is 39 types owning 615 rows in public-api.rest.txt, DevcontainerRefError among them -- the error type of api::resolve_devcontainer_ref, whose own row is in the promise file at the api path, so renaming one of its variants breaks every consumer matching on it and diffs only the file the docs call routine. --print-residual computes that list off the checked-in snapshots with no toolchain, and the doc guard now diffs the figures in all three descriptions against it, so the number goes red rather than stale. The old guard was near-vacuous: REST_SNAPSHOT alone is satisfied by a shell variable assignment, a table row and an unrelated sentence, so the whole caveat could be deleted from any site and all fourteen tests passed. It now pins the mechanism ('never re-exports'), the example, and the command. Proven by deleting the paragraph at each of the three sites in turn: two tests red each time. Also from the review: promised_paths() welded 'X as Y' into the identifier 'XasY' while its comment claimed it refused what it could not parse -- it now resolves the rename to the canonical path, which is what the classifier wants, and strips block comments as well as line comments. The subject anchor allows more than one keyword after pub, so a promised type's 'pub const fn' is claimed (no row moves today). The impl anchor's comment no longer claims generics never follow a space. And #352's 'check whether cargo public-api offers this directly' now has its answer written down: 0.52.0 has --omit, --include, features, target and -p, and no path, module or reachability filter at all. --- .github/workflows/ci.yml | 22 +++-- CHANGELOG.md | 13 +++ docs/development.md | 27 +++++- rust/devlaunch-core/src/lib.rs | 24 ++++- scripts/public-api-snapshots.sh | 126 +++++++++++++++++++++++--- test/test_public_api_snapshots_doc.py | 88 ++++++++++++++++++ 6 files changed, 274 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e182dd58..3a10ca31 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -470,12 +470,22 @@ jobs: # devlaunch-core/public-api.rest.txt the tripwire over the binary surface, and # devlaunch-runner/public-api.txt the process seam. # - # "As a path match can see it" is the honest scope, and it is narrower than it - # reads: cargo public-api renders methods and impls only at a type's canonical - # path, so a promised type's constructors, methods and derived impls are in - # the rest file, and renaming `api::Launch::run` leaves the promise file - # byte-identical. A diff in the rest file touching a promised type is a - # contract change too. Widening the classifier is #352. + # "As a path match can see it" was the honest scope until #352 and is no + # longer the scope at all. cargo public-api renders methods and impls only at + # a type's canonical path, so matching the `api` path alone kept the promised + # types' names and left every one of their constructors, methods and derived + # impls in the rest file — renaming `api::Launch::run` diffed neither file + # where a reader would look. The classifier now resolves each `api` re-export + # back to the path it names and claims that item's rows too, which is why most + # of the promise file is written at `flows::` and `domain::` paths. + # + # What is left over is still real and is not one type: 39 types that a + # promised signature hands back but `api` never re-exports own 615 rows in the + # rest file, `domain::spec::DevcontainerRefError` — the error type of the + # promised `api::resolve_devcontainer_ref` — among them. So a rest-file diff + # is routine for a row whose subject nothing promised names and a contract + # change for a row whose subject is one of those; `--print-residual` tells + # them apart. # # Nightly because cargo-public-api's rustdoc-JSON backend needs it; the crates # themselves still build on the stable pin everywhere else. The pinned diff --git a/CHANGELOG.md b/CHANGELOG.md index cd7cac27..32ff5cbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -185,6 +185,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 and that is the promise rather than strays. And moving a promised type between modules churns it, which is the price of the guard working at all. + What the widened classifier still does not reach is written down as a number + rather than as an example, because the example was doing the work of the number. + Every place describing the promise file named `flows::launch::Launched` and + stopped, which reads as one type; the residual is 39 types owning 615 rows in + `public-api.rest.txt`, and one of them is `DevcontainerRefError`, the error type + of `api::resolve_devcontainer_ref`, whose own row is in the promise file at the + `api` path. Rename a variant of it and every consumer matching on it breaks + while the only diff is in the file the docs call freely regenerated. + `scripts/public-api-snapshots.sh --print-residual` lists them from the + checked-in snapshots with no toolchain, and `test/test_public_api_snapshots_doc.py` + holds the figures in all three descriptions to what it prints, so the sentence + goes red rather than stale. + ### Fixed - **A captured command no longer pays the drain grace twice.** When a command diff --git a/docs/development.md b/docs/development.md index 3bd4bf6b..78ba1eb9 100644 --- a/docs/development.md +++ b/docs/development.md @@ -67,10 +67,29 @@ cost is real and worth knowing before you see it: moving a promised type between the file where churn is expensive. Some rows appear twice, because the generator emits them twice, once under the `api` section and once under the module that owns them. -**What it still does not reach.** A type `api` never re-exports but a promised signature hands back -is reachable from outside and classified as binary surface. `Launch::run` returns -`flows::launch::Launched`, so renaming one of that type's fields breaks a consumer and diffs -`public-api.rest.txt` alone. The `-ss` flag also omits blanket and auto-trait impls from both files, +**What it still does not reach, and it is not one type.** A type `api` never re-exports but a +promised signature hands back is reachable from outside and classified as binary surface. Counted +on the checked-in files rather than guessed at, that is **39 types owning 615 rows** in +`public-api.rest.txt`, and the command that lists them needs no toolchain: + +```bash +scripts/public-api-snapshots.sh --print-residual +``` + +`domain::spec::DevcontainerRefError` is the one to hold the limit against. `api` promises +`resolve_devcontainer_ref`, whose row sits in the promise file at the `api` path itself, and it +returns that error: rename a variant of it and every consumer matching on it breaks, while the only +file that moves is the one this page calls freely regenerated. +`domain::metadata::MetadataError` is the same shape, carried by the promised +`StartupError::Metadata`. `flows::launch::Launched`, returned by `Launch::run`, is the example this +section used to give on its own, which made 615 rows read as one. + +So a diff in `public-api.rest.txt` is routine for a row whose subject nothing promised names, and a +contract change for a row whose subject is one of the 39. `--print-residual` is how you tell the two +apart, and `test/test_public_api_snapshots_doc.py` diffs the figures in this paragraph against it, +so they go red rather than stale. + +The `-ss` flag also omits blanket and auto-trait impls from both files, deliberately: those rows move when rustdoc moves, and a tripwire that fires on toolchain drift teaches people to update snapshots unread. diff --git a/rust/devlaunch-core/src/lib.rs b/rust/devlaunch-core/src/lib.rs index ad8d5fbe..6b422e36 100644 --- a/rust/devlaunch-core/src/lib.rs +++ b/rust/devlaunch-core/src/lib.rs @@ -63,10 +63,26 @@ //! is the promise and not a stray, and the cost of that is real: moving a //! promised type between modules now churns the file where churn is expensive. //! -//! What it still does not reach is a type [`api`] never re-exports but a -//! promised signature hands back — `flows::launch::Launched`, which -//! `Launch::run` returns. That is reachable from outside and classified as -//! binary surface, so a break in it diffs `public-api.rest.txt` alone. +//! **What it still does not reach, and it is not one type.** A type [`api`] +//! never re-exports but a promised signature names is reachable from outside +//! and classified as binary surface, so a break in it diffs +//! `public-api.rest.txt` alone. Counted rather than guessed at: **39 such types +//! own 615 rows over there**, and `scripts/public-api-snapshots.sh +//! --print-residual` lists them. +//! +//! The pointed one is `domain::spec::DevcontainerRefError`. [`api`] promises +//! `resolve_devcontainer_ref`, which returns it, so the *function* is a +//! promise-file row at the `api` path itself while the error it hands you is +//! not: rename one of that error's variants and every consumer matching on it +//! breaks, with the only diff in the file this doc calls freely regenerated. +//! `domain::metadata::MetadataError` is the same shape, carried by the promised +//! `StartupError::Metadata`. `flows::launch::Launched`, returned by +//! `Launch::run`, is the example this paragraph used to give as though it were +//! the whole of the residual, which is what made 615 rows read as one. +//! +//! So a `public-api.rest.txt` diff is routine for a row whose subject nothing +//! promised names, and a contract change for a row whose subject is one of the +//! 39. `--print-residual` is how you tell which you are looking at. //! //! `scripts/public-api-snapshots.sh` regenerates both; see "The public-API //! snapshots" in docs/development.md. diff --git a/scripts/public-api-snapshots.sh b/scripts/public-api-snapshots.sh index bb3b1e4c..1d42ab66 100755 --- a/scripts/public-api-snapshots.sh +++ b/scripts/public-api-snapshots.sh @@ -34,10 +34,36 @@ # below does. Some rows are in the file twice, because the generator emits them # twice: once under the `api` section and once under the module that owns them. # -# The limit that is left. A type `api` never re-exports but a promised signature -# hands back -- `flows::launch::Launched`, which `Launch::run` returns -- is -# reachable from outside and is classified as binary surface, so a break in it -# diffs public-api.rest.txt alone. +# The limit that is left, and it is not one type. A type `api` never re-exports +# but a promised signature names is reachable from outside and is classified as +# binary surface, so a break in it diffs public-api.rest.txt alone. Measured on +# the checked-in files rather than guessed at: 39 such types own 615 rows over +# there. `--print-residual` lists them, needs no toolchain, and is what the +# number above is checked against (`test/test_public_api_snapshots_doc.py`). +# +# The pointed one is `domain::spec::DevcontainerRefError`, because +# `pub fn devlaunch_core::api::resolve_devcontainer_ref(&str) -> Result<..., +# DevcontainerRefError>` is a promise-file row at the `api` path itself: rename +# one of that error's variants and every consumer matching on it breaks, while +# the only file that moves is the one this header calls routine. So is +# `domain::metadata::MetadataError`, which the promised `StartupError::Metadata` +# carries. `flows::launch::Launched`, which `Launch::run` returns, is the +# example this comment used to give as though it were the whole of it. +# +# What that means for reading a rest-file diff: it is routine for a row whose +# subject is nothing a promised signature names, and a contract change for a row +# whose subject is one of the 39. `--print-residual` is how you tell. +# +# Whether the tool does this already, since the obvious first question is why +# any of it is hand-rolled (#352 asked it explicitly). It does not, in the pin +# this script names. `cargo public-api 0.52.0` offers exactly four ways to +# select what is rendered -- `--omit blanket-impls|auto-trait-impls| +# auto-derived-impls` (and the `-s` shorthands), `--include +# function-parameter-names`, the feature and target flags, and `-p` for which +# package -- and not one of them is a path, module or reachability filter. There +# is no upstream notion of "the surface reachable from this module", so the +# choice is a filter over rendered rows or nothing. Re-check it when the pin +# moves; `cargo public-api --help` is the whole answer. # # The classification lives here, in the script CI runs, because the alternative # is two copies of it -- one in the workflow, one in whatever regenerates the @@ -53,6 +79,7 @@ # scripts/public-api-snapshots.sh --print-pin # scripts/public-api-snapshots.sh --print-files # scripts/public-api-snapshots.sh --print-promised +# scripts/public-api-snapshots.sh --print-residual # the limit, counted # scripts/public-api-snapshots.sh --classify api|rest # rows on stdin # # Needs a nightly toolchain (cargo-public-api's rustdoc-JSON backend is @@ -106,21 +133,39 @@ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # Reading Rust with awk is not free, and the alternative was worse: a list of # paths kept here by hand is a second declaration of what is promised, and the # day it falls behind `api` the promise file quietly stops covering whatever was -# added. This reads the one declaration there is. It understands the two forms -# the module uses -- `pub use crate::a::b::Name;` and `pub use crate::a::b::{X, -# Y};`, either wrapped across lines -- and refuses anything else rather than -# guessing, because a form it half-understands is a path it gets wrong. +# added. This reads the one declaration there is. +# +# What it understands, exactly, because a form it half-understands is a path it +# gets wrong: `pub use crate::a::b::Name;`, `pub use crate::a::b::{X, Y};`, and +# either of those with an `as` rename on any name, all of them free to wrap +# across lines. `//` and `/* */` comments are removed first, so a commented-out +# re-export is not a promise. Anything else -- a glob, a path that is not an +# identifier chain -- is refused by the shape check below rather than guessed at. +# +# The `as` form matters more than it looks, and the previous version of this +# parser got it wrong *quietly*: it stripped whitespace before looking, so +# `pub use crate::domain::spec::WorkspaceSpec as Spec;` welded into +# `...::WorkspaceSpecasSpec`, which is a valid identifier chain and so passed +# the shape check on its way to matching nothing. The alias is not what to +# match: `cargo public-api` renders the item's methods at its canonical path, +# and the alias only ever appears on the `api`-path row that the first clause of +# the pattern already claims. So the rename is dropped and the canonical path +# kept, which is the right answer rather than merely a loud one. promised_paths() { awk ' /^pub mod api \{/ { inside = 1; next } inside && /^\}/ { inside = 0 } inside { sub(/\/\/.*/, ""); body = body " " $0 } END { + # Block comments only after the lines are joined: one can span them. + gsub(/\/\*([^*]|\*+[^*\/])*\*+\//, " ", body) n = split(body, statements, ";") for (i = 1; i <= n; i++) { if (statements[i] !~ /pub[ \t]+use[ \t]+crate::/) continue item = statements[i] sub(/^.*pub[ \t]+use[ \t]+crate::/, "", item) + # Before the whitespace goes, or `X as Y` becomes the identifier `XasY`. + gsub(/[ \t]+as[ \t]+[A-Za-z_][A-Za-z0-9_]*/, "", item) gsub(/[ \t]/, "", item) if (match(item, /\{/)) { module = substr(item, 1, RSTART - 1) @@ -155,10 +200,20 @@ promised_paths() { # The anchors are the whole difficulty. A promised type appears in a signature # somewhere in most of this crate, so claiming any row that *mentions* one would # drag the binary surface into the promise file. A row's subject is what is -# claimed: the path right after `pub` and its item keyword, or -- for an impl, +# claimed: the path right after `pub` and its item keywords, or -- for an impl, # where the subject is either side of `for` -- any path in the header that -# follows a space, which a path nested in generics (`From<...>`, `Vec<...>`) -# never does. +# follows a space. +# +# That second anchor is an approximation and it is worth saying which way it is +# wrong, since a comment that oversells it is how the next person stops +# checking. `Vec` and `From` are safe: a path nested in a +# single-argument generic follows a `<`, never a space. A path nested in a +# *multi*-argument generic, a tuple, or a trait bound does follow a space, so +# `impl From<(u8, Promised)> for Internal` and `impl Trait for +# Internal` would be claimed as promises. No row of that shape exists today (no +# `where` clause is rendered at all, and no promised path appears in a bound), +# which is why this is a note rather than a second clause: the fix is to parse +# the header rather than match it, and there is nothing yet to parse it for. # # Note what this costs, and it is not nothing: the canonical path is now part of # the promise file, so moving a promised type between modules diffs it. That is @@ -172,7 +227,50 @@ promised_row_pattern() { echo "the api module re-exports nothing: rust/$CORE_LIB no longer declares 'pub mod api'?" >&2 exit 1 fi - printf '%s|^pub ([a-z]+ )?(%s)\\b|^impl.* (%s)\\b' "$API_ROW" "$paths" "$paths" + # `([a-z]+ )*`, not `?`: a row is `pub ` and there can be + # more than one keyword. With `?` a promised type's `pub const fn` or `pub + # unsafe fn` fell to the tripwire file, which is an ordinary thing to add and + # a silent hole. Widening cannot over-claim, because every rendered path + # starts `devlaunch_core::` and no `[a-z]+ ` alternative can consume a segment + # of one: there is no space inside a path. Measured: no row moves today. + printf '%s|^pub ([a-z]+ )*(%s)\\b|^impl.* (%s)\\b' "$API_ROW" "$paths" "$paths" +} + +# The limit, counted: every type the promise file names that is not itself +# promised and owns rows in the tripwire file, as `\t` sorted by +# rows. Those are the types a promised signature hands you and the promise file +# does not cover, so a diff to one of them reads as routine churn and is not. +# +# Read off the checked-in snapshots, so this needs no toolchain and nothing is +# generated. It exists because the header used to name one such type as though +# it were the whole residual, and prose that carries a number nothing recomputes +# is prose that is wrong a release later: `test/test_public_api_snapshots_doc.py` +# diffs the figures in the three places the limit is described against this. +# +# A "type" is a path whose last segment is capitalised, which is what separates +# `domain::spec::DevcontainerRefError` from the module it lives in and from the +# free functions beside it. Ownership is the same anchored subject test the +# classifier uses, not a mention: a row that merely takes one as an argument +# belongs to whatever declares it. +residual_types() { + local api_file="$repo_root/rust/$API_FILE" rest_file="$repo_root/rust/$REST_FILE" + for file in "$api_file" "$rest_file"; do + [[ -r "$file" ]] || { + echo "--print-residual reads the checked-in snapshots and $file is not there." >&2 + exit 1 + } + done + local promised + promised="$(promised_paths)" + grep -oE 'devlaunch_core(::[A-Za-z_][A-Za-z0-9_]*)+' "$api_file" | + grep -E '::[A-Z][A-Za-z0-9_]*$' | + sort -u | + grep -Fxv -f <(printf '%s\n' "$promised") | + while read -r path; do + local rows + rows="$(grep -cE "^pub ([a-z]+ )?$path\\b|^impl.* $path\\b" "$rest_file" || true)" + [[ "$rows" -gt 0 ]] && printf '%s\t%s\n' "$rows" "$path" + done | sort -rn } # One side of the split, over rows on stdin. `grep` says "no match" with status @@ -197,6 +295,10 @@ case "${1:-}" in promised_paths exit 0 ;; +--print-residual) + residual_types + exit 0 + ;; --classify) # The classification, reachable without a nightly toolchain and without # generating anything -- so the rule above can be exercised on rows chosen to diff --git a/test/test_public_api_snapshots_doc.py b/test/test_public_api_snapshots_doc.py index b44b8281..d5b8c225 100644 --- a/test/test_public_api_snapshots_doc.py +++ b/test/test_public_api_snapshots_doc.py @@ -46,6 +46,22 @@ # only here, so a diff in it can still be a contract change. REST_SNAPSHOT = "public-api.rest.txt" +# The residual caveat, pinned by the phrases only it uses. Naming the file was +# not enough on its own and it is worth saying why, because the weak version of +# this guard looked exactly like the strong one: `REST_SNAPSHOT` alone is +# satisfied by `REST_FILE=devlaunch-core/public-api.rest.txt` in the script, by +# the sentence describing the split in lib.rs, and by a table row in the doc, so +# a reviewer deleted the entire caveat from two of the three sites and all +# fourteen tests passed. Each of these appears only in the caveat: +# +# the mechanism -- a type that is handed back but never re-exported; +# the example that makes the scale land, since it is the error type of a +# function that is itself a promise-file row at the `api` path; +# the command that recomputes the scale, so the prose stays checkable. +RESIDUAL_MECHANISM = "never re-exports" +RESIDUAL_EXAMPLE = "DevcontainerRefError" +RESIDUAL_COMMAND = "--print-residual" + def script_files() -> list[str]: """The snapshots the script says it writes, as it says them. @@ -179,6 +195,25 @@ def test_the_ci_error_string_sends_a_reader_to_the_document_that_has_the_section ) +def residual() -> tuple[int, int]: + """The limit, as the script counts it: (types, rows). + + Types a promised signature hands back that `api` never re-exports, and the + rows they own in the tripwire file. Asked of the script for the same reason + the file list is: a figure this test kept itself would be a third copy, and + the copy that never goes stale is the one nobody wrote down. + """ + printed = subprocess.run( + [str(SCRIPT), "--print-residual"], + capture_output=True, + text=True, + check=True, + ) + counts = [int(line.split("\t")[0]) for line in printed.stdout.splitlines() if line.strip()] + assert counts, "--print-residual found no residual types at all; the split cannot be that clean" + return len(counts), sum(counts) + + @pytest.mark.unit def test_the_docs_say_how_the_promise_file_is_filled_and_what_it_still_misses(): """The overclaim this section is one edit away from becoming again. @@ -191,6 +226,11 @@ def test_the_docs_say_how_the_promise_file_is_filled_and_what_it_still_misses(): the rest file can still carry a contract change, because a type the `api` module never re-exports but a promised signature hands back is reachable from outside and is classified as binary surface. + + The second half is what this asserts properly now. It used to be spelled as + "the text names public-api.rest.txt", which every one of these files does + for reasons that have nothing to do with the caveat, so the caveat could be + deleted outright and this stayed green. """ for path in (DEV_DOC, SCRIPT, RUST / "devlaunch-core" / "src" / "lib.rs"): text = path.read_text(encoding="utf-8") @@ -202,6 +242,48 @@ def test_the_docs_say_how_the_promise_file_is_filled_and_what_it_still_misses(): f"{path.name} describes the promise file without naming {REST_SNAPSHOT}, " "which is where a promised signature's own types are still classified" ) + assert RESIDUAL_MECHANISM in text, ( + f"{path.name} describes the promise file and not the limit on it: no " + f"sentence saying a type the api module {RESIDUAL_MECHANISM} but a " + "promised signature hands back is still classified as binary surface" + ) + assert RESIDUAL_EXAMPLE in text, ( + f"{path.name} states the limit without naming {RESIDUAL_EXAMPLE}, which " + "is the case that shows what it costs: the error type of " + "api::resolve_devcontainer_ref, a promise-file row at the api path, " + "while renaming one of its variants diffs only the tripwire file" + ) + assert RESIDUAL_COMMAND in text, ( + f"{path.name} states the limit without naming {RESIDUAL_COMMAND}, so a " + "reader is told the residual exists and given no way to see what is " + "in it today" + ) + + +@pytest.mark.unit +def test_the_documented_scale_of_the_limit_is_the_measured_one(): + """The number, because naming one type made 615 rows read as one. + + Every site above described the residual as `flows::launch::Launched` and + nothing else. It is 39 types today, and a reviewer who has been told the + limit is one type reads 38 others' diffs as routine churn. So the figure is + written down, and written down means diffed: this recomputes it from the + checked-in snapshots and holds the prose to it. A surface change that moves + the count turns this red in the same run that regenerates the snapshots, + which is when the sentence is cheapest to fix. + """ + types, rows = residual() + for path in (DEV_DOC, SCRIPT, RUST / "devlaunch-core" / "src" / "lib.rs"): + text = path.read_text(encoding="utf-8") + for figure, what in ((types, "types"), (rows, "rows")): + # Bounded, or `39` is satisfied by the unrelated "395 rows moved" + # that all three of these files also carry. + assert re.search(rf"\b{figure}\b", text), ( + f"{path.name} does not carry the current count of {what} in the " + f"residual ({figure}). Regenerating the snapshots moved it; update " + f"the sentence there, or run {SCRIPT.name} {RESIDUAL_COMMAND} to see " + "what changed" + ) def classify(kind: str, rows: list[str]) -> list[str]: @@ -230,6 +312,12 @@ def classify(kind: str, rows: list[str]) -> list[str]: "pub struct devlaunch_core::api::Launch<'a, 'r, 'l>", "impl<'a, 'r, 'l> devlaunch_core::flows::launch::Launch<'a, 'r, 'l>", "pub fn devlaunch_core::flows::launch::Launch<'a, 'r, 'l>::run(&mut self) -> ()", + # More than one keyword between `pub` and the path. No row of this shape + # exists yet, and the point is that one can be added without noticing: a + # `const fn` constructor on a promised type is an ordinary thing to write, + # and the pattern used to allow exactly one keyword, so it went to the + # tripwire file where a rename of it reads as routine churn. + "pub const fn devlaunch_core::flows::launch::Launch<'a, 'r, 'l>::konst() -> ()", "impl core::fmt::Debug for devlaunch_core::flows::launch::Host", ] UNPROMISED_ROWS = [ From a757428b9d8d6e54ae72622d527dd51ec7e46500 Mon Sep 17 00:00:00 2001 From: Austin Gregg-Smith Date: Sat, 29 Aug 2026 21:57:53 +0100 Subject: [PATCH 3/3] Pin the residual count to the noun it counts A bare word-boundary match on 39 was satisfied twice over by things that are not the residual: '395 rows moved' two paragraphs up, and '39 of them' about orphaned Docker volumes five hundred lines away in the same document. Deleting the caveat from docs/development.md left this green. It now matches '39 types' and '39 such types', and deleting the caveat from any of the three sites fails two tests. --- test/test_public_api_snapshots_doc.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/test_public_api_snapshots_doc.py b/test/test_public_api_snapshots_doc.py index 39732654..ef6eb746 100644 --- a/test/test_public_api_snapshots_doc.py +++ b/test/test_public_api_snapshots_doc.py @@ -282,9 +282,12 @@ def test_the_documented_scale_of_the_limit_is_the_measured_one(): types, _rows = residual() for path in (DEV_DOC, SCRIPT, RUST / "devlaunch-core" / "src" / "lib.rs"): text = path.read_text(encoding="utf-8") - # Bounded, or `39` is satisfied by the unrelated row counts these files - # also carry (`395`, `39x` of anything). - assert re.search(rf"\b{types}\b", text), ( + # The number *and* the noun it counts. A bare `\b39\b` was satisfied + # twice over by things that are not this: `395 rows moved` two + # paragraphs up, and "39 of them" about orphaned Docker volumes five + # hundred lines away in the same document. Matching "39 types" and "39 + # such types" is what makes deleting the caveat show up here. + assert re.search(rf"\b{types}\b(?: \w+)? types", text), ( f"{path.name} does not carry the current number of types in the " f"residual ({types}). Regenerating the snapshots moved it; update the " f"sentence there, or run {SCRIPT.name} {RESIDUAL_COMMAND} to see what "