From 6e43d78218ee17f3a2ea1f16af22443e655a5520 Mon Sep 17 00:00:00 2001 From: Esteban Zimanyi Date: Tue, 1 Sep 2026 10:56:17 +0200 Subject: [PATCH] Hold the lattice to the temporal types MEOS admits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `temporal_type()` admits 20 user-facing types; the lattice's leaves model 14. Six have no leaf class — `T_TH3INDEX`, `T_TQUADBIN`, `T_TS2CELL`, `T_TPOSECHAIN`, `T_TPCPOINT`, `T_TPCPATCH` — so the object model cannot express them and a binding projected from it reaches those values only through the flat C surface. The gate already here could not see it. Its assertions hold each node to its own predicate, which passes whether or not a type has a class at all: the root's membership comes from `temporal_type()` and already names `T_TPOSECHAIN`, while nothing asked which types a LEAF claims. Coverage is a different question from membership, and only the second was being asked. `_UNMODELLED` records the six as a RATCHET rather than a permission. The assertion compares against it exactly, so a newly uncovered type fails the suite, and giving one of these a class fails it too until its entry goes — the gap can neither grow nor close unnoticed. Both directions are exercised: removing an entry fails, and a leaf that stops claiming its type fails. A type claimed by two leaves fails as well, since two classes for one type is an ambiguity the projection cannot resolve. What the six need is not uniform. `T_TPOSECHAIN` sits beside `TPose`, and the point-cloud pair beside the other spatial leaves. The three cell-index families are one question rather than three: H3, quadbin and S2 are the same kind of thing, a temporal value over a discrete global grid, and whether they share an abstract parent the way `TPoint` groups `TGeomPoint` and `TGeogPoint` is a taxonomy decision for the model's owner. The comment says so where the next reader will meet it. --- tests/test_object_model.py | 53 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/tests/test_object_model.py b/tests/test_object_model.py index fd6f582..dca65d4 100644 --- a/tests/test_object_model.py +++ b/tests/test_object_model.py @@ -28,6 +28,23 @@ MODEL = ROOT / "meta" / "object-model.json" _INTERNAL = {"T_TDOUBLE2", "T_TDOUBLE3", "T_TDOUBLE4"} # not public classes +#: Temporal types MEOS admits that no leaf class models yet, so the lattice +#: cannot express them and a binding projected from it reaches them only +#: through the flat C surface. This set is a RATCHET, not a permission: the +#: coverage test compares against it exactly, so a new uncovered type fails the +#: suite, and giving one of these a class fails it too until the entry goes. +#: +#: The three cell-index families are one open question rather than three: H3, +#: quadbin and S2 are the same kind of thing — a temporal value over a discrete +#: global grid — and whether they sit under a shared abstract parent, the way +#: TPoint groups TGeomPoint and TGeogPoint, is a taxonomy decision the model +#: owner makes. The other three have obvious homes beside their siblings. +_UNMODELLED = { + "T_TH3INDEX", "T_TQUADBIN", "T_TS2CELL", # cell-index grouping undecided + "T_TPOSECHAIN", # sibling of TPose + "T_TPCPOINT", "T_TPCPATCH", # the point-cloud pair +} + def _nodes(d): return {k: v for k, v in d.items() if not k.startswith("_")} @@ -446,6 +463,42 @@ def test_traits_match_source(self): derived = set(_predicate_temptypes(self.cat, t["predicate"])) self.assertEqual(set(t["temptypes"]), derived - _INTERNAL, name) + def test_every_temporal_type_has_a_leaf_class(self): + # The root's membership comes from `temporal_type()`, so the root + # already knows every temporal type MEOS admits. A LEAF is what gives + # one of them a class, and a type the root admits while no leaf claims + # it is a type the object model cannot express: a binding projected + # from this lattice reaches it only through the flat C surface. + # + # This is coverage, not membership — the assertions above hold each + # node to its own predicate and pass whether or not a type has a class + # at all, which is why the gap survived them. + root = self.lat["Temporal"] + admitted = set(root["temptypes"]) - _INTERNAL + self.assertTrue(admitted, "the root claims no temporal type") + + claimed = {t for spec in self.lat.values() if spec["kind"] == "leaf" + for t in spec["temptypes"]} + self.assertTrue(claimed, "no leaf claims a temporal type") + + # A type may not be claimed twice: two classes for one type is an + # ambiguity the projection cannot resolve. + seen = {} + for node, spec in self.lat.items(): + if spec["kind"] != "leaf": + continue + for t in spec["temptypes"]: + self.assertNotIn(t, seen, + f"{t} is claimed by both {seen.get(t)} and {node}") + seen[t] = node + + self.assertEqual(admitted - claimed, _UNMODELLED, + "the set of temporal types with no leaf class moved; " + "give the new one a class, or record it in _UNMODELLED " + "with the reason it has none") + self.assertEqual(claimed - admitted, set(), + "leaf classes modelling a type MEOS does not admit") + def test_leaf_base_types_match_catalog(self): # The relation is a `.temptype_basetype` field of the type-indexed # MEOS_RELTYPE_CATALOG; read it through the parser that already reads