Repository navigation
fix(lint): os validate, os build and the object save door refuse a validations[] predicate that reads parent - #24763
Conversation
The rule evaluator binds record and previous only; parent is bound for a field's own readonlyWhen / requiredWhen. Refuse it at os validate / build and the object save door, with the field-rule-slot prescription. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
…lidations-parent-unbound Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
…also refuses Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
📓 Docs Drift Check5 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin c7708ad942b66f60d0942582c0c75bab626a9f99 && git checkout c7708ad942b66f60d0942582c0c75bab626a9f99
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin feb09db14a6b84dd91ca756160ef101222bdd1dc 687bb33a5f11062b37b99d73c6fff4df5cd6d89a && git checkout -B drift-repro feb09db14a6b84dd91ca756160ef101222bdd1dc && git merge --no-ff 687bb33a5f11062b37b99d73c6fff4df5cd6d89a
node scripts/docs-audit/affected-docs.mjs --json feb09db14a6b84dd91ca756160ef101222bdd1dc |
… the option visibleWhen verdict was Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
…lidations-parent-unbound Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
…usal planned for 19.0 Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
…h with no narrowing Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
Closes #24720
Clause-②: no
What changes
A
validations[]rule's predicates are evaluated by ObjectQL inruleScope(record, previous)(packages/objectql/src/validation/rule-validator.ts,checkPredicateandcheckConditional). That scope bindsrecordandprevious, and no master-detail header. The header is bound asparentonly for a field's ownreadonlyWhen/requiredWhen, on an object with exactly onemaster_detail. A rule that readparent(for exampleparent.status == 'paid') passedos validatesilently, becauseparentis aSCOPE_ROOTSmember and the bare-reference check stays silent on it. At runtime every write the rule judged was refused as unevaluable (Unknown variable: parent), the seed's rows at boot included.warningin 18.x.validationRuleParentIssueinpackages/lint/src/validate-expressions.tsreports a predicate that readsparent. It covers ascript/cross_fieldrule'scondition, aconditionalrule'swhen, and the same two slots on every rule nested in itsthen/otherwise. "Readsparent" is decided from the parsed AST (readsParentRoot, the same reader the field-rule verdict uses), sorecord.parent_codeand the string'parent'are not judged.check()locates the same predicate (object 'NAME' · validation 'RULE', itswhen, or the nested rule's label). The message names the slot, the rule and the root, and points at the field-rule slots whereparentIS bound (readonlyWhen/requiredWhen), with the denormalise-onto-the-detail remedy for any other check. Its rule id is the existingexpression-invalid.os validate/os buildand the object save door run the same pass (runStackExpressionPasses). A warning fails no command, and the door admits the write and lists the finding among itsadvisories.parent. The same finding aterroris planned for 19.0. The docblock and the message say so, and the ⛔ withdrawal note stays: the verdict goes in the same change that bindsparentfor avalidations[]rule.Evidence
All readings at the PR head on top of
origin/maina7199ef974.packages/lint/src/validate-expressions.validation-rule-parent.test.ts: 12 tests pass. It assertswarningon thescript,cross_fieldandconditional(whenand nestedthen) slots, and on objects with 0, 1 and 2 masters. Foros validateandos buildit asserts onewarningfinding and noerrorfinding. At the object door it asserts thaterrorsis empty, that the finding is inadvisories, and that the door finding equals the build finding. Controls:parenton a field's ownreadonlyWhen/requiredWhengives no finding at the build or at the door, andrecord/previousreads,record.parent_codeand the literal'parent'stay clean.scripts/ablation-replace.mjs; each landed on disk and was restored to blob == HEAD withgit diff HEADempty. Making the verdict returnnullturned 9 of 12 tests red, and the 3 CONTROL tests stayed green. Setting the severity back toerrorturned 7 of 12 red: the warning assertions bite.{ type: 'script', name: 'probe_paid_lock', events: ['insert'], condition: "parent.status == 'paid'" }onshowcase_invoice_line,pnpm --filter @objectstack/example-showcase validateexits 0 (Validation passed). It prints the finding as a warning:rule: expression-invalid at object 'showcase_invoice_line' · validation 'probe_paid_lock'. The probe was applied and restored byablation-replace.mjs.pnpm --filter @objectstack/lint testpassed 151 files and 7000 tests.pnpm --filter @objectstack/lint typecheckpassed.check-adr-0087-registrationreports "this PR adds no declared-breaking changeset".check-changeset-no-major,check-empty-changeset,check:changeset-gate-self-testsandcheck:spec-changesare green.git grepoverexamples/**and the package objects findsparent.only in fieldreadonlyWhenslots and in novalidations[]rule.Acceptance notes
active: false, a rule oneventsa stack never fires, and aconditionalbranch whosewhenis never true. The 19.0 refusal will cover them too, which is why it waits for a major.patchwithClause-②: no: a warning narrows nothing. The earlier breaking-minordeclaration was withdrawn under 「甲」.runRuntimeAuthoringRules({ type: 'object' }). There is nosaveMetaItempin inpackages/metadata-protocol, because that package is outside this card's file surface.expression-invalidrule, andcontent/docs/data-modeling/fields.mdxalready states thatparentis bound for the field-rule slots only.Generated by Claude Code