# α-6 Engineering Pre — 5 Sector Flag PR Sequence Plan

> **Status**: ready-to-execute PR sequence. Each PR is bounded
> (≤ 1.5h code work including tests). Land in the order listed;
> each unblocks more α-6 cell definitions.
>
> **작성일**: 2026-05-31 (T1 background-running, FFF prep)
> **Companion**: `docs/design/v0.4-alpha-6-sector-llm-ablation-matrix.md`
> §5 (engineering preconditions) §5.6 (layer-intent matrix)

---

## 0. Convention — disable-polarity flags

Existing patterns in `core/feature_flags.py`:
- **enable-polarity** flags: default OFF, env=`"1"` turns ON
  (`JAMES_ENABLE_*`)
- **disable-polarity** flags: default ON, env=`"1"` turns OFF
  (`JAMES_DISABLE_*`)

For α-6 sector ablation we add **disable-polarity** flags because
production behavior keeps every sector ON; α-6 cells opt-OUT of a
sector for ablation. This preserves CLAUDE.md rule 5 (default-off
invariant) — the **opt-in** here is the *opt-OUT-of-sector* flag.

---

## 1. PR sequence (5 flags, in landing order)

Order by **independence + smallest-first**. Each PR is reviewable
and revertible in isolation.

### PR 1 — S4 Citation/Sources flag (~30 min code + test)

**Flag**: `JAMES_DISABLE_SOURCES_FIELD` (default 0 = sources emitted)

**Effect when `=1`**: `response.sources` field stays empty `[]`
even when retrieval surfaced documents. JAMES still consults
those documents internally for the answer; only the
external-facing citation list is suppressed.

**Code touch**:
- `core/reasoning/pipeline.py` near line 343 (the
  `sources: [d["source"] for d in docs[:3]]` emission). Wrap in
  `if not os.environ.get("JAMES_DISABLE_SOURCES_FIELD") == "1":`
- `core/feature_flags.py` — add to `COGNITIVE_FLAGS` dict (or new
  `SECTOR_FLAGS` dict) with `"disable"` polarity.

**Contract test** (`tests/test_sector_flag_s4_citation.py`):
- Flag unset → `response["sources"]` has 1-3 entries on a query
  that retrieved docs
- Flag `=1` → `response["sources"] == []` even on same query
- Flag `=1` does NOT change `response["answer"]` text by more than
  expected variance (sources-suppression should be answer-neutral —
  pin via byte-identical comparison of the answer body)

**Verification PR body**:
- α-6 enables `C_rag-cited` vs `C_rag-graph` separation by toggling
  this one flag

**Branch**: `feat/v0.4-alpha-6-s4-citation-flag`

---

### PR 2 — S2 Graph flag (~60 min code + test)

**Flag**: `JAMES_DISABLE_GRAPH` (default 0 = graph traversal on)

**Effect when `=1`**: `loop_state["graph_paths"]` stays empty `[]`;
the entity graph is not consulted for path traversal. Vector RAG
(`docs`) still runs.

**Code touch**:
- `core/reasoning/pipeline.py` line 162 (`"graph_paths": []`) and
  the population path around line 237. Add early-return guard.
- `core/feature_flags.py` — add S2 entry.

**Contract test** (`tests/test_sector_flag_s2_graph.py`):
- Flag unset → `graph_paths` populated for a query touching graph
  entities
- Flag `=1` → `graph_paths == []` for the same query
- Answer may differ (graph context absent); pin only on
  `len(graph_paths) == 0` invariant + smoke that pipeline doesn't
  crash

**Verification PR body**:
- α-6 enables `C_rag-basic` vs `C_rag-graph` separation by toggling
  this one flag.

**Branch**: `feat/v0.4-alpha-6-s2-graph-flag`

---

### PR 3 — S1 RAG Retrieval flag (~60 min code + test)

**Flag**: `JAMES_DISABLE_RAG_RETRIEVAL` (default 0 = chroma top-k on)

**Effect when `=1`**: chroma top-k retrieval is skipped; `docs` is
empty `[]`. The model gets the question only, no context.
This is the `C_minus` cell baseline (pure LLM).

**Code touch**:
- `core/retrieval_engine.py` early-return when flag set.
- `core/reasoning/pipeline.py` — accept empty `docs` gracefully
  (already does for "no results found" branch, but verify).
- `core/feature_flags.py` — add S1 entry.

**Contract test** (`tests/test_sector_flag_s1_rag.py`):
- Flag unset → `docs` populated (≥ 1 entry on a known-good query)
- Flag `=1` → `docs == []`
- Smoke: pipeline produces an answer (model answers from
  parametric knowledge or refuses)

**Verification PR body**:
- α-6 enables `C_minus` (pure LLM) cell.

**Branch**: `feat/v0.4-alpha-6-s1-rag-flag`

---

### PR 4 — S5 Abstention flag (~30 min code + test)

**Flag**: `JAMES_DISABLE_ABSTENTION` (default 0 = abstention on)

**Effect when `=1`**: the abstention short-circuit (empty docs +
refusal-phrase post-check) is skipped. Model always tries to
answer, never refuses programmatically.

**Code touch**:
- `core/reasoning/pipeline.py` — locate the empty-results
  short-circuit and the post-answer abstention check. Wrap both.
- `core/feature_flags.py` — add S5 entry.

**Contract test** (`tests/test_sector_flag_s5_abstention.py`):
- Flag unset + empty docs → answer is a refusal phrase
- Flag `=1` + empty docs → answer is whatever the model produces
  (often a hallucination — that's the point of the ablation)
- Pin: with flag `=1`, `response["abstained"] != True` even when
  docs are empty (telemetry contract)

**Verification PR body**:
- α-6 enables ablation of the abstention layer for the abst_f1
  axis breakdown.

**Branch**: `feat/v0.4-alpha-6-s5-abstention-flag`

---

### PR 5 — S6 Cognitive Stages flag (~90 min code + test)

**Flag**: `JAMES_DISABLE_COGNITIVE_STAGES` (default 0 = stages on)

**Effect when `=1`**: planner / reflect / verify cognitive stages
are skipped; only the synth stage runs. Effectively
"single-shot LLM call to synth" instead of multi-stage reasoning.

**Code touch**:
- `core/reasoning/orchestrator.py` (or wherever the stage loop
  lives) — guard each non-synth stage with the flag.
- `core/feature_flags.py` — add S6 entry.

**Contract test** (`tests/test_sector_flag_s6_cognitive.py`):
- Flag unset → audit_log contains rows for planner / reflect /
  verify stages (depending on which are also opt-in by their own
  flags)
- Flag `=1` → audit_log only contains synth (+ retrieve/auth)
  rows; planner / reflect / verify rows absent

**Verification PR body**:
- α-6 enables S6 ablation cell.

**Branch**: `feat/v0.4-alpha-6-s6-cognitive-flag`

---

## 2. Total budget

| PR | Code | Test | PR body | Total |
|---|---|---|---|---|
| 1 (S4 citation) | 30 min | 20 min | 10 min | ~1 h |
| 2 (S2 graph) | 60 min | 30 min | 10 min | ~1.5 h |
| 3 (S1 RAG) | 60 min | 30 min | 10 min | ~1.5 h |
| 4 (S5 abstention) | 30 min | 20 min | 10 min | ~1 h |
| 5 (S6 cognitive) | 90 min | 30 min | 10 min | ~2 h |
| **Total** | **~4.5 h** | **~2 h** | **~50 min** | **~7 h** |

Realistic single-day plan: 4 PRs day 1 (S4 + S2 + S1 + S5 ≈ 5h),
S6 day 2 (~2h). Total wall-clock ~1 working day with breaks.

---

## 3. Post-merge — Phase 1 measurement (~5.5h compute, background)

After all 5 flags land:

```bash
JAMES_WORKSPACE=./workspaces/hotpot_eval \
PYTHONIOENCODING=utf-8 \
  python scripts/qvt_ablation_matrix.py \
    --tiers M_M \
    --rows L1 \
    --suite multihop_rag \
    --n-runs 1 \
    --sector-cells C_minus,C_rag-basic,C_rag-graph,C_rag-cited
```

(Matrix runner needs a small extension to accept `--sector-cells`
— this is the **final piece** of the engineering pre, ~1 h code.)

3 new cells × 110 min = ~5.5h. Yields the answer to user's actual
question: *"Does each JAMES sector add value vs vanilla gemma4?"*

---

## 4. Phase 1 expected cell labels

Per α-6 design memo §2:

| Cell | S1 RAG | S2 Graph | S3 Preproc | S4 Cite | S5 Abst | S6 Cog | S7 Route |
|---|---|---|---|---|---|---|---|
| C_minus | OFF | OFF | OFF | OFF | OFF | OFF | OFF |
| C_rag-basic | ON | OFF | OFF | OFF | OFF | OFF | OFF |
| C_rag-cited | ON | OFF | OFF | ON | OFF | OFF | OFF |
| C_rag-graph | ON | ON | ON | ON | OFF | OFF | OFF |
| C_rag-full | ON | ON | ON | ON | ON | ON | OFF (= α-5 L1) |
| C_rag-routed | ON | ON | ON | ON | ON | ON | ON (= α-5 L5) |

Phase 1 measures the first 4 (`C_minus` through `C_rag-graph`); the
last two are already captured by α-5.

---

## 5. Out of scope for this plan

- AUTO_ROUTER multi-tier engineering (Pattern A in α-6 §5.5) —
  separate sequence, ~1 day, gated on Phase 1 verdict
- LLM provider adapters (claude / openai / gemini) — Phase 4+, ~1-2
  days per provider
- Matrix runner `--sector-cells` extension — 1h, lands with PR 5 or
  separately

---

## 6. Risk register

| Risk | Mitigation |
|---|---|
| Sector flags interact in ways that don't decompose linearly | Each PR's contract test pins the flag's *isolated* behavior, not interactions. Phase 1 is the interaction check; if it surprises, re-examine the flag definitions. |
| Some sectors (e.g. S5 abstention) can't be cleanly toggled without changing answer text | Acceptable — that's the measurement. Pin only the structural invariants (e.g., `response["abstained"]` boolean), not the answer body. |
| PR 5 (cognitive) is bigger than the rest | Already budgeted at 2h. If it grows, split into S6a (planner) + S6b (reflect+verify) follow-up PRs. |
| Adding 5 flags clutters `core/feature_flags.py` | Create a new `SECTOR_FLAGS` dict alongside `COGNITIVE_FLAGS` to keep concerns separate. |

---

## 7. References

- α-6 design memo: `docs/design/v0.4-alpha-6-sector-llm-ablation-matrix.md`
- 4-step rule + layer-intent matrix: `memory/feedback_oracle_phrase_artifacts.md` §"확장 3"
- α-5 closure: ROADMAP §v0.4.x α-5 cycle "T0 verdict + post-closure"
- Existing flag pattern: `core/feature_flags.py` (`COGNITIVE_FLAGS`)
- Matrix runner CLI: `scripts/qvt_ablation_matrix.py` (extension needed for `--sector-cells`)
