Mission Specification: Event write hygiene: retire dead write doors and private events imports

Mission Branch: feat/event-write-hygiene (lands as issue-6614-6607-event-write-hygiene) Created: 2026-10-22 Status: Draft Input: Issues #6614 and #6607 (epic #6608, ADR 2026-10-14-1), operator steer of 2026-10-22.

Intent Summary (confirmed)

  • Primary actor: a core maintainer preparing the one-canonical-writer cut (ADR 2026-10-14-1, EventBridge #6603). A second actor is the reviewer of a future spec-kitty-events lock refresh.
  • Trigger: the event-ledger writers-and-readers map (gap rows G-11, G-16 and G-18) lists write doors nobody uses, a private library import, and hosted-era names on the produce seam.
  • Outcome:
  • Every write door to status.events.jsonl and to the invocation index that production does not use is gone.
  • Core imports no private spec_kitty_events / spec_kitty_tracker symbol.
  • The produce seam's names no longer say "saas".
  • Gates keep each of these from coming back.
  • Invariant: observable behaviour does not change.
  • Every surviving write path writes the same bytes.
  • The produce seam still has no default handler, and every fire_* call is a no-op until something registers.
  • The dependency range and lockfile are untouched.
  • Operator rulings (Decision Moments recorded):
  • Delete the dead hosted lifecycle builder, rather than replacing its private import.
  • Migrate the test seeding in this mission.
  • Keep no compatibility aliases for the frozen spec-kitty/event-router, including the old timeout environment variable.

This mission is a bulk edit. It renames the produce-seam identifiers (saas_fanout → status_fanout / lifecycle_fanout). It also moves every test that seeds the status log through the retired door onto a test-only seeding helper. occurrence_map.yaml (written during plan) records per-category rules.

User Scenarios & Testing (mandatory)

User Story 1 - Only real write doors remain (Priority: P1)

A maintainer wrapping the status write path in the EventBridge port can list every module that may append to status.events.jsonl or to the invocation index. Every name on that list is a live production path, and a gate refuses any new door.

Why this priority: dead doors make the canonical-writer cut look larger than it is, and one of them is a non-atomic append.

Independent Test: the raw-append allowlist gate and a new door-absence gate pass on the mission branch and fail when a deleted door is reintroduced.

Acceptance Scenarios:

1. Given the mission branch, When a module re-adds a plain append_event(feature_dir, event) to the status store, Then the door gate fails and names it. 2. Given the mission branch, When any module other than the invocation writer opens ops-index.jsonl for writing, Then the invocation-index gate fails. 3. Given a test that needs a pre-seeded status log, When it seeds rows, Then the rows go through the atomic write primitive and land byte-identical to what the old door wrote.

User Story 2 - A lock refresh cannot flip strictness behind core's back (Priority: P1)

A reviewer of a spec-kitty-events lock refresh does not need to audit private library internals. Core imports none, and a boundary gate fails on any private import.

Why this priority: 11.1.0 widens a private map that used to switch strict validation on. Core's only reader of that map sits on a dead hosted path.

Independent Test: the shared-package boundary gate fails when a src/ module imports spec_kitty_events.<module>._name or a _-prefixed module, and passes on the mission branch.

Acceptance Scenarios:

1. Given the mission branch, When the boundary gate scans src/, Then it finds no private spec_kitty_events / spec_kitty_tracker import. 2. Given a synthetic module that imports a private symbol, When the gate's scanner runs on it, Then it reports the import (non-vacuity). 3. Given the dead hosted lifecycle builder, When the mission lands, Then it no longer exists, and nothing in src/ referenced it.

User Story 3 - The produce seam speaks transport-neutral language (Priority: P2)

A future transport owner registers a status or lifecycle fan-out handler through transport-neutral names. The seam behaves exactly as before: no default handler, non-raising, bounded per-handler wall time.

Why this priority: "saas" is residue vocabulary after #6164 and misleads readers about what the seam is.

Independent Test: a stub-handler contract test registers a handler through the new names, fires each fire_* call and observes delivery. A reset registry is a no-op.

Acceptance Scenarios:

1. Given a fresh registry, When any fire_ is called, Then nothing is invoked and nothing raises. 2. Given a stub registered via the renamed registration function, When the matching fire_ is called, Then the stub receives the kwargs once. 3. Given the new timeout environment variable set to a value, When a handler runs, Then that bound applies. The old variable name has no effect.

Edge Cases

  • A test monkeypatches the deleted store.append_event to simulate a write failure. It is retargeted to the surviving primitive, or removed if it only tested the dead door.
  • A test uses append_event_log for its contract validation. That validation lives on in append_event_stream_log, so the test moves there.
  • Test files that reach into spec_kitty_events private internals (reducer helpers) are not production code and are out of scope. They get no allowlist (that would be priced debt); public equivalents are requested upstream.
  • Documentation, the dead-symbol allowlist and the release coverage baseline that name deleted or renamed symbols are updated in the same change.

Requirements (mandatory)

Functional Requirements

IDTitleUser StoryPriorityStatusDeliveryNo-op passable?
FR-001Delete dead status write doorsAs a maintainer, I want the non-atomic store.append_event, append_event_verified and append_primary_checkout_event_verified removed, with _unsafe.__all__ shrunk (the ALLOWED_CALLERS baseline stays: status_service still imports live primitives), so that only live atomic primitives remain.HighOpen[build]no — the door gate fails on main, where these names still exist
FR-002Delete the dead coordination wrapperAs a maintainer, I want coordination.status_service.append_event_log removed, with its test seeds moved to append_event_stream_log, so that the coordination shell has one write funnel.HighOpen[build]no — the gate's forbidden-name list fails on main
FR-003Delete the duplicate invocation-index writerAs a maintainer, I want invocations_cmd.append_to_index removed and its tests retargeted onto the invocation writer, so that one module writes the index.HighOpen[build]no — the index gate fails on main
FR-004Test seeding helperAs a test author, I want a test-only helper that seeds status rows through the atomic primitive, so that about 195 test files keep their fixtures without the deleted door.HighOpen[build]no — imports of the deleted door fail collection
FR-005Door-absence and index-writer gatesAs a reviewer, I want architectural gates that fail when a deleted door returns or a second index writer appears. Each has a non-vacuity floor and a self-test.HighOpen[build]no — red on main, with a same-fixture positive control
FR-006Delete the dead hosted lifecycle builderAs a maintainer, I want build_saas_lifecycle_queue_event, _canonical_lifecycle_payload_for_saas and _validate_lifecycle_payload removed, with the facade export and tests, so that core's only private library import goes with them.HighOpen[build]no — the private-import gate is red on main
FR-007Private-import boundary gateAs a reviewer, I want the shared-package boundary gate to fail on any _-prefixed module segment or imported name under spec_kitty_events / spec_kitty_tracker in every production root under src/, with no allowlist, so that the boundary is public-only. Test files that read library internals stay out of scope; public equivalents are requested upstream.HighOpen[build]no — red on main through lifecycle_events.py
FR-008Transport-neutral produce-seam namesAs a maintainer, I want every "saas" identifier, log label and docstring in status/adapters.py, and their references, renamed to transport-neutral names with no aliases. The timeout variable becomes SPEC_KITTY_STATUS_FANOUT_TIMEOUT.MediumOpen[build]no — the "no saas in adapters.py" check is red on main
FR-009Stub-handler contract testAs a future transport owner, I want a contract test that pins the empty-registry no-op, delivery to a registered stub and the bounded timeout through the new names, so that the rename keeps behaviour.MediumOpen[ratchet]yes — paired with the empty-registry control on the same fixture
FR-010Docs and gap tableAs a reader, I want the event-ledger architecture page's G-11, G-16 and G-18 rows and a changelog entry to reflect the closure, so that the gap table stays truthful.LowOpen[build]no

Non-Functional Requirements

IDTitleRequirementCategoryPriorityStatus
NFR-001Behaviour preservation0 changes to bytes written by surviving write paths. The seeding helper produces byte-identical rows, proven by a test comparing against serialize_event_line.ReliabilityHighOpen
NFR-002Quality gatesruff check, ruff format --check --force-exclude and mypy report 0 issues on changed modules. No new suppressions. Complexity stays at or below 15.MaintainabilityHighOpen
NFR-003CoverageDiff coverage of at least 90% on changed src/ lines.MaintainabilityMediumOpen
NFR-004Gate non-vacuityEvery new gate has a concrete floor of at least 1 scanned module or file and a self-mutation test that turns it red.ReliabilityHighOpen

Constraints

IDTitleConstraintCategoryPriorityStatus
C-001No dependency changeuv.lock and the spec-kitty-events / spec-kitty-tracker ranges stay untouched.TechnicalHighOpen
C-002No canonical-writer workDo not start the EventBridge port (#6603) or the Java writer (#6185 ruling). Add no new append path.TechnicalHighOpen
C-003No hosted surfacesDo not reintroduce or point at any hosted surface. No aliases are kept for spec-kitty/event-router.BusinessHighOpen
C-004Packs untouchedDo not edit packs/built-in/ or packs/internal/.TechnicalHighOpen
C-005Red-first gatesEach new or extended gate is committed red against the pre-change tree before the change that turns it green.TechnicalHighOpen

Key Entities

  • Write door: a function outside the canonical shells that appends to an event log.
  • Produce seam: the status/lifecycle fan-out registry that transports subscribe to (status/adapters.py).
  • Test seeding helper: a test-only function that writes fixture rows through the production atomic primitive.

Success Criteria (mandatory)

Measurable Outcomes

  • SC-001: 0 production-unused write doors to status.events.jsonl or the invocation index remain, verified by the gates. — [build] · no-op passable: no
  • SC-002: 0 private spec_kitty_events / spec_kitty_tracker imports in src/. — [build] · no-op passable: no
  • SC-003: 0 occurrences of "saas" (any case) in status/adapters.py. — [build] · no-op passable: no
  • SC-004: Every test file that seeded the status log through the deleted door still passes with the same assertions. — [ratchet] · no-op passable: yes — paired with SC-001's gate on the same tree

Assumptions

  • A test that only exercised a deleted door's own behaviour (for example, the non-atomic append's monkeypatch tests) is deleted, not retargeted.
  • Other "saas"/"hosted" residue outside the seam (for example fanout_lifecycle_event_hosted in lifecycle_events.py) is renamed only where it names the seam's own identifiers. The rest is recorded as follow-up, per Locality of Change.