Observation-class finding, recorded while landing PR #15010 (the tool-choice line in skills/objectstack-data). Not fixed there: it falls outside the two-file, two-section surface that flight was scoped to. No assignee — for triage.
What is unstated
skills/objectstack-data/rules/validation.md opens with the complete rule-type table (script, state_machine, format, cross_field, json_schema, conditional). Every row says what the type is for and when it fails. No row says which record the predicate is judged against, and the answer is not uniform across the six.
Measured on origin/main, packages/objectql/src/validation/rule-validator.ts, the evaluateRule dispatch:
script and cross_field are handed ctx.merged — prior row overlaid with the patch. They are invariants: a row that already violates one is refused on any edit until a repairing write lands.format and json_schema are handed ctx.data — the write payload only. On an update that omits the field, there is nothing for them to judge, so a stored row that violates them is never revisited.conditional evaluates its own when against ctx.merged, then dispatches the nested rule back through the same switch — so a nested format still reads the payload while the branch that selected it read the merged record.
Why it is worth a row
This is the same invariant-versus-gate confusion PR #15010 corrects for script versus requiredWhen, one rule type over and still unsaid. An author who reads "use a script rule for an invariant" and reasonably generalises it to "validations[] holds invariants" will reach for format to enforce a stored-data invariant and get a write gate instead — no error, no signal, and legacy rows never checked.
Shape of a fix, if triage wants one
Smallest form is one clause per row (or one extra column) in the existing table — no new section. rules/validation.md has 610 tokens of headroom against its ceiling after PR #15010, so the ratchet is not the constraint here. The engine behaviour is the source of truth; nothing in the spec needs to change for this.
Generated by Claude Code
Observation-class finding, recorded while landing PR #15010 (the tool-choice line in
skills/objectstack-data). Not fixed there: it falls outside the two-file, two-section surface that flight was scoped to. No assignee — for triage.What is unstated
skills/objectstack-data/rules/validation.mdopens with the complete rule-type table (script,state_machine,format,cross_field,json_schema,conditional). Every row says what the type is for and when it fails. No row says which record the predicate is judged against, and the answer is not uniform across the six.Measured on
origin/main,packages/objectql/src/validation/rule-validator.ts, theevaluateRuledispatch:scriptandcross_fieldare handedctx.merged— prior row overlaid with the patch. They are invariants: a row that already violates one is refused on any edit until a repairing write lands.formatandjson_schemaare handedctx.data— the write payload only. On an update that omits the field, there is nothing for them to judge, so a stored row that violates them is never revisited.conditionalevaluates its ownwhenagainstctx.merged, then dispatches the nested rule back through the same switch — so a nestedformatstill reads the payload while the branch that selected it read the merged record.Why it is worth a row
This is the same invariant-versus-gate confusion PR #15010 corrects for
scriptversusrequiredWhen, one rule type over and still unsaid. An author who reads "use ascriptrule for an invariant" and reasonably generalises it to "validations[]holds invariants" will reach forformatto enforce a stored-data invariant and get a write gate instead — no error, no signal, and legacy rows never checked.Shape of a fix, if triage wants one
Smallest form is one clause per row (or one extra column) in the existing table — no new section.
rules/validation.mdhas 610 tokens of headroom against its ceiling after PR #15010, so the ratchet is not the constraint here. The engine behaviour is the source of truth; nothing in the spec needs to change for this.Generated by Claude Code