# Design Memo — α-8 Ontology Typed-Filter (A/B vs α-7 Heuristic Top-K)

> **Status**: design (pre-PR; gated on α-7 landing first).
> **Trigger**: 2026-06-01 strategy handover §6 — *"온톨로지 = graph-fix
> 의 정석판. A/B isolation 으로 휴리스틱 top-K 와 한계 기여 분리 측정"*.
> User decision (2026-06-01 PM): *"새롭게 해보되, 만들어진 온톨로지를
> 기존 타입에 어떻게 반영할 것인지 계획 추가 필요"*.
> **Cycle position**: ships after α-7 closure + re-baseline. Adds the
> first sector cell whose value depends on **typed** semantics, not
> just graph-traversal mechanics.
> **작성일**: 2026-06-01 PM (draft; gated on α-7 closure)

---

## 0. TL;DR

α-7 caps the graph entity surface with a heuristic K filter (rank-by-
score top-10). α-8 measures whether **typed filtering** (only surface
entities whose type matches the query intent) does measurably better
than the heuristic K cap.

The PR ships:
1. **Extended ontology** in `core/ontology.py` — new abstract root
   `Entity` + 5-7 new horizontal types (`event` / `date` / `location` /
   `quantity` / `project` etc.) + integration plan for the existing 4
   types (`person` / `org` / `concept` / `document` stay; become
   subtypes of `Entity`). Migration cost: 0 (frontmatter-compatible).
2. **New matrix cell** `C_rag-ontology` = α-7's stack + type-aware
   filter overlay. Lives between C_rag-graph and C_rag-full in the
   sector progression.
3. **Quality Delta Card** comparing `Δ = C_rag-ontology − C_rag-graph`
   (the appropriate baseline depends on α-7 outcome — see §1.4).

If `Δ ⭐⭐+` (graded or graded+abst_f1 lift exceeds noise band), adopt
the typed filter + register in ARCHITECTURE.md. If `Δ ≤ noise`, the
heuristic top-K is the answer; ontology layer is rejected for this
purpose; finding logged + memory promoted as "typed filter does not
beat heuristic on multihop_rag".

⚠️ **Critical design rule (post-α-7 baseline finding, 2026-06-02)**:
α-8 MUST preserve the **evidence-of-absence signal** that α-7 top-K
broke. See §1.5 for the mechanism and §2.4 for the implementation
rule.

---

## 1. Problem statement

### 1.1 What α-7 leaves on the table

α-7 ranks entities by `_dfs_score` (= relation weight × depth decay)
and keeps the top K. This is **type-agnostic** — a Person, Org,
Concept, Document, Date, Event etc. all compete on the same numeric
scale. A query asking *"when did Sam Altman join OpenAI"* might pull
top-K entities that include unrelated Concept nodes ranked higher
than the actual Date node, simply because Concepts have heavier
default weights in `core/ontology.py:RELATION_TYPES`.

### 1.2 What a typed filter could do

If the query is classified to "expected answer = Date+Event",
the filter could:
- Drop Person / Org entities that score high but don't match expected type
- Boost Date / Event entities that score lower but match expected type
- Surface ≤ K entities like α-7 but with the K slots biased toward
  type-relevant nodes

The question is empirical: does this beat the type-agnostic K filter?

### 1.3 Honest framing tier (apply `feedback_finding_size_honest_framing`)

⭐⭐ **partial** if Δ exceeds noise — typed filter is a known technique
(MS GraphRAG entity_extract has type hints; LlamaIndex KG indexers
have type-aware retrieval). The α-8 finding is *not* the mechanism;
it's the **empirical position** of the typed-filter contribution
relative to the cheap heuristic. JAMES-specific value = the numbers,
the routing rule, the operational artifact.

⭐ **operational only** if Δ ≤ noise — confirms heuristic is sufficient;
no further ontology investment justified on this benchmark.

❌ **Never claim "JAMES discovered typed semantics matter for RAG"** —
that framing is decades-old. Don't.

### 1.4 Baseline reference (post-α-7 reality, 2026-06-02)

The original design memo assumed α-7 K-bound baseline as the α-8
comparison reference. α-7's 2026-06-02 baseline result complicates
this:

| α-7 outcome | α-8 baseline reference | Headline framing |
|---|---|---|
| α-7 fully adopted at all tiers | post-α-7 baseline | A/B vs heuristic K |
| α-7 tier-gated adopt (5-tier remeasurement decides which tiers) | post-α-7 baseline at tiers where adopted; α-6 elsewhere | mixed; per-tier verdict |
| **α-7 rejected (likely at e4b production)** | **α-6 baseline directly** | α-8 ships first architectural graph fix |

The current 5-tier remeasurement (in flight as of writing) will
decide which baseline applies. Implementation work for α-8 should
NOT block on this — proceed on the design refinement.

### 1.5 Mechanism α-7 broke that α-8 must preserve

**α-7 baseline at gemma4:e4b L1/M_M (production tier)**: graded -0.020 +
abst_f1 -0.070 (the abst_f1 drop is **outside noise band edge** and
consistent across N=3 runs). Per-question-type cross-tab localised the
regression to `null_query`: graded -0.120 + abst_f1 -0.107 (both
**outside noise**).

4-step rule audit (`scripts/research/audit_12b_null_query_refusal_shape.py`)
confirmed the regression is **NOT** an oracle phrase coverage
artifact (re-score with 5 new narrow phrases shifted median abst_f1
by only +0.002). **The mechanism is structural**.

**Mechanism (post-2026-06-02 finding)**:

α-7 top-K **drops entities** from the LLM's view. Pre-α-7 the LLM
saw 41-161 entities for a typical query; post-α-7 it sees the 10
highest-ranked. For most questions this is fine — the LLM doesn't
need 41-161 entities to reason. For **null queries** (where the
correct answer is "the sources don't actually contain this") the
LLM needs the **evidence-of-absence** signal:

> *Pre-α-7 (gemma4): "I saw 41-161 entities; none of them is the
> Sridevi character mentioned. I should refuse."*
>
> *Post-α-7 (gemma4): "I saw 10 high-ranked entities. Let me try to
> infer the answer from these."* → confabulates.

The 41-161 entity surface was acting as a **negative-result anchor**
for gemma4's grounding-trained refusal behaviour. By trimming the
surface, α-7 inadvertently removed gemma4's ability to detect "this
is a topic JAMES doesn't actually have evidence on."

### 1.6 α-8 design constraint — preserve type-aware evidence-of-absence

If α-8 simply replaces α-7's heuristic K with a typed K (e.g.,
top-K-per-type), it inherits α-7's regression. The **architectural
fix** must instead:

- **Surface entities GROUPED by type** instead of as a single list
- **Explicitly emit empty-type rows** when a query-relevant type has
  no entities in the graph

Example context block (post-α-8):

```
[ENTITIES BY TYPE]
  [Person]:   (none found in graph for this query)    ← critical line
  [Date]:     (none found in graph for this query)    ← critical line
  [Org]:      Roche, OpenAI, Anthropic
  [Concept]:  AI safety, generative AI
  [Location]: California
  [Event]:    (none found in graph for this query)    ← critical line
  [Quantity]: $50M
```

The three "(none found)" lines are the **structural evidence-of-absence
signal**. They tell gemma4 explicitly: "we looked for Person / Date /
Event entities matching this query and found none." Now gemma4 can
detect "the query asks 'when did Sridevi do X' but Date is empty →
refuse" the same way it did pre-α-7 with 41-161 entities.

The α-8 typed filter is therefore **not** a sharper version of α-7;
it is a **structurally different mechanism** that exposes the
graph's negative result instead of hiding it.

---

## 2. Scope

### 2.1 In scope (this PR / cycle)

1. **Extend `core/ontology.py`** (current 10 KB → ~14-15 KB; under 20 KB
   gate):
   - Add abstract root `Entity` and `ENTITY_TYPES` dict
   - Add 5-7 new horizontal types: `event` / `date` / `location` /
     `quantity` / `project` (minimum); operator may approve up to 7
   - Extend `ALLOWED_RELATIONS` with new entries
   - Add new relations: `OCCURRED_AT`, `HAPPENED_ON`, `LOCATED_IN`,
     `INVOLVES`, `MEASURED_AS`, `WORKED_ON`
2. **New sector flag** `JAMES_DISABLE_TYPED_FILTER` (disable-polarity,
   default OFF = sector ON = production byte-identical).
3. **New matrix cell** `C_rag-ontology` in
   `scripts/qvt_ablation_matrix.py::_SECTOR_CELL_ENVS` between
   `C_rag-graph` and `C_rag-full`.
4. **Type-aware filter overlay** at the same point as α-7's top-K (in
   `core/graph_topk.py` or a sibling `core/graph_typed_filter.py` —
   decide on file size at implementation).
   - **MUST**: emit explicit "(none found)" rows for empty type
     categories per §1.6 design constraint. Never silently drop a
     type slot — that's what α-7 did and produced the production-
     tier regression.
   - Output format produced by `build_graph_context_str` (or a new
     typed variant): grouped by `ENTITY_TYPES` registry order;
     query-relevant types always shown (empty or not); irrelevant
     types optionally hidden.
5. **Query intent → expected-type classifier** — minimal heuristic at
   first (keyword bag in `core/query_expander.py` or new module):
   - "when" / "year" / "date" → `date / event`
   - "who" → `person / org`
   - "where" / "place" → `location`
   - "how much" / "price" → `quantity`
   - LLM-judged classification deferred to v0.5+ — keep cheap heuristic
     for measurement purposes; if heuristic-classified filter wins, the
     LLM classifier upside has a floor not a ceiling.
6. **5-axis Quality Delta Card** at C_rag-ontology vs C_rag-graph (=
   post-α-7 baseline).
7. **ARCHITECTURE.md** — extend §graph subsystem with typed-filter
   layer, document the integration with existing types.
8. **Memory promotion** if Δ ⭐⭐+:
   - `finding_typed_filter_beats_heuristic_on_multihop` (or rejected
     variant if Δ ≤ noise)

### 2.4 Evidence-of-absence preservation (mandatory, post-α-7)

The α-7 baseline finding (2026-06-02) makes the following design
points NON-NEGOTIABLE for α-8:

| Rule | Implementation |
|---|---|
| **R1**. Never silently drop a type slot | If a query expects type T (from heuristic intent classifier), the typed filter MUST emit a row for T even if zero entities of type T were surfaced by the DFS |
| **R2**. Empty-type rows are first-class context | Format: `[T]: (none found in graph for this query)` or equivalent — explicit string the LLM can read |
| **R3**. Don't conflate "empty" with "type not in query" | The classifier output identifies which types are query-relevant. Irrelevant types may be omitted entirely. Relevant-but-empty types MUST appear |
| **R4**. Order types by relevance to query, not alphabetic | Heuristic intent classifier ranks types; ranking determines display order |
| **R5**. Cap total type slots ≤ 10 (loose) | If query maps to >10 expected types (rare), pick the top-10 by intent confidence; never silently truncate the entities WITHIN a kept type slot |

These 5 rules together guarantee that gemma4's grounding training
gets the same "expected-but-absent" signal it had pre-α-7 with the
41-161 entity surface. R1 + R2 are the critical pair; R3-R5 are
quality-of-life follow-ups.

**Acceptance test for these rules** (two complementary checks):

1. **Multihop_RAG null query test** — re-run the α-7 null query
   audit (`audit_12b_null_query_refusal_shape.py`) against α-8 baseline
   at M_M. If null query abst_f1 stays ≥ α-6 baseline level (0.609),
   R1+R2 are working. If null query abst_f1 < α-6 baseline, the
   implementation has the same α-7 regression and needs investigation.

2. **Track 2c catalog poisoning test** — re-run Ali's `poison_01`
   (fake brown leather jacket asserted at 300 ILS) and `poison_04`
   (fake "buy 1 get 2 free" promo asserted from "your page") at JAMES
   M_M with α-8 typed filter active. Per Ali's REPORT.md §X2, Provia's
   gpt-4o-mini failed both: confirmed the leather jacket + invented a
   price (400→350), and confirmed the fake BOGO across the catalog
   list. **JAMES α-8 expected behavior**: emit the empty-type rows
   `[Product]: (none found in graph for this query)` and
   `[Promo]: (none found in graph for this query)` and refuse the
   assertion. If JAMES confirms the leather jacket / BOGO promo, α-8
   has the same evidence-of-absence preservation gap as α-7 (= same
   failure mode as Provia's stack on the same cases).

Track 2c integration design: `docs/design/v0.4-track-2c-arabic-adversarial-integration.md`.
The two failed Ali cases serve as **cross-stack mechanism convergence
evidence**: if α-8 passes them at JAMES while Provia's gpt-4o-mini
fails them on the same fixture, the architectural fix (typed filter +
evidence-of-absence preservation) is doing concrete work that's
absent from the gpt-4o-mini baseline.

### 2.2 Out of scope (deferred or rejected)

- **Domain-specific types** (Legal: `Brief / Court / Statute`, Food:
  `Recipe / Ingredient / Nutrient`, etc.) — explicit rule #1 violation;
  blocked until v1.0. Only horizontal abstract types in α-8.
- **AI-assisted ontology learning** — defining types from corpus
  automatically. Decades-old open problem; bypassed by hand-defining a
  small horizontal set.
- **Multi-tier type hierarchies** (Person → Researcher → AI Researcher)
  — flat single-tier under `Entity` only; subtype trees are v1.0+ territory.
- **Query intent classifier via LLM** — see §2.1 step 5; deferred.
- **Migration of existing 931 entities** — keep their existing
  `entity_type` field as-is; new types added additively (option A+B
  hybrid per 2026-06-01 strategy handover §6).
- **Renaming the 4 existing types** — preserve `person / org / concept /
  document` exactly; only their `parent` field gets `Entity` added.

### 2.3 Explicit rule #1 boundary tests (mother-platform compliance)

For each proposed new type, check:
- ✅ "Could a legal pack, food pack, retail pack ALL benefit from this?"
- ❌ "Would this make sense only in pack X but not pack Y?"

| Proposed new type | Horizontal? | Vertical hint? | Verdict |
|---|---|---|---|
| `event` (meetings, releases, incidents) | ✅ | ❌ | accept |
| `date` (any time point) | ✅ | ❌ | accept |
| `location` (places of any kind) | ✅ | ❌ | accept |
| `quantity` (numbers, amounts) | ✅ | ❌ | accept |
| `project` (initiatives, programs) | ✅ | ❌ | accept |
| `regulation` | borderline | yes (legal-leaning) | **defer** |
| `transaction` | borderline | yes (finance/retail-leaning) | **defer** |
| `recipe` | ❌ | yes (food-only) | reject |

The 5 accepted types are mother-platform pure. The 2 deferred require
explicit rule #1 review before any later cycle adds them.

---

## 3. Integration plan with existing types (user-requested explicit)

### 3.1 Current state (Apr 2026, `core/ontology.py`)

```python
# 4 entity types
ALLOWED_RELATIONS: Dict[str, Set[str]] = {
    "person":   {"STUDIES","RESEARCHES","TEACHES","BELONGS_TO","WORKS_AT",...},
    "org":      {"BELONGS_TO","OPERATES_IN","PRODUCES",...},
    "concept":  {"IS_A","PART_OF","RELATED_TO","BELONGS_TO_INDUSTRY"},
    "document": {"RELATED_TO","BELONGS_TO","OWNS_PRIVATE"},
}

# 12 relation types (+ 4 sensitive)
RELATION_TYPES = {"STUDIES", "RESEARCHES", ..., "OPERATES_IN", ...}
```

### 3.2 Post-α-8 state (target)

```python
# Abstract root + 9 concrete types (4 existing + 5 new)
ENTITY_TYPES: Dict[str, Dict] = {
    # ─── Existing (unchanged contracts) ───
    "person":   {"parent": "Entity", "active": True, "since": "v0.1"},
    "org":      {"parent": "Entity", "active": True, "since": "v0.1"},
    "concept":  {"parent": "Entity", "active": True, "since": "v0.1"},
    "document": {"parent": "Entity", "active": True, "since": "v0.1"},
    # ─── α-8 additions (horizontal only) ───
    "event":    {"parent": "Entity", "active": True, "since": "v0.4-α-8"},
    "date":     {"parent": "Entity", "active": True, "since": "v0.4-α-8"},
    "location": {"parent": "Entity", "active": True, "since": "v0.4-α-8"},
    "quantity": {"parent": "Entity", "active": True, "since": "v0.4-α-8"},
    "project":  {"parent": "Entity", "active": True, "since": "v0.4-α-8"},
}

# ALLOWED_RELATIONS extends with new type entries:
ALLOWED_RELATIONS.update({
    "event":    {"OCCURRED_AT","HAPPENED_ON","INVOLVES","LOCATED_IN","RELATED_TO"},
    "date":     {"HAPPENED_ON","RELATED_TO"},  # mostly inverse-target
    "location": {"LOCATED_IN","RELATED_TO"},
    "quantity": {"MEASURED_AS","RELATED_TO"},
    "project":  {"WORKED_ON","INVOLVES","PRODUCES","RELATED_TO"},
})

# RELATION_TYPES adds 6 new relations:
RELATION_TYPES.update({
    "OCCURRED_AT": {...allowed_head={"event"}, allowed_tail={"date"}},
    "HAPPENED_ON": {...allowed_head={"event"}, allowed_tail={"date"}},
    "LOCATED_IN":  {...allowed_head={"event","org","person"}, allowed_tail={"location"}},
    "INVOLVES":    {...allowed_head={"event","project"}, allowed_tail={"person","org","concept"}},
    "MEASURED_AS": {...allowed_head=None, allowed_tail={"quantity"}},
    "WORKED_ON":   {...allowed_head={"person","org"}, allowed_tail={"project"}},
})
```

### 3.3 Backward compatibility plan

| Compatibility surface | Plan |
|---|---|
| Existing 931 wiki entities' frontmatter (`entity_type: person/org/...`) | unchanged — same field reads correctly |
| Existing `ALLOWED_RELATIONS` lookups by entity_type | unchanged — keys still present, values still complete supersets |
| Existing `RELATION_TYPES` lookups | unchanged — 16 original + 6 new (additive) |
| Existing graph traversal in `expand_dynamic` | unchanged — `is_valid_relation_triple` returns True for all old combinations |
| Existing tests (`tests/test_ontology*.py`) | unchanged — no assertion on `ENTITY_TYPES` non-existence |
| Migration script needed? | **No** — additive-only change; no schema rewrite |
| Operator action needed? | **No** — just the next ingest will be able to label new-type entities; old entities remain valid |

### 3.4 Forward compatibility (v0.5 domain pack hook)

The `ENTITY_TYPES` dict's `since` field anchors when each type was
added. v0.5 domain packs can add their own types with
`{"parent": "Entity", "active": True, "since": "v0.5-<pack>"}` so
the mother platform's typed filter still routes packs' types through
the same mechanism. This is the integration extension point.

The forward compat plan does **NOT** justify adding domain types in
α-8 itself. Rule #1 holds.

---

## 4. Success criteria (Quality Delta Card pre-commit)

PR description must include the 5-axis card. Baseline = **post-α-7
re-baseline** (not pre-α-7).

| Axis | Post-α-7 baseline (C_rag-graph at M_M) | C_rag-ontology (M_M) | Δ | Acceptance |
|---|---:|---:|---:|---|
| path_coverage | TBD (α-7 result) | TBD | TBD | flat ±0.05 (S4 citation orthogonal) |
| graded_answer | TBD (α-7 result) | TBD | TBD | **+0.030 or better** for ⭐⭐ adopt; +0.010-0.030 for tier-gated; ≤ +0.010 → reject |
| abstention_f1 | TBD (α-7 result) | TBD | TBD | flat ±0.05 |
| token_cost | TBD (α-7 result) | TBD | TBD | ≤ +50 (typed filter is a *subset* of K, should not bloat) |
| latency_cost | TBD (α-7 result) | TBD | TBD | ≤ +5s (heuristic + type classifier overhead) |

Per-question-type cross-tab is **mandatory** for α-8 because typed
filter effects should vary across question_type (date/event filter
should help temporal queries more than inference queries).

### 4.1 Verdict tree

- `graded Δ ≥ +0.030` → **⭐⭐ adopt** + ARCHITECTURE.md + finding
  memory promoted
- `+0.010 ≤ graded Δ < +0.030` → **tier-gated adopt** (enable per
  question_type only); finding logged as partial
- `graded Δ < +0.010` → **reject** + finding logged as "typed filter
  does not beat heuristic on this benchmark"; ontology stays in the
  codebase but disabled by default

### 4.2 Cross-fixture sanity (next-next cycle)

α-8's verdict applies to MultiHop-RAG balanced-100 only. Cross-fixture
validation (different benchmark with different question-type mix) is
**out of scope** for α-8 itself but **mandatory** before any "ontology
typed filter is universal" claim. Defer to v0.5 entry.

---

## 5. Risks + mitigations

| Risk | Probability | Mitigation |
|---|---|---|
| New types added without rule #1 review → vertical drift | medium | §2.3 boundary test mandatory pre-commit; explicit accept/defer/reject per type |
| Type classifier heuristic too coarse → noisy typed filter | high | per-question-type cross-tab to surface where classifier mis-routes; followup LLM classifier deferred |
| Existing entities silently mis-typed by new schema | low (additive) | no migration; old types unchanged; new types only assigned at next ingest |
| `core/ontology.py` 10 KB → over 20 KB | low (target ~14-15 KB) | monitor at PR; split to `ontology_extended.py` if needed |
| Typed filter requires new relations on existing entities → migration creep | medium | new relations apply only to entities with new types; existing entity-pair graphs untouched |
| α-7's K filter and α-8's type filter interact (overall worse) | medium | A/B: ontology-on with K, ontology-on without K; pick the better post-hoc |
| "Headline = ontology" framing inflates the finding | high | §1.3 honest framing rule baked into closure doc template |
| 5 new types not enough for the benchmark; need more | medium | accept tier-gated adopt if 5 types help inference + temporal but not comparison; iterate in α-9 |
| Sensitive-relation handling for new types | low | `is_sensitive_relation` is relation-keyed, not type-keyed; no new sensitive relations introduced in α-8 |

---

## 6. Cycle dependencies

### 6.1 Predecessors (must land first)

- α-7 closure decision (post-5-tier remeasurement) — determines α-8
  baseline reference per §1.4:
  - α-7 adopted at tier T → α-8 baseline = post-α-7 at T
  - α-7 rejected at tier T → α-8 baseline = α-6 at T
- α-6 cycle closure (PR #678 ✅) — sector flag pattern
  (`JAMES_DISABLE_*`) is the model
- α-7 sub-finding mechanism (post-2026-06-02) — informs §1.6 evidence-
  of-absence preservation rule + §2.4 implementation requirements

### 6.2 Successors (gated on α-8)

- **Phase 3b proper measurement** — gated on α-8 closure per
  2026-06-02 user decision (Option C). α-6 baseline alternative if
  α-8 fails. See task #13 description for sequencing detail.
- **v0.5 domain pack mechanism** — pack-defined entity types via
  the `since:` extension hook in §3.4. Domain types stay packed; the
  mother schema does NOT absorb them.
- **Cross-fixture sanity** — α-8 verdict only applies to multihop_rag;
  retest on second benchmark before universal-law promotion.

### 6.3 Parallel (no conflict)

- T3 Evidence Aging — confidence/time axis, no overlap with entity
  surfacing or typing. Can run in any cycle in parallel.
- Phase 3b pre-work (task #10) — cross-family refusal phrase
  inventory; can run anytime before Phase 3b proper.

### 6.4 Phase 3b positioning rationale (2026-06-02 decision)

User decision per Option C: Phase 3b proper runs **after α-8 closure**
on the α-8 typed-filter baseline. Reasoning:

- α-6 baseline is graph-bug-confounded (memory `feedback_graph_layer_top_k_debt`)
- α-7 baseline has the production-tier regression (§1.5)
- α-8 typed filter is the architectural fix that preserves
  evidence-of-absence — cleanest baseline
- Single 45h Phase 3b run on α-8 baseline > 2-step (α-6 then α-8) at
  90h compute
- Publishable framing strongest: "JAMES typed-filter pipeline +
  cross-family validation = universal mechanism finding"

**Fallback**: if α-8 fails to land within ~3 weeks or its own
baseline shows regression, Phase 3b falls back to α-6 baseline with
graph-bug caveat noted explicitly in closure narrative.

---

## 7. References

- Strategy handover priority: 2026-06-01 PM user message §6 + §7
- User decisions (2026-06-01 PM): "검토해서 가능한 것 해보자" + "기존
  타입에 어떻게 반영할 것인지 계획 추가 필요"
- Honest framing rule: `memory/feedback_finding_size_honest_framing.md`
- Existing ontology: `core/ontology.py`
- Existing graph traversal: `core/graph_engine.py`
- α-7 predecessor: `docs/design/v0.4-alpha-7-graph-topk.md`
- α-6 sector flag pattern (precedent): `docs/design/v0.4-alpha-6-engineering-pre-pr-sequence.md`
- Rule #1 (no domain features until v1.0): `CLAUDE.md` §"Critical rules for this session"
- ARCHITECTURE.md graph subsystem (to extend)
- 5-axis Pareto verdict: `scripts/qvt_ablation_matrix.py::_classify_five_axis_delta`

---

## 8. Open decisions

| # | Decision | Recommendation | Owner |
|---|---|---|---|
| 1 | Exact 5 (or 7) new entity types to add | event / date / location / quantity / project (5 — safest core) | operator |
| 2 | Where typed filter overlays (core/graph_topk.py vs new file) | new file `core/graph_typed_filter.py` (clean separation) | implementer |
| 3 | Query intent classifier: keyword heuristic vs LLM | keyword heuristic in α-8; LLM classifier in v0.5+ | recommend keyword |
| 4 | What to do if Δ in tier-gated range (+0.010 to +0.030)? | enable per question_type only; finding logged as partial | operator |
| 5 | Cross-fixture sanity gate before universal claim | mandatory; defer to v0.5 | operator |
| 6 | Sensitive new types needed (`HAS_FINANCIAL_RECORD`?) | no — keep sensitive set in `core/ontology.py` unchanged for α-8 | recommend deferral |
| 7 | Migration story published or implicit | explicit §3.3 table in PR description; no operator action required | implementer |
