docs(adr): ADR-0088 + ADR-0089 — unblock Findings 2 + 4 (#81)
Two ADRs that unblock the remaining items from the 2026-05-20 audit that could not ship as direct PRs. ADR-0088 — Realizer-Grounded Authority (Finding 2 retry) ======================================================== The first-response audit remedy (wire ``ground_graph`` between ``runtime.chat`` and ``realize_semantic``) was empirically attempted on ``fix/ground-graph-wiring`` and reverted: the grounded realizer's template output (e.g. ``"Light is a visible medium that reveal truth"``) is grammatically and stylistically weaker than the runtime path's ADR-0085-polished pack-grounded surface, so the realizer wins the surface resolver (PR #76) and the user-visible surface regresses on 23 byte-identical tests + ``register_invariant_grounding``. ADR-0088 reframes Finding 2 as a two-phase rollout: * Phase A (no behavior change) — realizer fluency parity. Templates consult the same gloss source ADR-0085 wired into the CAUSE composer, emit 3sg verb agreement, and carry the same pack-provenance tag the runtime path emits. Byte-identical today because the realizer is still gated by ``_is_useful_surface``. * Phase B (substantive) — ground the graph and let the realizer compete. Surfaces change exactly once, with a per-case re-baseline justified by a "fluency ≥ pre-fix runtime surface" invariant. The audit's final-draft remedy (hot-path short-circuit only) is explicitly rejected — pure perf cleanup, no metric lift since ``core eval cognition`` is already at 100% groundedness. ADR-0089 — Compound-Intent Pipeline Dispatch (Finding 4) ======================================================== ``classify_compound_intent`` is implemented but never reaches ``CognitiveTurnPipeline.run()``. Compound inputs like *"What is X and how does it relate to Y?"* silently drop the second clause. Naive multi-node dispatch breaks every downstream stage: PropositionGraph (one root), plan_articulation (single-root), realize_semantic (one target), surface resolver (one surface per turn), compute_trace_hash (one intent_tag + articulation_surface), teaching loop (one correction-source proposal), register / anchor- lens telemetry (one variant per turn). ADR-0089 proposes a three-phase rollout: * Phase C1 (no behavior change) — call ``classify_compound_intent`` in step 1b, record dropped clauses on ``CognitiveTurnResult.dropped_compound_clauses`` for observability while routing the dominant clause through the existing single-intent path. * Phase C2 (opt-in substantive) — flag-gated multi-node graph dispatch with new ``CompoundEdge`` / ``ConjunctionRelation``, widened ``compute_trace_hash`` carrying a ``compound_clauses_hash``, and a ``multi_clause_surface`` field on the resolver. Flag-off preserves byte-identity. * Phase C3 — telemetry alignment + demo + docs. Each phase is independently shippable and preserves the existing null-lift / byte-identity invariants register and anchor-lens established (ADR-0072, ADR-0073d) as the project's pattern for substantive runtime behavior changes. Both ADRs are Proposed; ratification follows the existing pack / ADR review process. No code lands in this commit.
This commit is contained in:
parent
ff1dcb2594
commit
8b51813c73
2 changed files with 202 additions and 0 deletions
98
docs/adr/ADR-0088-realizer-grounded-authority.md
Normal file
98
docs/adr/ADR-0088-realizer-grounded-authority.md
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
# ADR-0088 — Realizer-Grounded Authority (Finding 2 retry)
|
||||
|
||||
**Status:** Proposed
|
||||
**Date:** 2026-05-20
|
||||
**Author:** Shay
|
||||
**Supersedes:** none
|
||||
**Supersedes attempts:** `fix/ground-graph-wiring` (reverted; not landed)
|
||||
**Related:** ADR-0046 (forward graph constraint), ADR-0047 (wire forward constraint), ADR-0048 (pack-grounded surface), ADR-0085 (gloss content style pass), PR #76 (surface authority resolver)
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The 2026-05-20 second-opinion audit identified that `CognitiveTurnPipeline.run()` calls `realize_semantic(target, graph)` on **every turn** but never calls `ground_graph(graph, recalled_words)` first. Every non-COMPARISON, non-CORRECTION node in the `PropositionGraph` is born with `obj = "<pending>"`. The realizer therefore renders surfaces like `"Truth is defined as ..."` which `_is_useful_surface()` correctly rejects.
|
||||
|
||||
The graph is structurally sound but never grounded in the hot pipeline path, so the realizer is perpetually firing blanks. The audit's first-response remedy was to wire `ground_graph` in and let the realizer become a real surface authority. The audit's final-draft remedy retreated to a hot-path short-circuit (skip the dead computation when the teaching store has no relevant triple).
|
||||
|
||||
**Empirical attempt (`fix/ground-graph-wiring`, 2026-05-20).** The first-response remedy was implemented — `ground_graph` was wired between `runtime.chat` and `realize_semantic`, with recalled words sourced from a new `ChatResponse.recalled_words` field populated from the alphabetic walk-token filter. The change passed `core eval cognition` (byte-identical metrics across all three splits) but broke 23 byte-identical tests in `tests/test_realizer_guard_holdout.py`, `tests/test_register_invariant_grounding.py`, `tests/test_pack_glosses_content.py`, and `tests/test_warmed_session_lane.py`.
|
||||
|
||||
Inspection of a single failure reveals the root cause:
|
||||
|
||||
| Expected (runtime, post-ADR-0085) | Actual (grounded realizer) |
|
||||
|---|---|
|
||||
| `Light is a source of revelation that makes things knowable.` | `Light is a visible medium that **reveal** truth.` |
|
||||
|
||||
The grounded realizer **does** produce real content (no `<pending>` / `...` markers) — but its templates lack the runtime path's ADR-0085 gloss content-style pass, so the output carries grammar bugs (subject-verb disagreement here) and weaker phrasing. The realizer wins the surface resolver (introduced in PR #76) because it now passes `_is_useful_surface`, and the user-visible surface regresses.
|
||||
|
||||
This is not a wiring bug. It is a **layering inversion**: today the runtime path (`articulate_with_intent` + `pack_grounded_surface` + ADR-0085 fluency) is the only source that has been polished. Making the realizer a real authority before its templates match that fluency standard is a user-visible regression even when groundedness is preserved.
|
||||
|
||||
The change was reverted; the branch was never pushed.
|
||||
|
||||
---
|
||||
|
||||
## Decision
|
||||
|
||||
ADR-0088 reframes Finding 2 as a **two-phase rollout** instead of a single wiring change.
|
||||
|
||||
### Phase A — Realizer fluency parity (prerequisite, no behavior change)
|
||||
|
||||
Before the realizer is allowed to be a surface authority, its templates must produce surfaces that are at least as fluent as the runtime's pack-grounded path. Specifically:
|
||||
|
||||
1. **Gloss-aware realizer templates.** `realize_semantic` (and its template library in `generate/templates.py` / `semantic_templates.py`) must consult the same gloss source ADR-0085 wired into the CAUSE composer — `lemma.gloss` from the cognition pack manifest — and prefer gloss-derived phrasing over the current bare-template phrasing when a gloss exists for the subject lemma.
|
||||
2. **Subject-verb agreement.** The realizer must emit a 3sg verb form when the subject is a 3sg noun. Today's templates leak the lemma form (`reveal` for plural-or-bare) into 3sg subject contexts (`a visible medium that reveal truth`). This is the same fix ADR-0085 / PR #75 made on the content-style pass for glosses; the realizer must inherit it.
|
||||
3. **Pack-provenance tag parity.** The runtime path appends `pack-grounded (en_core_cognition_v1).` to the surface. The realizer's grounded output must carry the same tag, drawn from `intent.subject`'s resolving pack via the same resolver the runtime path uses.
|
||||
|
||||
Phase A is **byte-identical** by construction: it touches realizer templates but the realizer is still gated by `_is_useful_surface` on `<pending>` markers, so its output is still discarded today. No surfaces change.
|
||||
|
||||
### Phase B — Ground the graph and let the realizer compete
|
||||
|
||||
After Phase A merges and the realizer's standalone fluency is verified (a new lane that runs `realize_semantic` directly on a primed graph and asserts surfaces match the runtime path's grammar / gloss / provenance shape):
|
||||
|
||||
1. Add `recalled_words: tuple[str, ...] = ()` to `ChatResponse`, populated from the alphabetic walk-token filter inside `ChatRuntime._chat` (same source `articulate_with_intent` already uses).
|
||||
2. Reorder `CognitiveTurnPipeline.run()`:
|
||||
- keep `graph_from_intent` + `plan_articulation` before `runtime.chat`,
|
||||
- call `runtime.chat` next,
|
||||
- call `ground_graph(graph, response.recalled_words)` between chat and realization,
|
||||
- call `realize_semantic(target, grounded_graph)`,
|
||||
- resolver (PR #76) selects among the now-real candidates.
|
||||
3. Re-baseline the 23 byte-identical tests against the new (fluent) realizer surfaces, and re-pin `test_register_invariant_grounding::test_trace_hash_invariant_across_registers` for the post-grounding trace_hash.
|
||||
|
||||
Phase B is **substantive** — surfaces change — but every changed surface must clear a new invariant: "fluency ≥ pre-fix runtime surface on the same prompt." A regression dashboard pairs the pre- and post-grounding surface per case and a reviewer signs off before re-baselining.
|
||||
|
||||
### Out of scope
|
||||
|
||||
- The audit's final-draft remedy (hot-path short-circuit only) is **rejected**. It delivers no metric lift since `core eval cognition` already scores 100% groundedness across every split, and the dead computation is fast (a few µs per turn). Short-circuiting purely for perf is not load-bearing under CLAUDE.md's "small, load-bearing PRs" doctrine.
|
||||
- Cross-pack realization (the realizer consulting `en_core_relations_v1` glosses for a `family.parent` proposition) is deferred to a later ADR.
|
||||
|
||||
---
|
||||
|
||||
## Consequences
|
||||
|
||||
- **Realizer becomes a real authority for the first time** (Axiom 3 + Axiom 5 honored — propagation-over-mutation, articulation-from-grounded-propositions).
|
||||
- **One forward pass, one correction pass** (Axiom 4) — the three-way surface race PR #76 named is fully resolved because both candidates the resolver picks among are now substantively meaningful.
|
||||
- **Trace-hash invariant moves once** — a single re-baseline at Phase B merge. After that, the post-grounding trace_hash is the new permanent invariant.
|
||||
- **Realizer template surface area widens** — ADR-0085's content-style pass becomes load-bearing on a second consumer (the realizer), so future content-style work has to update both call sites.
|
||||
|
||||
---
|
||||
|
||||
## Rejected alternatives
|
||||
|
||||
1. **Land Phase B without Phase A.** Empirically attempted on `fix/ground-graph-wiring`; produces user-visible fluency regressions on 23 cases. Rejected.
|
||||
2. **Land the hot-path short-circuit only** (audit's final draft). Pure perf cleanup, no metric lift, not load-bearing. Rejected.
|
||||
3. **Gate Phase B behind a `RuntimeConfig.realizer_grounded_authority: bool = False` flag.** Defers the decision rather than resolving it; cognition eval at 100% groundedness means the flag would never flip in practice and the realizer stays a placeholder. Rejected — the right shape is to land Phase A unconditionally and then flip Phase B with a clean re-baseline.
|
||||
|
||||
---
|
||||
|
||||
## Validation gate
|
||||
|
||||
Phase A merge must demonstrate:
|
||||
|
||||
- Every existing byte-identical surface test still passes.
|
||||
- A new lane `tests/test_realizer_fluency_parity.py` runs `realize_semantic` directly on a manually-grounded graph for every cognition eval case and asserts surfaces match `^[A-Z][^<>]+\\.\\s*pack-grounded \\(.*\\)\\.$` (no `<pending>`, no `...`, leading capital, ends with `.`, carries provenance tag, subject-verb agreement).
|
||||
|
||||
Phase B merge must demonstrate:
|
||||
|
||||
- All three `core eval cognition` splits at ≥ MEMORY baselines.
|
||||
- The re-baselined 23 surface tests pass with a per-case justification comment naming the new fluency contract.
|
||||
- A new trace-hash invariant for the post-grounding `register_invariant_grounding` test, with a one-line ADR-0088 reference in the assertion.
|
||||
104
docs/adr/ADR-0089-compound-intent-pipeline-dispatch.md
Normal file
104
docs/adr/ADR-0089-compound-intent-pipeline-dispatch.md
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
# ADR-0089 — Compound-Intent Pipeline Dispatch (Finding 4)
|
||||
|
||||
**Status:** Proposed
|
||||
**Date:** 2026-05-20
|
||||
**Author:** Shay
|
||||
**Related:** `generate.intent.classify_compound_intent`, ADR-0066 (thread context), PR #76 (surface authority resolver)
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The 2026-05-20 audit identified that `classify_compound_intent()` is fully implemented in `generate/intent.py` but **never called by the cognitive pipeline**. The two existing call sites are:
|
||||
|
||||
- `chat/runtime.py:961` — inside `_maybe_apply_discourse_planner`, gated by `RuntimeConfig.discourse_planner=False` (default off).
|
||||
- `evals/compound_intent_decomposition/runner.py` and `evals/cold_start_grounding/runner.py` — eval-only.
|
||||
|
||||
`CognitiveTurnPipeline.run()` calls only the single-intent `classify_intent()` at line 110 and routes the result straight into `graph_from_intent()`. For an input like *"What is X and how does it relate to Y?"* the pipeline picks one intent by regex precedence (probably `DEFINITION` on `X`) and silently drops the `CAUSE` / `TRANSITIVE_QUERY` clause on `Y`. The graph never sees both subjects, the resolver never sees the second clause, and the trace hash records only the dominant clause.
|
||||
|
||||
Wiring compound intent into the pipeline is **not a single small PR**. Every downstream stage today assumes one intent + one subject:
|
||||
|
||||
1. `PropositionGraph` carries one root node per turn.
|
||||
2. `plan_articulation` topologically orders the nodes of a single-root graph.
|
||||
3. `realize_semantic` builds one `ArticulationTarget` per call.
|
||||
4. The surface resolver (PR #76) picks one surface per turn.
|
||||
5. `compute_trace_hash` records one `intent_tag` and one `articulation_surface` per turn.
|
||||
6. The teaching loop binds one correction-source proposal per turn.
|
||||
7. The speculative-subject cache (PR #77) tracks subjects per proposal, but the marker decision assumes one subject set per surface.
|
||||
8. The register / anchor-lens telemetry (ADR-0072 / ADR-0073d) emits one `(register_id, register_variant_id)` and one `(anchor_lens_id, anchor_lens_mode_label)` per turn.
|
||||
|
||||
Naive multi-node dispatch breaks every one of these assumptions.
|
||||
|
||||
---
|
||||
|
||||
## Decision
|
||||
|
||||
ADR-0089 proposes a **three-phase rollout** that lands compound intent without breaking any existing invariant. Each phase is independently shippable.
|
||||
|
||||
### Phase C1 — Substrate (no behavior change)
|
||||
|
||||
- Add `CompoundIntent` plumbing to `CognitiveTurnPipeline.run()`:
|
||||
- Call `classify_compound_intent(text)` at step 1b alongside the existing `classify_intent(text)`.
|
||||
- When the compound classifier returns a single-clause result, the pipeline takes the existing single-intent path byte-identically.
|
||||
- When it returns multiple clauses, the pipeline **still routes the dominant clause through the existing path** and records the other clauses on a new `CognitiveTurnResult.dropped_compound_clauses: tuple[DialogueIntent, ...] = ()` field for observability.
|
||||
- Add a CI invariant `tests/test_compound_intent_substrate.py::test_compound_dropped_clauses_recorded` that asserts: for the four canonical compound shapes (`AND`, `BUT`, `BECAUSE`, comma-joined), the dropped clauses appear in the result and the dominant-clause surface is byte-identical to today.
|
||||
|
||||
Phase C1 is **byte-identical** at the surface and trace_hash level. It surfaces the second-clause loss as observable telemetry instead of silent drop.
|
||||
|
||||
### Phase C2 — Multi-node graph (opt-in)
|
||||
|
||||
- Add `RuntimeConfig.compound_intent_dispatch: bool = False`.
|
||||
- When enabled and the compound classifier returns multiple clauses, `graph_from_intent` is widened to accept a `tuple[DialogueIntent, ...]` and emits one node per clause with explicit `CompoundEdge(source=p0, target=p1, relation=ConjunctionRelation.AND|BUT|BECAUSE)` edges.
|
||||
- `plan_articulation` walks the multi-node graph in topological order; each node yields an `ArticulationStep` with `move=RhetoricalMove.CONTINUE` for non-root nodes.
|
||||
- `realize_semantic` consumes the multi-step target and emits one surface per clause joined by the appropriate conjunction (`and`, `but`, `because`).
|
||||
- The surface resolver (PR #76) is widened to accept a `multi_clause_surface` field that, when non-empty, wins over the single-clause runtime surface. Walk / compose folds append to the multi-clause surface as before.
|
||||
- `compute_trace_hash` is widened with a `compound_clauses_hash: str = ""` field — a deterministic hash of the dropped (Phase C1) or routed (Phase C2) clauses' intent tags + subjects. Empty string preserves pre-ADR-0089 trace hashes byte-identically when the dispatch is off.
|
||||
|
||||
Phase C2 is **opt-in substantive**. Default off preserves every existing invariant.
|
||||
|
||||
### Phase C3 — Telemetry alignment
|
||||
|
||||
- Widen `TurnEvent` and `ChatResponse` with `compound_clause_count: int = 0` and `compound_relation_chain: tuple[str, ...] = ()` so operators can see compound dispatch outcomes without parsing surfaces.
|
||||
- Document the new fields in `docs/runtime_contracts.md` under a new "compound intent" subsection.
|
||||
- Add a `core demo compound-tour` walking four prompts (`AND`, `BUT`, `BECAUSE`, comma-joined) × two modes (flag off, flag on) and asserting:
|
||||
- flag-off: byte-identical surfaces and trace_hashes (null-lift invariant).
|
||||
- flag-on: distinct surfaces, distinct trace_hashes, all clauses represented in the surface, all clauses recorded in the new telemetry fields.
|
||||
|
||||
---
|
||||
|
||||
## Consequences
|
||||
|
||||
- **Pipeline becomes compositionally complete** — input shape (compound) maps to output shape (multi-clause) deterministically.
|
||||
- **The teaching loop will need to learn which clause a correction binds to.** Today a CORRECTION turn binds to the prior turn's single proposal; with multi-clause prior turns, the correction-binding rule must specify which clause is being corrected. Out of scope for ADR-0089 itself; a follow-up ADR handles correction routing under multi-clause priors.
|
||||
- **Speculative-subject cache (PR #77) gains multi-subject inserts.** No code change required — `_remember_speculative_subject` is per-subject and idempotent; multi-clause proposals call it once per clause.
|
||||
- **Register / anchor-lens dispatch is unaffected** — both axes apply to the realizer output as a whole, not per clause. ADR-0072 + ADR-0073d invariants remain pinned.
|
||||
|
||||
---
|
||||
|
||||
## Rejected alternatives
|
||||
|
||||
1. **Land compound intent as a single unified PR.** The blast radius (graph_planner + plan_articulation + realize_semantic + surface resolver + trace_hash + telemetry + tests) is too large for a load-bearing PR per CLAUDE.md. Rejected.
|
||||
2. **Route compound intent through `_maybe_apply_discourse_planner` only.** That path is already wired but defaults off and produces multi-clause output via a different code path (the discourse planner builds clauses from a `GroundingBundle`, not from a multi-node graph). Doubling up the paths is structural drift. Rejected — Phase C2 supersedes the discourse planner's compound dispatch.
|
||||
3. **Auto-enable compound dispatch by default.** Compound prompts are a minority of input shapes; flag-off byte-identity is load-bearing for re-baseline confidence. Rejected.
|
||||
|
||||
---
|
||||
|
||||
## Validation gate
|
||||
|
||||
Phase C1 merge:
|
||||
|
||||
- Compound classifier output appears on `CognitiveTurnResult.dropped_compound_clauses` for every multi-clause input.
|
||||
- All existing tests pass byte-identically.
|
||||
- New invariant test on the four canonical compound shapes.
|
||||
|
||||
Phase C2 merge (separate PR):
|
||||
|
||||
- Flag-off byte-identity invariant (`test_compound_intent_null_lift`).
|
||||
- Flag-on multi-clause surface invariant (`test_compound_clauses_in_surface`).
|
||||
- Flag-on distinct-trace-hash invariant (analogous to register / anchor-lens patterns).
|
||||
- `core eval cognition` byte-identical with the flag off; flag-on is exercised via a new lane `evals/compound_intent_pipeline/`.
|
||||
|
||||
Phase C3 merge (separate PR):
|
||||
|
||||
- `core demo compound-tour` ships; both flag-on and flag-off assertions in CI.
|
||||
- `docs/runtime_contracts.md` updated.
|
||||
Loading…
Reference in a new issue