Data Model: FSM Write-Path Integrity

Mission: fsm-write-path-integrity-01M1TZV6 · Date: 2026-09-06 Scope: entities the mission creates or changes, their invariants, and the state transitions it touches. No event schema changes (non-goal 7). No new persistent formats except a rekeyed run index.


1. Status event log — status.events.jsonl (existing; write discipline changes)

FieldTypeNotes
(row)one JSON object per line, StatusEventunchanged schema

Invariants (after this mission)

  • I-1 Every append is performed while feature_status_lock(<key>) is held by the appending process (SC-008 lock-held assertion, all seven writer families + batch door).
  • I-2 Every append is atomic at the store primitive (append_*_atomic_verified family or the transaction's append) — never a bare open(path, "a") outside status/store.py (WP03 AST gate).
  • I-3 A rollback truncate (transaction.py:944, fh.truncate(self._pre_emit_size)) can only run while the same lock is held, so no concurrent legitimate append can land inside the window (SC-001).
  • I-4 The log is the sole authority; replay (reducer.reduce) is a pure fold with zero guard evaluation (NFR-002).

2. Writer family (concept; census governed by FR-001)

#FamilyModule:line (HEAD)Lock todayAfter
①emit single + inner-statestatus/emit.py:634, :1013L1L1 (flat shell)
②lifecycle appenderstatus/lifecycle_events.py:538 → _lifecycle_write_lock:234-254L1L1
③BookkeepingTransactioncoordination/transaction.py:290L1 (txn lifetime)L1
④retrospective run-terminusretrospective/events.py:213 raw open("a")noneL1 + atomic (WP01)
⑤retrospective lifecycleretrospective/lifecycle_events.py:250-256; Lamport read :265noneL1 + atomic; read in-lock (WP01)
⑥verdict-provenance backfillmigration/verdict_provenance_backfill.py:419none (atomic)L1 (WP01)
⑦runtime-state backfillmigration/backfill_runtime_state.py:1533, :1535; idempotency read :1497none (atomic ×2)one L1 acquisition covering read + both appends (WP01)
—batch doorstatus/emit.py:808-960noneL1 (WP02, FR-018)

Excluded: core/mission_creation.py (routes via ②; direct touch is touch(exist_ok=True)).


3. Mission status lock — L1 (existing; key changes)

AttributeTodayAfter (WP01, FR-004)
Path<git common dir>/<LOCK_DIRECTORY>/<mission_slug>.status.lock (locking.py:57-60)<git common dir>/<LOCK_DIRECTORY>/<feature_dir.name>.status.lock, root via resolve_status_lock_root(feature_dir, repo_root)
Re-entrancyyes (3 deep in production)unchanged
Default timeout-1 (unbounded)unchanged by default (Q2 follow-up); finite on L3-reachable takes and the merge-path retrospective take (NFR-003)
Timeout error—structured error naming the holder (new, on the finite-timeout sites)

Invariants

  • L-1 Two missions with distinct feature_dir.name never share a lock file, even with equal mission_slug.
  • L-2 A bare/legacy slug directory's writers use the same lock file as that mission's pipeline writers.
  • L-3 No new code path acquires L1 and then spawns a git subprocess while holding it (NFR-001); the standing BookkeepingTransaction case is documented, not changed.
  • L-4 Hierarchy has no inverse edges: L5 → L1; L3 → L1 (bounded); L1 → L4 → git (transaction only).

4. Transition pipeline (NEW, WP02) — value-level contract in contracts/emit-pipeline.md

Type: pure function; inputs are values + injected readers; output is a value.

InputTypeSource
requestTransitionRequestcaller
from_lanestrshell derives in-lock (_derive_from_lane, ≤1 call, one full read — NFR-004)
mission_slug, mission_idstr, `str \None`
feature_dirPaththe shell's write surface (txn.feature_dir for transactional)
readiness`DependencyReadiness \None`
subtasks_dir_resolver, evidence_readercallablesinjected I/O (default = today's helpers)
OutputTypeMeaning
PreparedTransitionfrozen dataclass`event: StatusEvent \

Invariants

  • P-1 Contains no lock acquisition, no file writes, no git, no fan-out. (Enforced by test_status_module_boundary.py plus a unit test that runs it against an in-memory feature dir.)
  • P-2 validate_transition is called exactly once per emit across the whole tree (the aggregate's duplicate at aggregate.py:685 is deleted).
  • P-3 Imports nothing from coordination (C-006).

5. Composition shells (NEW as named concepts, WP02)

ShellModuleSteps (in order)Failure policy (C-007)
flat/primarystatus/emit.py (emit_status_transition, batch)lock → derive from-lane → readiness → pipeline → atomic append → materialize → mirror → release → fan-outflat/LANES/SINGLE_BRANCH: uncommitted primary write; alias-collapse arm: mirror only, no append
transactionalcoordination/status_transition.py (single :1318, batch :1574, inner-state :1446)txn.acquire (L1) → derive → readiness → pipeline → txn.append → defer_outbound(fan-out) → commit → release → deferred fan-out firessingle: fail-closed on unresolvable coord worktree (#1848); batch: :1574 policy; inner-state: degrade on BookkeepingWorktreeMissing (#3460)
coord fallback armstatus_transition.py:401-432 (calls flat shell on the coord fd)flat shell with fan-out suppressed → commit → on success fan-out; on failure truncate-restore, no fan-outfail-loud FallbackCoordWorktreeUnresolved

Invariants

  • S-1 External fan-out fires only after the durable commit succeeds in any coord arm (SC-002).
  • S-2 The batch shell applies the owned-mission ActionContextError check and effective_root acquisition identically to the single shell (Q5 parity).
  • S-3 Exactly two shell kinds exist; a third composition of lock + append is a gate violation (R14).

6. GuardContext (existing; +1 field, WP04)

FieldTypeDefaultSemantics
… existing 10 fields …unchanged
dependency_ready`bool \None`None

State transitions touched (9-lane machine unchanged):

planned ──claim──▶ claimed ──start──▶ in_progress
        [dependency_ready is not False, or force]   [same]

All other edges, force, terminal rules: unchanged (non-goal 6).

Readiness source: dependency_readiness_for_wp(wp_id, dependencies, wp_lanes, provenance) (core/dependency_graph.py:34); satisfied iff every dependency is approved | done | canceled-with-operator-provenance.


7. status/_unsafe.py (NEW, WP03)

SymbolTypeNotes
append_event, append_event_verified, append_event_stream_atomic_verified, append_events_atomic_verified, append_primary_checkout_event_verified, append_primary_checkout_events_atomic_verifiedre-exports from status/store.pyremoved from status/__init__.__all__ (:518-537)
ALLOWED_CALLERSfrozenset[str] of module pathsseeded from WP01's fixed census (families ②③⑤⑥⑦ + both shells); shrink-only — a test asserts the set is a subset of the committed baseline

8. Run-state store (existing; rekeyed + hardened, WP05)

8.1 feature-runs.json index

TodayAfter
`{ "<mission_slug>": { "run_id", "run_dir", "mission_type"\"mission_key" } } (runtime_bridge_io.py:611-660`)

Invariants

  • RS-1 Two missions with equal mission_slug and distinct mission_id resolve distinct runs (SC-006).
  • RS-2 A live index entry whose run_dir/state.json is absent raises a structured error (loud), never falls through to "start a new run" (:617-627 today).

8.2 Run cursor state.json and journal run.events.jsonl (_internal_runtime/engine.py)

WriteTodayAfter
_write_snapshot (:128-130)open("w") + json.dump (torn-write window)write state.json.tmp → os.replace (the reducer.materialize shape)
_append_event (:110-119)bare open("a")same tmp/replace or atomic-append discipline (journal is local; no L1 involved)

Invariant RS-3: a process kill at any instruction boundary leaves either the previous state.json or the complete new one; never a partial file.

8.3 Progress read (runtime/next/decision.py:369-377)

TodayAfter
materialize(lane_read_dir) (writes status.json) inside try/except: passmaterialize_snapshot(lane_read_dir) (pure)

Invariant RS-4: tracked status.json is byte-identical before and after the progress query.


9. Externally visible events

Unchanged payloads: SaaS transition emission (queue_saas_emission / _saas_fan_out), resolved-binding fan-out, zeitgeist via status/adapters.py. Only timing changes: in every coord arm, fan-out is deferred until after the coord commit succeeds; on commit failure, zero fan-out (SC-002).