Skip to content

refactor(apply): unify init'd and stateless dispatch behind ApplyTarget#37

Merged
outergod merged 2 commits intomasterfrom
fix/stateless-streaming-apply
May 7, 2026
Merged

refactor(apply): unify init'd and stateless dispatch behind ApplyTarget#37
outergod merged 2 commits intomasterfrom
fix/stateless-streaming-apply

Conversation

@outergod
Copy link
Copy Markdown
Owner

@outergod outergod commented May 7, 2026

Summary

`core-ops apply --source-repo PATH` (stateless mode, spec/017) printed a wall-of-text summary at the end of apply regardless of TTY state. End-users running the canonical `examples/03-immich` walkthrough saw a multi-second pause followed by a fully-rendered final report — none of the line-by-line "creating... → created" progression that init'd-mode apply has rendered since spec/006.

Root cause: stateless dispatch in `src/main.rs` called a separate batch-only `apply_with_report_stateless` entry point. The streaming + interactive variants that init'd dispatch routes to (`apply_with_report_streaming{,_interactive}`) never existed for stateless mode. The two paths drifted, and future agent + human work was at risk of compounding the drift while the two implementations remain parallel.

This PR introduces a single `pub enum ApplyTarget { Initd { ... }, Stateless { ... } }` abstraction and routes both modes through the same three streaming variants. The three init'd-mode entry points are unchanged in shape (still `apply_with_report{,_streaming,_streaming_interactive}`); they now accept `&ApplyTarget` and dispatch on a small set of mode-dependent helpers (`target.load_desired()`, `target.state_path()`, `target.classify_run_display(...)`). `apply_with_report_stateless` is deleted. `src/main.rs` collapses the two parallel apply ladders into one.

Patch bump (`rust_source_surface`); fixture pin updated in lock-step. Six integration test files updated to call through the new `ApplyTarget` API. No behavioural change to init'd mode.

Test plan

  • `cargo test` — 473 passed
  • `cargo clippy --all-targets -- -D warnings` — clean
  • `cargo run --bin core-ops-release -- validate --base-ref master` — passed (patch)
  • End-to-end on `core-ops-uat` (Fedora CoreOS guest), TTY-allocated SSH:
    `sudo core-ops apply --source-repo examples/03-immich --host example` now streams the "creating... → created" progression line-by-line for all 10 services. Same UX as init'd-mode apply.
  • CI green
  • Post-merge: spec/018 picks this up via rebase + can record a meaningful asciicast against the canonical walkthrough.

🤖 Generated with Claude Code

`core-ops apply --source-repo PATH` (stateless mode, spec/017) printed
a single wall-of-text summary at the end of apply regardless of TTY
state. End users running the canonical `examples/03-immich`
walkthrough saw a multi-second pause followed by a fully-rendered
final report — none of the line-by-line "creating... → created"
progression that init'd-mode apply has rendered since spec/006.

Root cause: `src/main.rs:143-207` (stateless dispatch) called
`apply_cmd::apply_with_report_stateless` unconditionally and printed
the final `human_report` once. The init'd dispatch (lines 216-269)
had a `if json / if interactive / else` ladder that selected between
`apply_with_report` (batched), `apply_with_report_streaming_interactive`
(TTY spinner), or `apply_with_report_streaming` (plain streaming).
Stateless never picked up the latter two because it had its own
dedicated entry point.

This is the kind of init'd-vs-stateless drift future agent + human
work would compound, not resolve, while the two paths remain
separately implemented.

Resolution: introduce `pub enum ApplyTarget { Initd { repo_source,
revision, state_path }, Stateless { source } }` (`src/cli/apply.rs`)
that abstracts the desired-state source and the state-backend
behaviour. The three streaming variants now accept `&ApplyTarget`
and dispatch on `target.is_stateless()` for the few mode-dependent
decisions: `target.load_desired()` (path-vs-git loader),
`target.state_path()` (None for stateless, suppresses
persist_in_progress / persist_finished / deterministic-state
writes), `target.classify_run_display(...)` (uses the new
`Stateless` variant for non-empty stateless hosts).

`apply_with_report_stateless` is deleted; stateless callers
construct `ApplyTarget::stateless(source)`. `src/main.rs` collapses
the two parallel apply ladders into a single ladder that selects
between the three shared streaming variants based on `--json` /
TTY-detection — for both modes. Audit emission still branches on
mode (init'd reads persisted state; stateless builds a synthetic
provenance via `synthetic_stateless_provenance`).

Verified end-to-end on `core-ops-uat` (Fedora CoreOS guest):

    $ ssh -t core@uat 'cd .../coreops-uat && sudo core-ops apply \
        --source-repo examples/03-immich --host example'
    Apply for host example @ (stateless) (first run)
    ────────────────────────────────────────────────

    Execution
    ─────────
    [+] volume/immich-db-data.volume			creating...
    [+] volume/immich-ml-cache.volume			creating...
    ...
    [+] container/immich-database.container			creating...
        requires
          - network/immich-internal.network
          - volume/immich-db-data.volume
    [+] container/immich-ml.container			creating...
    ...
    Outcome: converged

The "creating..." progress lines stream in their natural cadence;
container creation interleaves with volume/network creation as
podman processes each unit. Identical UX to init'd-mode apply.

Patch bump (`rust_source_surface` for `src/`); fixture pin updated
in lock-step. Six integration test files updated to call
`apply_with_report` / `apply_with_report_streaming` / etc. through
the new `ApplyTarget` API. No behavioural change to init'd mode.

$ cargo test                                                                  473 passed
$ cargo clippy --all-targets -- -D warnings                                   clean
$ cargo run --bin core-ops-release -- validate --base-ref master              passed (patch)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@outergod outergod deployed to homelab-e2e May 7, 2026 10:22 — with GitHub Actions Active
@outergod outergod merged commit ddb02a1 into master May 7, 2026
5 checks passed
@outergod outergod deleted the fix/stateless-streaming-apply branch May 7, 2026 10:39
outergod added a commit that referenced this pull request May 7, 2026
PR #37 (refactor(apply): unify init'd and stateless dispatch behind
ApplyTarget) landed on master and the post-merge promote tagged
v2.2.4. The fix delivers the spec/006 streaming output to stateless
mode, which is the prerequisite spec/018's recording was waiting on.

Bump 2.2.4 -> 2.2.5 (`packaged_readme_surface` carve-out;
provenance-state fixture pinned in lock-step).

FR-007 amendments:

- The recording is now **apply-only**. The static plan-output and
  idempotent-re-run blocks in the README walkthrough section (per
  FR-006) are the canonical source of plan/re-plan content;
  including those beats in the recording adds duration without
  motion. The 90s budget is preserved for the apply step where
  the line-by-line streaming output now matters.
- The recording MUST be produced on a host where `core-ops` runs
  natively (not via SSH delegation). SSH transport (even with
  `-t` PTY allocation) collapses the streaming timing that the
  recording is meant to capture; SSH-delegated recording is
  explicitly out of spec going forward. The decision file
  decision_018-recording-ssh-delegation captures the prior
  procedure as a session-3 historical artifact, not a forward
  recipe.

Tooling amendment:

- `docs/onboarding-script.sh` now records a single beat (apply
  only) instead of the prior plan -> apply -> re-plan trio. The
  command sequence in the BASH heredoc is one `demo 'sudo
  core-ops apply --source-repo examples/03-immich --host
  example'` invocation. Header rewritten to match.

The actual cast + GIF re-recording is the operator's next step,
to be done natively on a host with `core-ops` 2.2.5 + asciinema
2.4.0 + agg 1.7.0 + the repo present (or an equivalent on-host
setup). The current `docs/onboarding.cast` and
`docs/assets/core-ops-demo.gif` remain in tree from the prior
SSH-delegated session-3 work and will be replaced once the
operator records natively.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
outergod added a commit that referenced this pull request May 7, 2026
…s v3

Operator re-recorded `docs/onboarding.cast` natively against the
PR #37 fix (core-ops 2.2.5 with stateless streaming). Duration
4.97 s. The recording captures the line-by-line "creating... →
created" progression that asciinema 3.x's PTY observation preserves
end-to-end — exactly the UX FR-007 was rewritten to require, and the
SSH-delegation procedure was unable to capture.

Cast was produced by asciinema 3.x, which writes asciicast v3 by
default. Spec amendments to accommodate:

- FR-008: now accepts asciicast v2 OR v3. Both are rendered by `agg`
  1.7.0 and played by asciinema.org. The header MUST declare
  `"version": 2` or `"version": 3`.
- SC-005: same — version MUST be 2 or 3.
- SC-005a: duration calculation now branches on version. v2 has an
  absolute `duration` header field; v3 events are `[delta, type, data]`
  triples and total duration = sum of all deltas. Either MUST be ≤ 90.
- Checklist C-010 updated with the version-branching one-liner.

Post-recording sanitization (per decision_018-recording-ssh-delegation
and checklist C-006):

- OSC 3008 sequences stripped from event data (pam_systemd-emitted
  `hostname=core-ops-uat`, `machineid=...`, PID metadata under sudo).
  The post-processor is v3-aware (event shape is identical to v2;
  header schema differs but the SHELL/duration handling is unchanged
  for our purposes).
- nix-store-path SHELL env value cleaned to `/bin/bash`.

GIF re-rendered from the sanitized cast: 56 KB GIF89a, well under
SC-005b's 1 MB soft cap.

Verification:

$ head -1 docs/onboarding.cast | jq '.version'                                3
$ awk 'NR>1' docs/onboarding.cast | jq -s 'map(.[0]) | add'                  4.965  (≤ 90)  PASS  SC-005a
$ grep -c '3008' docs/onboarding.cast                                         0          PASS  FR-009a
$ grep -iE '(not\.one|ulthar|192\.168\.|10\.0\.|172\.16\.)' docs/onboarding.cast docs/onboarding-script.sh
                                                                              (no matches) PASS  SC-006a
$ head -c 6 docs/assets/core-ops-demo.gif                                     GIF89a       PASS  SC-005b
$ wc -c < docs/assets/core-ops-demo.gif                                       56923  (≤ 1MB) PASS  SC-005b
$ cargo run --bin core-ops-release -- validate --base-ref master              passed (patch)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant