# v0.4.1 — T6 Causality Chain Entry

> Status: **entry / pending user review** — 2026-05-28
> Origin: this memo translates `docs/design/v0.4-lifecycle-semantics-roadmap.md`
> §9 (the strategic design) into the **execution plan for v0.4.1**.
> The strategic memo answers *what to build*; this memo answers *how to ship it*.
> v0.4.1 ships when all PRs below land + invariants stay green + the
> ingestion-path caller for `dispatch_contradiction` (v0.4.0 carry-over) is wired.
>
> Companion documents (read in this order if entering fresh):
>   - `docs/handovers/v0.4.0-sprint5-layer4-first-bundle-entry.md` — v0.4.0 Sprint 5 closure (T1+T7+T2 first bundle)
>   - `docs/design/v0.4-lifecycle-semantics-roadmap.md` §9 — full T6 strategic design
>   - `docs/architecture/memory-lifecycle-architecture.md` — CASCADE vs EVENT axiom (§1.5)
>   - `docs/ARCHITECTURE.md` §5.7.10 — QVT subsystem (every PR pastes a Quality Delta Card)
>   - Memory: `project_state.md` — current main HEAD + open queues

---

## 0. TL;DR

Ship **T6 (Causality Chain Tracking)** as a CASCADE extension across
**5 sequenced PRs**: schema field + validator → derivation extraction
→ invalidation cascade → integration with `cascade_remove_doc_from_sources`
+ invariants → v0.4.1 closure docs.

Plus one v0.4.0 carry-over PR: wire `dispatch_contradiction(...)` to the
ingestion path (v0.4.0 shipped the router but left the caller as a v0.4.1
scope item).

T6's purpose: when a base fact is invalidated (CASCADE source removal),
**facts derived from it should auto-invalidate** so the wiki stays
internally consistent without manual intervention. v0.4.0's Sprint 5
bundle handles edges with explicit sources; T6 closes the loop for
edges whose sources are *other edges*.

Why T6 needs its own cycle (not bolted onto v0.4.0):
- New `derived_from` schema field on every relation → schema migration
- Depends on T2 A-path infrastructure (already in v0.4.0) — so it lands
  in v0.4.1, not in parallel
- Cycle-safety primitive analogous to T7's `walk_supersede_chain` but
  on a different axis (derivation vs supersession) — keeping these
  separate keeps the invariants legible

What's deferred to v0.4.2+:
- **T3** Aging — confidence decay over time
- **T4** Reviewer rank / approval state
- **T5** Snapshot replay primitive

T3/T4/T5 are EVENT-track or governance-track; T6 is the last CASCADE-track
piece. v0.4.1 closes the CASCADE pillar.

---

## 1. Why T6 is the v0.4.1 priority

Sprint 5 (v0.4.0) closed the EVENT track first because **T7 + T2** were
the load-bearing pieces for the "Replayable RAG" positioning — without
the supersede chain there's nothing to replay. Now that EVENT is shipped:

- Doc deletion + `cascade_remove_doc_from_sources` removes the source
  of an edge but leaves **derived facts** (facts whose `derivation` is
  a function of that edge) untouched.
- These orphaned derived facts stay in the wiki, slowly polluting
  retrieval — the very thing CASCADE was designed to prevent.
- The fix is small in scope (~3 new modules + 1 schema field + 4
  invariants) but architecturally clean — it completes the CASCADE
  pillar that v0.4.0 only half-finished.

The strategic memo's §9.1 example is concrete:
```
base fact:    (Joby, BASED_IN, California)   ← from doc A
derived fact: (Joby, IS_A,     미국 회사)    ← inferred from base
doc A deletion → base fact disappears → derived MUST auto-invalidate
```

Without T6, the second row survives doc A's deletion. With T6, it
cascades along the derivation chain.

---

## 2. §12 4-decision LOCK (must resolve before PR-T6.A)

These map 1-to-1 to Sprint 5's §12 LOCK pattern. Each decision lives
in this memo; the implementation PRs reference back here.

### Decision 1 — Detection trigger: eager vs lazy

> **LOCKED: eager**. `cascade_remove_doc_from_sources` calls
> `invalidate_derived_facts(removed_fact_id)` in the same transaction
> as the source removal. Lazy (defer until next query) is rejected
> because it lets stale derived facts surface in `/query/` responses
> between the cascade and the next sweep — defeating the "internally
> consistent without manual intervention" goal.

Trade-off accepted: cascade transaction takes O(N) where N = derived
facts depending on the removed fact. Empirically N ≤ 10 on the current
wiki (most relations are not inferred). Re-evaluate if a single cascade
ever touches >100 derived facts (then switch to lazy + scheduled sweep).

### Decision 2 — Derivation source: LLM-inferred vs operator-tagged vs both

> **LOCKED: operator-tagged at ingestion time, LLM-inferred behind a
> flag (`JAMES_T6_LLM_DERIVATION=0` default OFF).**

Operator-tagged path is deterministic, replay-safe, and uses the
existing `core/relations_schema.py` validator. LLM-inferred path is a
strict superset that uses gemma4:e4b to identify derivation candidates
on ingest — useful for legacy data backfill but introduces nondeterminism.
Default-off matches the "every claim is sourced" invariant from the
Replayable RAG positioning (#548).

### Decision 3 — Cycle handling: strict reject vs silent drop

> **LOCKED: strict reject at schema validation time.** A relation
> whose `derived_from` chain (transitive) includes itself fails the
> `core/relations_schema.py` validator. The migration PR (T6.A)
> includes a one-time scan that flags any pre-existing cycle for
> operator review rather than silently fixing.

Parallels T7's `walk_supersede_chain(max_length=...)` cycle safety
but stricter: T7 truncates at max_length; T6 rejects at write time.
Rationale: derivation cycles indicate genuine schema confusion (a
fact deriving from itself = nonsense), while supersede cycles can
appear during migration and need graceful handling.

### Decision 4 — Confidence propagation: strict invalidate vs gradual decay

> **LOCKED (refined 2026-05-28 in T6.C.b): per-derivation-type
> foundational-vs-corroborative semantics. Gradual confidence decay
> stays deferred to T3.**

T6 handles the binary case: each base fact has sources or doesn't.
The "base confidence dropped from 0.9 to 0.4 — should derived also
drop?" case is T3's territory (Aging). Keeping T6 binary keeps it
composable with T3 later.

**The binary trigger is per-derivation-type** (refined during T6.C
review):

  - `derivation: "transitive"` (chain inference, e.g. A→B→C ⇒ A→C):
    **hard dep**. Loss of any one base breaks the chain → invalidate
    the derived edge.
  - `derivation: "inferred"` (LLM-suggested single conclusion):
    **hard dep**. The inference rests on the cited base; if that
    base is gone, the inference is suspect → invalidate.
  - `derivation: "operator"` (human-tagged multi-source support):
    **soft / corroborative**. Loss of any one leaves remaining bases
    as alternative support (and any surviving hard dep still
    supports the structure) → keep alive.

Combined invalidation rule (T6.C.b in `core/lifecycle/causality.py`):

```
invalidate :=
    (any hard dep base empty)                            # structural break
  OR
    (no hard deps AND all operator bases empty)          # lone-corroborator collapse
```

The earlier memo text *"strict invalidate when any base fact is
fully removed"* read as a single ANY-trigger across all derivation
types. The user-clarified intent during T6.C review was the
per-type semantics above — schema already encoded the distinction
(`T6_DERIVATION_TRANSITIVE` / `_OPERATOR` / `_INFERRED`); the cascade
module now respects it.

**Real-world example** (Joby-USA via California):

  derived edge "Joby is in USA":
    - `b_california_in_usa` → `transitive` (foundation, chain link)
    - `b_witness_X_testimony` → `operator` (corroborator)

  Lose `b_witness_X_testimony`:
    - foundation alive → preserve (corroborator loss only weakens,
      doesn't break the chain).

  Lose `b_california_in_usa`:
    - foundation broken → invalidate (rule 1 fires).

---

## 3. PR sequence (5 PRs + 1 v0.4.0 carry-over)

| # | PR | scope | depends on |
|---|---|---|---|
| 0 | **PR-T2.D** (v0.4.0 carry-over) | wire `dispatch_contradiction` to the ingestion path (`core/learning/ingest.py` or equivalent caller). Adds the canonical CEO-change STEP 7 bench query that exercises A/B routing end-to-end. | v0.4.0 final (already merged) |
| 1 | **PR-T6.A** | `derived_from` schema field on relation. Validator: list of `{base_fact_id, derivation: "transitive" \| "operator" \| "inferred"}`. `core/relations_schema.py` validator + 8 contract tests. Migration script: `scripts/migrate_v041_lifecycle.py` adds empty `derived_from: []` to every existing relation (idempotent, byte-stable). | PR-T2.D |
| 2 | **PR-T6.B** | derivation extraction — `core/lifecycle/derivation.py` (≤15 KB). Functions: `extract_derivation_chain(relation, *, context)` returns the list of base_fact_ids implied by the relation's source documents. Operator-tagged path (deterministic) + LLM-inferred path (gated by `JAMES_T6_LLM_DERIVATION=1`, default OFF). 12 contract tests. | PR-T6.A |
| 3 | **PR-T6.C** | invalidation cascade — `core/lifecycle/causality.py` (≤12 KB). Functions: `invalidate_derived_facts(base_fact_id, *, audit_emit) → List[invalidated_relation_ids]`. Walks `derived_from` reverse index, emits `mutation_type=invalidated_by_cascade` audit rows. 10 contract tests including cycle-rejection invariant. | PR-T6.B |
| 4 | **PR-T6.D** | integration — `core.cascade.cascade_remove_doc_from_sources` now calls `invalidate_derived_facts` for every fact whose `sources` becomes empty. 4 release-gating invariants under `tests/test_t6_release_gating_invariants.py` (real wiki fixture, NOT mocks): `test_derived_invalidated_when_base_removed`, `test_partial_base_loss_preserves_derived` (Decision 4 binary), `test_causality_chain_acyclic_rejected_at_write`, `test_cascade_invalidate_emits_audit_row`. | PR-T6.C |
| 5 | **PR-T6.E** | v0.4.1 closure docs — `ROADMAP.md` v0.4.1 ✅ + `CHANGELOG.md` `[0.4.1]` + `.zenodo.json` v0.4.1 + `README.md` Status badge v0.4.0 → v0.4.1 + `docs/release_notes_v0.4.1.md`. v0.4.1 release prep. | PR-T6.D |

**Sprint contract tests total**: ~34 (8 schema + 12 extraction + 10
cascade + 4 release-gating). Same density pattern as Sprint 5 (123+
tests for 8 PRs).

---

## 4. Acceptance criteria — v0.4.1 release gate

Same shape as Sprint 5 (v0.4.0) acceptance:

| # | criterion | how verified |
|---|---|---|
| 1 | All 5 T6 PRs + carry-over PR merged | `git log origin/main` |
| 2 | All 34 new contract tests green | `pytest` |
| 3 | `tests/test_t6_release_gating_invariants.py` green against real wiki fixture | dedicated test file |
| 4 | Migration script idempotent | `scripts/migrate_v041_lifecycle.py --apply --verify` (double-run = byte-stable) |
| 5 | No regression in Sprint 5 invariants | `tests/test_t7_release_gating_invariants.py` still green |
| 6 | Quality Delta Card on every `core/`-touching PR | manual review via PR template (α-4) |
| 7 | `JAMES_T6_LLM_DERIVATION` flag default OFF | byte-identical retrieval behavior to v0.4.0 unless flag flipped |

---

## 5. Out of scope (deferred to v0.4.2+)

- **T3** Aging — confidence decay over time (memory-lifecycle §10)
- **T4** Reviewer rank / approval state (governance track)
- **T5** Snapshot replay primitive (already partially in v0.4.0 via
  `reconstruct_view_at(t)` from T7.A; full materialization deferred)
- **LLM-derivation backfill** — running `JAMES_T6_LLM_DERIVATION=1`
  across the entire existing wiki to populate retrospective derivation
  chains. Operator action, separate cycle after v0.4.1 lands.
- **Per-domain-pack derivation policies** — v0.5+ Domain Pilot scope.
  legal might require strict chains; food might allow inferred ones.
  Platform v0.4.1 ships a single global policy.

---

## 6. Risks & mitigations

| risk | severity | mitigation |
|---|---|---|
| Migration touches every relation → corrupts existing wiki | high | `scripts/migrate_v041_lifecycle.py --dry-run` default. Pre-write snapshot to `wiki.pre-v041-migration/` (same pattern as PR #525). `--verify` mode for byte-stability check. |
| Cycle rejection breaks legitimate cases | low | The strategic memo §9.3 only allows cycle when an inference is genuinely circular ((A→B→A)). Empirically the current wiki has zero such cycles; the migration script logs any flagged cases for operator review BEFORE rejecting. |
| Eager cascade O(N) blows up | low | Empirical N ≤ 10 on current wiki. Re-evaluate if any single cascade ever touches >100 derived facts. Audit row `mutation_type=invalidated_by_cascade` makes this measurable. |
| `core/lifecycle/causality.py` exceeds 20 KB | low | Strategic sketch ≤ 12 KB. Split into `causality.py` + `causality_audit.py` if extension ever pushes past 18 KB (same pattern as `pipeline_context.py` PR #518). |
| T6.B LLM-derivation introduces nondeterminism into a deterministic track | medium | Default OFF behind `JAMES_T6_LLM_DERIVATION`. Replayable RAG audit-replay invariant preserved: a `derived_from` entry with `derivation: "inferred"` carries a `created_with_model` field so replay can reproduce the inference. |

---

## 7. v0.4.1 timeline (estimated)

| phase | duration | gating |
|---|---|---|
| User review of this entry memo + §2 4 decisions | 1 session | user redirects on any LOCK |
| PR-T2.D + PR-T6.A + PR-T6.B | 1 session | tests green |
| PR-T6.C + PR-T6.D | 1 session | invariants green |
| PR-T6.E + release publish | <1 session | Zenodo mint |

Cumulative: ~3-4 sessions if no major redirect. v0.4.1 cuts ~mid-June
absent collaboration interruptions. If mid-June 3-author joint piece
prep work absorbs sessions, slip to late-June.

The QVT track (α-1~α-3 closed 2026-05-28) gates v0.4.1: every PR-T6.x
PR will paste a Quality Delta Card paired against `eval/qvt/baseline_2a31b20.json`
(or a fresh capture if v0.4.1 changes the production env enough to
warrant re-baselining; current expectation is no re-baseline needed —
T6 doesn't touch retrieval / graph / reasoning paths).

---

## 8. 한 줄 요약

> **T6 = CASCADE 마지막 한 조각 (derived fact 가 base 사라지면 같이
> 사라짐). 5 PR + v0.4.0 carry-over 1 PR = 6 PR. 4 LOCK 결정 (eager
> trigger / operator-tagged + flag / strict cycle reject / 이진 invalidate).
> v0.4.1 = CASCADE 파일러 닫힘 + Replayable RAG 정합 유지.**
