Skip to content

feat(state): FileSystem trait, LiveFs, StateStore with read/write/append#7

Merged
UnbreakableMJ merged 1 commit into
mainfrom
feat/state-io
Apr 27, 2026
Merged

feat(state): FileSystem trait, LiveFs, StateStore with read/write/append#7
UnbreakableMJ merged 1 commit into
mainfrom
feat/state-io

Conversation

@UnbreakableMJ

Copy link
Copy Markdown
Contributor

Chunk M1-W1-E of M1 plan. Lands the I/O surface for `state.toml`.

Public surface

  • `trait FileSystem` — three primitives: `read_string`, `write_temp_then_rename`, `fsync_dir`. Splitting rename and dir-fsync lets a future `MockFs` simulate "crashed between rename and fsync".
  • `struct LiveFs` — production impl backed by `std::fs` and `tempfile::NamedTempFile`.
  • `struct StateStore<F: FileSystem = LiveFs>` with: `new`, `with_fs`, `path`, `read`, `write_atomic`, `append_history`, `append_failure`, `record_reconciliation`.

Atomic write per PRD §7.4: temp file in same dir + `sync_all` + rename + fsync of parent directory entry.

Tests (8 total, all passing)

  • `full_state_round_trip`
  • `unknown_fields_preserved_in_reserved`
  • `read_missing_file_yields_not_found`
  • `read_unsupported_schema_version`
  • `write_fails_when_parent_missing`
  • `history_append_does_not_rewrite_managed` — asserts `managed` / `adopted` / `reserved` unchanged in value, history grows.
  • `append_failure_grows_failures_array`
  • `record_reconciliation_appends_entry`

Out of scope

  • Migration framework — chunk M1-W1-F.
  • `MockFs` feature-gate + crash-during-write tests — chunk M1-W1-F.
  • Property tests via proptest — chunk M1-W1-F.
  • `state_host_mismatch_detected` contract — engine-side concern; the `StateError` variant is defined but not raised here.

Verification

  • `cargo test -p pearlite-state` — 8 passed; 0 failed
  • `cargo clippy --workspace --all-targets -- -D warnings`
  • `cargo fmt --all --check`
  • `scripts/ci/check-spdx.sh`
  • `pearlite-audit check .` — 1 check, 0 violations

🤖 Generated with Claude Code

…append

Chunk M1-W1-E of M1 plan. Lands the I/O surface for state.toml.

Public surface:

- pub trait FileSystem with three primitives: read_string,
  write_temp_then_rename, fsync_dir. Splitting rename and dir-fsync
  lets a future MockFs simulate "crashed between rename and fsync"
  in chunk M1-W1-F.
- pub struct LiveFs — production impl backed by std::fs and
  tempfile::NamedTempFile.
- pub struct StateStore<F: FileSystem = LiveFs> with:
    fn new(path) (default LiveFs)
    fn with_fs(fs, path) (test injection)
    fn path() -> &Path
    fn read() -> Result<State, StateError>
    fn write_atomic(&State) -> Result<(), StateError>
    fn append_history(HistoryEntry)
    fn append_failure(FailureRef)
    fn record_reconciliation(ReconciliationEntry)

Atomic write per PRD §7.4: temp file in same dir + sync_all + rename
+ fsync of parent directory entry. The two-step decomposition (write
+ fsync_dir) keeps the parent fsync separately mockable.

read() rejects future schema_version with
StateError::UnsupportedSchemaVersion. Migration framework lands in
chunk M1-W1-F.

append_*() are read-modify-write_atomic — file is rewritten in full
but [managed], [adopted], and [reserved] sections retain their values
on round-trip. Byte-level surgical preservation of unknown sections
outside [reserved] is deferred (would require toml_edit; the
[reserved] BTreeMap covers the documented forward-compat namespace).

Cargo.toml: tempfile is now a real (non-dev) dependency because
LiveFs uses NamedTempFile in production.

Tests (LiveFs against tempdir):
- full_state_round_trip
- unknown_fields_preserved_in_reserved
- read_missing_file_yields_not_found
- read_unsupported_schema_version
- write_fails_when_parent_missing
- history_append_does_not_rewrite_managed (asserts managed/adopted/
  reserved unchanged in value, history grows)
- append_failure_grows_failures_array
- record_reconciliation_appends_entry

What's NOT in this chunk:
- Migration framework (chunk M1-W1-F).
- MockFs feature-gate + crash-during-write tests (chunk M1-W1-F).
- Property tests via proptest (chunk M1-W1-F).
- contract: state_host_mismatch_detected (engine-side concern; the
  StateError variant is defined but not raised here — the engine
  compares state.host to hostname() at preflight).

Verification:
- cargo test -p pearlite-state  (8 passed; 0 failed)
- cargo clippy --workspace --all-targets -- -D warnings  (zero warnings)
- cargo fmt --all --check
- scripts/ci/check-spdx.sh
- pearlite-audit check .  (1 check, 0 violations)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@UnbreakableMJ
UnbreakableMJ merged commit c15abb7 into main Apr 27, 2026
3 checks passed
@UnbreakableMJ
UnbreakableMJ deleted the feat/state-io branch April 27, 2026 14:40
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