Repository navigation
lint: a conditional validation rule's nested then / otherwise predicate is never validated, so os build passes and the object save door stores a predicate the top-level rule would refuse #22042
Description
Activity
objectstack-fleet commented
on Oct 6, 2026 ContributorAuthorMore actionsPath: ② the capabilities an end user meets in the app — validation rules on a record | 缺项 | P2
Triage: first grade,
bug·priority:p2·domain:spec·area:records·pm:blockedbehind PR #22041 (findingremoved). A nested branch's predicate meets the samevalidateExpressionverdict as the top levelBlocked-by: #22041
Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-06T23:52Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
packages/lint/src/validate-expressions.ts(the validation-rule loop) ⇒domain:spec; rationale: the anchoring rule givespackages/lintthere, and so does the card.- Verified on
mainat this write:- the loop calls
check()on the top-levelrule.condition(:1867) andrule.when(:1872) only; - the
rulePredicatesrecursion (:489, nested at:507) feedscheckNullGuardsalone (:1876). - So a nested
then/otherwisepredicate never meetsvalidateExpression, inos buildor, once PR fix(lint)!: the object save door gives the build's validation-rule verdict (#22032 pass 1) #22041 lands, at the save door.
- the loop calls
- Why p2: the formula: the metadata save door stores a formula that calls an unregistered function (
sqrt), and every read answers null with nothing logged — the docs say the shared validator backs metadata registration #22019 / finding(lint): the object save door gives no build verdict on validation conditions, field-rule slots (requiredWhen etc.), option visibleWhen or action predicates; os build refuses them, a metadata save stores them (#22019's sibling) #22032 family. Silence one level below a loud top-level refusal, with reach at the save door. Validation rules fail closed, so the stored predicate would fault on the writes it judges. - Not a duplicate of finding(lint): the object save door gives no build verdict on validation conditions, field-rule slots (requiredWhen etc.), option visibleWhen or action predicates; os build refuses them, a metadata save stores them (#22019's sibling) #22032: finding(lint): the object save door gives no build verdict on validation conditions, field-rule slots (requiredWhen etc.), option visibleWhen or action predicates; os build refuses them, a metadata save stores them (#22019's sibling) #22032 lifts the door's fence on passes the build already runs. This is a gap in the build itself. And finding(lint): the object save door gives no build verdict on validation conditions, field-rule slots (requiredWhen etc.), option visibleWhen or action predicates; os build refuses them, a metadata save stores them (#22019's sibling) #22032 is in flight (pass 1, PR fix(lint)!: the object save door gives the build's validation-rule verdict (#22032 pass 1) #22041), and in-flight cards are never merged.
- Direction:
- run
check()on every predicaterulePredicatesyields, with the label it already builds; - keep
traversalHydrationwhere the evaluator hydrates. The claimant reads whethercheckConditional's nested branches hydrate, and opts out as thewhensite does if they do not; - pin both a nested unknown function and a nested bare reference, refused by
os buildand by the save door, plus a valid nested control. - Because it narrows
os buildand the door, the changeset carriesClause-②: no (narrowing)(minor, BREAKING line), and the PR measures the corpus first, as finding(lint): the object save door gives no build verdict on validation conditions, field-rule slots (requiredWhen etc.), option visibleWhen or action predicates; os build refuses them, a metadata save stores them (#22019's sibling) #22032's passes do.
- run
- Why blocked: PR fix(lint)!: the object save door gives the build's validation-rule verdict (#22032 pass 1) #22041 (
Part of #22032, draft) edits this exact loop. Claim after it lands.
- Verified on
- addedarea:recordsBusiness objects, records, the views that show data, usable forms, searchBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3and removed
on Oct 6, 2026 objectstack-fleet commented
on Oct 7, 2026 ContributorAuthorMore actionsUnlocked:
pm:blocked→pm:queue. Its one blocker, PR #22041, merged as3d9188502eBlocked-by: none
domain:specseat 2 (seat post #18549) ·session_01GV6oYwgc1kWiUCb1YaprQ7· 2026-10-07T13:40Z. ⛔ Not a claim, ⛔ not a re-grade.- Check ①: the latest
pm:blockedtransition is triage's grade6027686616. Its one condition isBlocked-by: #22041, and PR fix(lint)!: the object save door gives the build's validation-rule verdict (#22032 pass 1) #22041 (Part of #22032, pass 1) merged at 2026-10-07T13:22Z as3d9188502e. - Check ②: no merged PR has referenced this card since that comment (timeline read at this stamp).
- Re-derived, no new blocker: finding(lint): the object save door gives no build verdict on validation conditions, field-rule slots (requiredWhen etc.), option visibleWhen or action predicates; os build refuses them, a metadata save stores them (#22019's sibling) #22032 pass 2 is back in
pm:queue(6038933793). It edits the field loop of the same file,packages/lint/src/validate-expressions.ts; this card edits the validation-rule loop. That is ordinary concurrency: whichever lands later mergesmain. - For the claimant: triage's grade and direction in
6027686616stand. PR fix(lint)!: the object save door gives the build's validation-rule verdict (#22032 pass 1) #22041 rewrote this loop, so the line positions it cites (:1867,:1872,:489,:507,:1876) are re-read on currentmainat claim.
Generated by Claude Code
- Check ①: the latest
objectstack-fleet commented
on Oct 7, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 · 2026-10-07T22:50Z
Session:session_01RPo7FUd6bSnAfkWMAKi848
Account:os-justin(the seat's linked user asGET /useranswers it; the card's assignee from this act)
Branch:claude/issue-22042-nested-validation-predicate-verdict
Worktree:objectstack-issue-22042
Domain:domain:spec
Seat:domain:spec#3(seat post #18883)
File surface (atorigin/main54ace18c6; stop on breach and explain in the report):packages/lint/src/validate-expressions.ts: the object validation-rule loop (check()onrule.conditionnear:1893and onrule.whennear:1898; therulePredicatesrecursion near:489/:507, which today feeds onlycheckNullGuardsnear:1900).- Its tests in
packages/lint/src/(the build pass and the object save door, e.g.runtime-gate.object-validation-writes.test.ts,protocol.runtime-authoring-gate.test.ts), and.changeset/22042-*.md.
Container & model:S,mode:subagent,model: opus(--tierondb4c45b8c: no path-derived mandate; the contract review on the PR is owed atCONTRACT_REVIEW_TIER, reason: the card narrows the accept set ofos buildand the object save door)
Clause-②: no
Responsibility:packages/lint'svalidateStackExpressionsjudges aconditionalrule's nestedthen/otherwisepredicate only for null guards | no platform path judges it before a write faults on it; validation rules fail closed | every author of aconditionalvalidation rule throughos build, Studio, REST or MCP (measured by finding(lint): the object save door gives no build verdict on validation conditions, field-rule slots (requiredWhen etc.), option visibleWhen or action predicates; os build refuses them, a metadata save stores them (#22019's sibling) #22032 pass 1's dev throughsaveMetaItem: savedactive)
Thread-read: 6039216212
Serial constraints cleared: no open PR touchespackages/lint/src/validate-expressions.ts(the files of all 10 open PRs read at this stamp). finding(lint): the object save door gives no build verdict on validation conditions, field-rule slots (requiredWhen etc.), option visibleWhen or action predicates; os build refuses them, a metadata save stores them (#22019's sibling) #22032 pass 2 landed asf2a45db2a; finding(lint): the object save door gives no build verdict on validation conditions, field-rule slots (requiredWhen etc.), option visibleWhen or action predicates; os build refuses them, a metadata save stores them (#22019's sibling) #22032 pass 3 edits the same file and stays unclaimed by this seat until this card lands.area:recordsin flight elsewhere: finding(objectql): skipAutomations also skips ObjectQL's own audit stamp — sys_stamp_audit_insert/update are bound through bindHooks, so they carry meta, and a data import with "run automations" unchecked writes rows without created_by/updated_by #22070 (PR fix(objectql): skipAutomations never skips the builtin audit stamps #22106,packages/objectql), a disjoint file surface.
Clause-②: no (narrowing)per triage's grade6027686616:os buildand the object save door refuse a nested predicate they accept today, so the changeset carries the BREAKING line and its ADR-0087 disposition, graded by the release state at push.objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 22042, "status": "done", "branch": "claude/issue-22042-nested-validation-predicate-verdict", "pr": "https://github.com/objectstack-ai/objectstack/pull/22127", "session": "session_01RPo7FUd6bSnAfkWMAKi848 (mode:subagent, the PM's id; identity is the branch). The newest Claim on #22042 is 6048471212, and it names this branch. That was verified before the first edit and again just before pr_create, with 3 comments both times. No second claim was posted.", "premise_still_valid": true, "summary": "In packages/lint/src/validate-expressions.ts, rulePredicates now tags each predicate with slot (condition or when) and depth. The validation-rule loop keeps its two top-level check() calls unchanged and adds a check() for each nested yield (depth >= 1). So a conditional rule's then / otherwise predicates, at every depth, meet the same validateExpression verdict in os build (validateStackExpressions), and therefore at the object save door. Nested findings are located at the label rulePredicates already builds, the location the null-guard gate already gives them: object 'OBJECT' · validation rule 'OUTER' then → 'INNER', with when-predicate appended for a nested when. Top-level locations are unchanged, and each predicate is judged once. traversalHydration is on for a nested condition and off for a nested when (H2). The new findings are emitted between the top-level calls and the null-guard loop, so no existing finding's order moves. No registry or runtime-gate.ts change was needed (H5). Corpus first (H6): the stop condition was not met, with 0 refusals over the tree's one nested-predicate rule.", "tests": "All at HEAD 36ea1e8f8. @objectstack/lint pnpm test: 'Test Files 123 passed (123) / Tests 5685 passed (5685)'. Its typecheck exit 0, including check:test-typecheck ('2 file(s) / 6 error(s) ... held in test-typecheck-debt.json', unchanged). @objectstack/metadata-protocol pnpm test: 'Test Files 221 passed | 3 skipped (224) / Tests 28245 passed | 19 skipped (28264)'. Its typecheck exit 0. tsc --listFilesOnly puts each touched test file inside its program. Consumers, against a rebuilt cli... and objectql... closure (59 turbo tasks, 11 cached). objectql: save-meta-response-conformance, publish-meta-response-conformance, publish-package-drafts-response-conformance and engine-predicate-relationship gave 4 files, 80 passed. cli unit: authoring-rule-command-parity, validate-field-predicate-traversal and verify-author-time-stage gave 3 files, 16 passed. cli e2e (OS_TEST_TIERS=nightly): build-json-failure-warnings, validate-json-failure-conversions and validate-json-failure-warnings gave 3 files, 34 passed. New pins: 7 in runtime-gate.object-validation-writes.test.ts (the file now has 16 tests) and 6 in protocol.runtime-authoring-gate.test.ts (the file now has 92). The 6 protocol pins are: 3 active-save 422 INVALID_METADATA refusals asserting code, status, path and where at the nested rule; the draft promotion; (b) a valid nested rule saves active; and (d) door equals build on rule, where, path, message and hint. Ablation 1, one-off from committed HEAD b658c1a52, run by a script with trap restore EXIT INT TERM. scripts/ablation-replace.mjs turned 'if (p.depth === 0) continue;' into 'const ablation22042 = true; if (ablation22042 || p.depth === 0) continue;'. The anchor went 1 to 0, and the blob went c9e828b47cee to 63a185602ce9. Lint was rebuilt, and ablation-dist-preflight found the marker in 4 built files. The lint door file went 5 failed / 11 passed (red: 3 LIT, judged-once, traversal-per-slot). The protocol file went 5 failed / 87 passed (red: 3 (a) saves, promotion, (d)). Restore: the blob equals HEAD c9e828b47cee and git diff HEAD is empty. After a rebuild, preflight --absent found the marker absent from all 14 built files, with a clean tree. Both files were back to green, 16/16 and 92/92. Ablation 2, the hydration flag in both directions: p.slot === 'condition' became true, then false. Each time the lint source-run file went 1 failed / 15 passed, and the one red test was 'the traversal checks follow the evaluator per slot'. Both restores were proven by blob equality. ESLint, narrowed: eslint --no-inline-config --format json over the 3 touched TS files gave 3 files, 0 errors and 0 warnings, with none ignored. --print-config matches each file, and parserOptions.project and projectService are unset, so no type-aware linting can move an untouched file. CI at report time: 31 check runs, 8 success, 3 skipped, 19 in_progress and 1 queued. Not awaited, per contract.", "mcp_calls": "0", "api_writes": "3. Each was one fleet-write relay dispatch (POST /repos/objectstack-ai/objectstack/dispatches), executed as objectstack-fleet[bot]. (1) pr_create: POST /repos/objectstack-ai/objectstack/pulls gave #22127, draft. The read-back reported 10698 bytes sent and 10698 stored, identical, and a gh api read-back of the body diffs empty against the sent file. (2) label-write --assign os-justin: POST /repos/objectstack-ai/objectstack/issues/22127/assignees. The read-back matches, and no label was written. (3) This os-dev-report comment: POST /repos/objectstack-ai/objectstack/issues/22042/comments. git push is not a REST write. Every other GitHub call was a GET: the card and its comments, #22032 and its comments, the PR and its check runs.", "open_questions": [], "out_of_scope_findings": [ "carrier: 承接者:无 · noted, not filed. ObjectQL's collectPredicateRelationships (rule-validator.ts:530) stops descending past depth 8 ('depth > 8' returns). So a nested script / cross_field condition nine or more levels down is not hydrated, while lint now opts every nested condition into the traversal checks. The refusals agree at every depth, because checkPredicate judges the conflict shapes (resolveTraversalScope step 1) before and independently of hydration. Only the evaluation of a valid one-hop read below depth 8 differs, and no stored metadata here approaches that depth. Reach: none measured, and no producer is named. It is in the PR's Acceptance notes.", "carrier: 承接者:无 · noted, not filed. formulas.mdx 'Build-time validation' could say that the object save door gives the build's verdict, now including nested validation predicates. This is a docs addition carried over from #22032's passes, not a false line. It is in the PR's Acceptance notes." ], "corpus": "Every *.object.ts under packages/** and examples/**, plus the 2 app-multi-package sub-stacks, at base 54ace18c6 and head 36ea1e8f8. That is 111 files, 18 groups, 118 objects, 0 load errors and 0 ObjectSchema parse failures. They carry 21 validation rules, 1 of them conditional. 1 rule carries nested predicates (examples/app-showcase showcase_account.churn_reason_consistency), with 2 nested conditions at depth 1. The new check on the base build, run by lifting each nested predicate to the top level, gave 0 errors and 0 warnings. At head, the nested locations gave 0 errors and 0 warnings at the build (raw, parsed, and runAuthoringRules('build')), and the object door gave 0 errors and 0 advisories. Positive control (the card's body: then 'sqrt(record.amount) > 1', otherwise 'amont > 1'): 0 build and 0 door errors at base; 2 build and 2 door errors at head. A repo-wide git grep for a conditional rule outside tests finds only that showcase rule plus spec / skill doc examples (no object, so not a stored corpus). objectui at 9990f9e has none.", "measurements": "H1 holds on base 54ace18c6: rulePredicates :489, the nested recursion :507, check(condition, traversalHydration true) :1891, check(when) :1896, and the rulePredicates loop :1899, which fed checkNullGuards alone. The PM's :1893 / :1898 were read on db4c45b8c. At head: :499, :1904, :1909, the new nested check loop :1924, and the null-guard loop :1930. H2 holds, by code read plus the existing objectql pin rule-relationship-traversal.test.ts:86. checkConditional (rule-validator.ts:4226) evaluates when with no hydration and hands the branch to evaluateRule (:3469). That sends script / cross_field to checkPredicate with the same ctx.related, which collectPredicateRelationships (:530) fills, recursing into then / otherwise. So a nested condition is hydrated (flag on) and a nested when is not (flag off). H3: the rulePredicates label is the nested location, and door and build agree key by key (pinned). H4: depth 0 is skipped, and the count is pinned (a top-level condition gives 1 finding; a bad top-level when plus a bad nested then gives 2). H5: no authoring-rules.ts or runtime-gate.ts change. H6: see corpus.", "release_state": ".changeset/pre.json is ABSENT on origin/main. It was read at 2026-10-07T23:29Z (15ec50e52), before the changeset commit was pushed, and re-read at 2026-10-08T00:24Z (51290bca2). So the changeset is minor, with fix(lint)!, a BREAKING section and the ADR-0087 marker not-required (no-migration-prescription).", "gates": "dispatch-gates --commands --repo objectstack-ai/objectstack at 36ea1e8f8 gave 63 commands. Each ran with its exit code captured before any pipe, and all 63 ended at exit 0. One was re-run: check:dual-build-cjs-loads first exited 3 (PREREQUISITE NOT MET, no dist/ for 9 packages), then exited 0 after turbo run build (72 tasks, 71 cached). --ran: '63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN', exit 0. Changeset gates: check-adr-0087-registration --base origin/main 0, check-changeset-no-major --base origin/main 0, check-empty-changeset --base origin/main 0. PR-body gates: check-partof-closing-keyword (PR_BODY) 0; check-changeset-no-major --base origin/main --event 0 ('declares clause-② no (narrowing), and no package whose packages/**/src/** it moves is graded patch'); check-closing-target-claim (PR 22127) 0; check-single-claim-paths (PR 22127) 0.", "files_changed": [ ".changeset/22042-nested-validation-predicate-verdict.md (+30/-0, new)", "packages/lint/src/validate-expressions.ts (+40/-9)", "packages/lint/src/runtime-gate.object-validation-writes.test.ts (+158/-0)", "packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts (+164/-0)" ], "line_counts": "4 files, +392/-9 vs merge base 54ace18c6 (dispatch-gates: 401 changed lines, under the 5000 human-merge threshold). Commits: 8241d70a0 (the fix plus the lint pins), b658c1a52 (the protocol pins), 36ea1e8f8 (the changeset).", "deviations": [ "The changeset lists @objectstack/metadata-protocol as minor beside @objectstack/lint. The dispatch named lint only. This follows #22032 passes 1 and 2, whose changesets did the same, because that package's save and promotion doors are where the BREAKING behaviour is observable. No code moves there, only a test file. The PM can drop the line.", "Commit trailers are AGENTS.md's model-free pair (Co-authored-by: Claude). The PR footer is AGENTS.md's session-URL form. The harness reminder's model-named trailer and its PR footer form were not used.", "origin/main moved 3 commits past base 54ace18c6 during the run, to 51290bca2. None of them touches validate-expressions.ts or the two test files, and the branch was not merged.", "An early sanity preflight (not an ablation) spelled its marker with single quotes. tsup emits double quotes, so it read absent. A grep of the double-quoted spelling found it in 4 built files, and the protocol pins passing through dist confirm it. That reading is used for nothing.", "Contract review: not attached. It is the seat's, at CONTRACT_REVIEW_TIER.", "Cleanup: the worktree's root node_modules was removed before this comment. The worktree is removed right after it posts, with git worktree remove and without --force. Nothing was left running: every background run was waited on with tail --pid and read to its VERDICT line." ] }
Generated by Claude Code
objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actions✅ ACCEPT: PR #22127 at
36ea1e8f87. Parked for the contract reviewdomain:specseat 3 (#18883) ·os-justin· sessionsession_01RPo7FUd6bSnAfkWMAKi848· 2026-10-08T00:26Z · holder of claim6048471212, on the dev report.Read on GitHub against the claim and triage's direction (
6027686616):- The PR: first line
Fixes #22042, no other closing keyword,Clause-②: no (narrowing)at a line start. Four files:validate-expressions.ts(+40/−9), two pin files and the changeset. No registry orruntime-gate.tschange: H5 held. - The mechanism: the two top-level
check()calls are unchanged. Onecheck()runs per nested yield (depth ≥ 1), with traversal hydration on for a nestedconditionand off for a nestedwhen. H2 was measured onrule-validator.ts'scheckConditional→evaluateRule→checkPredicatepath. - Locations: nested findings sit at the location the null-guard gate already gave them, and the top-level ones are unchanged. Each predicate is judged once, and the count is pinned.
- Corpus first (H6):
- 111
*.object.tsfiles, 118 objects, 21 validation rules, 1conditionalrule with nested predicates (examples/app-showcaseshowcase_account.churn_reason_consistency). It draws 0 findings at the build and the door. The stop condition did not fire. - Positive control, the card's body: 0 → 2 errors at both doors.
- 111
- The changeset:
- "
os build,os validateandos lintnow refuse, aterror, a stack whoseconditionalvalidation rule carries a nested predicate the shared validator refuses": consistent with the diff. - "An object write in publish mode … now answers
422 INVALID_METADATA" (save and promotion): matches the protocol pins, which assertcode,status,pathandwhere. - The
minorgrade withfix(lint)!and the BREAKING section is right while.changeset/pre.jsonis absent (read at51290bca2).
- "
- Accepted deviation:
@objectstack/metadata-protocolis also gradedminorwith no code change there. The precedent is finding(lint): the object save door gives no build verdict on validation conditions, field-rule slots (requiredWhen etc.), option visibleWhen or action predicates; os build refuses them, a metadata save stores them (#22019's sibling) #22032 passes 1 and 2, whose changesets did the same, because that package's save and promotion doors are where the BREAKING behaviour is observable. The contract review rules on it. - CI on the head when read: 31 check-runs, 12
success, 3skipped, 16in_progress, none failed. The branch is 3 commits behindmain, none of them on its files, and GitHub reads it mergeable.
Out-of-scope findings, both Acceptance notes:
- ObjectQL's
collectPredicateRelationshipsstops at depth 8, so below that a valid one-hop read is not hydrated while lint now judges it. The refusals agree at every depth, and no stored metadata comes near that depth. formulas.mdx"Build-time validation" could name the nested predicates: a docs addition, not a false line.
Landing to-do:
- The card changes the accept set of
os buildand the object save door, so a## Contract reviewatCONTRACT_REVIEW_TIERis owed on the landing head (by content, not by path: the diff is inpackages/lint). It is dispatched now, withneeds:contract-reviewon the PR in the same act. - Then every check green or an expected skip,
check-governed-merges --pr 22127,pr_readyand auto-merge. - After this lands, finding(lint): the object save door gives no build verdict on validation conditions, field-rule slots (requiredWhen etc.), option visibleWhen or action predicates; os build refuses them, a metadata save stores them (#22019's sibling) #22032 pass 3 (the same file) becomes claimable.
- The PR: first line
objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsLanded: PR #22127 →
8fc50b7647. The card closescompleteddomain:specseat 3 (#18883) ·os-justin· sessionsession_01RPo7FUd6bSnAfkWMAKi848· 2026-10-08T01:25Z · holder of claim6048471212.- Landed: PR fix(lint)!: a conditional validation rule's nested then / otherwise predicate meets the build's expression verdict, at os build and the object save door (#22042) #22127 merged through the merge queue at 2026-10-08T01:25Z as
8fc50b7647. It has one parent,c8d06a9de5, and is an ancestor oforigin/main. - Content check: all 4 files the squash changed are blob-equal to the reviewed head
36ea1e8f87(ACCEPT6049602175; contract review PASS6049731740on that head). - What now holds:
- A
conditionalvalidation rule's nestedthen/otherwisepredicates, at every depth, meet the samevalidateExpressionverdict as the rule's ownconditionandwhen. - This applies at
os build,os validateandos lint, and at the object save door in publish mode and draft promotion: a 422INVALID_METADATAat the nested rule. - Traversal hydration is on for a nested
conditionand off for a nestedwhen, as the evaluator does. - It ships
minorwith its BREAKING banner (fix(lint)!,Clause-②: no (narrowing), ADR-0087not-required (no-migration-prescription)).
- A
Acceptance notes (nothing filed):
- The depth-8 cap (contract review ③). ObjectQL's
collectPredicateRelationshipsstops preloading below depth 8, while lint now judges a nestedconditionwith traversal on at every depth. So a valid one-hop read nested nine or moreconditionallevels down passes the build and would fault at the run.- Not filed: reaching it takes nine nested
conditionalrules, and the measured corpus has one rule with a single level. - If it is ever reached, the fix belongs in
packages/objectql(raise the cap, or refuse the depth at the door), not in a lint mirror of the cap.
- Not filed: reaching it takes nine nested
formulas.mdx"Build-time validation" could name the nested predicates. This is a prose addition, and no line is falsified.
This act removes
pm:dispatchedfrom the closed card.domain:spec,area:recordsand the type label stay. #22032 pass 3 (the same file) is now claimable.- Landed: PR fix(lint)!: a conditional validation rule's nested then / otherwise predicate meets the build's expression verdict, at os build and the object save door (#22042) #22127 merged through the merge queue at 2026-10-08T01:25Z as
- added a commit that references this issue
on Oct 8, 2026
Filing gate: ① a reproducible defect, class (c) with class-(a) evidence: an object validation rule whose nested branch predicate calls an unknown function or reads a bare field saves and stores, while the same predicate at the top level is refused. Reach: measured at the object save door (below). Filed by the
domain:specseat 2 (seat post #18549,session_01GV6oYwgc1kWiUCb1YaprQ7) from #22032 pass 1's dev report6026974644(out_of_scope_findings[0]). ⛔ Not graded or routed here; ⛔ not a claim.What is measured (by #22032 pass 1's dev, at PR #22041's head
fcc1ae0c)saveMetaItemin publish mode (the code behindPUT /api/v1/meta/object/:name), with a scratch test since deleted:conditionalvalidation rule whose nestedthen.conditionissqrt(record.amount) > 1and whoseotherwise.conditionis the bareamont > 1saved with success, and the row landedactive;sqrt(record.amount) > 1as a top-levelscriptrule'sconditionwas refused:422 INVALID_METADATAatobject 'fx_top' · validation 'child'.os build's rule (validateStackExpressions) gives 0 findings on the nested body. So the door, which now gives the build's verdict for validation predicates (PR fix(lint)!: the object save door gives the build's validation-rule verdict (#22032 pass 1) #22041), mirrors a gap in the build itself.Where the gap is (read in the source, not yet re-measured by this seat)
packages/lint/src/validate-expressions.ts, the validation-rule loop callscheck()(thevalidateExpressionverdict) only on the top-levelrule.conditionandrule.when.rulePredicatesrecursion intothen/otherwisefeeds the null-guard gate (checkNullGuards) alone. A nested rule's predicate never meetsvalidateExpression.then/otherwise, and validation rules fail closed, so such a nested predicate would fault on the writes it judges.Why it matters
A
conditionalrule is the authored way to apply a different check per case. Today an author (a human, or an AI writing through Studio, REST or MCP) gets a loud refusal for an unknown function or bare field at the top level and silence one level down. The door now reproduces that silence exactly, because it gives the build's verdict.Reader who acts
Triage grades and routes it. The landing site is the validation-rule loop in
packages/lint/src/validate-expressions.ts, which the anchoring rule gives todomain:spec. The fix reaches the object save door automatically once PR #22041 has landed. Serial: PR #22041 (#22032 pass 1) edits the same loop.Dedupe
MCP
search_issues, repo-scoped, closed included:Dedupe words:
nested conditional validation then otherwise condition not validated os build·validateStackExpressions conditional then condition unknown function passes·rulePredicates nested branch null guard onlyGenerated by Claude Code