Repository navigation
devlaunch-core module architecture and data model #251
Description
Activity
- added a parent issue
on Aug 18, 2026 breadcrumb: runner seam settled — one low-level
Runnertrait (spawn-spec in, outcome out) carrying the four exotics as data (stdin-from-file, setsid detach, stderr-only capture, timeout); typed per-tool modules are plain code building specs through it; single fake = recorder + argv→response table + devpod state machine (devpod_mock's design re-homed). PATH-shims only for the 96-test black-box acceptance harness. Per-tool traits and shim-only both rejected for fake drift / argv invisibility.breadcrumb: workspace layout settled — three crates:
devlaunch-core(lib),dl(bin),devlaunch-test-support(dev-only: fake runner, argv→response table, devpod state machine, git fixtures; importable by core, dl, and wf's contract tests). Feature-flag variant rejected — physical separation over flag hygiene.breadcrumb: module boundaries settled — redraw, don't transliterate ('this is a time to do a clean rewrite' — owner). Four layers, deps strictly downward: runner → tool clients (devpod/git/gh/ssh; #54's duplicated git helpers merge here) → domain (workspace_id, spec, model, metadata, config, xdg — triplication fixed) → flows (launch designed fresh, lifecycle, listing/workspace_state, provision, completion, disk_usage, timing). Python's domain vocabulary kept so the mock-based behavior spec stays traceable to modules.
breadcrumb: steering from owner — the port's aim is correctness and maintainability, not performance. Tiebreaker for all remaining design choices: prefer illegal-states-unrepresentable types, total functions, and the simpler structure over the faster or cleverer one.
breadcrumb: consumption model settled — in-process (option A). Core's public API is the operations: list() → Vec (state enum + Unsaved sum), remove(id, force) → typed refusal, up(spec), spec/branch-convention helpers, handoff constants. wf calls them directly; core spawns devpod/git itself via the runner; the dl binary is a thin frontend over the same calls; --ls --json becomes a serde rendering of the same Workspace type, kept for CLI consumers. Kills the wire/version-skew bug class (all four documented wf incidents) structurally; makes #53's two invariants structural rather than disciplinary. Peer-process coexistence on shared disk state rides on the existing flock design; Python-coexistence rules route to #253.
breadcrumb: error model settled — typed per-operation error enums in the public API (option A); the signature is the list of ways an operation can fail. No user-facing English inside core: text and exit codes are rendering, owned solely by the dl binary (preserves #57's exit-127 devpod-missing behavior as a rendering rule). Crate-wide catch-all rejected. Data types confirmed as carried: WorkspaceId per #55/#64, versioned metadata doc per #56 with forward-tolerance, container state enum with explicit Unknown(String) arm, Unsaved sum as wf modeled it.
breadcrumb: dl binary settled — clap (derive): declarative grammar, generated --help (feeds the README-matches---help rule); binary = clap defs → one call into core's API → rendering of typed results to text/exit codes + completion-cache writing. Hand-rolled dispatcher port rejected.
New requirement from owner, folded into the grammar: verb-first forms —
dl stop,dl prune, etc. with no target open the fuzzy selector to pick the workspace(s) to act on, alongside the existing workspace-first grammar (dl <spec> [verb]) which stays pinned by the acceptance harness. Consequences recorded: (1) reserved verb words win over bare workspace specs — a deliberate, listed divergence from today's parser (dl stoptoday would read 'stop' as a spec); (2) interactive selection lives in the dl binary, not core — core supplies list(), the binary picks — widening invariant 1 to 'parsing, rendering, and interactive selection'; (3) selector engine: embedded skim rather than spawning fzf, removing the runtime fzf dependency (a launch-failure class) now that the selector is more load-bearing.Resolution
Settled by grilling with the owner, grounded in the port inventory at 0.0.27 (#249) and the wf consumption surface (#250). Steering principle set by the owner mid-session: the port's aim is correctness and maintainability, not performance — applied as the tiebreaker throughout.
1. The runner seam: one trait, at the bottom
One low-level
Runnertrait — spawn-spec in, outcome out — with a single production implementation overstd::process::Command. The spawn-spec type carries the four load-bearing exotics as data: stdin-from-real-file,setsiddetach, stderr-only capture, per-call timeout. Typed tool modules (devpod,git,gh,ssh) are plain code that builds specs and parses outcomes through the one trait. The single test fake = call recorder + argv→response table + devpod workspace state machine —test/fixtures/devpod_mock.py's proven design re-homed behind a typed interface, and the argv-sequence assertions (#78's durable artifact) fall out of the recorder for free. Rejected: per-tool traits (fake drift, argv invisible to tests), PATH-shims-only (shims reserved for the black-box acceptance harness).2. Workspace: three crates
devlaunch-core(lib) +dl(bin) +devlaunch-test-support(dev-only: the fake runner, fixtures, scenario builders; importable by core's tests, dl's tests, and wf's contract tests). Physical separation chosen over atest-utilfeature flag: nothing test-only can leak into shipped builds even by accident.3. Module boundaries: redraw, don't transliterate
"This is a time to do a clean rewrite" — owner. Four layers, dependencies strictly downward:
runner → tool clients (devpod/git/gh/ssh — #54's duplicated git helpers merge here structurally) → domain (workspace_idper #55/#64,spec,model,metadataper #56,config,xdg— written once, ending the triplication) → flows (launchdesigned fresh — the one genuinely new design task — pluslifecycle,listing/workspace_state,provision,completion,disk_usage,timing). Python's domain vocabulary is kept so the 1,202-test behavior spec stays traceable to modules; only the boundaries move, exactly where #54 said the seams belonged. dl.py's 4,909-line shape is not imported.4. wf consumes core in-process
Core's public API is the operations, not a wrapper over the
dlbinary:list() → Vec<Workspace>,remove(id, force)→ typed refusal,up(spec), spec/branch-convention helpers, handoff constants. wf calls them directly; core spawns devpod/git itself; thedlbinary is a thin frontend over the same calls;--ls --jsonbecomes a serde rendering of the sameWorkspacetype, kept for CLI consumers. This makes the wire/version-skew bug class — all four incidents documented in #250's research — unrepresentable, and turns #53's two invariants from discipline into structure. Consequence routed to #253: linked wf is a peer process ofdlon shared disk state (metadata.json, flocks), not a child.5. Errors: typed, per-operation, rendered only in the binary
Each public operation returns its own error enum (e.g.
remove:WouldLoseWork{what}/NotFound/NotOurs/DevpodMissing/CommandFailed{…}) — the signature is the list of failure modes. No user-facing English inside core: text and exit codes are rendering, owned solely bydl(preserving #57's exit-127 rule as a rendering decision). Crate-wide catch-all rejected — it recreates Python's broad-handler misreporting class. Data types confirmed: container state as an enum of devpod's vocabulary plus an explicitUnknown(String)arm (total over devpod's future);Unsavedas the three-arm sum; versioned metadata document with #56's forward-tolerance.6. The dl binary: clap, plus the verb-first selector grammar
clap (derive style): declarative grammar, generated
--help(which the README rule tracks). The binary is: clap definitions → one call into the public API → rendering + completion-cache writing (keepingdl.bashbyte-identical). New requirement from the owner, folded in here: verb-first forms —dl stop,dl prune, … — with no target open the fuzzy selector to choose the workspace(s), alongside the existing workspace-first grammar. Consequences: reserved verb words win over bare specs (a deliberate, listed divergence — todaydl stopwould parse "stop" as a spec); interactive selection lives in the binary, widening invariant 1 to "parsing, rendering, and interactive selection"; the selector embeds skim rather than spawning fzf, removing a runtime-dependency failure class now that the selector is load-bearing. Where clap is stricter than the hand-rolled parser (unknown flags rejected), that is the correctness aim winning — each such case goes on the divergence list that #252's parity definition owns.7. Public API: minimal and synchronous
Day-one public surface is exactly #250's closure — the typed listing,
remove/upwith typed errors, spec/branch helpers, handoff constants — and nothing else; every other module stays crate-private until a consumer exists (publishing later is free, un-publishing is a break). The API is synchronous, no async runtime: the work is subprocess- and file-bound, wf does its own threading, and tokio would tax every consumer with coloring and a dependency tree for zero correctness gain. Explicitly deferred from the surface: the launch exec, devcontainer.json reading, any filesystem model of~/.cache.Routed onward
- Acceptance harness: which tests gate cutover, run against both binaries #252 (acceptance harness): owns the parity definition including the deliberate-divergence list (clap strictness, reserved verb words).
- Porting a moving target: freeze, checkpoint, or continuous tracking #253 (moving target): the peer-process coexistence contract — two implementations sharing metadata.json and flocks during the parallel period.
- Port order, milestone slicing, and cutover criteria #255 (port order): the minimal public API of §7 defines the first wf-useful slice; the verb-first grammar is in build scope.
- added a commit that references this issue
on Sep 16, 2026
Question
What is the module architecture and data model of the cargo workspace —
devlaunch-corelib +dlbin — such that the two binding invariants from #53 hold by construction?To settle (with
/constructive-modelingon every data shape, sincewfinherits these types):devlaunch-core— informed by the duplication catalog from #54 (git helpers duplicated acrossdl/BranchManager, triplicated XDG-path logic) and the refreshed inventory.run_devpod/mock-patching, designed so the mock-based Python tests' design (call recorder + workspace state machine + injectable failures, pertest/fixtures/devpod_mock.py) maps onto it — the harness rebuild cost lives or dies here.WorkspaceId(the parse boundary decided in #55/#64 carries over — one constructor over the unsanitized triple), metadata document (serde over theversion-headeredmetadata.jsonfrom #56), backend selection (worktree vs devpod).dlthe binary is allowed to contain — the enforcement story for "no logic beyond arg parsing and rendering", and the first API surfacewfneeds (from the consumption-surface research).