# Sprint 5 — Layer 4 First-Bundle Entry (T1 + T7 + T2)

> Status: **entry / pending user review** — 2026-05-26
> Origin: this memo translates `docs/design/v0.4-lifecycle-semantics-roadmap.md`
> (the 948-line strategic design) into the **execution plan for Sprint 5**.
> The strategic memo answers *what to build*; this memo answers *how to ship it*.
> Sprint 5 is the **v0.4.0 final release trigger** — when all PRs below land
> + invariants stay green, v0.4.0 cuts.
>
> Companion documents (read in this order if entering fresh):
>   - `docs/handovers/v0.4.0-entry-track.md` — v0.4 sprint sequence (this is Sprint 5)
>   - `docs/design/v0.4-lifecycle-semantics-roadmap.md` — full strategic design (T1-T7)
>   - `docs/architecture/memory-lifecycle-architecture.md` — CASCADE vs EVENT axiom (§1.5)
>   - `ROADMAP.md` §v0.4.0 — release-level scope definition
>   - Memory: `project_state.md` — current main HEAD + open queues

---

## 0. TL;DR

Ship **T1 (Temporal Validity) + T7 (Supersede Chain) + T2 (Deterministic
Contradiction Arbitration)** as a **single coherent first bundle** across
7 sequenced PRs (PR-T1.A/B + PR-T7.A/B + PR-T2.A/B/C).

The bundle is coherent because:

- **T1 + T7** together implement the **CASCADE vs EVENT separation** —
  T1 marks expired sources without deleting (EVENT); T7 supersedes edges
  without invalidating (EVENT). Either alone leaves the separation
  half-baked.
- **T2** is the **routing rule** that decides A (CASCADE) vs B (EVENT)
  at write time. Without T2 the separation can be violated silently.

T3-T6 are deferred to v0.4.1+ — see §Out-of-scope.

**Critical invariant for v0.4.0 release**: a "B-class fact" (e.g.,
`(Joby, CEO, Bob)` arriving after existing `(Joby, CEO, Alice)`) routes
through T2 to EVENT supersede chain, the old Alice edge survives with
`status.superseded_by` link, and the CASCADE log shows **zero
`cascade_remove` calls** for this mutation. Conversely a "withdrawn
report" routes through T2 to CASCADE, its sourced facts cleanly
vanish, but unrelated supersede chains stay intact. Both directions
must be provable as `pytest` invariants.

---

## 1. Position in the v0.4 sprint sequence

```
Sprint 0 (entry, done)
   ↓
Sprint 1 (data correctness, done — v0.4.0-alpha.1 partial)
   ↓
Sprint 2 (UI consistency, done — v0.4.0-alpha.2 prepped)
   ↓
Sprint 3 (plumbing, done — D1 5-stage surface)
   ↓
Sprint 4 (retrieval quality — BL-9 swap PR deferred to operator window)
   ↓
Sprint 5 ← **THIS MEMO** ← v0.4.0 final
   ↓
Sprint 6 (long-term backlog, trigger-gated)
```

Sprint 5 carries the **architecture label** — it's the v0.4 cycle's
architectural shift. Every PR in this bundle lands behind that label
and amends `docs/ARCHITECTURE.md` where relevant (new
`core/lifecycle/` module surface + new schema fields on sources +
edges + relations).

---

## 2. First-bundle scope — recap from strategic memo

| Area | Track | Strategic ref | First-bundle PR |
|---|---|---|---|
| **T1** Temporal Validity & Expiration | EVENT | §4 | PR-T1.A schema + PR-T1.B expiration batch |
| **T7** Supersede Chain | EVENT | §9.5 | PR-T7.A operations + PR-T7.B invariants |
| **T2** Deterministic Contradiction Arbitration | GOVERNANCE → routing | §5 | PR-T2.A classifier + PR-T2.B A-path + PR-T2.C B-path |

Out of bundle (deferred to v0.4.1+):
- **T3** Evidence Aging (§6) → v0.4.2
- **T4** Reviewer Authority (§7) → v0.4.3 / v0.5
- **T5** Replayable Audit Graph (§8) → v0.4.3 / v0.5
- **T6** Causality Chain (§9) → v0.4.1

The strategic memo's §10 migration script (v0.3 → v0.4 schema) covers
all seven areas with safe defaults — PR-T1.A bundles the **subset
needed for T1+T7** (validity + status + mutation_type fields).
T3/T4/T6 fields get their defaults added later by their own migration
extensions.

---

## 3. The CASCADE vs EVENT separation — critical invariant

Without this separation the entire Layer 4 design collapses into noise.
Pre-ship invariants (these block the v0.4.0 tag):

| Invariant | Where pinned | What it proves |
|---|---|---|
| `test_supersede_does_not_trigger_cascade` | `tests/test_t7_supersede_chain.py` | An EVENT mutation never calls `cascade_remove` on the old edge's sources. |
| `test_cascade_preserves_supersede_chain` | `tests/test_t7_supersede_chain.py` | A CASCADE invalidation (Layer 3) does not break `status.superseded_by` links on unrelated edges. |
| `test_historical_replay_via_chain` | `tests/test_t7_supersede_chain.py` | `reconstruct_view_at(head, predicate, t)` returns the edge that was active at time `t`, walking the chain backward through CASCADE-survived edges only. |
| `test_t2_b_class_routes_to_supersede` | `tests/test_t2_contradiction.py` | A new fact with a later `valid_from` triggers `supersede_edge()`, not `cascade_remove()`. |
| `test_t2_a_class_routes_to_cascade` | `tests/test_t2_contradiction.py` | A new fact flagged as a correction (older timestamp + higher confidence + same predicate) triggers `cascade_remove()` on the wrong source. |

These five invariants are **non-negotiable for v0.4.0 release**. If any
fails on the release candidate, the bundle does not ship.

---

## 4. PR sequence (7 PRs + 1 closure)

The sequence is dependency-ordered. Each PR is **single-responsibility
and individually mergeable**; one PR per merge keeps STEP 7 deltas
attributable.

### PR-T1.A — Source / edge schema extension

**Scope**:
- `scripts/migrate_v04_lifecycle.py` (new, dry-run by default + `--apply`).
  Adds `valid_from` / `valid_until` to every source, adds `validity` +
  `status` + `mutation_type` to every edge, with safe v0.3-compatible
  defaults (all `None` / `active=True` / `mutation_type="active"`).
- `core/relations_schema.py` — extend the source / edge validator to
  accept the new fields with backward-compat (missing fields treated
  as v0.3 default).
- New tests: schema accepts both v0.3 and v0.4 shapes; migration is
  idempotent (running twice is a no-op).

**Bench (rule #2)**: STEP 7 baseline pinned at flag-OFF. No behaviour
change at default — every existing query path treats the new fields as
defaults equivalent to v0.3 semantics.

**Done when**: `migrate_v04_lifecycle.py` clean dry-run on the current
wiki + commit-tested rollback path documented.

### PR-T1.B — Expiration batch + `status: expired` mark

**Scope**:
- `core/lifecycle/expiration_cascade.py` (new) —
  `expiration_cascade(entity_root, current_time)` walks every wiki
  entity's sources and marks `status: expired` on those whose
  `valid_until ≤ current_time`. **Does not delete** — that is a
  CASCADE concern, separated by design.
- Side effect: a relation whose only active sources are now expired
  surfaces in the audit log via Layer 3's existing orphan-sweep
  (which is correct — the relation is no longer supported by active
  evidence, even though the sources themselves are preserved for
  replay).
- `scripts/run_expiration_cascade.py` (new) — operator-runnable
  daily-batch invocation. Dry-run + `--apply` pattern.
- Tests: `test_source_expires_at_valid_until`,
  `test_relation_dropped_when_all_active_sources_expire`,
  `test_valid_until_null_means_indefinite`,
  `test_temporal_cascade_preserves_manual_immunity`.

**Bench (rule #2)**: STEP 7 with one entity migrated to have an
expired source — verify `grounded=true` rate stays within ±5% (the
expired source dropping out should not cause confidence collapse
because the rest of the sources remain).

### PR-T7.A — Supersede chain operations

**Scope**:
- `core/lifecycle/supersede_chain.py` (new). Three public functions:
  - `supersede_edge(old_edge, new_fact, supersede_ts)` — creates the
    new edge, marks the old one with
    `status.superseded_by = new_edge.id`, sets `mutation_type =
    "superseded"`. Does **not** call `cascade_remove`.
  - `walk_supersede_chain(edge)` — follows `status.superseded_by`
    links to the chain head. Returns the full ordered list (caller
    decides what to do with it).
  - `reconstruct_view_at(head, predicate, t)` — replay primitive.
    Walks the chain backward and returns the edge whose `validity`
    window contained `t`, skipping `mutation_type == "invalidated"`.
- Tests: `test_supersede_preserves_old_sources` (no CASCADE side
  effect), `test_supersede_chain_walks_to_active`,
  `test_supersede_chain_acyclic` (rejects cycle at insert).

**Bench (rule #2)**: deferred to PR-T2.C where the production caller
lands. PR-T7.A is the pure-function layer with no engine wiring yet.

### PR-T7.B — Supersede invariant suite

**Scope**:
- `tests/test_t7_supersede_chain.py` extended with the three
  release-gating invariants from §3:
  `test_supersede_does_not_trigger_cascade`,
  `test_cascade_preserves_supersede_chain`,
  `test_historical_replay_via_chain`.
- Each invariant runs against the actual wiki fixture (a small
  curated entity directory under `tests/fixtures/lifecycle/`) — not
  mocks, so a regression in the production CASCADE/EVENT separation
  surfaces on every PR thereafter.
- Layer 3 `cascade_remove` is patched in one test to count call
  attempts; the assertion is **zero calls** when an EVENT mutation
  fires.

**Bench (rule #2)**: N/A — test surface only.

### PR-T2.A — A/B classifier (Stage 1)

**Scope**:
- `core/lifecycle/contradiction_arbiter.py` (new). Single deterministic
  function:
  ```python
  def classify_contradiction(
      old_edge: Edge, new_fact: Dict, *, now: datetime,
  ) -> Literal["A_invalidate", "B_supersede", "ignore"]:
      ...
  ```
- Decision rules (deterministic, LLM-free — Mem0 differentiator):
  1. If `new_fact.valid_from > old_edge.validity.to or now`
     → **B** (world changed; supersede the old one).
  2. If `new_fact.source.confidence > old_edge.best_source.confidence`
     AND `new_fact.timestamp ≤ old_edge.validity.from`
     → **A** (a correction; invalidate the wrong source).
  3. If `(s, p)` keys match but `new_fact.timestamp` is within
     `old_edge.validity` window AND no confidence delta
     → **ignore** (duplicate / equivalent — keep one, log audit).
  4. Edge cases (missing timestamps, missing confidence) → **B**
     default (safer than CASCADE, easier to undo).
- Tests: 12 parametrized cases covering each branch + edge cases +
  the `mutation_type` field is set correctly by the downstream caller.

**Bench (rule #2)**: STEP 7 unchanged — classifier is not yet wired
into the ingestion path. That happens in PR-T2.B/C.

### PR-T2.B — A-path: CASCADE routing

**Scope**:
- Wire the classifier into the ingestion path at the single point where
  Layer 3's `cascade_remove` is currently called from. When
  `classify_contradiction → "A_invalidate"`, the existing v0.3
  CASCADE pipeline runs unchanged — this PR is the **routing wire**,
  not new CASCADE logic.
- `mutation_type = "invalidated"` written to the audit row so the
  replay primitive in T7 can filter correctly.
- Tests: `test_t2_a_class_routes_to_cascade` from §3,
  `test_correction_invalidates_wrong_source_only` (other sources
  preserved).

**Bench (rule #2)**: STEP 7 with a synthetic correction scenario —
verify `grounded=true` rate on the corrected entity ≥ pre-correction
baseline (the wrong source is gone, the corrected source remains).

### PR-T2.C — B-path: EVENT routing + final wiring

**Scope**:
- When `classify_contradiction → "B_supersede"`, call
  `supersede_edge` from PR-T7.A. This is the live wiring of T2
  routing → T7 chain.
- Audit row written with `mutation_type = "superseded"` +
  `superseded_by = <new edge id>` so the replay primitive sees the
  chain correctly.
- Tests: `test_t2_b_class_routes_to_supersede` from §3,
  `test_supersede_audit_payload_carries_old_and_new_ids`.

**Bench (rule #2)**: STEP 7 with the canonical supersede scenario
(CEO change). Verify (a) the historical query about the old CEO at
the old timestamp returns Alice via `reconstruct_view_at`, (b) the
current query returns Bob, (c) `grounded=true` rate unchanged.

### PR-T7.C — Sprint 5 closure (docs PR)

**Scope**:
- `ROADMAP.md` v0.4.0 marked closed (T1+T7+T2 first bundle delivered).
- `docs/architecture/memory-lifecycle-architecture.md` updated to
  reflect the shipped scope and pin the separation invariants from §3
  as production properties.
- `reports/promo-assets/v0.4.0-layer4-first-bundle-result.md` (new) —
  bench delta table across the 7 PRs + the operator runbook for
  migration.
- `.zenodo.json` v0.4.0 metadata + CHANGELOG `[0.4.0]` entry +
  README Status badge bump.
- Memory: `project_state.md` Sprint 5 entry → closure;
  `MEMORY.md` index update.

**Bench (rule #2)**: full STEP 7 sweep — the bundle's net delta vs
v0.4.0-alpha.3 baseline. Required for the v0.4.0 release.

---

## 5. New module placement under `core/lifecycle/`

| File | Source PR | Size budget |
|---|---|---|
| `core/lifecycle/__init__.py` | PR-T1.A | < 1 KB (re-exports only) |
| `core/lifecycle/expiration_cascade.py` | PR-T1.B | ≤ 10 KB |
| `core/lifecycle/supersede_chain.py` | PR-T7.A | ≤ 10 KB |
| `core/lifecycle/contradiction_arbiter.py` | PR-T2.A | ≤ 8 KB |
| `core/lifecycle/temporal_replay.py` | PR-T7.A (helper bundled in supersede_chain initially; split if it grows) | ≤ 6 KB |

Module-size gate (CLAUDE.md rule #5): each file targets **half the
20 KB cap** so a v0.4.1+ extension (T3/T6) has headroom without
forcing a split right away.

The existing `core/relations_schema.py` extends by ~3 KB to host the
new field acceptance + validator branches; if that pushes it past
the cap, split `relations_schema_v04.py` companion (same pattern as
`pipeline_context.py` split in PR #518).

---

## 6. Schema migration plan

Single migration script `scripts/migrate_v04_lifecycle.py` covers
the first-bundle field set. Operator-runnable, dry-run default:

```bash
# Inspect: what would change without writing
python scripts/migrate_v04_lifecycle.py

# Apply: write new fields with v0.3-equivalent defaults
python scripts/migrate_v04_lifecycle.py --apply

# Verify: every entity has the new fields
python scripts/migrate_v04_lifecycle.py --verify
```

Fields added per the strategic memo §10.1:

| Object | New fields | Default |
|---|---|---|
| `source` | `valid_from`, `valid_until` | `None`, `None` |
| `edge` | `validity` (`{from, to}`) | `{None, None}` |
| `edge` | `status` (`{active, superseded_by, superseded_at}`) | `{True, None, None}` |
| `edge` | `mutation_type` | `"active"` |

The `aging_policy` / `half_life_days` / `reviewer_rank` fields from
strategic memo §10.1 are **deferred** to v0.4.1+ — adding them now
would require T3/T4 logic that this bundle does not include.

Rollback path: the migration writes only new fields; rolling back
the v0.4 schema reader to v0.3 leaves the extra fields untouched and
ignored. No destructive change.

---

## 7. Integration points with existing layers

| Layer | Integration | Risk |
|---|---|---|
| **Layer 3 cascade** (v0.3) | T2's A-path calls the existing `cascade_remove` unchanged. T7's `supersede_edge` does NOT touch CASCADE pipeline. | Test `test_supersede_does_not_trigger_cascade` pins it. |
| **`core/graph_engine.py`** | The DFS expansion (`expand_dynamic`) must filter edges with `status.active = False` (the superseded-but-preserved ones) out of the active graph view, while keeping them readable for `reconstruct_view_at`. | One-line filter in DFS + parallel "include-historical" path for the replay primitive. |
| **`core/relations_schema.py`** | Existing v0.3 confidence computation (`compute_confidence_from_sources`) extends to filter expired sources at the `current_time` parameter (already present at the function signature — passing `None` preserves v0.3 behaviour). | Test pins backward compat. |
| **`audit_log`** | Both A-path and B-path write the mutation_type to the row, so the replay primitive can filter on it. No schema change — the existing 11-column table absorbs the new value via the existing `answer` column. | None — same pattern as L.C's evidence_scope addition (PR #516). |
| **STEP 7 baseline** | Schema migration adds fields; flag-OFF behaviour byte-identical. STEP 7 baseline pins the invariant. | PR-T1.A's bench is the canary. |

---

## 8. Test invariant count

| Layer | Pre-bundle | Post-bundle | New invariants |
|---|---|---|---|
| Layer 3 (v0.3) | 12 | 12 (unchanged) | 0 |
| T1 Temporal Validity | 0 | 4 | 4 (from strategic §4.3) |
| T7 Supersede Chain | 0 | 6 | 6 (from strategic §9.5.3) |
| T2 Contradiction Arbitration | 0 | 5 | 5 (3 classifier branches + 2 routing tests) |
| **Total** | **12** | **27** | **+15** |

All 27 must stay green on every PR going forward. The first-bundle
adds **15 new contract tests** — well-below the strategic memo's
"~27 new tests cumulative across T1-T7" estimate because T3/T4/T5/T6
are deferred.

---

## 9. Operator runbook (Sprint 5 deployment)

Once all 7 PRs land:

```bash
# 1. Pre-migration snapshot
python scripts/wiki_snapshot.py --tag pre-v0.4.0-layer4

# 2. Dry-run the migration — review the diff
python scripts/migrate_v04_lifecycle.py

# 3. Apply (writes new fields with v0.3-equivalent defaults)
python scripts/migrate_v04_lifecycle.py --apply

# 4. Verify every entity has the new fields
python scripts/migrate_v04_lifecycle.py --verify

# 5. (Optional) First expiration cascade — schedule daily
python scripts/run_expiration_cascade.py            # dry-run
python scripts/run_expiration_cascade.py --apply    # writes status=expired

# 6. STEP 7 sanity — flag-OFF default
python scripts/bench.py --suite=step7 --check
```

Operator decision: schedule the daily expiration cascade as a cron /
systemd timer (out of scope for this PR — operator's existing batch
infrastructure). The strategic memo §4.2 describes the cadence.

---

## 10. Done when (Sprint 5 release gate)

- [ ] PR-T1.A schema migration script lands + verifier passes on the
      current wiki snapshot.
- [ ] PR-T1.B expiration cascade lands + 4 T1 invariants green.
- [ ] PR-T7.A supersede operations land + 3 T7 unit tests green.
- [ ] PR-T7.B 3 release-gating separation invariants green.
- [ ] PR-T2.A classifier lands + 12-case parametrize green.
- [ ] PR-T2.B A-path lands + correction-routing invariant green.
- [ ] PR-T2.C B-path lands + supersede-routing invariant green.
- [ ] All 27 cumulative invariants green on `main`.
- [ ] STEP 7 bench Δ ≤ ±5% on bench-required paths.
- [ ] Operator scenario A (CASCADE — withdrawn report) and B (EVENT —
      CEO change) both demonstrated end-to-end on the live wiki.
- [ ] PR-T7.C closure docs land + `v0.4.0` GitHub release published +
      Zenodo mint received + DOI chain extended.

---

## 11. Out of scope (deferred to v0.4.1+)

- **T3 Evidence Aging** (strategic §6) — v0.4.2.
  Requires per-domain aging functions + the half-life-decay infrastructure
  that's not blocking the CASCADE vs EVENT separation.
- **T4 Reviewer Authority Hierarchy** (strategic §7) — v0.4.3 / v0.5.
  Requires user-management surface; deferred until the domain-pilot
  cycle.
- **T5 Replayable Audit Graph** (strategic §8) — v0.4.3 / v0.5.
  T7's `reconstruct_view_at` covers the per-edge replay primitive
  already; T5's full event-store reconstruction is a separate layer.
- **T6 Causality Chain** (strategic §9) — v0.4.1.
  CASCADE extension for derived facts. Lands first in v0.4.1 because
  it depends on T2's A-path infrastructure being live.
- **Domain packs** — banned until v1.0 (CLAUDE.md rule #1).
- **Cross-workspace federation primitives** — the `JAMES_WORKSPACE=` env
  already seeded in v0.3.0 PR-C6 covers the read-only path. The
  full federation primitive (workspace-to-workspace push) is a
  v0.4.x follow-up that depends on T5's event store.

---

## 12. Open questions — **LOCKED 2026-05-26**

Four decision points reviewed and locked by operator on 2026-05-26
during the GitHub Actions outage window. Each lock listed with the
chosen option and the rationale that drove it.

### 12.1 Migration timing → **separate prep-PR** ✅ LOCKED

The schema validator extension lands in a small prep-PR (PR-0)
**before** PR-T1.A. PR-T1.A then carries only the migration script
+ idempotency tests, referencing an already-extended schema.

Rationale:
- Risk separation: validator change is code; migration is data
  mutation. Splitting them keeps the revert path narrow either
  direction.
- JAMES precedent: Sprint 1 #1 used the same pattern (PR #493
  ingestion fix + PR #494 retroactive backfill script).
- CLAUDE.md rule #5: smaller, single-responsibility PRs.

### 12.2 `current_time` source → **hybrid** ✅ LOCKED

`core/lifecycle/clock.py` exports a single `now() -> datetime`
helper. Production code calls `clock.now()` unconditionally. The
`reconstruct_view_at(head, predicate, t)` replay primitive accepts
an explicit `t` parameter — that's the only forensic path that
bypasses `clock.now()`. Tests use `monkeypatch.setattr` against
`clock.now` to freeze time deterministically.

`compute_confidence_from_sources(sources, current_time=None)`
defaults `current_time` to `clock.now()` when `None` and to the
caller's explicit value otherwise — matches the existing v0.3
signature shape so the backward-compat path stays clean.

Rationale:
- Single source of truth for "current" simplifies testing.
- Replay primitive entry point already designed in strategic memo
  §9.5.4 with explicit `t` — no new plumbing needed elsewhere.
- Matches the conftest pre-import pattern (PR #515) the test suite
  already uses for module-level state.

### 12.3 `mutation_type` audit field default → **unconditional emit + absent = `"active"`** ✅ LOCKED

PR-T2.A / B / C emit `mutation_type=<value>` on every routing
decision. The replay primitive treats absence of the field as
`"active"` when reading pre-bundle audit rows (which is correct —
pre-bundle JAMES had no mutation-type concept; every state was
implicitly active).

Rationale:
- JAMES audit-log pattern (append-only, absence = default) already
  established by `evidence_scope` (PR #516) and `reason:retry`
  (PR #487).
- Backfill (option C in the analysis) would violate the append-only
  invariant — rejected.
- Semantic correctness: pre-bundle rows were genuinely active at
  the time they were written; treating absent as `"active"` is
  semantically right, not a workaround.

### 12.4 Daily expiration cascade cadence → **on-demand only for v0.4.0** ✅ LOCKED

`scripts/run_expiration_cascade.py` ships with dry-run default +
`--apply` flag. Operator runs manually whenever desired. No
server-side timer, no systemd / cron / Task Scheduler dependency
in v0.4.0. Operator can wrap the script with their own
scheduler if they want automation.

Cadence revisit deferred to v0.4.1 (T6 lands — causality cascade
will want similar batch infrastructure) or v0.4.2 (T3 lands — aging
genuinely needs a fixed cadence).

Rationale:
- JAMES local-first / single-operator model: scheduler infrastructure
  out-of-fit.
- Safety profile: expired-but-unmarked sources keep contributing to
  confidence (slightly stale), but no security / correctness break.
- JAMES precedent: PR #517 `bench_lc_scope_arms.py` and Sprint 1
  `backfill_entity_overlap_relations.py` both use the same
  "operator-runnable script, manual apply" pattern.

---

**Lock effect**: Sprint 5 implementation now unblocked. PR-0 (validator
prep) is the next code PR after this entry memo lands.

---

## 13. Risks + mitigations

| Risk | Likelihood | Mitigation |
|---|---|---|
| CASCADE inadvertently triggered by EVENT path | medium | `test_supersede_does_not_trigger_cascade` is a release-gating invariant; one of the three §3 properties. Mocked CASCADE call counter pins zero. |
| Schema migration writes wrong defaults → existing queries break | low | Migration is field-additive only; flag-OFF baseline pinned via STEP 7 baseline test. Dry-run + verify steps in §9 catch drift. |
| Replay primitive misses `mutation_type` filter → returns invalidated edges | medium | `test_historical_replay_via_chain` pins it. Replay fixture includes both an invalidated edge (CASCADE) and a superseded edge (EVENT) on the same `(head, predicate)`. |
| T2 classifier false-positive A-path on a B-class fact (incorrectly invalidates a superseded edge) | medium | `test_t2_b_class_routes_to_supersede` pins B-path routing; classifier has 12-case parametrize + the §11 "missing timestamps → B default" rule is the safety net. |
| `core/lifecycle/` modules grow past 20 KB after T3-T6 land | low | Each module starts at half-cap. Split before exceeding 18 KB (per the same pattern as `pipeline_context.py` split in PR #518). |
| Bench gate regression on PR-T1.A schema-only PR | low | Migration writes defaults that map to v0.3 semantics. STEP 7 baseline pinned at flag-OFF; PR-T1.A is the canary. |

---

## 14. CLAUDE.md rule check

- **rule #1** (no domain features): ✅ Sprint 5 is generic Layer 4
  infrastructure. No domain pack work.
- **rule #2** (bench on `core/{retrieval,graph,reasoning}`): ✅
  Required for PR-T1.A (graph confidence path), PR-T1.B (cascade
  semantics overlap), PR-T2.B/C (live wiring at ingestion). PR
  bodies paste STEP 7 numbers.
- **rule #3** (self-evolution opt-in): N/A — Sprint 5 does not touch
  the self-evolution surface.
- **rule #4** (architecture changes): ✅ All seven implementation PRs
  carry the `architecture` label. `docs/ARCHITECTURE.md` amendments
  land with PR-T7.C closure.
- **rule #5** (module ≤ 20 KB): ✅ Each `core/lifecycle/*.py` file
  targets half-cap (8-10 KB); enforcement checkpoint at every PR
  review.

---

## 15. Sprint 5 closure → v0.4.0 release

Once PR-T7.C lands, the release sequence mirrors the v0.4.0-alpha.3
pattern (proven in PRs #519 + #520):

1. `gh release create v0.4.0 --target main --title "..." --notes-file <CHANGELOG excerpt>`
2. Wait for Zenodo automatic mint (~3-5 min)
3. Open `chore/v0.4.0-doi-chain` PR — `.zenodo.json` `isNewVersionOf`
   chain back to v0.4.0-alpha.3 + README DOI badge bump

After v0.4.0 lands, v0.4.1 (T6) becomes the next architectural cycle.
T6 depends on T2's A-path infrastructure being live, so this ordering
is **required**, not optional.

---

## 16. References

- **Strategic design**: `docs/design/v0.4-lifecycle-semantics-roadmap.md`
  (948 lines, §0-16). This memo is the execution-side complement.
- **Reference architecture**: `docs/architecture/memory-lifecycle-architecture.md`
  §1.5 (CASCADE vs EVENT axiom).
- **v0.4 entry sprint sequence**: `docs/handovers/v0.4.0-entry-track.md`
  (Sprint 0 → Sprint 6 plan).
- **Layer 3 (v0.3) cascade**: `docs/design/v0.3-knowledge-cascade.md`
  (the format this memo follows).
- **CLAUDE.md** rules — every Sprint 5 PR re-checks them.

---

**End of Sprint 5 Layer 4 first-bundle entry memo.**
