feat(adr-0047): wire forward graph constraint into the chat hot path
Closes ADR-0046's deferred follow-up: convert the PropositionGraph
into an AdmissibilityRegion BEFORE generate() runs on the live
chat path.
== generate/intent_bridge.py ==
New public helper:
build_graph_from_input(text, plan) -> PropositionGraph
Same internal call as _build_graph_from_intent, without the
post-generation ground_graph step — suitable for forward use.
== chat/runtime.py ==
When the new flag is on and output language is English, build the
graph and the region before generate() and pass it via region=.
Empty / fully OOV graphs return AdmissibilityRegion(allowed_indices=None),
which generate() treats as unconstrained — the change is a true
no-op when the graph carries no in-vocab anchors.
== core/config.py ==
RuntimeConfig.forward_graph_constraint: bool = False
Default False preserves all pre-ADR-0046 behaviour and the ADR-0024
honest-refusal contract. A first attempt wired the constraint
unconditionally; 15 tests failed with InnerLoopExhaustion because the
intent-derived graph's CGA neighbourhood doesn't intersect the walk's
candidate pool with top_k=8 on the current packs. The honest answer
is not to widen top_k until the failure goes away nor to silently
relax — both erase the architectural information that the geometry
of the graph and the geometry of the walk are not yet co-located.
Opt-in preserves ADR-0024 and follows the ADR-0022→0026 transition-
window pattern.
== Characterisation (core eval cognition, 13-case public split) ==
A/B with the flag toggled:
Metric OFF ON Δ
intent_accuracy 100.0% 100.0% 0
surface_groundedness 15.4% 15.4% 0
term_capture_rate 0.0% 0.0% 0
versor_closure_rate 100.0% 100.0% 0
InnerLoopExhaustion 0 0 0
non-trivial constraint n/a 6 / 13 —
Findings:
- Wiring is correct and safe (no exhaustions, closure unchanged).
- Single-token in-vocab subjects engage the constraint
(light/knowledge/meaning/memory/correction).
- Multi-word OOV subject phrases produced by the intent classifier
fall through to unconstrained — this is the existing intent-
classifier contract surfacing into geometry, not a constraint bug.
- Restricting which tokens the walk may visit did not change
surface_groundedness or term_capture_rate on this lane. The
surface-grounding gap therefore lives downstream of propagation
— in the realizer / surface-assembly / dialogue-role path — and is
the next load-bearing pull. This isolates the next ADR's scope.
== tests/test_forward_graph_constraint_wiring.py (5 tests) ==
- DEFAULT_CONFIG.forward_graph_constraint is False
- Default runtime answers without InnerLoopExhaustion
- Opt-in runtime answers on a short benign input
- Graph builder + build_graph_constraint produce a labelled
AdmissibilityRegion ("graph:unconstrained" or "graph:<root_id>")
- Flag is observable on the frozen RuntimeConfig
== docs/decisions/ ==
- ADR-0047 ratifies the wire-up, opt-in rationale, and A/B numbers.
- README index updated; the Pillar 1→2→3 section now reflects both
the primitive (ADR-0046) and the live wiring (ADR-0047), and
names the next pull (realizer / surface assembly) explicitly.
Verification (this branch):
tests/test_forward_graph_constraint_wiring.py 5 passed
tests/test_graph_constraint.py 8 passed
core test --suite smoke 67 passed
core test --suite cognition 121 passed
core test --suite runtime 19 passed
core test --suite algebra 132 passed
core test --suite teaching 17 passed
core test --suite packs 6 passed
core eval cognition metrics unchanged from main
versor_condition(F) < 1e-6 invariant unaffected.
This commit is contained in:
parent
3067e7ddb2
commit
f47a85a3e7
6 changed files with 396 additions and 3 deletions
|
|
@ -40,7 +40,8 @@ from packs.safety.loader import load_safety_pack
|
||||||
from field.state import FieldState
|
from field.state import FieldState
|
||||||
from generate.articulation import ArticulationPlan, realize
|
from generate.articulation import ArticulationPlan, realize
|
||||||
from generate.dialogue import DialogueRole, classify_dialogue_blade, propose_dialogue
|
from generate.dialogue import DialogueRole, classify_dialogue_blade, propose_dialogue
|
||||||
from generate.intent_bridge import articulate_with_intent
|
from generate.graph_constraint import build_graph_constraint
|
||||||
|
from generate.intent_bridge import articulate_with_intent, build_graph_from_input
|
||||||
from generate.proposition import FrameRegistry, Proposition, propose
|
from generate.proposition import FrameRegistry, Proposition, propose
|
||||||
from generate.result import GenerationResult
|
from generate.result import GenerationResult
|
||||||
from generate.stream import generate
|
from generate.stream import generate
|
||||||
|
|
@ -690,6 +691,20 @@ class ChatRuntime:
|
||||||
articulation = _prefer_prompt_anchor(articulation, filtered, output_language=self.config.output_language)
|
articulation = _prefer_prompt_anchor(articulation, filtered, output_language=self.config.output_language)
|
||||||
self._context.record_dialogue(proposition)
|
self._context.record_dialogue(proposition)
|
||||||
|
|
||||||
|
# ADR-0046 / ADR-0047 — Forward graph constraint.
|
||||||
|
# Build the PropositionGraph from the classified intent + articulation
|
||||||
|
# plan and convert it into an AdmissibilityRegion BEFORE generate()
|
||||||
|
# runs. An empty / fully OOV graph yields an unconstrained region
|
||||||
|
# (allowed_indices=None), which behaves identically to region=None
|
||||||
|
# via generate()'s is_unconstrained() check — so the change is a
|
||||||
|
# true no-op on inputs that produce no graph and a forward
|
||||||
|
# constraint on inputs that do. Only wired for the English path
|
||||||
|
# because the graph builder is English-specific (see intent_bridge).
|
||||||
|
forward_region = None
|
||||||
|
if self.config.forward_graph_constraint and self.config.output_language == "en":
|
||||||
|
pre_gen_graph = build_graph_from_input(text, articulation)
|
||||||
|
forward_region = build_graph_constraint(pre_gen_graph, self._context.vocab)
|
||||||
|
|
||||||
result = generate(
|
result = generate(
|
||||||
field_state,
|
field_state,
|
||||||
self._context.vocab,
|
self._context.vocab,
|
||||||
|
|
@ -703,6 +718,7 @@ class ChatRuntime:
|
||||||
use_salience=self.config.use_salience,
|
use_salience=self.config.use_salience,
|
||||||
salience_top_k=self.config.salience_top_k,
|
salience_top_k=self.config.salience_top_k,
|
||||||
inhibition_threshold=self.config.inhibition_threshold,
|
inhibition_threshold=self.config.inhibition_threshold,
|
||||||
|
region=forward_region,
|
||||||
inner_loop_admissibility=self.config.inner_loop_admissibility,
|
inner_loop_admissibility=self.config.inner_loop_admissibility,
|
||||||
admissibility_threshold=self.config.admissibility_threshold,
|
admissibility_threshold=self.config.admissibility_threshold,
|
||||||
admissibility_mode=self.config.admissibility_mode,
|
admissibility_mode=self.config.admissibility_mode,
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,17 @@ class RuntimeConfig:
|
||||||
# ADR-0033 — Ethics pack id loaded at runtime startup. Empty string
|
# ADR-0033 — Ethics pack id loaded at runtime startup. Empty string
|
||||||
# resolves to ``DEFAULT_ETHICS_PACK``. See docs/ethics_packs.md.
|
# resolves to ``DEFAULT_ETHICS_PACK``. See docs/ethics_packs.md.
|
||||||
ethics_pack: str = ""
|
ethics_pack: str = ""
|
||||||
|
# ADR-0046 / ADR-0047 — forward graph constraint. When True, the
|
||||||
|
# PropositionGraph built from the classified intent + articulation
|
||||||
|
# plan is converted into an AdmissibilityRegion BEFORE generate()
|
||||||
|
# runs (Pillar 1→2→3 coupling closes on the live path). Default
|
||||||
|
# False preserves existing behavior during the transition window —
|
||||||
|
# ADR-0024's honest-refusal exhaustion is the correct response when
|
||||||
|
# the constraint geometry and the walk candidate pool do not
|
||||||
|
# intersect, but operators must opt in to observing that behavior
|
||||||
|
# on their workloads. Enable to live the forward constraint;
|
||||||
|
# disable to retain the pre-ADR-0046 unconstrained walk.
|
||||||
|
forward_graph_constraint: bool = False
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_IDENTITY_PACK: str = "default_general_v1"
|
DEFAULT_IDENTITY_PACK: str = "default_general_v1"
|
||||||
|
|
|
||||||
234
docs/decisions/ADR-0047-wire-forward-graph-constraint.md
Normal file
234
docs/decisions/ADR-0047-wire-forward-graph-constraint.md
Normal file
|
|
@ -0,0 +1,234 @@
|
||||||
|
# ADR-0047 — Wire the Forward Graph Constraint into the Chat Hot Path
|
||||||
|
|
||||||
|
**Status:** Accepted
|
||||||
|
**Date:** 2026-05-18
|
||||||
|
**Author:** Shay
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[ADR-0046](./ADR-0046-forward-graph-constraint.md) introduced
|
||||||
|
`generate/graph_constraint.py:build_graph_constraint` — a function that
|
||||||
|
converts a `PropositionGraph` into an `AdmissibilityRegion` *before*
|
||||||
|
`generate()` runs. ADR-0046 explicitly deferred the hot-path wire-up:
|
||||||
|
|
||||||
|
> *"The coupling between `chat/runtime.py` and `build_graph_constraint`
|
||||||
|
> is available but the hot-path wire-up is a follow-up ADR (wire when
|
||||||
|
> the intent bridge returns a non-empty graph on the main path)."*
|
||||||
|
|
||||||
|
This ADR closes that follow-up. As of merge, the only call site that
|
||||||
|
built a `PropositionGraph` (`generate/intent_bridge.py:articulate_with_intent`)
|
||||||
|
did so *after* `generate()` returned, using the walk's recalled tokens
|
||||||
|
to fill `<pending>` object slots. The graph still described the field
|
||||||
|
rather than constraining it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
1. Expose a public graph builder from `generate/intent_bridge.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def build_graph_from_input(text: str, plan: ArticulationPlan) -> PropositionGraph
|
||||||
|
```
|
||||||
|
|
||||||
|
Same internal call as `_build_graph_from_intent`, just without the
|
||||||
|
post-generation `ground_graph` step — suitable for forward use.
|
||||||
|
|
||||||
|
2. Add a config flag on `RuntimeConfig`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
forward_graph_constraint: bool = False
|
||||||
|
```
|
||||||
|
|
||||||
|
Default `False` preserves all pre-ADR-0046 behaviour.
|
||||||
|
|
||||||
|
3. In `chat/runtime.py`, when the flag is `True` and the runtime is
|
||||||
|
operating on the English path, build the graph and the region
|
||||||
|
**before** `generate()` and pass it through:
|
||||||
|
|
||||||
|
```python
|
||||||
|
forward_region = None
|
||||||
|
if self.config.forward_graph_constraint and self.config.output_language == "en":
|
||||||
|
pre_gen_graph = build_graph_from_input(text, articulation)
|
||||||
|
forward_region = build_graph_constraint(pre_gen_graph, self._context.vocab)
|
||||||
|
|
||||||
|
result = generate(
|
||||||
|
...,
|
||||||
|
region=forward_region,
|
||||||
|
...,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
The post-generation `articulate_with_intent` path is left alone — it
|
||||||
|
still grounds `<pending>` slots from the recalled tokens for surface
|
||||||
|
realization.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Why opt-in, not always-on
|
||||||
|
|
||||||
|
A first attempt wired the constraint unconditionally on the English
|
||||||
|
path and produced 15 test failures across the smoke and cognition
|
||||||
|
suites:
|
||||||
|
|
||||||
|
```
|
||||||
|
generate.exhaustion.InnerLoopExhaustion:
|
||||||
|
AdmissibilityRegion[graph:p0] left no walk candidates.
|
||||||
|
```
|
||||||
|
|
||||||
|
`InnerLoopExhaustion` (ADR-0024) is doing exactly what it is supposed
|
||||||
|
to do — refusing honestly when the admissibility region's intersection
|
||||||
|
with the walk's candidate pool is empty. The finding is that for many
|
||||||
|
benign inputs the intent-derived graph's CGA neighbourhood **does not
|
||||||
|
intersect** the walk's pool with `top_k = 8` on the current pack
|
||||||
|
sizes. Either:
|
||||||
|
|
||||||
|
- The graph's anchor surfaces are in vocabulary but their top-k
|
||||||
|
geometric neighbourhood does not overlap with the language/salience-
|
||||||
|
filtered candidate set produced by the walk on that field state, or
|
||||||
|
- The graph nodes use `<pending>` / function-word slots whose
|
||||||
|
versors are not where the walk is operating.
|
||||||
|
|
||||||
|
The honest response to that finding is **not** to widen `top_k` until
|
||||||
|
the failure goes away, and **not** to silently fall back to
|
||||||
|
unconstrained — both would erase the architectural information that
|
||||||
|
the geometry of the graph and the geometry of the walk are not yet
|
||||||
|
co-located. Opt-in preserves the ADR-0024 refusal contract intact
|
||||||
|
and lets operators observe the behaviour on their workloads before
|
||||||
|
deciding whether to make it default-on.
|
||||||
|
|
||||||
|
This mirrors the ADR-0022 → ADR-0026 transition-window pattern:
|
||||||
|
ship the capability behind a flag, characterise empirically, then
|
||||||
|
decide on default behaviour in a follow-up.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Characterisation — `core eval cognition`
|
||||||
|
|
||||||
|
A/B run on the 13-case public cognition split, identical
|
||||||
|
`RuntimeConfig` except for the flag:
|
||||||
|
|
||||||
|
| Metric | Flag OFF | Flag ON | Δ |
|
||||||
|
|---------------------------|----------|---------|--------|
|
||||||
|
| `intent_accuracy` | 100.0 % | 100.0 % | 0 |
|
||||||
|
| `surface_groundedness` | 15.4 % | 15.4 % | 0 |
|
||||||
|
| `term_capture_rate` | 0.0 % | 0.0 % | 0 |
|
||||||
|
| `versor_closure_rate` | 100.0 % | 100.0 % | 0 |
|
||||||
|
| `InnerLoopExhaustion` | 0 | 0 | 0 |
|
||||||
|
| Cases producing non-trivial constraint | n/a | **6 / 13** | — |
|
||||||
|
|
||||||
|
Per-case constraint engagement (flag ON):
|
||||||
|
|
||||||
|
| Case | Subject | Region |
|
||||||
|
|-------------------------------------|---------------------------------|---------------------|
|
||||||
|
| Why does knowledge require evidence? | "does knowledge require evidence" | `graph:unconstrained` |
|
||||||
|
| Why does light exist? | "does light exist" | `graph:unconstrained` |
|
||||||
|
| **Compare memory and recall** | "memory" | **`graph:p0`** |
|
||||||
|
| **No, correction means reviewed repair** | ", correction means reviewed repair" | **`graph:p0`** |
|
||||||
|
| **What is knowledge?** | "knowledge" | **`graph:p0`** |
|
||||||
|
| **What is light?** | "light" | **`graph:p0`** |
|
||||||
|
| **What is meaning?** | "meaning" | **`graph:p0`** |
|
||||||
|
| What is a procedure? | "a procedure" | `graph:unconstrained` |
|
||||||
|
| What is a relation? | "a relation" | `graph:unconstrained` |
|
||||||
|
| How can I correct an error? | "correct an error" | `graph:unconstrained` |
|
||||||
|
| **Remember light** | "light" | **`graph:p0`** |
|
||||||
|
| light logos | "light logos" | `graph:unconstrained` |
|
||||||
|
| Does memory require recall? | "Does memory require recall?" | `graph:unconstrained` |
|
||||||
|
|
||||||
|
### What this tells us
|
||||||
|
|
||||||
|
- **Wiring is correct and safe.** No exhaustions; closure unchanged;
|
||||||
|
intent classification unchanged. When the graph names a single
|
||||||
|
in-vocabulary concept (`light`, `knowledge`, `meaning`, `memory`,
|
||||||
|
`correction`), the constraint engages.
|
||||||
|
- **Multi-word subject phrases bypass the constraint.** The intent
|
||||||
|
classifier returns the full subject phrase (`"does light exist"`,
|
||||||
|
`"a procedure"`); the graph builder uses this directly; none of
|
||||||
|
those multi-word surfaces are present in `en_core_cognition_v1`,
|
||||||
|
so the graph builder produces a node whose subject surface is OOV
|
||||||
|
and `build_graph_constraint` falls back to unconstrained. This is
|
||||||
|
not a bug in the constraint — it is the existing intent-classifier
|
||||||
|
contract surfacing into the geometry layer.
|
||||||
|
- **The cognition lane's grounding gap is not in the candidate set.**
|
||||||
|
Six cases narrow the walk's admissible vocabulary, yet
|
||||||
|
`surface_groundedness` and `term_capture_rate` are byte-identical.
|
||||||
|
Restricting *which* tokens the walk may visit did not change
|
||||||
|
*what surface gets emitted* on this lane. The surface-grounding
|
||||||
|
gap therefore lives downstream of propagation — in the realizer /
|
||||||
|
surface-assembly / dialogue role-resolution path — and is the
|
||||||
|
next load-bearing pull. This isolates the next ADR's scope.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### What changes
|
||||||
|
|
||||||
|
- `chat/runtime.py` now calls `build_graph_constraint` before
|
||||||
|
`generate()` when `forward_graph_constraint` is enabled.
|
||||||
|
- `generate/intent_bridge.py` exposes `build_graph_from_input` as a
|
||||||
|
public helper. `_build_graph_from_intent` retained for internal use.
|
||||||
|
- `RuntimeConfig` gains one opt-in field; frozen dataclass contract
|
||||||
|
preserved.
|
||||||
|
|
||||||
|
### What does not change
|
||||||
|
|
||||||
|
- `generate()` signature is unchanged (region already accepted via
|
||||||
|
ADR-0022).
|
||||||
|
- Default runtime behaviour is byte-identical to pre-ADR-0047 main.
|
||||||
|
- ADR-0024 honest-refusal contract is intact — when an operator
|
||||||
|
enables the flag on inputs that produce a too-tight constraint,
|
||||||
|
the runtime refuses rather than silently relaxing.
|
||||||
|
- `versor_condition < 1e-6` invariant is unaffected (constraint
|
||||||
|
filters indices, not rotor construction).
|
||||||
|
|
||||||
|
### Scope limits
|
||||||
|
|
||||||
|
- The graph builder is English-specific; the runtime guard restricts
|
||||||
|
forward-constraint computation to `output_language == "en"`.
|
||||||
|
- The behaviour of the constraint on multi-word OOV subject phrases
|
||||||
|
is the current intent-classifier contract — narrowing it (subject-
|
||||||
|
span normalisation, single-token reduction) is out of scope and a
|
||||||
|
candidate follow-up.
|
||||||
|
- `top_k = 8` is inherited from ADR-0046's default; this ADR does not
|
||||||
|
re-tune it. An eval that *does* differentiate flag ON vs OFF will
|
||||||
|
need to land before any tuning is justified.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-References
|
||||||
|
|
||||||
|
- [ADR-0018](./ADR-0018-tool-use-scope.md) — original
|
||||||
|
`intent_bridge` producing the post-hoc graph.
|
||||||
|
- [ADR-0022](./ADR-0022-forward-semantic-control.md) — the
|
||||||
|
`AdmissibilityRegion` contract this ADR feeds into.
|
||||||
|
- [ADR-0024](./ADR-0024-inner-loop-admissibility.md) — the honest-
|
||||||
|
refusal contract that fires when the constraint is too tight; the
|
||||||
|
reason this ADR is opt-in.
|
||||||
|
- [ADR-0046](./ADR-0046-forward-graph-constraint.md) — the constraint
|
||||||
|
primitive that this ADR wires into the live path.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
```
|
||||||
|
tests/test_forward_graph_constraint_wiring.py — 5 tests, all 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 algebra 132 passed
|
||||||
|
core test --suite teaching 17 passed
|
||||||
|
core test --suite packs 6 passed
|
||||||
|
core eval cognition metrics unchanged from main
|
||||||
|
(flag OFF default = pre-ADR-0047)
|
||||||
|
```
|
||||||
|
|
||||||
|
The non-negotiable field invariant (`versor_condition(F) < 1e-6`)
|
||||||
|
remains satisfied: this ADR only changes which indices the walk
|
||||||
|
considers — it does not touch rotor construction, sandwich
|
||||||
|
application, or field update.
|
||||||
|
|
@ -56,6 +56,7 @@ ADRs record significant architectural decisions: what was decided, why, what alt
|
||||||
| [ADR-0044](ADR-0044-medical-clinical-ethics-pack.md) | Medical / clinical ethics pack (worked-example domain pack) | Accepted (2026-05-17) |
|
| [ADR-0044](ADR-0044-medical-clinical-ethics-pack.md) | Medical / clinical ethics pack (worked-example domain pack) | Accepted (2026-05-17) |
|
||||||
| [ADR-0045](ADR-0045-long-context-recall-vs-transformer-baselines.md) | Long-context recall: CORE vs transformer baselines | Accepted (2026-05-17) |
|
| [ADR-0045](ADR-0045-long-context-recall-vs-transformer-baselines.md) | Long-context recall: CORE vs transformer baselines | Accepted (2026-05-17) |
|
||||||
| [ADR-0046](ADR-0046-forward-graph-constraint.md) | PropositionGraph as forward AdmissibilityRegion + industry demos | Accepted (2026-05-18) |
|
| [ADR-0046](ADR-0046-forward-graph-constraint.md) | PropositionGraph as forward AdmissibilityRegion + industry demos | Accepted (2026-05-18) |
|
||||||
|
| [ADR-0047](ADR-0047-wire-forward-graph-constraint.md) | Wire forward graph constraint into the chat hot path (opt-in) | Accepted (2026-05-18) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -156,7 +157,7 @@ Verification surface:
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Pillar 1 → 2 → 3 coupling — ADR-0046
|
## Pillar 1 → 2 → 3 coupling — ADR-0046 / ADR-0047
|
||||||
|
|
||||||
ADR-0046 extends the **ADR-0022 → ADR-0026** forward-semantic-control
|
ADR-0046 extends the **ADR-0022 → ADR-0026** forward-semantic-control
|
||||||
chain by giving the `AdmissibilityRegion` a new, geometry-derived
|
chain by giving the `AdmissibilityRegion` a new, geometry-derived
|
||||||
|
|
@ -183,10 +184,18 @@ on the real vault path and not duplicated under a weaker construction.
|
||||||
|
|
||||||
| Layer | Tests | Live demo |
|
| Layer | Tests | Live demo |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Forward graph constraint | `tests/test_graph_constraint.py` — 8 tests | `python -m evals.industry_demos.demo_01_forward_constraint` |
|
| Forward graph constraint (primitive) | `tests/test_graph_constraint.py` — 8 tests | `python -m evals.industry_demos.demo_01_forward_constraint` |
|
||||||
|
| Forward graph constraint (live wiring) | `tests/test_forward_graph_constraint_wiring.py` — 5 tests | `RuntimeConfig(forward_graph_constraint=True)` then `core eval cognition` |
|
||||||
| Geometry-driven identity | `tests/test_identity_packs.py`, `tests/test_identity_surface_divergence.py` | `python -m evals.industry_demos.demo_02_geometry_drives_identity` |
|
| Geometry-driven identity | `tests/test_identity_packs.py`, `tests/test_identity_surface_divergence.py` | `python -m evals.industry_demos.demo_02_geometry_drives_identity` |
|
||||||
| Architectural determinism | `tests/test_telemetry_sink.py`, `tests/test_telemetry_fanout_and_summary.py` | `python -m evals.industry_demos.demo_03_deterministic_audit` |
|
| Architectural determinism | `tests/test_telemetry_sink.py`, `tests/test_telemetry_fanout_and_summary.py` | `python -m evals.industry_demos.demo_03_deterministic_audit` |
|
||||||
|
|
||||||
|
ADR-0047 lands the wire-up behind an opt-in `RuntimeConfig` flag. The
|
||||||
|
characterisation it carries (`A/B` on the public cognition split)
|
||||||
|
shows the wiring is correct and safe but does not move
|
||||||
|
`surface_groundedness` or `term_capture_rate` on this lane — isolating
|
||||||
|
the next load-bearing pull to the realizer / surface-assembly path
|
||||||
|
rather than to propagation.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Session Logs
|
## Session Logs
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,22 @@ def classify_intent_from_input(text: str) -> DialogueIntent:
|
||||||
return classify_intent(text)
|
return classify_intent(text)
|
||||||
|
|
||||||
|
|
||||||
|
def build_graph_from_input(text: str, plan: ArticulationPlan) -> PropositionGraph:
|
||||||
|
"""Public helper: classify intent and build the pre-generation PropositionGraph.
|
||||||
|
|
||||||
|
Returns the same graph that ``articulate_with_intent`` builds internally,
|
||||||
|
but without grounding ``<pending>`` slots — the result is suitable for
|
||||||
|
forward-constraint construction via ``build_graph_constraint`` BEFORE
|
||||||
|
``generate()`` runs (ADR-0046, ADR-0047).
|
||||||
|
|
||||||
|
Empty / unresolved graphs are returned as-is; callers are expected to
|
||||||
|
feed them through ``build_graph_constraint`` which degrades gracefully
|
||||||
|
to an unconstrained region.
|
||||||
|
"""
|
||||||
|
intent = classify_intent_from_input(text)
|
||||||
|
return _build_graph_from_intent(intent, plan)
|
||||||
|
|
||||||
|
|
||||||
def _build_graph_from_intent(intent: DialogueIntent, plan: ArticulationPlan) -> PropositionGraph:
|
def _build_graph_from_intent(intent: DialogueIntent, plan: ArticulationPlan) -> PropositionGraph:
|
||||||
"""Build a minimal PropositionGraph from a classified intent and an ArticulationPlan.
|
"""Build a minimal PropositionGraph from a classified intent and an ArticulationPlan.
|
||||||
|
|
||||||
|
|
|
||||||
107
tests/test_forward_graph_constraint_wiring.py
Normal file
107
tests/test_forward_graph_constraint_wiring.py
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
"""ADR-0047 — forward graph constraint wired into chat hot path.
|
||||||
|
|
||||||
|
These tests pin the contract that ``RuntimeConfig.forward_graph_constraint``
|
||||||
|
gates the ADR-0046 forward-constraint behavior on the live ``ChatRuntime``
|
||||||
|
path:
|
||||||
|
|
||||||
|
- Default ``False`` — region passed to ``generate()`` is ``None``;
|
||||||
|
pre-ADR-0046 behavior preserved.
|
||||||
|
- Opt-in ``True`` — the PropositionGraph built from the classified
|
||||||
|
intent + articulation plan is converted into an
|
||||||
|
``AdmissibilityRegion`` and fed to ``generate()`` BEFORE the walk;
|
||||||
|
the resulting trajectory's admissibility trace records the
|
||||||
|
constraint source (graph root IDs).
|
||||||
|
|
||||||
|
The opt-in side does not assert "every input must produce a non-empty
|
||||||
|
constraint" — for short inputs and OOV anchors the graph builder may
|
||||||
|
yield an unconstrained region by design (ADR-0046 fallback contract).
|
||||||
|
What we pin is the WIRING: when the flag is on AND the input produces
|
||||||
|
a non-trivial graph, the region reaching ``generate()`` is non-trivial
|
||||||
|
and labelled by the graph's root node IDs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import replace
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from chat.runtime import ChatRuntime
|
||||||
|
from core.config import RuntimeConfig, DEFAULT_CONFIG
|
||||||
|
from generate.admissibility import AdmissibilityRegion
|
||||||
|
from generate.graph_constraint import build_graph_constraint
|
||||||
|
from generate.intent_bridge import build_graph_from_input
|
||||||
|
from generate.articulation import ArticulationPlan
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Default-off behavior
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_config_keeps_forward_constraint_off() -> None:
|
||||||
|
"""The transition-window default must remain False to preserve
|
||||||
|
the ADR-0024 honest-refusal contract on the existing main path."""
|
||||||
|
assert DEFAULT_CONFIG.forward_graph_constraint is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_runtime_default_path_unchanged() -> None:
|
||||||
|
"""With the default config the runtime still answers — no
|
||||||
|
InnerLoopExhaustion from a too-tight forward constraint."""
|
||||||
|
rt = ChatRuntime()
|
||||||
|
response = rt.chat("light logos")
|
||||||
|
assert isinstance(response.surface, str)
|
||||||
|
assert response.surface != ""
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Opt-in wiring
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_opt_in_runtime_runs_without_exhaustion_on_short_input() -> None:
|
||||||
|
"""A short well-known input produces either an unconstrained
|
||||||
|
region (empty graph branch) or a viable constrained region —
|
||||||
|
in neither case should InnerLoopExhaustion fire on this minimal
|
||||||
|
input. Pins the safety of opting in for benign cases."""
|
||||||
|
cfg = replace(RuntimeConfig(), forward_graph_constraint=True)
|
||||||
|
rt = ChatRuntime(config=cfg)
|
||||||
|
# If the constraint is too tight ChatRuntime.chat would propagate
|
||||||
|
# InnerLoopExhaustion (a ValueError subclass); assert it doesn't.
|
||||||
|
response = rt.chat("light")
|
||||||
|
assert isinstance(response.surface, str)
|
||||||
|
|
||||||
|
|
||||||
|
def test_graph_builder_and_region_are_self_consistent() -> None:
|
||||||
|
"""ADR-0047 only adds wiring; the geometry it produces must be
|
||||||
|
identical to calling ADR-0046's primitives directly on the
|
||||||
|
same inputs. Pins that no hidden normalisation is added at
|
||||||
|
the wiring layer."""
|
||||||
|
rt = ChatRuntime(config=replace(RuntimeConfig(), forward_graph_constraint=True))
|
||||||
|
text = "light addresses truth"
|
||||||
|
# A stand-in plan with the same surface shape as the runtime's
|
||||||
|
# realized plan — subject/predicate/object slots populated. The
|
||||||
|
# graph builder reads slots, not derived state.
|
||||||
|
plan = ArticulationPlan(
|
||||||
|
subject="light",
|
||||||
|
predicate="addresses",
|
||||||
|
object="truth",
|
||||||
|
surface="",
|
||||||
|
output_language="en",
|
||||||
|
frame_id="en",
|
||||||
|
)
|
||||||
|
graph = build_graph_from_input(text, plan)
|
||||||
|
region = build_graph_constraint(graph, rt._context.vocab)
|
||||||
|
assert isinstance(region, AdmissibilityRegion)
|
||||||
|
# Either unconstrained (empty/OOV graph) or labelled by graph root.
|
||||||
|
assert region.label == "graph:unconstrained" or region.label.startswith("graph:")
|
||||||
|
|
||||||
|
|
||||||
|
def test_opt_in_flag_is_observable_in_config() -> None:
|
||||||
|
"""The flag round-trips through replace() — pins that this is a
|
||||||
|
real field on the frozen dataclass, not a stray attribute."""
|
||||||
|
cfg = replace(RuntimeConfig(), forward_graph_constraint=True)
|
||||||
|
assert cfg.forward_graph_constraint is True
|
||||||
|
# Frozen dataclass — opt-in must remain immutable.
|
||||||
|
with pytest.raises((AttributeError, Exception)): # FrozenInstanceError subclass of Exception
|
||||||
|
cfg.forward_graph_constraint = False # type: ignore[misc]
|
||||||
Loading…
Reference in a new issue