Skip to content

Commit 18f7094

Browse files
author
TNFR AI Agent
committed
grammar: derive error-factory invariants + operator-metadata roles from canon
Intent: continue centralising the grammar by removing two remaining parallel tables that could drift from grammar_canon (the single source of truth). grammar_canon.py — two new derivations: - related_invariants(rule_id): the rule->canonical-invariant annotation (primary physics invariant + Grammar Compliance #4), reconciled to the 6-invariant canon. - u_rules_for_operator(op) + ROLE_TO_URULE: the per-operator active U1-U5 rule ids, derived from the operator's canonical role set (accepts function name or glyph mnemonic). grammar_error_factory.py: _RULE_INVARIANTS is now DERIVED from grammar_canon.GRAMMAR_RULES via related_invariants (was a hardcoded dict that referenced invariants 7 and 9 — stale numbering from the pre-optimization 10-invariant model that no longer exists). Behaviour is now canonical: e.g. U4b -> (4,) not (3,4,7). No test asserted the old values. introspection.py: OPERATOR_METADATA.grammar_roles fixed to match the canonical derivation, correcting three stale divergences (same class of bug the prior audit fixed in the classification sets): OZ gains U1b (it IS a closure ∈ CLOSURES), ZHIR gains U2 (it IS a destabilizer — the recurrent ZHIR-omission bug), REMESH gains U5 (it IS the recursive generator). Now sourced-by-contract from grammar_canon.u_rules_for_operator. Tests: test_grammar_canon.py +8 (TestRelatedInvariants, TestOperatorMetadata RolesAreCanonical) pinning both derivations against the canon. Full suite 2021 passed, 2 skipped, 0 failed. Operators: none modified. Affected invariants: #4 Grammar Compliance (annotation accuracy; behaviour preserved).
1 parent 94f4799 commit 18f7094

4 files changed

Lines changed: 158 additions & 23 deletions

File tree

src/tnfr/operators/grammar_canon.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@
112112
"GrammarRule",
113113
"GRAMMAR_RULES",
114114
"rule",
115+
"GRAMMAR_COMPLIANCE_INVARIANT",
116+
"related_invariants",
117+
"ROLE_TO_URULE",
118+
"u_rules_for_operator",
115119
"StructuralType",
116120
"StructuralTypeSpec",
117121
"STRUCTURAL_TYPOLOGY",
@@ -330,6 +334,63 @@ def rule(rule_id: str) -> GrammarRule:
330334
raise KeyError(f"Unknown grammar rule id: {rule_id!r}")
331335

332336

337+
#: Canonical invariant index for "Grammar Compliance" (AGENTS.md §Canonical
338+
#: Invariants, the 6-invariant model). Every grammar-rule violation relates to
339+
#: this invariant by definition, in addition to the rule's primary physics
340+
#: invariant.
341+
GRAMMAR_COMPLIANCE_INVARIANT = 4
342+
343+
344+
def related_invariants(rule_id: str) -> tuple[int, ...]:
345+
"""Canonical invariants a violation of ``rule_id`` relates to.
346+
347+
Returns the rule's primary physics invariant plus Grammar Compliance (#4),
348+
sorted and de-duplicated. This is the single source of the rule→invariant
349+
annotation, reconciled to the 6-invariant canon (AGENTS.md §Canonical
350+
Invariants); it replaces the stale pre-optimization 10-invariant numbering.
351+
"""
352+
try:
353+
primary = rule(rule_id).invariant
354+
except KeyError:
355+
return (GRAMMAR_COMPLIANCE_INVARIANT,)
356+
return tuple(sorted({primary, GRAMMAR_COMPLIANCE_INVARIANT}))
357+
358+
359+
#: Map each grammatical role to the active U1-U5 rule id it participates in.
360+
#: (U6 confinement is telemetry-only and is not an active operator role.)
361+
ROLE_TO_URULE: dict[GrammarRole, str] = {
362+
GrammarRole.GENERATOR: "U1a",
363+
GrammarRole.CLOSURE: "U1b",
364+
GrammarRole.STABILIZER: "U2",
365+
GrammarRole.DESTABILIZER: "U2",
366+
GrammarRole.COUPLING: "U3",
367+
GrammarRole.TRIGGER: "U4a",
368+
GrammarRole.HANDLER: "U4a",
369+
GrammarRole.TRANSFORMER: "U4b",
370+
GrammarRole.RECURSIVE: "U5",
371+
}
372+
373+
#: Resolve a glyph mnemonic (e.g. "ZHIR") back to its function name.
374+
_OPERATOR_BY_GLYPH: dict[str, str] = {
375+
g.glyph: op for op, g in OPERATOR_ROLES.items()
376+
}
377+
378+
379+
def u_rules_for_operator(op: str) -> tuple[str, ...]:
380+
"""The active U1-U5 rule ids an operator participates in (sorted, unique).
381+
382+
Derived from the operator's canonical role set. ``op`` may be a function
383+
name (e.g. ``"mutation"``) or a glyph mnemonic (e.g. ``"ZHIR"``). U6
384+
(confinement) is telemetry-only and is not an active operator role, so it
385+
never appears here. Single source of the per-operator grammar-role table.
386+
"""
387+
name = _OPERATOR_BY_GLYPH.get(op, op)
388+
grammar = OPERATOR_ROLES.get(name)
389+
if grammar is None:
390+
return ()
391+
return tuple(sorted({ROLE_TO_URULE[r] for r in grammar.roles}))
392+
393+
333394
#: The TNFR.pdf §2.3.3 "Esquema formal de sintaxis" positions (theory anchor).
334395
#: Quoted Spanish terms are verbatim citations of the source schema headers.
335396
FORMAL_SYNTAX_SCHEMA: dict[str, tuple[str, ...]] = {

src/tnfr/operators/grammar_error_factory.py

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,22 @@
2323
make_grammar_error(rule, candidate, message, sequence, index=None)
2424
-> ExtendedGrammarError
2525
26-
Invariants Mapping (Minimal)
27-
----------------------------
28-
U1a -> (1,4) # EPI initiation & operator closure precondition
29-
U1b -> (4) # Closure / bounded sequence end
30-
U2 -> (3,4) # ΔNFR semantics & closure (stabilizer presence)
31-
U3 -> (5) # Phase verification
32-
U4a -> (3,4,5) # Trigger handling (ΔNFR pressure + handlers + phase)
33-
U4b -> (3,4,7) # Transformers need stabilised base & fractality preserved
34-
U6 -> (3,9) # Potential confinement + metrics integrity
26+
Invariants Mapping (canonical, derived)
27+
---------------------------------------
28+
Each grammar-rule violation relates to its primary physics invariant plus
29+
Grammar Compliance (#4). The mapping is DERIVED from
30+
``grammar_canon.GRAMMAR_RULES`` (the single source of truth) via
31+
``related_invariants``, reconciled to the 6-invariant canon (AGENTS.md
32+
§Canonical Invariants):
3533
36-
NOTE: Mapping kept intentionally lean; can be extended in future without
37-
breaking existing consumers.
34+
U1a/U1b/U2 -> (1, 4) # Nodal Equation Integrity + Grammar Compliance
35+
U3 -> (2, 4) # Phase-Coherent Coupling + Grammar Compliance
36+
U4a/U4b -> (4,) # Grammar Compliance (bifurcation dynamics)
37+
U5 -> (3, 4) # Multi-Scale Fractality + Grammar Compliance
38+
U6 -> (4, 5) # Grammar Compliance + Structural Metrology
39+
40+
NOTE: Sourced from grammar_canon so the annotation cannot drift from the
41+
canonical rule registry; a consistency test pins the agreement.
3842
"""
3943

4044
from __future__ import annotations
@@ -43,6 +47,10 @@
4347
from typing import Any, Sequence
4448

4549
from .definitions import get_operator_meta
50+
from .grammar_canon import (
51+
GRAMMAR_RULES as _GRAMMAR_RULES,
52+
related_invariants as _related_invariants,
53+
)
4654
from .grammar_core import GrammarValidator
4755
from .grammar_types import StructuralGrammarError
4856

@@ -52,15 +60,16 @@
5260
"make_grammar_error",
5361
]
5462

55-
_RULE_INVARIANTS = {
56-
"U1a": (1, 4),
57-
"U1b": (4,),
58-
"U2": (3, 4),
59-
"U3": (5,),
60-
"U4a": (3, 4, 5),
61-
"U4b": (3, 4, 7),
62-
"U6_CONFINEMENT": (3, 9),
63+
# U-rule violation → canonical invariants it relates to (its primary physics
64+
# invariant + Grammar Compliance #4). DERIVED from grammar_canon.GRAMMAR_RULES,
65+
# the single source of truth, reconciled to the 6-invariant canon (AGENTS.md
66+
# §Canonical Invariants). This replaces the stale pre-optimization 10-invariant
67+
# numbering (which referenced invariants 7/9 that no longer exist). The
68+
# "U6_CONFINEMENT" key aliases the canonical "U6" telemetry rule.
69+
_RULE_INVARIANTS: dict[str, tuple[int, ...]] = {
70+
r.rule_id: _related_invariants(r.rule_id) for r in _GRAMMAR_RULES
6371
}
72+
_RULE_INVARIANTS["U6_CONFINEMENT"] = _related_invariants("U6")
6473

6574
@dataclass(slots=True)
6675
class ExtendedGrammarError:

src/tnfr/operators/introspection.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@
2929
OperatorMeta.doc Concise physics rationale (1-2 sentences)
3030
3131
Note: Grammar rule U6 (confinement) is telemetry-only and not included
32-
as an active role.
32+
as an active role. The ``grammar_roles`` tuples are the canonical per-operator
33+
U1-U5 roles derived from the operator's role set in
34+
:mod:`tnfr.operators.grammar_canon` (single source of truth); the agreement is
35+
pinned by ``test_grammar_canon.py`` (``u_rules_for_operator``).
3336
"""
3437

3538
from __future__ import annotations
@@ -91,7 +94,7 @@ class OperatorMeta:
9194
name="Dissonance",
9295
mnemonic="OZ",
9396
category="destabilizer",
94-
grammar_roles=("U2", "U4a"),
97+
grammar_roles=("U1b", "U2", "U4a"),
9598
contracts=(
9699
"Increases |ΔNFR|",
97100
"May trigger bifurcation",
@@ -155,7 +158,7 @@ class OperatorMeta:
155158
name="Mutation",
156159
mnemonic="ZHIR",
157160
category="transformer",
158-
grammar_roles=("U4a", "U4b"),
161+
grammar_roles=("U2", "U4a", "U4b"),
159162
contracts=(
160163
"Phase transform threshold",
161164
"Requires prior IL",
@@ -175,7 +178,7 @@ class OperatorMeta:
175178
name="Recursivity",
176179
mnemonic="REMESH",
177180
category="generator",
178-
grammar_roles=("U1a", "U1b"),
181+
grammar_roles=("U1a", "U1b", "U5"),
179182
contracts=("Cross-scale echoing", "Supports fractality"),
180183
doc="Echoes patterns across scales for memory/nesting.",
181184
),

tests/operators/test_grammar_canon.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,65 @@ def test_formal_syntax_schema_positions(self) -> None:
171171
schema = gc.FORMAL_SYNTAX_SCHEMA
172172
assert "AL" in schema["start"]
173173
assert "SHA" in schema["closure"]
174+
175+
176+
class TestRelatedInvariants:
177+
"""The rule→invariant annotation is canonical (6-invariant model)."""
178+
179+
def test_every_rule_relates_to_grammar_compliance(self) -> None:
180+
# Grammar Compliance (#4) is in every grammar-rule's related set.
181+
for r in gc.GRAMMAR_RULES:
182+
assert gc.GRAMMAR_COMPLIANCE_INVARIANT in gc.related_invariants(
183+
r.rule_id
184+
)
185+
186+
def test_invariants_are_in_the_six_invariant_canon(self) -> None:
187+
# No stale references to the old 10-invariant numbering (7, 9, …).
188+
for r in gc.GRAMMAR_RULES:
189+
for inv in gc.related_invariants(r.rule_id):
190+
assert 1 <= inv <= 6
191+
192+
def test_primary_invariant_is_included(self) -> None:
193+
for r in gc.GRAMMAR_RULES:
194+
assert r.invariant in gc.related_invariants(r.rule_id)
195+
196+
def test_error_factory_mapping_is_derived(self) -> None:
197+
# The error factory's _RULE_INVARIANTS must come from grammar_canon.
198+
from tnfr.operators import grammar_error_factory as gef
199+
200+
for r in gc.GRAMMAR_RULES:
201+
assert gef._RULE_INVARIANTS[r.rule_id] == gc.related_invariants(
202+
r.rule_id
203+
)
204+
# The U6 confinement alias maps to the canonical U6 rule.
205+
assert gef._RULE_INVARIANTS["U6_CONFINEMENT"] == gc.related_invariants(
206+
"U6"
207+
)
208+
209+
210+
class TestOperatorMetadataRolesAreCanonical:
211+
"""introspection.OPERATOR_METADATA.grammar_roles == the canonical roles."""
212+
213+
def test_every_operator_metadata_matches_canon(self) -> None:
214+
from tnfr.operators.introspection import OPERATOR_METADATA
215+
216+
for mnemonic, meta in OPERATOR_METADATA.items():
217+
expected = gc.u_rules_for_operator(mnemonic)
218+
assert tuple(meta.grammar_roles) == expected, (
219+
f"{mnemonic}: metadata {meta.grammar_roles} != canon {expected}"
220+
)
221+
222+
def test_u_rules_accepts_function_name_and_glyph(self) -> None:
223+
# Same result whether queried by function name or glyph mnemonic.
224+
assert gc.u_rules_for_operator("mutation") == gc.u_rules_for_operator(
225+
"ZHIR"
226+
)
227+
assert gc.u_rules_for_operator("ZHIR") == ("U2", "U4a", "U4b")
228+
229+
def test_remesh_carries_the_recursive_rule(self) -> None:
230+
# REMESH is the U5 recursive generator (was historically omitted).
231+
assert "U5" in gc.u_rules_for_operator("REMESH")
232+
233+
def test_dissonance_is_also_a_closure(self) -> None:
234+
# OZ is a closure (∈ CLOSURES) → U1b (was historically omitted).
235+
assert "U1b" in gc.u_rules_for_operator("OZ")

0 commit comments

Comments
 (0)