Merge ADR-0047: wire forward graph constraint into the chat hot path
Closes ADR-0046's deferred follow-up. Opt-in via RuntimeConfig.forward_graph_constraint (default False). A/B characterisation on the 13-case cognition lane shows wiring is correct and safe, six cases produce a non-trivial constraint, and the lane's surface_groundedness / term_capture_rate gap is downstream of propagation — isolating the next ADR's scope to realizer / surface assembly. Lanes green: smoke 67 / cognition 121 / runtime 19 / algebra 132 / teaching 17 / packs 6. core eval cognition unchanged with the flag off (default), unchanged with the flag on (constraint engages but does not alter surface for this lane).
This commit is contained in:
commit
5194a14788
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 generate.articulation import ArticulationPlan, realize
|
||||
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.result import GenerationResult
|
||||
from generate.stream import generate
|
||||
|
|
@ -690,6 +691,20 @@ class ChatRuntime:
|
|||
articulation = _prefer_prompt_anchor(articulation, filtered, output_language=self.config.output_language)
|
||||
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(
|
||||
field_state,
|
||||
self._context.vocab,
|
||||
|
|
@ -703,6 +718,7 @@ class ChatRuntime:
|
|||
use_salience=self.config.use_salience,
|
||||
salience_top_k=self.config.salience_top_k,
|
||||
inhibition_threshold=self.config.inhibition_threshold,
|
||||
region=forward_region,
|
||||
inner_loop_admissibility=self.config.inner_loop_admissibility,
|
||||
admissibility_threshold=self.config.admissibility_threshold,
|
||||
admissibility_mode=self.config.admissibility_mode,
|
||||
|
|
|
|||
|
|
@ -36,6 +36,17 @@ class RuntimeConfig:
|
|||
# ADR-0033 — Ethics pack id loaded at runtime startup. Empty string
|
||||
# resolves to ``DEFAULT_ETHICS_PACK``. See docs/ethics_packs.md.
|
||||
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"
|
||||
|
|
|
|||
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-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-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
|
||||
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 |
|
||||
|---|---|---|
|
||||
| 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` |
|
||||
| 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
|
||||
|
|
|
|||
|
|
@ -39,6 +39,22 @@ def classify_intent_from_input(text: str) -> DialogueIntent:
|
|||
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:
|
||||
"""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