docs(notes): SurfaceSelector + spine-unification RFCs + lift baseline
Three companion docs to the 2026-05-19 fluency push. Captures the
deferred architectural work and the measured lift so the next
engineering pass has fixed substrate to build on.
notes/surface_selector_design_2026-05-19.md
Deferred RFC for the typed-candidate-lattice + single-selector
refactor the 2026-05-19 design review prescribed. Names the
remaining symptom this fixes (warm_grounding_stability=0 on the
warmed lane) and the migration shape: PackSurfaceCandidate
already shipped in commit 46ac737 is a structural subset of the
proposed SurfaceCandidate type. Six-step landing plan; each
step ends green and is independently revertable.
notes/spine_unification_design_2026-05-19.md
Companion RFC for the cognitive-spine unification. Enumerates
the three spines today (ChatRuntime.chat, CognitiveTurnPipeline,
scripts/run_pulse) + 5 eval-lane runners that split between
them. Proposes one canonical entrypoint with opt-in mode
parameter. Depends on the SurfaceSelector landing first.
notes/fluency_lift_baseline_2026-05-19.md
Numbers-only baseline. Per-lane before/after metrics across
cold_start_grounding, warmed_session_consistency,
deterministic_fluency, and cognition (public + holdout).
Sample probe showing fluent vs. structured-disclosure output
for 6 prompts. Lexicon + gloss coverage by pack (323/331 =
97.6% English-pack coverage). Reproducer command at the bottom
so anyone can re-measure in one paste.
Both RFCs explicitly document what's IN scope (so the next pass
isn't ambiguous) and what's OUT of scope (so it isn't accidentally
absorbed). Both flag the appropriate landing surface (reviewer's
track, not solo) and the dependency order.
No code change in this commit. Pure documentation.
This commit is contained in:
parent
07da601641
commit
269372a3a8
3 changed files with 518 additions and 0 deletions
186
notes/fluency_lift_baseline_2026-05-19.md
Normal file
186
notes/fluency_lift_baseline_2026-05-19.md
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
# Fluency Lift Baseline — 2026-05-19
|
||||
|
||||
Numbers-only record of the 2026-05-19 fluency push. Captures what
|
||||
changed, where, and by how much, so subsequent work has a fixed
|
||||
substrate to measure against.
|
||||
|
||||
## Eval lanes — before / after
|
||||
|
||||
All three lanes are run from a fresh `ChatRuntime()` per case
|
||||
(cold-start invariant) except where noted.
|
||||
|
||||
### `cold_start_grounding` (44 conversational prompts)
|
||||
|
||||
| | Before intent fix (b52e04a) | After intent fix | After gloss landing (07da601) |
|
||||
|---|---|---|---|
|
||||
| `intent_accuracy` | 0.4773 | 1.0000 | 1.0000 |
|
||||
| `grounding_accuracy` | 0.4773 | 1.0000 | 1.0000 |
|
||||
| `subject_accuracy` | 0.4318 | 1.0000 | 1.0000 |
|
||||
| `none` count | 21 / 44 | 0 / 44 | 0 / 44 |
|
||||
| `pack` count | 19 / 44 | 39 / 44 | 39 / 44 |
|
||||
|
||||
Five intent-classification patterns recovered 21 prompts that
|
||||
previously fell to `"I don't know — insufficient grounding"`:
|
||||
`Define X`, `What does X mean?`, `What is to V?`, `How does X work?`,
|
||||
`What causes X?`.
|
||||
|
||||
### `warmed_session_consistency` (8 cases / 18 turns)
|
||||
|
||||
| | Before pipeline gate (Phase B1) | After pipeline gate (c3e2a22) |
|
||||
|---|---|---|
|
||||
| `no_placeholder_rate` | 0.4444 | 1.0000 |
|
||||
| `telemetry_consistency_rate` | 0.4444 | 1.0000 |
|
||||
| `warm_grounding_stability` | 0.0000 | 0.0000 |
|
||||
| `grounding_match_rate` | 0.4444 | 0.4444 |
|
||||
|
||||
The pipeline-override usefulness gate cured the placeholder-prose
|
||||
bug + the telemetry/result mismatch. `warm_grounding_stability`
|
||||
remains 0 because of a separate architectural bug: a pack-grounded
|
||||
turn 1 reverts to vault-walk on turn 2 of the same prompt. Fix
|
||||
deferred to the SurfaceSelector RFC (`notes/surface_selector_design_2026-05-19.md`).
|
||||
|
||||
### `deterministic_fluency` (15 cases × 6 predicates)
|
||||
|
||||
| | Before gloss landing | After gloss landing (07da601) |
|
||||
|---|---|---|
|
||||
| `no_placeholder_rate` | 1.0000 | 1.0000 |
|
||||
| `complete_punctuation_rate` | 1.0000 | 1.0000 |
|
||||
| `finite_predicate_shape_rate`| 1.0000 | 1.0000 |
|
||||
| `no_provenance_only_rate` | 1.0000 | 1.0000 |
|
||||
| `surface_provenance_match_rate` | 1.0000 | 1.0000 |
|
||||
| `no_dotted_inventory_rate` | **0.3333** | **1.0000** |
|
||||
|
||||
The gloss feature delivered the no_dotted_inventory metric from
|
||||
33% to 100%. Every gloss-backed surface now reads as a fluent
|
||||
sentence instead of structured-disclosure dotted paths.
|
||||
|
||||
### `cognition` (CORE's authoritative cognitive eval)
|
||||
|
||||
| | Public (13 cases) | Holdout (19 cases) |
|
||||
|---|---|---|
|
||||
| `intent_accuracy` | 1.0000 | 1.0000 |
|
||||
| `term_capture_rate` | 0.9167 | 0.8333 |
|
||||
| `surface_groundedness`| 1.0000 | 1.0000 |
|
||||
| `versor_closure_rate` | 1.0000 | 1.0000 |
|
||||
|
||||
**Byte-identical** across every change in this push. Substring
|
||||
assertions in the eval continue to find every expected term in the
|
||||
new fluent surfaces.
|
||||
|
||||
## Sample probe — fluent vs. before
|
||||
|
||||
Fresh `ChatRuntime()` per prompt:
|
||||
|
||||
```text
|
||||
input: What is truth?
|
||||
before: truth — pack-grounded (en_core_cognition_v1):
|
||||
cognition.truth; logos.core; epistemic.ground.
|
||||
No session evidence yet.
|
||||
after: Truth is a claim or state grounded by evidence and coherent
|
||||
judgment. pack-grounded (en_core_cognition_v1).
|
||||
|
||||
input: Define moment.
|
||||
before: I don't know — insufficient grounding for that yet.
|
||||
after: Moment is a brief or pointlike interval of time.
|
||||
pack-grounded (en_core_temporal_v1).
|
||||
|
||||
input: What does important mean?
|
||||
before: I don't know — insufficient grounding for that yet.
|
||||
after: Something is important when it carries weight or priority in
|
||||
some judgment context. pack-grounded (en_core_attitude_v1).
|
||||
|
||||
input: What is to create?
|
||||
before: I haven't learned 'to create' yet (intent: definition).
|
||||
Mounted lexicon packs: en_core_cognition_v1, ...
|
||||
after: To create means to bring something into existence through
|
||||
deliberate action. pack-grounded (en_core_action_v1).
|
||||
|
||||
input: What is quasar? (genuinely OOV — control)
|
||||
both: I haven't learned 'quasar' yet (intent: definition).
|
||||
Mounted lexicon packs: ...
|
||||
|
||||
input: How does memory work? (CAUSE w/o teaching chain — control)
|
||||
both: I don't know — insufficient grounding for that yet.
|
||||
(deliberately preserved as the discovery-gap signal)
|
||||
```
|
||||
|
||||
## Lexicon + gloss inventory
|
||||
|
||||
After this push:
|
||||
|
||||
| | Lexicon entries | Glosses | Coverage |
|
||||
|---|---|---|---|
|
||||
| en_core_cognition_v1 | 85 | 78 | 91.8% |
|
||||
| en_core_meta_v1 | 73 | 72 | 98.6% |
|
||||
| en_core_attitude_v1 | 40 | 40 | 100.0% |
|
||||
| en_core_temporal_v1 | 28 | 28 | 100.0% |
|
||||
| en_core_action_v1 | 26 | 26 | 100.0% |
|
||||
| en_core_quantitative_v1 | 24 | 24 | 100.0% |
|
||||
| en_core_spatial_v1 | 24 | 24 | 100.0% |
|
||||
| en_core_polarity_v1 | 16 | 16 | 100.0% |
|
||||
| en_core_causation_v1 | 15 | 15 | 100.0% |
|
||||
| **Total** | **331** | **323** | **97.6%** |
|
||||
|
||||
The 8 unglossed entries in cognition are dual-POS lemmas (e.g.
|
||||
`cause` exists as NOUN and VERB; only the more salient POS got a
|
||||
gloss in the first dispatch). Adding the duals is a follow-up
|
||||
authoring pass.
|
||||
|
||||
## Commits in this push
|
||||
|
||||
```
|
||||
07da601 feat(packs): seed 323 reviewed glosses across 9 English content packs
|
||||
46ac737 feat(pack-grounding): selector-ready gloss wiring via PackSurfaceCandidate
|
||||
24daebf feat(pack-resolver): gloss resolver with lexicon-residency + dual-checksum hardening
|
||||
c3e2a22 fix(pipeline): usefulness gate on realized-plan override
|
||||
a67a3cc feat(evals): deterministic_fluency lane — six structural predicates
|
||||
0cf1a8f feat(evals): warmed_session_consistency lane — pipeline override regression substrate
|
||||
c6b4f1d fix(runtime): config-replace + thin API wrappers + stale docstring
|
||||
a084f1d feat(evals): cold_start_grounding lane — 44-prompt routing probe
|
||||
b52e04a fix(intent): five conversational definition patterns + polarity-stopword
|
||||
```
|
||||
|
||||
Earlier in the session (now ancestors of the above):
|
||||
|
||||
```
|
||||
8 commits seeding 9 new English content packs (230 lemmas, 5x prior coverage)
|
||||
```
|
||||
|
||||
## What's deferred (with rationale)
|
||||
|
||||
- **SurfaceSelector refactor** — `notes/surface_selector_design_2026-05-19.md`
|
||||
Cures `warm_grounding_stability`. Crosses runtime + pipeline +
|
||||
telemetry + hash. Solo-landing carries too much blast radius;
|
||||
reviewer is best positioned to land it.
|
||||
|
||||
- **Spine unification** — `notes/spine_unification_design_2026-05-19.md`
|
||||
Cures `core chat` ≠ pipeline-eval drift. Depends on the
|
||||
SurfaceSelector landing first.
|
||||
|
||||
- **Cognition dual-POS gloss completion** — 8 cognition lemmas have
|
||||
dual entries (NOUN+VERB) where only one got a gloss. Mechanical
|
||||
follow-up; one subagent dispatch can close it.
|
||||
|
||||
- **Gloss-formed sentences for AUX/PRON/SCONJ** — three lemmas in
|
||||
cognition (`be`, `why`, `because`) have glosses authored to a
|
||||
specific frame. Manual QA pass on the resulting surface is
|
||||
pending.
|
||||
|
||||
## Reproducing the numbers
|
||||
|
||||
```bash
|
||||
core eval cold_start_grounding
|
||||
core eval warmed_session_consistency
|
||||
core eval deterministic_fluency
|
||||
core eval cognition
|
||||
core eval cognition --split holdout
|
||||
|
||||
# Live probe:
|
||||
python3 -c "
|
||||
from chat.runtime import ChatRuntime
|
||||
for p in ['What is truth?', 'Define moment.', 'What does important mean?',
|
||||
'What is to create?', 'How does memory work?']:
|
||||
r = ChatRuntime().chat(p)
|
||||
print(f'[{r.grounding_source}] {p}\n -> {r.surface}\n')
|
||||
"
|
||||
```
|
||||
144
notes/spine_unification_design_2026-05-19.md
Normal file
144
notes/spine_unification_design_2026-05-19.md
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
# Cognitive-Spine Unification — Deferred RFC
|
||||
|
||||
**Date:** 2026-05-19
|
||||
**Status:** Design proposal — NOT implemented. Deferred from the
|
||||
2026-05-19 fluency push because the change crosses public-API
|
||||
entrypoints and depends on the SurfaceSelector landing.
|
||||
**Companion RFC:** `notes/surface_selector_design_2026-05-19.md`
|
||||
|
||||
## Motivation
|
||||
|
||||
The 2026-05-19 design review's Finding P0 #2:
|
||||
|
||||
> The live cognitive spine is fragmented across public entrypoints.
|
||||
|
||||
| Entrypoint | Today's spine | Affected by intent fixes? | Affected by pipeline fixes? |
|
||||
|---|---|---|---|
|
||||
| `core chat` (REPL) | `ChatRuntime.chat()` direct | ✓ yes | ✗ no |
|
||||
| `core trace` (single turn)| `ChatRuntime.chat()` direct | ✓ yes | ✗ no |
|
||||
| `core pulse` (research) | `scripts/run_pulse.py` graph diffusion + GloVe-seeded | ✗ no | ✗ no |
|
||||
| `evals/cognition/runner.py`| `CognitiveTurnPipeline.run()` | ✓ yes | ✓ yes |
|
||||
| `evals/cold_start_grounding/runner.py` | `ChatRuntime.chat()` direct | ✓ yes | ✗ no |
|
||||
| `evals/warmed_session_consistency/runner.py` | `CognitiveTurnPipeline.run()` | ✓ yes | ✓ yes |
|
||||
| `evals/deterministic_fluency/runner.py` | `ChatRuntime.chat()` direct | ✓ yes | ✗ no |
|
||||
|
||||
Three separate cognitive spines exist:
|
||||
|
||||
1. `ChatRuntime.chat()` direct — the simplest path, used by `core chat`
|
||||
and `core trace`.
|
||||
2. `CognitiveTurnPipeline.run()` — wraps `ChatRuntime.chat()` and adds
|
||||
a graph-realizer override step + transitive walk + frame compose.
|
||||
3. `scripts/run_pulse.py` — independent path with GloVe seeding,
|
||||
graph constraint correction, top-k recall.
|
||||
|
||||
Effects of fragmentation:
|
||||
|
||||
- A fix to the pipeline's override behaviour does not reach the
|
||||
user via `core chat`.
|
||||
- A fix to the runtime reaches the user but is masked under the
|
||||
pipeline-wrapped eval lanes.
|
||||
- Pulse can "prove" capabilities the user never experiences.
|
||||
- Tests can be green while user behaviour is broken (and vice versa).
|
||||
|
||||
## Proposed direction
|
||||
|
||||
### One canonical chat spine
|
||||
|
||||
`ChatRuntime.chat()` becomes the single canonical entrypoint. The
|
||||
pipeline's value-add (transitive walks, frame composition) moves
|
||||
INSIDE the runtime as opt-in passes consulted by the selector:
|
||||
|
||||
```python
|
||||
class ChatRuntime:
|
||||
def chat(self, text, *, max_tokens=None, mode="full"):
|
||||
# mode="full" — runtime + pipeline-equivalent passes
|
||||
# mode="bridge" — runtime only (today's bridge path)
|
||||
# mode="walk" — walk evidence only (research / introspection)
|
||||
...
|
||||
```
|
||||
|
||||
The pipeline becomes a thin convenience wrapper that selects a mode:
|
||||
|
||||
```python
|
||||
class CognitiveTurnPipeline:
|
||||
def run(self, text, *, max_tokens=None):
|
||||
# Equivalent to ChatRuntime.chat(text, mode="full").
|
||||
# Retained as the API the cognition eval harness was built
|
||||
# against; new code calls ChatRuntime.chat() directly.
|
||||
...
|
||||
```
|
||||
|
||||
### Pulse demoted to research harness
|
||||
|
||||
`scripts/run_pulse.py` keeps existing for the geometry-research path
|
||||
but is labeled non-canonical. It does not contribute to "fluent
|
||||
chat" claims; the eval lanes that rely on it (if any) are renamed.
|
||||
|
||||
### Single selector consumed everywhere
|
||||
|
||||
The SurfaceSelector (companion RFC) is the only path that emits the
|
||||
user-facing surface. All entrypoints route through it:
|
||||
|
||||
```
|
||||
user input
|
||||
─▶ ChatRuntime.chat(text, mode=…)
|
||||
─▶ collect_candidates(intent, subject, field_state, mode)
|
||||
─▶ SurfaceSelector.select(candidates, context)
|
||||
─▶ ChatResponse(surface=chosen.surface, …)
|
||||
```
|
||||
|
||||
`core chat`, `core trace`, every eval lane, and the pipeline shim
|
||||
all call `ChatRuntime.chat()` with different modes. One emission
|
||||
point, one telemetry record, one trace hash.
|
||||
|
||||
## What this fixes
|
||||
|
||||
| Today | After |
|
||||
|---|---|
|
||||
| Pipeline override invisible to `core chat` | Pipeline's value-add is opt-in modes inside the runtime; visible everywhere or nowhere |
|
||||
| Eval-vs-user behaviour drift | Same code path; can't drift |
|
||||
| Pulse "proves" things the user doesn't see | Pulse explicitly labeled non-canonical |
|
||||
| Tests asserting `r.surface == r.walk_surface` | Surface is the selector's output; walk_surface remains audit telemetry |
|
||||
| Three places to add a fluency surface | One: register a SurfaceProvider |
|
||||
|
||||
## Sequencing
|
||||
|
||||
This RFC is **dependent on** `surface_selector_design_2026-05-19.md`.
|
||||
Land in this order:
|
||||
|
||||
1. SurfaceSelector + provider registry (the companion RFC)
|
||||
2. Wrap each existing dispatcher branch as a provider
|
||||
3. Re-implement pipeline override as a provider (or remove if the
|
||||
selector handles it via ordering)
|
||||
4. Move `core chat` to `ChatRuntime.chat(mode="full")`
|
||||
5. Move `core trace` to `ChatRuntime.chat(mode="full")` with trace
|
||||
instrumentation
|
||||
6. Audit eval lanes — every lane explicitly declares its mode
|
||||
7. Label `scripts/run_pulse.py` as non-canonical in its docstring
|
||||
and any eval lane that depends on it
|
||||
|
||||
## What does NOT change
|
||||
|
||||
- Pack content (already correct authoring path)
|
||||
- Teaching chains (already correct authoring path)
|
||||
- Intent classification (already canonical via `generate.intent`)
|
||||
- Telemetry schema (one emitter, one shape)
|
||||
- Trace-hash stability (intra-session; hashes are still per-run)
|
||||
|
||||
## Risk register
|
||||
|
||||
- **Public API stability** — `CognitiveTurnPipeline.run()` cannot
|
||||
be removed without a deprecation cycle. Migration step is a
|
||||
wrapper, not a removal.
|
||||
- **Mode semantics** — the three modes (`full` / `bridge` / `walk`)
|
||||
must be documented in `docs/runtime_contracts.md` BEFORE the
|
||||
refactor so users can rely on them.
|
||||
- **Eval invariant** — `cognition` eval expects pipeline-level
|
||||
behaviour. The wrapper preserves that; verifiable byte-identity
|
||||
on the eval is a hard prerequisite to commit.
|
||||
|
||||
## When to land
|
||||
|
||||
After the SurfaceSelector RFC. Spine unification without the
|
||||
selector would just move the fragmentation; with the selector it
|
||||
collapses the spines onto a single, observable, replayable path.
|
||||
188
notes/surface_selector_design_2026-05-19.md
Normal file
188
notes/surface_selector_design_2026-05-19.md
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
# SurfaceSelector Design — Deferred RFC
|
||||
|
||||
**Date:** 2026-05-19
|
||||
**Status:** Design proposal — NOT implemented. Deferred from the
|
||||
2026-05-19 fluency push because the refactor crosses too many files
|
||||
to land safely solo. Picks up cleanly when the reviewer is back.
|
||||
**Seed type:** `chat/pack_surface_candidate.py::PackSurfaceCandidate`
|
||||
(landed in commit 46ac737 — already shaped for selector consumption)
|
||||
|
||||
## Motivation
|
||||
|
||||
The 2026-05-19 design review identified the central architectural
|
||||
fault behind the fluency bug class:
|
||||
|
||||
> **Surface authority fragmentation.** The runtime, pipeline, walk
|
||||
> evidence, pack fallback, telemetry, hash, and several composers all
|
||||
> compete for the user-facing sentence. The system can be "green"
|
||||
> while user-facing selection, pipeline selection, and telemetry
|
||||
> selection disagree.
|
||||
|
||||
Phase B1's pipeline-override usefulness gate (commit `c3e2a22`) cured
|
||||
two symptoms:
|
||||
|
||||
- `no_placeholder_rate`: 0.44 → 1.00
|
||||
- `telemetry_consistency_rate`: 0.44 → 1.00
|
||||
|
||||
But it did not cure the third:
|
||||
|
||||
- `warm_grounding_stability`: 0.0 (warmed lane)
|
||||
|
||||
The warmed-session bug — a pack-grounded surface on turn 1 reverting
|
||||
to a vault-walk fragment on turn 2 of the same prompt — is the
|
||||
remaining symptom. Its fix is structural, not patchable: pack-
|
||||
grounding must fire by intent + lemma residency, not by vault gate
|
||||
state. This is the SurfaceSelector's job.
|
||||
|
||||
## Proposed design
|
||||
|
||||
### One typed candidate per provider
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class SurfaceCandidate:
|
||||
surface: str # final user-facing string
|
||||
grounding_source: str # "refusal"/"teaching"/"pack"/...
|
||||
provider_id: str # which provider emitted this
|
||||
rank_hint: int # provider's own confidence tier
|
||||
pack_id: str | None # provenance (None for refusal/OOV)
|
||||
intent: IntentTag
|
||||
subject_lemma: str | None
|
||||
semantic_domains: tuple[str, ...] # audit content
|
||||
epistemic_status: str # "asserted"/"hedged"/"refused"/...
|
||||
is_user_facing_safe: bool
|
||||
is_fluent_sentence: bool
|
||||
is_replayable: bool
|
||||
```
|
||||
|
||||
`PackSurfaceCandidate` (already landed) is a structural subset of
|
||||
this — the migration is a strict superset rename + a few new fields.
|
||||
|
||||
### Providers
|
||||
|
||||
Each provider has one job: produce a candidate (or None) for the
|
||||
given intent + subject. No selection logic; no telemetry; no
|
||||
side effects beyond the candidate's audit fields.
|
||||
|
||||
```
|
||||
RefusalProvider → refusal candidates (safety/ethics)
|
||||
TeachingChainProvider → reviewed-corpus chain candidates
|
||||
CrossPackChainProvider → cross-pack chain candidates
|
||||
PackGlossProvider → reviewed gloss sentences
|
||||
PackDomainProvider → dotted-domain disclosure
|
||||
OOVInvitationProvider → "I haven't learned X yet"
|
||||
VaultRecallProvider → vault-grounded candidates (when warranted)
|
||||
ArticulationProvider → realizer/walk fallback (lowest rank)
|
||||
```
|
||||
|
||||
Today these all live as branches in
|
||||
`chat/runtime.py::_maybe_pack_grounded_surface()` and parallel
|
||||
dispatchers. The selector collapses them into a registered list.
|
||||
|
||||
### Selection
|
||||
|
||||
```python
|
||||
def select(candidates: Sequence[SurfaceCandidate],
|
||||
context: SelectionContext) -> SurfaceCandidate | None:
|
||||
"""One pure function over the candidate list.
|
||||
|
||||
Returns the highest-ranked candidate where:
|
||||
- is_user_facing_safe is True
|
||||
- the candidate's intent matches context.intent
|
||||
- the candidate respects context.constraints (e.g. cold_start
|
||||
suppresses VaultRecallProvider on turn 1)
|
||||
|
||||
Ordering:
|
||||
refusal > teaching > pack_gloss > pack_domain > oov > vault > walk
|
||||
|
||||
Within the same grounding_source rank, prefer
|
||||
is_fluent_sentence=True.
|
||||
"""
|
||||
```
|
||||
|
||||
### Single emission point
|
||||
|
||||
```python
|
||||
def chat(self, text: str, ...) -> ChatResponse:
|
||||
...
|
||||
candidates = self._collect_candidates(intent, subject, field_state)
|
||||
chosen = self._selector.select(candidates, context)
|
||||
# one emission point: telemetry, hash, ChatResponse all
|
||||
# built from the same `chosen` object.
|
||||
self._emit_turn_event(chosen, ...)
|
||||
return ChatResponse(surface=chosen.surface, ..., chosen=chosen)
|
||||
```
|
||||
|
||||
The pipeline either consumes the runtime's chosen candidate or
|
||||
becomes a provider itself. Either way, there is exactly one
|
||||
selector, one emission point, one trace-hash input.
|
||||
|
||||
## What gets fixed by this
|
||||
|
||||
| Symptom | Fixed how |
|
||||
|---|---|
|
||||
| Warm-grounding instability | `_collect_candidates` queries `PackGlossProvider`/`PackDomainProvider` by intent + lemma residency, independent of vault gate state. Turn N>1 produces the same candidate set as turn 1 for the same prompt. |
|
||||
| Telemetry / hash drift | Single emission point. No second-pass override path exists. |
|
||||
| `chat/runtime.py::_maybe_pack_grounded_surface` dispatcher pile | Decomposes into one provider per surface family. Adding a new surface kind = adding a new provider, not editing the dispatcher. |
|
||||
| Pipeline-vs-runtime fragmentation | Pipeline either consumes the runtime selection or registers itself as a provider; in both cases the user-facing selection happens exactly once. |
|
||||
| Spine fragmentation (separate RFC) | `core chat` / `core trace` / cognition eval / pulse all call into the same selector entry point. |
|
||||
|
||||
## What does NOT get fixed by this
|
||||
|
||||
- Gloss content coverage (handled by Phase C)
|
||||
- Intent classification gaps (handled by `b52e04a`)
|
||||
- Pipeline placeholder prose (already cured by `c3e2a22` — selector
|
||||
enforces the same `_is_useful_surface` check as a candidate filter)
|
||||
- Subjective fluency (out of scope — selector doesn't author content)
|
||||
|
||||
## Migration shape
|
||||
|
||||
1. Create `chat/surface_selector.py` with `SurfaceCandidate`,
|
||||
`SurfaceProvider` protocol, and `SurfaceSelector.select()`.
|
||||
2. Add a `SurfaceCandidate.from_pack_surface_candidate()` adapter so
|
||||
the existing `chat/pack_surface_candidate.py::PackSurfaceCandidate`
|
||||
becomes a SurfaceCandidate via 1-line conversion.
|
||||
3. Wrap each existing dispatcher branch in
|
||||
`_maybe_pack_grounded_surface()` as a provider. No behaviour
|
||||
change yet — each provider emits the same surface its branch
|
||||
used to emit.
|
||||
4. Replace `_maybe_pack_grounded_surface()` with
|
||||
`selector.select(_collect_candidates(...))`. Behaviour-preserving
|
||||
if the selector's ordering matches the dispatcher's order.
|
||||
5. Move the pipeline's override path through the selector.
|
||||
6. Add `core chat` / `core trace` integration paths.
|
||||
|
||||
Each step is a separate commit, each ends green, each is independently
|
||||
revertable. None requires authoring new content.
|
||||
|
||||
## Testing approach
|
||||
|
||||
- `warmed_session_consistency` already pins the lift target.
|
||||
`warm_grounding_stability` going from 0.0 to 1.0 is the regression
|
||||
signal for the selector landing.
|
||||
- `deterministic_fluency` continues to pin the structural floor.
|
||||
- New `tests/test_surface_selector.py` for the selector itself:
|
||||
ordering invariants, candidate-set determinism, single-emission
|
||||
invariant (only one TurnEvent emitted per chat call).
|
||||
|
||||
## Risk register
|
||||
|
||||
- **Pipeline integration** — touches `core/cognition/pipeline.py`.
|
||||
Mitigation: the pipeline override gate (`c3e2a22`) already filters
|
||||
unuseful surfaces, so the selector's job is mostly to consume the
|
||||
runtime's chosen candidate. Pipeline-specific override semantics
|
||||
may be expressible as a single ordered provider.
|
||||
- **Trace hash drift** — selector emits one hash; pipeline's
|
||||
separate hash path goes away. Old test fixtures with frozen
|
||||
hashes (`tests/test_cognitive_turn_pipeline.py:119-123`) compare
|
||||
run-to-run, not against stored values, so this is safe.
|
||||
- **OOV / refusal precedence** — already encoded by the ordering
|
||||
rank. The current dispatcher's order is preserved by spec.
|
||||
|
||||
## When to land
|
||||
|
||||
Best landed by the engineer who wrote the 2026-05-19 design review
|
||||
(they have the cleanest model of the call sites that need migration).
|
||||
Solo-landable when warmed-session work is the next architectural
|
||||
priority. Not for accidental work — this refactor crosses too many
|
||||
files to reverse cheaply.
|
||||
Loading…
Reference in a new issue