core/docs/decisions/ADR-0050-pack-grounded-comparison.md
Shay ecd580479a feat(adr-0050): pack-grounded COMPARISON surface
Sibling to ADR-0048's DEFINITION/RECALL pack-grounded surface for
the COMPARISON intent.  `pack_grounded_comparison_surface(a, b)` in
`chat/pack_grounding.py` composes a deterministic side-by-side
surface from both lemmas' pack `semantic_domains`, joined by the
fixed connective "contrasts with":

  "{a} (d_a1; d_a2) contrasts with {b} (d_b1; d_b2) — pack-grounded
   ({pack_id}). No session evidence yet."

`chat/runtime.py:_maybe_pack_grounded_surface` gains a COMPARISON
branch that runs before the DEFINITION/RECALL check.  Engages only
when both `intent.subject` and `intent.secondary_subject` are pack
lemmas and differ (identical-lemma comparison defers to disclosure).
Order-sensitive by design — matches the graph-layer's directional
CONTRAST edge.

Cognition eval (13-case public split):
  surface_groundedness  61.5% → 69.2%  (+7.7 pp)
  term_capture_rate     50.0% → 58.3%  (+8.3 pp)
  intent_accuracy            100.0%        (=)
  versor_closure_rate        100.0%        (=)

Case lifted: comparison_memory_recall_030 ("Compare memory and
recall").  Remaining unlift cases (CAUSE×2, VERIFICATION×1,
CORRECTION×1) need teaching-store chains or operator-driven
inference — pack lookup cannot supply causal explanations,
verifications, or corrections without fabrication.

Tests: tests/test_pack_grounded_comparison.py (15 tests).
Lanes green: smoke (67), cognition (121), runtime (19), algebra
(132), teaching (17), packs (6).
2026-05-18 06:59:53 -07:00

254 lines
9.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ADR-0050 — Pack-Grounded Surface for Cold-Start COMPARISON
**Status:** Accepted
**Date:** 2026-05-18
**Author:** Shay
---
## Context
[ADR-0048](./ADR-0048-pack-grounded-surface.md) added a pack-grounded
surface for cold-start DEFINITION / RECALL intents — a deterministic,
verbatim composition of the cognition pack's `semantic_domains`
for a single subject lemma. [ADR-0049](./ADR-0049-intent-subject-extraction.md)
then tightened intent classifier subject extraction so that prompts
like `"What is a procedure?"` produce a clean lemma the pack can
match.
The cognition lane's COMPARISON case
(`comparison_memory_recall_030` — `"Compare memory and recall"`)
still missed. Investigation:
- Intent classifier correctly tags the prompt `COMPARISON`,
`subject="memory"`, `secondary_subject="recall"`.
- Both lemmas are in `en_core_cognition_v1` with curated
`semantic_domains`:
- `memory``("cognition.memory", "memory.semantic", ...)`
- `recall``("operation.recall", "cognition.memory", ...)`
- `_maybe_pack_grounded_surface` was scoped to DEFINITION / RECALL
only; COMPARISON fell through to the universal disclosure.
The structure is exactly the situation ADR-0048's pattern was
designed for: two pack-known lemmas, no session evidence, deterministic
surface compositable entirely from pack atoms. The doctrinally
clean fix is a sibling pack-path branch for COMPARISON, with the
same trust-boundary discipline.
---
## Decision
Add a deterministic COMPARISON-shaped pack-grounded surface as a
second branch of `_maybe_pack_grounded_surface`, identical guardrails
to the DEFINITION / RECALL branch.
### Surface format
```text
{a} ({d_a1}; {d_a2}) contrasts with {b} ({d_b1}; {d_b2}) — pack-grounded ({pack_id}). No session evidence yet.
```
Up to two `semantic_domains` per side are emitted to keep the surface
compact. Every visible non-template token is either one of the two
lemmas or a verbatim pack `semantic_domains` string.
The connective `"contrasts with"` is a fixed-template constant —
identical to the human-readable form of the `contrasts_with` relation
predicate already produced by
`generate/semantic_templates.py:humanize_predicate("contrasts_with")`.
The COMPARISON intent's downstream graph node already uses this
predicate (`graph_planner.graph_from_intent` builds a
`Relation.CONTRAST` edge), so this ADR preserves the existing
COMPARISON connective vocabulary rather than introducing a new one.
### Engagement conditions
`pack_grounded_comparison_surface(a, b)` returns a non-`None` surface
**only** when **all** hold:
- both `a` and `b` are non-empty strings,
- both `a` and `b` are pack lemmas (with `semantic_domains`),
- `a ≠ b` after lowercasing.
`_maybe_pack_grounded_surface` invokes this path **only** when **all**
hold:
- gate fired with `source="empty_vault"` (cold-start session),
- `config.output_language == "en"`,
- intent is `COMPARISON`,
- both `intent.subject` and `intent.secondary_subject` are non-empty.
Any other condition returns `None` and the runtime falls through to
the universal disclosure unchanged. Safety / ethics refusal still
takes priority above this branch.
### Identical-lemma defer
`pack_grounded_comparison_surface("memory", "memory")` returns `None`.
A comparison between a term and itself carries no contrastive
evidence; the universal disclosure is the correct surface in that
case. Callers that want a single-lemma surface for `"X"` should use
`pack_grounded_surface("X")` directly via the DEFINITION / RECALL
path.
### Order sensitivity
The COMPARISON surface is **order-sensitive** by design:
`compare(a, b)` and `compare(b, a)` produce distinct surfaces because
the `"contrasts with"` connective is directional. This matches the
graph-layer behaviour where `Relation.CONTRAST(a → b)` and
`Relation.CONTRAST(b → a)` are distinct edges.
---
## Why this is doctrine-aligned
CLAUDE.md prohibits *opaque LLM fallbacks, stochastic sampling,
hidden normalisation, hot-path repair, and approximate recall*. This
ADR is:
- **Not opaque.** Every visible atom is either a lemma supplied by
the intent classifier or a verbatim pack `semantic_domains` string.
Pack ID is named in the surface.
- **Not stochastic.** Deterministic JSONL read, deterministic string
composition; identical input produces byte-identical output
(`test_comparison_surface_is_deterministic`).
- **Not hidden normalisation.** The pack lookup is a separate source
of grounding, not a normalisation step inside an existing operator.
No versor, no manifold, no field state touched.
- **Not hot-path repair.** `UnknownDomainGate` semantics are
unchanged — the gate still fires; this ADR only broadens what the
stub-path emits *after* the gate fires, in a narrow intent-typed
branch.
- **Not approximate recall.** Exact dictionary lookup on the pack
lexicon by lemma. No metric, no neighbourhood, no threshold.
The fundamental architectural move is the same as ADR-0048: the
cognition pack contributes a second source of grounding alongside
the session vault, with provenance preserved end-to-end. This ADR
extends that contribution from the DEFINITION / RECALL shape to the
COMPARISON shape.
---
## Characterisation — `core eval cognition`
A/B run on the 13-case public cognition split, identical
`RuntimeConfig` except for the merge of this ADR (build on top of
ADR-0049's article-stripping):
| Metric | Pre-ADR-0050 | Post-ADR-0050 | Δ |
|---------------------------|--------------|---------------|-------------|
| `intent_accuracy` | 100.0 % | 100.0 % | 0 |
| `surface_groundedness` | 61.5 % | **69.2 %** | **+7.7 pp** |
| `term_capture_rate` | 50.0 % | **58.3 %** | **+8.3 pp** |
| `versor_closure_rate` | 100.0 % | 100.0 % | 0 |
| `versor_condition < 1e-6` | preserved | preserved | invariant |
The case that lifts is exactly `comparison_memory_recall_030`:
```text
"Compare memory and recall"
-> intent.tag = COMPARISON, subject="memory", secondary_subject="recall"
-> both pack lemmas
-> "memory (cognition.memory; memory.semantic) contrasts with
recall (operation.recall; cognition.memory) — pack-grounded
(en_core_cognition_v1). No session evidence yet."
-> grounding_source = "pack"
```
The remaining unlift cases (CAUSE × 2, VERIFICATION × 1,
CORRECTION × 1) need either teaching-store chains (ADR-0018) or
operator-driven inference — pack lookup cannot supply causal
explanations, verifications, or corrections without fabrication.
---
## Consequences
### What changes
- `chat/pack_grounding.py` gains
`pack_grounded_comparison_surface(lemma_a, lemma_b) -> str | None`.
- `chat/runtime.py:_maybe_pack_grounded_surface` gains a COMPARISON
branch that runs before the DEFINITION / RECALL check.
- One new case lifts in the cognition eval.
### What does not change
- `_UNKNOWN_DOMAIN_SURFACE` constant retained; non-pack-lemma,
non-English, non-DEFINITION/RECALL/COMPARISON paths still return
the universal disclosure unchanged.
- `UnknownDomainGate` semantics unchanged.
- `ChatResponse.grounding_source` / `TurnEvent.grounding_source`
enum unchanged — COMPARISON pack-grounded surfaces carry the same
`"pack"` tag as single-lemma pack-grounded surfaces. Downstream
audit consumers do not need a new value to filter on; they already
distinguish via the surface contents if needed.
- Safety / ethics refusal still takes priority above pack grounding.
- `versor_condition(F) < 1e-6` invariant unaffected (no algebra
changes).
### Scope limits
- English only (`en_core_cognition_v1`). Same constraint as
ADR-0048; multilingual extension is a separate ADR.
- Exactly two operands. ``"Compare a, b, and c"`` is not yet
supported by the COMPARISON regex in `generate/intent.py` and is
out of scope here.
- Connective is fixed to `"contrasts with"` — matching the
`contrasts_with` predicate already in the relation vocabulary.
Other comparison flavours (`similar to`, `differs from`, `is like`)
are not modelled; they would need their own predicates in the
pack.
- Identical-lemma comparison defers to disclosure. Callers wanting
a single-lemma surface should use the DEFINITION / RECALL path.
---
## Cross-References
- [ADR-0018](./ADR-0018-tool-use-scope.md) — `DialogueIntent` and the
COMPARISON regex this ADR consumes.
- [ADR-0048](./ADR-0048-pack-grounded-surface.md) — the
DEFINITION / RECALL pack-grounded surface this ADR mirrors for
COMPARISON; same trust-boundary discipline, same `grounding_source`
tag.
- [ADR-0049](./ADR-0049-intent-subject-extraction.md) — clean
subject extraction the COMPARISON path inherits (though COMPARISON
uses its own named-group regex, not the post-processor;
``"memory"`` and ``"recall"`` are already clean from the
``_COMPARE_RE`` capture).
- [ADR-0029](./ADR-0029-safety-packs.md) /
[ADR-0033](./ADR-0033-ethics-packs.md) — sibling-pack-grounded
contributor pattern this ADR continues to extend from verdict
surfaces to the answer surface.
---
## Verification
```
tests/test_pack_grounded_comparison.py — 15 tests, all green
tests/test_pack_grounding.py — 18 pre-existing tests still green
tests/test_intent_subject_extraction.py — 30 pre-existing tests still green
tests/test_semantic_realizer_integration.py — pre-existing tests still green
Lanes (all green on this branch):
core test --suite smoke 67 passed
core test --suite cognition 121 passed
core test --suite runtime 19 passed
core test --suite packs 6 passed
core eval cognition (pre → post):
intent_accuracy 100.0% → 100.0% (=)
surface_groundedness 61.5% → 69.2% (+7.7 pp)
term_capture_rate 50.0% → 58.3% (+8.3 pp)
versor_closure_rate 100.0% → 100.0% (=)
```
The non-negotiable field invariant (`versor_condition(F) < 1e-6`) is
preserved: this ADR adds a surface-construction branch on the
existing stub path no algebra changes, no rotor construction, no
field update.