Skip to content

devlaunch-core module architecture and data model #251

Description

@blooop

Question

What is the module architecture and data model of the cargo workspace — devlaunch-core lib + dl bin — such that the two binding invariants from #53 hold by construction?

To settle (with /constructive-modeling on every data shape, since wf inherits these types):

  • Module boundaries inside devlaunch-core — informed by the duplication catalog from #54 (git helpers duplicated across dl/BranchManager, triplicated XDG-path logic) and the refreshed inventory.
  • The command-runner seam: the trait that replaces run_devpod/mock-patching, designed so the mock-based Python tests' design (call recorder + workspace state machine + injectable failures, per test/fixtures/devpod_mock.py) maps onto it — the harness rebuild cost lives or dies here.
  • The core data types: WorkspaceId (the parse boundary decided in #55/#64 carries over — one constructor over the unsanitized triple), metadata document (serde over the version-headered metadata.json from #56), backend selection (worktree vs devpod).
  • What dl the binary is allowed to contain — the enforcement story for "no logic beyond arg parsing and rendering", and the first API surface wf needs (from the consumption-surface research).

Activity

  1. self-assigned this
    on Aug 18, 2026
  2. blooop commented on Aug 18, 2026

    @blooop
    OwnerAuthor

    breadcrumb: session started — resolving via grilling, inputs: research/rust-rewrite-inventory (#249), research/wf-consumption-surface (#250).

  3. blooop commented on Aug 18, 2026

    @blooop
    OwnerAuthor

    breadcrumb: runner seam settled — one low-level Runner trait (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.

  4. blooop commented on Aug 18, 2026

    @blooop
    OwnerAuthor

    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.

  5. blooop commented on Aug 18, 2026

    @blooop
    OwnerAuthor

    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.

  6. blooop commented on Aug 18, 2026

    @blooop
    OwnerAuthor

    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.

  7. blooop commented on Aug 18, 2026

    @blooop
    OwnerAuthor

    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.

  8. blooop commented on Aug 18, 2026

    @blooop
    OwnerAuthor

    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.

  9. blooop commented on Aug 18, 2026

    @blooop
    OwnerAuthor

    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 stop today 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.

  10. blooop commented on Aug 18, 2026

    @blooop
    OwnerAuthor

    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 Runner trait — spawn-spec in, outcome out — with a single production implementation over std::process::Command. The spawn-spec type carries the four load-bearing exotics as data: stdin-from-real-file, setsid detach, 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 a test-util feature 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_id per #55/#64, spec, model, metadata per #56, config, xdg — written once, ending the triplication) → flows (launch designed fresh — the one genuinely new design task — plus lifecycle, 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 dl binary: 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; 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. 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 of dl on 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 by dl (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 explicit Unknown(String) arm (total over devpod's future); Unsaved as 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 (keeping dl.bash byte-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 — today dl stop would 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/up with 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions