A Rule was exactly a Check<Model.Value, Model.Value>: it fired on every
node, switched on the value itself, and carried the root. It was therefore a
second copy of the one check machinery, the duplication the validation idiom
exists to prevent (principle 10), now also resolved for the tagged layer.
Remove Rule and its Context. The model validator holds only checks; its
default is the type-directed BuiltinCheck.mappingKeysAreUnique; validating
takes a Check. Custom whole-tree rules become Check<Model.Value, Model.Value>
with the former context.root now context.document.
Behaviour is byte-identical: the mapping-key builtin reports the same issues
at the same paths in the same DFS order, so the public validate/collect/blank
surface is unchanged. All 399 tests pass; the coverage gate reconciles to 60
public types.
The tagged-node validator carried its own copy of the validation machinery:
a Tagged.Rule cloning Validation's check type and a Tagged.Context cloning
its context, differing only in subject type. That is the per-layer
duplication the validation idiom exists to prevent (principle 10).
Generalise the type-directed framework over both the subject and the
document: SubjectContext, Check, and AnyCheck now take a Document type
parameter for the validated tree's root. The model layer validates with a
Model.Value document, the tagged layer with a Tagged.Node document, each
truthful rather than forcing one root type to stand in for the other.
The tagged layer keeps only what is genuinely its own: the Node document
type, its built-in check (now Tagged.BuiltinCheck, a Check<Node, Node>), and
its traversal. Tagged.Rule and the tagged Context are removed. The validator
surface is unchanged but validating now takes a Check<Node, Node>.
All 399 tests pass; the coverage gate reconciles to 62 public types.
Introduce the core of the validation-rules idiom: a Check<Subject> with a
positive description, a when predicate, and a Bool or multi-error check
that fires only on values of its Subject type wherever they occur. AnyCheck
erases the subject and dispatches by runtime type, guarding an optional or
a wrong type from matching. The validator gains a checks list, a
validating(_:) overload, and a traversal that offers each node the values
it may specialise on. BuiltinCheck.mappingKeysAreUnique is the
type-directed counterpart of the legacy rule.
The machinery has its own negative tests (wrong type, optional, false
predicate yield nothing; same type twice yields two errors) and the builtin
ships a failing and a near-miss test. The coverage gate catches the five
new public types, which are classified as validation machinery. The default
validator is unchanged, so this is additive; retiring the legacy Rule,
moving the default onto the check, the combinator algebra, the error
rendering, and de-duplicating the Tagged validator are the remaining steps.