refactor: carry the connection's role in its phase #84

Merged
natsukium merged 3 commits from refactor/driver-phases-46 into main 2026-09-04 20:37:11 +09:00
Owner

Fixes #46.

Phase stopped at Steady, so one state served a welcomed connection
waiting to attach, an attached session, and a notification observer.
Kind-level admission had to be permissive enough for all three, and each
call site made up the difference by hand: the daemon's pre-attach loop
refused a second opener, the attached pump refused a second Attach, the
observer loop refused everything but a cancel, and the client decoded
uncorrelated session replies straight off a kind match, past the driver's
direction and phase columns. A peer reading the arm table saw none of that.

The phase splits into Setup | Attached | Observing, each arm declares its
phases in the same table felis.proto restates, and the driver admits every
inbound frame against it once: the kind fold before the body decodes, the
arm's own row after. The hand guards go with it, and DriverError::OutOfPhase
names the phases an arm does belong to. Mode is checked before phase so a
surface the connection never asked for is answered with a typed
Conn::Refused rather than told to wait for a state it can never reach.

Doc cascade: docs/reference/ipc.md gains the "Connection phases" ladder and
transition table beside the arm table's phases column; the explanation twin
docs/explanation/architecture/ipc.md records why the ladder is data the
driver owns rather than a typestate (the driver is shared behind a mutex in
the GUI and behind &mut in three daemon loops) and why there is no Closing
phase; REQ-114 in docs/reference/spec.md now names the out-of-phase frame
and the ladder; CHANGELOG.md records the wire-visible phases column change.

Verified: just check green (fmt, clippy, nextest, deny); reviewed by
plan/correctness/docs lenses and pi sol.

Fixes #46. `Phase` stopped at `Steady`, so one state served a welcomed connection waiting to attach, an attached session, and a notification observer. Kind-level admission had to be permissive enough for all three, and each call site made up the difference by hand: the daemon's pre-attach loop refused a second opener, the attached pump refused a second `Attach`, the observer loop refused everything but a cancel, and the client decoded uncorrelated session replies straight off a kind match, past the driver's direction and phase columns. A peer reading the arm table saw none of that. The phase splits into `Setup | Attached | Observing`, each arm declares its phases in the same table `felis.proto` restates, and the driver admits every inbound frame against it once: the kind fold before the body decodes, the arm's own row after. The hand guards go with it, and `DriverError::OutOfPhase` names the phases an arm does belong to. Mode is checked before phase so a surface the connection never asked for is answered with a typed `Conn::Refused` rather than told to wait for a state it can never reach. Doc cascade: `docs/reference/ipc.md` gains the "Connection phases" ladder and transition table beside the arm table's `phases` column; the explanation twin `docs/explanation/architecture/ipc.md` records why the ladder is data the driver owns rather than a typestate (the driver is shared behind a mutex in the GUI and behind `&mut` in three daemon loops) and why there is no `Closing` phase; REQ-114 in `docs/reference/spec.md` now names the out-of-phase frame and the ladder; `CHANGELOG.md` records the wire-visible `phases` column change. Verified: just check green (fmt, clippy, nextest, deny); reviewed by plan/correctness/docs lenses and pi sol.
The reference promised one shared validator for phase, direction, mode
and correlation while the phase model stopped at the handshake, so an
implementer of a non-Rust peer had to reconstruct the attach/subscribe
sequencing from felis's own handlers. Name the five phases and their
transitions where the arm table's `phases` column is defined, state that
teardown is the connection ending rather than a phase, and record in the
explanation twin why the ladder is data the driver owns rather than a
typestate — the driver is shared behind a mutex in the GUI and behind
`&mut` in three daemon loops, and a typestate would force three drivers.

Refs #46
refactor: carry the connection's role in its phase
Some checks failed
pr / nix flake check (pull_request) Has been cancelled
pr / cargo build / clippy / test / deny (pull_request) Has been cancelled
pr / wire schema is compatible with the base (pull_request) Has been cancelled
bench / Criterion full-suite snapshot (pull_request) Has been skipped
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / build felis (aarch64-darwin) (pull_request) Successful in 47s
windows / cargo nextest (Windows) (pull_request) Failing after 1m53s
windows / frontend smoke (Windows) (pull_request) Has been skipped
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been skipped
bench / Criterion regression gate (pull_request) Failing after 2m5s
windows / cargo clippy (Windows cross) (pull_request) Has been cancelled
pr / frontend smoke (x86_64-linux) (pull_request) Has been cancelled
pr / publish felis (x86_64-linux) (pull_request) Has been cancelled
fuzz / cargo fuzz smoke (per target) (pull_request) Has been cancelled
f4723e6bff
`Phase` stopped at `Steady`, so one state served a welcomed connection
waiting to attach, an attached session, and a notification observer.
Kind-level admission had to be permissive enough for all three, and each
call site made up the difference by hand: the daemon's pre-attach loop
refused a second opener, the attached pump refused a second `Attach`,
the observer loop refused everything but a cancel, and the client
decoded uncorrelated session replies straight off a kind match, past the
driver's direction and phase columns. A peer reading the arm table saw
none of that.

Split the phase into `Setup | Attached | Observing`, declare each arm's
phases in the same table `felis.proto` restates, and let the driver
admit every inbound frame against it once — the kind fold before the
body decodes, the arm's own row after. The hand guards go with it, and
`DriverError::OutOfPhase` names the phases an arm does belong to.

Mode is checked before phase so a surface the connection never asked for
is still answered with a typed `Conn::Refused` rather than told to wait
for a state it can never reach.

Refs #46
fix(daemon): gate the operation-echo attach test on unix
All checks were successful
bench / Criterion full-suite snapshot (pull_request) Has been skipped
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / build felis (aarch64-darwin) (pull_request) Successful in 47s
windows / cargo nextest (Windows) (pull_request) Successful in 5m5s
pr / cargo build / clippy / test / deny (pull_request) Successful in 1m37s
pr / wire schema is compatible with the base (pull_request) Successful in 12s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 46s
pr / publish felis (x86_64-linux) (pull_request) Has been skipped
windows / frontend smoke (Windows) (pull_request) Successful in 2m3s
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been skipped
bench / Criterion regression gate (pull_request) Successful in 2m8s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 1m4s
pr / nix flake check (pull_request) Successful in 25s
windows / cargo clippy (Windows cross) (pull_request) Successful in 12s
5b68d0d979
`an_attach_echoing_an_operation_lands_that_target` reaches for the
duplex-socket helpers (`create_parked_session`, `shell_factory`,
`framed`, `hello_welcome`, `send_kind`), every one of which is
`#[cfg(unix)]` because the harness drives a real PTY. Without the same
gate the Windows job fails to compile the daemon's test target, which
is what it is now doing on main; this branch only inherits the break.

The test keeps its coverage where the helpers exist rather than growing
a Windows path for them: what it pins is the ledger's attribution, not
anything platform-specific.
natsukium deleted branch refactor/driver-phases-46 2026-09-04 20:37:11 +09:00
Sign in to join this conversation.
No description provided.