#451 banned a rule carrying fields and no name_regex — no name narrowing, so it claimed every name whose diff fit its roles. The mirror image is still legal, and #452 made it worse by giving it a second job.
name_regex = "Smith" # and no `fields`
Measured: such a rule claims 127 of 127 possible diff shapes on any name its regex reaches. No role narrowing at all.
Why #452 made this urgent
over_declared_rules skips a rule with no fields — correctly, since a rule declaring no roles cannot over-declare them. But that turns deleting fields into an opt-out: the response to an OVER-DECLARED failure that requires the least thought both silences the check and makes the rule maximally permissive. That is the #372 failure mode, reached by following a gate error message.
The ban costs no migration
expected_since_1.4.0.toml: 70 rules, 0 without `fields`
expected_since_2.0.0.toml: 58 rules, 0 without `fields`
expected_since_2.1.0.toml: 51 rules, 0 without `fields`
That is exactly the argument validate_rules already makes for #451's ban in its own docstring: free to enforce, because nothing has the shape.
The symmetry is the point
validate_rules already rejects a rule with neither key ("it would match every diff and shadow every later rule") and, since #451, one with fields and no name_regex. This closes the third corner. After it, every ledger rule carries both — narrowed by name AND by role — and the three rejections read as one rule with one reason rather than three special cases.
Decided, so it is not re-litigated
No escape hatch for a "genuinely unbounded" rule, declined until one appears — the same call #452 made and for the same reason. dormant covers "explains nothing"; nothing covers "explains everything", and no such rule exists in any ledger. If one ever does, it should argue for a key the way dormant was argued for in #373, rather than finding one already built.
Open in the design
#451 banned a rule carrying
fieldsand noname_regex— no name narrowing, so it claimed every name whose diff fit its roles. The mirror image is still legal, and #452 made it worse by giving it a second job.Measured: such a rule claims 127 of 127 possible diff shapes on any name its regex reaches. No role narrowing at all.
Why #452 made this urgent
over_declared_rulesskips a rule with nofields— correctly, since a rule declaring no roles cannot over-declare them. But that turns deletingfieldsinto an opt-out: the response to anOVER-DECLAREDfailure that requires the least thought both silences the check and makes the rule maximally permissive. That is the #372 failure mode, reached by following a gate error message.The ban costs no migration
That is exactly the argument
validate_rulesalready makes for #451's ban in its own docstring: free to enforce, because nothing has the shape.The symmetry is the point
validate_rulesalready rejects a rule with neither key ("it would match every diff and shadow every later rule") and, since #451, one withfieldsand noname_regex. This closes the third corner. After it, every ledger rule carries both — narrowed by name AND by role — and the three rejections read as one rule with one reason rather than three special cases.Decided, so it is not re-litigated
No escape hatch for a "genuinely unbounded" rule, declined until one appears — the same call #452 made and for the same reason.
dormantcovers "explains nothing"; nothing covers "explains everything", and no such rule exists in any ledger. If one ever does, it should argue for a key the waydormantwas argued for in #373, rather than finding one already built.Open in the design
validate_rulesat startup, beside its two siblings, rather thantests/v2/test_ledger_guards.py— it matches the precedent and fails earlier, before the multi-minute worker pass.fieldsfamily so it does not pre-empt the precise messages for a malformedfields(that placement was itself a Retire the fields-only ledger rule, and forbid the shape #453 review finding). This one has the same hazard in reverse and needs the same care.