Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/workflows/bench.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ name: Launch benchmark
# never fail a build on a slow point. One quiet host's five identical
# cold-recreate runs spread 15.2-20.0s; a shared runner is worse, so a hard
# wall-clock gate would be either too loose to fire or flaky. Deterministic
# structural guards (test_devpod_spawn_counts.py and its kin) keep the gating
# role, and they live in ci.yml where a gate belongs.
# structural guards keep the gating role: the devpod spawn counts a launch is
# allowed, asserted argv by argv in `flows::launch`'s tests, and their kin
# across the cargo suite. Those run in ci.yml, where a gate belongs. (The
# spawn-count guard was `test_devpod_spawn_counts` in Python until the Python
# tree retired in #267, and the counts came across with it.)
#
# It is a separate file for exactly that reason: house convention is that a new
# job in ci.yml joins `gate`'s `needs` in the same pull request, and `needs`
Expand Down
12 changes: 12 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,18 @@ checking rather than trusting, because a private package fails at nothing. See
matching on headings. Moving one of those sections between files means moving
the path in its guard, in the same change.

- **A citation spelled as a path has to point somewhere.** Comments all over
`rust/` name the test that pins the behaviour they describe, which is what
makes the record worth reading. `test/test_citations_resolve.py` holds every
one of those names to being a file: a path is checked as written, a bare
filename against the test tree. Retiring the Python implementation (#267)
broke about sixty of them at once, which is why the rule exists. When a suite
is gone, name it without the extension and say it retired, the way the
comments now do: `test_purge_ownership` (Python, retired in #267). The
history stays greppable and stops impersonating a live guard. `CHANGELOG.md`
and `docs/rust-port-scope.md` are out of scope as records of what was true
when they were written.

- **No em or en dashes in the README or the `docs/` pages it links.** They read
as machine-written. Use a full stop or a comma, or end the sentence, and write
a numeric range as "18s to 28s". `test/test_docs_prose.py` asserts it and
Expand Down
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
died before any of this existed, and no route reaches them that is not the
pattern above. `docs/cleanup.md` carries the whole of it.

### Fixed

- **Sixty-six citations that pointed at nothing now point at something, and a
guard keeps it that way.** Comments across `rust/` name the test that pins the
behaviour they describe, which is most of what makes them worth reading.
Retiring the Python implementation (#267) deleted about forty of the files
those names referred to, in one commit, and the names stayed. A pointer into
nothing is worse than no pointer: it still reads as evidence, so the reader
goes looking and cannot tell whether the guard moved, was renamed, or never
existed.

Every one of them now names a suite that retired rather than a file to open,
and says so, in the form the surviving comments had already settled on. Two
were not merely stale but false. The bench workflow told a reader that a named
Python spawn-count guard "keeps the gating role" in `ci.yml`, when the counts
it meant have been argv assertions in the cargo suite since the port; and the
divergence table's row 30 claimed two pins it no longer has, an allowance in
the parity harness's case list and a set of `suffix_verb` tests, the first
retired with the harness and the second deleted with the behaviour itself. That
row is superseded, so nothing pins it, and it now says so.

`test/test_citations_resolve.py` is what stops it happening again: a token
spelled the way a Python test file is spelled has to resolve to a file, a path
as written and a bare name against the test tree. `CHANGELOG.md` and
`docs/rust-port-scope.md` are out of scope, being records of what was true when
they were written.

## [0.25.0] - 2026-08-28

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion docs/rust-rewrite-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ Nothing diverges silently.
| 28 | The `metadata.json` reader takes strict JSON where Python's `json.loads` took its extensions: `NaN`/`Infinity` literals, numbers beyond f64 range (`1e400`, which Python reads as `inf`), and lone-surrogate `\uD800` escapes all read as corruption, so a file carrying one is quarantined to `metadata.json.corrupt` (bytes intact, single slot) and the run starts with an empty cache where Python loaded the records around the value. | serde_json cannot represent a lone surrogate in a Rust `String` and f64 cannot hold `1e400`, so accepting them would mean values the store could not round-trip; no build of dl (Python or Rust) ever writes any of them — only a hand-edited or third-party-written file can carry one — and the quarantine keeps the bytes recoverable. Pinned by `pythons_json_extensions_read_as_corruption_here` in `domain/metadata.rs`. |
| 29 | A clone the host refused as not-found gets a second line naming the same repository under an owner the completion cache knows: `Did you mean 'kinisi-robotics/kinisi_ros'? …`. Python printed git's stderr and stopped. The line is added only for a host's own not-found wording (GitHub, GitLab, Bitbucket), only from a clone step, and only when the cache holds the same repo name under another owner *and* not under the owner typed; at most three candidates are named and any remainder is counted. The git text above it is unchanged in every case. | A mistyped owner and a revoked permission produce the same six lines from git, so git's words cannot tell them apart — and devlaunch already holds the answer in the list its own shell completion offers. Additive: nothing is removed or reworded, and a machine with no cache entry sees exactly what it saw before. The suggestion names a *spec*, not a `dl …` command line, because `aid` renders through the same function and its own invocation carries an agent prompt. Pinned by the `wrong_owner_hint` tests in `dl/tests/launch.rs` and `dl/src/render.rs`. |

| 30 | `--stop` and `--rm` are **appendable**: on a line that already said something else they win, and the words they displace are reported rather than refused. `dl <ws> 'review this pr' --rm` was `Unknown command 'review this pr'` (exit 1) in both Python and earlier Rust; `dl prune <ws> --force --rm` took `prune` as the *workspace* and reported it unknown. A leading verb word is now skipped when finding the target, a word spelling the flag's own verb is absorbed silently, and everything else displaced is named on stderr (`--rm overrode the rest of the line: 'review this pr' was not acted on.`) before anything is removed. `aid` peels the same two flags — plus a `--force` in their company — off the end of its argv before the prompt join, so `aid <ws> <prompt> --rm --force` removes the workspace and hands no agent command to dl; a verb flag before the spec is collected the same way rather than passed through beside a `--` tail dl would refuse. The peel is bounded to the exact words at the very end of the line, so a prompt that mentions `--rm` or ends in a bare `--force` is untouched, and a `--` command tail cannot be overridden at all. | **Superseded by row 32**, which takes both spellings back: `--rm` became docker's `--rm` and `--stop` was retired, so no flag overrides a line any more. A shell makes appending to a recalled line cheap and rewriting its front expensive, so "and now delete it" has to be spellable as a suffix. The notice is what makes it safe rather than a silent discard: the line may now carry an instruction it will not carry out, so a deliberate suffix and a slip have to be distinguishable. Row 15 made the flag spellings work; this makes them the one form that can be typed last. Pinned by the `suffix_verb` tests in `dl/src/cli.rs` and `aid/src/rewrite.rs`, and by the `--aid -- --help` allowance in `rust/tools/parity_cases.txt` — aid's help text is hand-written and was byte-compared against Python's, so documenting a flag Python's aid has not got is where this row becomes visible to the parity harness. |
| 30 | `--stop` and `--rm` are **appendable**: on a line that already said something else they win, and the words they displace are reported rather than refused. `dl <ws> 'review this pr' --rm` was `Unknown command 'review this pr'` (exit 1) in both Python and earlier Rust; `dl prune <ws> --force --rm` took `prune` as the *workspace* and reported it unknown. A leading verb word is now skipped when finding the target, a word spelling the flag's own verb is absorbed silently, and everything else displaced is named on stderr (`--rm overrode the rest of the line: 'review this pr' was not acted on.`) before anything is removed. `aid` peels the same two flags — plus a `--force` in their company — off the end of its argv before the prompt join, so `aid <ws> <prompt> --rm --force` removes the workspace and hands no agent command to dl; a verb flag before the spec is collected the same way rather than passed through beside a `--` tail dl would refuse. The peel is bounded to the exact words at the very end of the line, so a prompt that mentions `--rm` or ends in a bare `--force` is untouched, and a `--` command tail cannot be overridden at all. | **Superseded by row 32**, which takes both spellings back: `--rm` became docker's `--rm` and `--stop` was retired, so no flag overrides a line any more. A shell makes appending to a recalled line cheap and rewriting its front expensive, so "and now delete it" has to be spellable as a suffix. The notice is what makes it safe rather than a silent discard: the line may now carry an instruction it will not carry out, so a deliberate suffix and a slip have to be distinguishable. Row 15 made the flag spellings work; this makes them the one form that can be typed last. It was pinned by the `suffix_verb` tests in `dl/src/cli.rs` and `aid/src/rewrite.rs`, and by the `--aid -- --help` allowance in the parity harness's case list — aid's help text is hand-written and was byte-compared against Python's, so documenting a flag Python's aid had not got was where this row became visible to that harness. Neither is still standing: the harness retired with the Python tree (#267), and the `suffix_verb` tests were deleted along with the behaviour itself, in the change row 32 records. Nothing pins this row now, which is what superseded means here; row 32 names what pins the grammar that replaced it. |

| 31 | `prune` is retired as a workspace verb. `dl <ws> prune` / `dl prune <ws>` / `dl prune` were Python's second spelling of `rm` and now refuse with `'prune' is no longer a workspace verb. Use 'dl <workspace> rm' to delete a workspace, or 'dl --prune' to remove the clone directories no workspace opens any more.` (exit 1). The word stays in a `RETIRED` table rather than being dropped, so it is still never read as a workspace name: `dl prune <ws> --force --rm` removes `<ws>` (row 30's suffix form, with `prune` absorbed as the same verb the flag asks for), and a live verb beside it still wins from either position, so `dl stop prune` stops a workspace named `prune` as before. `dl --prune` is unchanged. Row 15's parenthetical about the `Ignoring --devcontainer` diagnostic naming `rm` for the `prune` spelling is now unreachable. | **Superseded in part by row 32**, which retires row 30's suffix form: `dl prune <ws> --rm` is now this row's refusal rather than a removal of `<ws>`, and the word is still in `RETIRED` and still never read as a workspace name. One word meant two unrelated commands, separated only by two dashes: `dl <ws> prune` deleted a workspace and `dl --prune` deletes clone directories and no workspace. The two failure modes were losing a workspace meant to be kept, and being refused by `--prune takes no workspace` for a reason that sentence cannot explain. `rm` has no such twin, and the retirement is a refusal that names both meanings rather than a silent removal. Pinned by the `prune_is_refused_as_a_verb_from_either_position`, `a_retired_word_is_never_read_as_the_workspace` and `a_recalled_prune_line_is_the_retirement_and_the_pair_is_named_ahead_of_it` tests in `dl/src/cli.rs`. |

Expand Down
18 changes: 13 additions & 5 deletions rust/aid/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,19 @@ repository.workspace = true

[dependencies]
# The only devlaunch crate named here, and deliberately: aid is a command-line
# rewrite that hands the result to dl, so the whole of devlaunch it can reach is
# dl's entry point and the quoting that composes a command line for it. Python
# pins the same rule as a test (`test/unit/test_aid.py` walks aid.py's AST and
# refuses any use of `dl` beyond `main`, `get_version` and `DL_VALUE_OPTIONS`);
# here it is the dependency graph.
# rewrite that hands the result to dl, so what it can reach is whatever dl makes
# public and nothing else. That is the whole of what this line enforces. aid
# cannot name `devlaunch-core` or `devlaunch-runner`, so a new reach into either
# has to be a `pub use` somebody adds to dl and reviews there.
#
# It is a looser rule than Python's, which is worth saying rather than glossing:
# `test_aid` walked aid.py's AST and refused any use of `dl` beyond `main`,
# `get_version` and `DL_VALUE_OPTIONS` (that suite retired with the Python tree
# in #267). Nothing counts names here, and the count is well past three: aid's
# source reaches dl's signal handlers, terminal helpers, version markers and
# rendering, and some of what it reaches is devlaunch-core's own arriving
# through a dl re-export (`shell`, `env_str`). The surface is a review question
# now, not a test.
dl = { path = "../dl" }

# Forwarded rather than declared, so the two binaries cannot report different
Expand Down
6 changes: 3 additions & 3 deletions rust/aid/src/rewrite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -763,9 +763,9 @@ pub(crate) fn build_dl_args(parsed: &AidArgs) -> Option<Vec<String>> {

#[cfg(test)]
mod tests {
//! `test/unit/test_aid.py`'s three parsing classes, which are the whole of aid's
//! own behaviour: what the command line splits into, what shell command comes
//! out, and what dl is handed.
//! The Python `test_aid`'s three parsing classes (retired with the Python tree
//! in #267), which are the whole of aid's own behaviour: what the command line
//! splits into, what shell command comes out, and what dl is handed.

use super::*;

Expand Down
10 changes: 6 additions & 4 deletions rust/aid/tests/rewrite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -385,10 +385,12 @@ fn a_remote_control_variable_that_is_neither_a_yes_nor_a_no_is_refused() {

#[test]
fn no_prompt_starts_the_agents_plain_session() {
// `test/test_interactive_command.py::TestAidReachesTheTtyTransport`'s first case
// at the boundary: no prompt, so no prompt flags — and the same transport `dl
// <ws>` uses, which is what makes aid a rewrite rather than a launcher. (The
// OpenSSH pty half of that class needs a published ssh alias and is M9's.)
// The Python `test_interactive_command::TestAidReachesTheTtyTransport`'s first
// case at the boundary (that suite retired with the Python tree in #267, and
// this is where the case lives now): no prompt, so no prompt flags — and the
// same transport `dl <ws>` uses, which is what makes aid a rewrite rather than
// a launcher. (The OpenSSH pty half of that class needs a published ssh alias
// and is M9's.)
let world = World::with(&["--warm"]);
let run = world.aid(&[MAIN]);
run.exited(0);
Expand Down
7 changes: 4 additions & 3 deletions rust/devlaunch-core/src/clients/devpod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1305,9 +1305,10 @@ mod tests {

// ------------------------------------------------------- the timing spans
//
// `test/test_timing.py`'s `TestDevpodRoundTripsAreNamed` asserts these labels
// through `main()`, where they arrive from Python's one spawn helper. They
// arrive from this module's three, so this is where they are pinned.
// The Python `test_timing`'s `TestDevpodRoundTripsAreNamed` asserted these
// labels through `main()`, where they arrived from Python's one spawn helper.
// They arrive from this module's three, so this is where they are pinned now;
// that suite retired with the Python tree (#267).

/// The span labels `record` produced, in order.
///
Expand Down
7 changes: 4 additions & 3 deletions rust/devlaunch-core/src/clients/gh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -589,9 +589,10 @@ mod tests {

// -------------------------------------------------------- the timing span
//
// `test/test_timing.py`'s `TestTransportAndGitGhCallsAreNamed` and
// `TestHostPrepIsAStage` assert this from outside; Python's span and stage sit
// inside `_token_from_gh_cli`, so this is the module that owns them.
// The Python `test_timing`'s `TestTransportAndGitGhCallsAreNamed` and
// `TestHostPrepIsAStage` asserted this from outside, and retired with the
// Python tree (#267); Python's span and stage sat inside `_token_from_gh_cli`,
// so this is the module that owns them.

/// The stages and spans a measured `record` produced.
fn measured(record: impl FnOnce()) -> timing::Document {
Expand Down
3 changes: 2 additions & 1 deletion rust/devlaunch-core/src/domain/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,8 @@ struct StoredCleanup {

#[cfg(test)]
mod tests {
//! `test/test_worktree_config.py`, re-pinned.
//! The Python `test_worktree_config`, re-pinned here when it retired with the
//! Python tree (#267).
//!
//! Python's tests construct `WorktreeConfig(...)` directly and check
//! `to_dict`; both are gone here, because nothing in devlaunch writes this
Expand Down
3 changes: 2 additions & 1 deletion rust/devlaunch-core/src/domain/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1190,7 +1190,8 @@ impl serde_json::ser::Formatter for PythonJsonFormatter<'_> {

#[cfg(test)]
mod tests {
//! `test/test_worktree_storage.py`, re-pinned.
//! The Python `test_worktree_storage`, re-pinned here when it retired with the
//! Python tree (#267).
//!
//! Python patched two seams to observe behaviour it could not otherwise see
//! — `Path.replace` to watch the atomic rename, `storage.save` to count
Expand Down
3 changes: 2 additions & 1 deletion rust/devlaunch-core/src/domain/workspace_state/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
//! walking up into an ancestor repository) were both invisible to everything
//! except a real git.
//!
//! Ported from `test/test_workspace_state.py`'s five module-level classes:
//! Ported from the Python `test_workspace_state` (retired with the Python tree
//! in #267), whose five module-level classes were:
//! `TestWhatACloneHolds`, `TestWhenGitCannotBeAsked`,
//! `TestGitIsPinnedToItsWorkTreeToo`, `TestADirectoryThatCannotBeLookedAt`,
//! `TestTheAnswersAreTotal` and `TestNamingWhatIsUnsaved`. Its remaining classes
Expand Down
3 changes: 2 additions & 1 deletion rust/devlaunch-core/src/flows/branch_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,8 @@ impl<'r> BranchManager<'r> {

#[cfg(test)]
mod tests {
//! `test/test_worktree_branch_manager.py`, re-pinned at the argv seam.
//! The Python `test_worktree_branch_manager`, re-pinned at the argv seam when
//! it retired with the Python tree (#267).
//!
//! Python's `TestEnsureBranchExists` classes patch the four verbs on the class
//! and assert which of them were called with what. There is nothing to patch
Expand Down
5 changes: 3 additions & 2 deletions rust/devlaunch-core/src/flows/completion_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -382,10 +382,11 @@ pub(crate) fn local_branches(git: &Git<'_>, repos_dir: &Path, owner_repo: &str)

#[cfg(test)]
mod tests {
//! Ported from `test/test_dl.py`'s `TestCacheFunctions`,
//! Ported from the Python `test_dl`'s `TestCacheFunctions`,
//! `TestCompletionCacheFreshness`, `TestNoRefreshForCacheFreeCommands` and
//! `TestCacheReadingCommandsWarmTheCache`, plus
//! `test/unit/test_workspace_listing.py::TestCompletionsAreBuiltFromWhateverCanBeRead`.
//! `test_workspace_listing::TestCompletionsAreBuiltFromWhateverCanBeRead`.
//! Both suites retired with the Python tree (#267).
//!
//! The two files' bytes are goldens: they were read off a scratch cache written
//! by the Python build, run against the devpod shim.
Expand Down
4 changes: 3 additions & 1 deletion rust/devlaunch-core/src/flows/disk_usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,9 @@ impl UsageWire {

#[cfg(test)]
mod tests {
//! What `test/unit/test_disk_usage.py` pins, on this side of the port.
//! What the Python `test_disk_usage` pinned, on this side of the port. That
//! suite retired with the Python tree (#267); this is where its subject lives
//! now.
//!
//! The number under test is *exclusive* bytes: what deleting this directory
//! would free, counting a file only when every one of its links lies inside
Expand Down
Loading
Loading