Found by the dispatched review of #398, in that PR's own new code.
PREDICATE_GATE_VIOLATION exists to be a read-time counterpart to a write-time gate. It shipped with its own cell parser (_gate_admits comparing raw strings) rather than reusing ground_causal_predicates._types() — and parity kept by duplication drifts. The two had diverged before the PR merged:
- The
NONE sentinel worked only by accident. subject_types = NONE means no node type can satisfy this domain (METPO:2000008/2000009, documented at docs/CURATION_PLAYBOOK.md:135). The string compare "passed" it because "NONE" happens not to be a CausalNodeTypeEnum member. Combined with the untyped-node case below, an edge grounded to those CURIEs with an untyped subject would have passed a gate that means nothing satisfies it.
* and NONE are opposite meanings — any type vs no type — and a raw-string membership test cannot tell them apart. _types returns None for the first and an empty set for the second.
- No
.strip() per name and no enum validation, where the writer treats an unknown type name as fatal on the stated grounds that "a constraint nobody can see is worse than no constraint".
Fixed in #398: the audit now imports _types and validates against CausalNodeTypeEnum read from the schema, so the two agree by construction rather than by review.
One asymmetry kept deliberately
An untyped node (node_type absent) is admitted by the audit and refused by the writer. Unreachable today — node_type is required by the schema and DANGLING_EDGE is at 0 — and if it ever became reachable, a type audit should not be the thing that reports a missing type. Now stated in the docstring and pinned by a test, rather than being an accident.
Worth generalising
This is the second time a check and its writer have been kept in step by hand in this repo — _topology() was extracted in #363 for the same reason ("the ratchet and the measurement cannot drift apart on what connected means"). Any future audit that re-tests a rule some writer enforces should import the writer's parser, not restate it.
Found by the dispatched review of #398, in that PR's own new code.
PREDICATE_GATE_VIOLATIONexists to be a read-time counterpart to a write-time gate. It shipped with its own cell parser (_gate_admitscomparing raw strings) rather than reusingground_causal_predicates._types()— and parity kept by duplication drifts. The two had diverged before the PR merged:NONEsentinel worked only by accident.subject_types = NONEmeans no node type can satisfy this domain (METPO:2000008/2000009, documented atdocs/CURATION_PLAYBOOK.md:135). The string compare "passed" it because"NONE"happens not to be aCausalNodeTypeEnummember. Combined with the untyped-node case below, an edge grounded to those CURIEs with an untyped subject would have passed a gate that means nothing satisfies it.*andNONEare opposite meanings — any type vs no type — and a raw-string membership test cannot tell them apart._typesreturnsNonefor the first and an empty set for the second..strip()per name and no enum validation, where the writer treats an unknown type name as fatal on the stated grounds that "a constraint nobody can see is worse than no constraint".Fixed in #398: the audit now imports
_typesand validates againstCausalNodeTypeEnumread from the schema, so the two agree by construction rather than by review.One asymmetry kept deliberately
An untyped node (
node_typeabsent) is admitted by the audit and refused by the writer. Unreachable today —node_typeis required by the schema andDANGLING_EDGEis at 0 — and if it ever became reachable, a type audit should not be the thing that reports a missing type. Now stated in the docstring and pinned by a test, rather than being an accident.Worth generalising
This is the second time a check and its writer have been kept in step by hand in this repo —
_topology()was extracted in #363 for the same reason ("the ratchet and the measurement cannot drift apart on what connected means"). Any future audit that re-tests a rule some writer enforces should import the writer's parser, not restate it.