Repository navigation
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
Conversation
…dicates meet the build's expression verdict The validation-rule pass called check() on a rule's own condition and when only; the rulePredicates recursion into then / otherwise fed the null-guard gate alone, so an unregistered function or a bare field one level down passed os build and, since the object save door gives the build's verdict, saved. Every nested predicate rulePredicates yields (depth >= 1) now meets check(), located at the label it already builds, with the traversal checks on a nested condition (ObjectQL hydrates it) and off on a nested when (checkConditional does not). The rule's own condition / when keep their calls and locations. Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848 Co-authored-by: Claude <noreply@anthropic.com>
…rdict at the object save door Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848 Co-authored-by: Claude <noreply@anthropic.com>
…(narrowing) Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check3 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 980d67426f372ef08bc8502cbb4905f406211a79 && git checkout 980d67426f372ef08bc8502cbb4905f406211a79
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 51290bca2cde5ed8befa3e8ee487ad56b3b88e58 36ea1e8f877f59744c7d2f4f9dbdf16e8150840a && git checkout -B drift-repro 51290bca2cde5ed8befa3e8ee487ad56b3b88e58 && git merge --no-ff 36ea1e8f877f59744c7d2f4f9dbdf16e8150840a
node scripts/docs-audit/affected-docs.mjs --json 51290bca2cde5ed8befa3e8ee487ad56b3b88e58 |
Contract reviewServed-tier: Reviewed read-only against card #22042 (body and all 5 comments, triage grade Check-runs on the head. First read 2026-10-08T00:28:34Z: 35 runs, 16 ① Derived judgmentsEvery accept-set and public-surface change the diff implies, each judged:
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
…its it can prove, you apply the rest (objectstack-ai#22142) Fixes objectstack-ai#9591 Clause-②: no (prescription text only; no input's accept or reject result changes) This is the spec-lane half of the card: the shared retirement sentence names `--write`, and the class-wide pin moves in the same PR. The codemod itself landed in PR objectstack-ai#22108 (`a959493cdf`). ## The sentence Before (the objectstack-ai#9529 wording): ```text Run `os migrate meta --from N` to list the mechanical edits for existing sources; apply them by hand. ``` After: ```text Run `os migrate meta --from N` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. ``` The one allowed two-clause variant (a conversion that covers only part of a value) carries the same clause: `… to list the mechanical edits for the X case; --write applies the ones it can prove, and WHAT-HAPPENS-TO-THE-REST.` Its two members are dashboard `compareTo.offset` and the script node's `config.actionType`. **Checked against the tool on `main` (`51290bca`).** `packages/cli/src/commands/migrate/meta.ts` declares `write: Flags.boolean({ … default: false, exclusive: ['stored'] })`. Its help text says it rewrites the authored sources in place "for each mechanical change traced to one literal in one project file; every other change is listed with the reason it was not written". Without the flag the run writes only the `--out` snapshot. The wording satisfies every ruling that binds it: - **Triage `6045697201`.** The sentence never says "rewrite existing sources automatically" unqualified ("the ones it can prove"), and it names `--write` because the default run still only lists. - **"It must be TRUE of the tool."** Every clause is a property of the command, read from `meta.ts`. - **"One antecedent."** "existing sources" still names one thing. "The ones" can only be edits, because an edit is what gets applied. The key's fate stays in the body prose. - **Vocabulary.** The wording matches PR objectstack-ai#22122's skill text ("lists the mechanical edits"; `--write` "rewrites in place each edit it can trace to one literal in one project file, lists every other with the reason it was not written"). ## Where it moved (counted at `017761f0`; the merge of `main` added no site) - **161 sentences the pin judges.** `packages/spec/src`: 157 (155 house form, 2 two-clause). `packages/lint/src`: 1. `packages/drivers/driver-turso/src`: 3. Every one passes the new anchors; `apply them by hand` survives only in the pin's own RED fixtures. - **Not judged by the pin, moved anyway:** - `migrations/registry.ts`: 3 sentences, regenerated from the moved `entries/semantic/18.*.ts` by `gen:migration-registry`. - The lint `chartConfig.xAxis.field` hint in `validate-widget-bindings.ts`: a template literal with interpolation after the sentence, so the pin cannot see it (Acceptance notes). - The `retiredKey()` docblock example. - **Mechanical replacement.** A script replaced the tail `apply them by hand` in 63 files (188 occurrences; old tail left: 0) and printed per-file before/after counts. Two seams split mid-phrase (`translation.zod.ts`) and the two two-clause sites were rewritten by anchored edits that had to hit exactly once. - **Pins in other test files.** 23 test files asserted the old sentence verbatim, as string or regex. Each now asserts the new sentence verbatim, so a revert reds them. The ones that assert only the unchanged prefix (`form-layout-inline-grid-retired.test.ts`, the turso / driver-memory `toContain('os migrate meta --from 17')`) are untouched, because they stay true. - **Changeset.** `.changeset/9591-retirement-sentence-write.md`: `patch` for `@objectstack/spec`, `@objectstack/lint` and `@objectstack/driver-turso`, the three packages whose shipped text moves. - **Generated.** `content/docs/references/**`: 32 files (+268/−268) from `pnpm --filter @objectstack/spec check:generated --fix`; `check:docs` was the only stale artifact. `check:generated` then exited 0. ## The class pin (`retired-key-migrate-sentence.test.ts`) - **Anchors.** `HOUSE_AT_MARKER` and `MIXED_AT_MARKER`, and their markdown twins, require the new clause. The objectstack-ai#9529 sentence, which does not name `--write`, is now RED. Two further spellings are RED: one that names `--write` without the qualification, and one that qualifies it but leaves the rest unowned. - **Withdrawn claim.** `WITHDRAWN_CLAIM` is unchanged: the unqualified automatic-rewrite claim stays a hard RED everywhere. A new non-vacuity case proves neither legal shape trips it. - **Truth anchor (new).** The pin reads `os migrate meta`'s own flag table. A `write` boolean flag must exist and must have `default: false`, the two facts the sentence rests on. The read is covered by `@objectstack/spec`'s existing `packages/**/*.ts` cross-package declaration. - **Corpus widened by one root.** `packages/drivers/driver-turso/src` joins, on objectstack-ai#7030's terms. Its three `turso` config tombstones carry the house sentence, and their docblock defers to `retired-key.ts`, but the pin never walked them. Without this, a rewording leaves them behind with every assertion green. The lint-only anti-vacuity case now covers each widened corpus. - **Header and docblock.** The pin header and the `retired-key.ts` module docblock record the new sentence and why. The "the claim may be restored" note is gone, replaced by what was restored and how far. **Reverse verification**, run from committed HEAD `017761f0` through `scripts/ablation-replace.mjs` (each anchor hit as declared and was restored to a blob equal to HEAD with `git diff HEAD` empty). Expected direction: red. | Mutation | Result | |:--|:--| | A. the three `turso.zod.ts` sentences back to the objectstack-ai#9529 wording (anchor ×3→0, blob `e25cca4d5508`→`a05fe3f09733`) | 3 failed / 12 passed, naming `driver-turso:spec/turso.zod.ts:63`, `:75`, `:82` | | B. `meta.ts` `write` flag `default: false` → `true` (blob `c036012c63c9`→`71acff21ef8c`) | 1 failed / 14 passed: "the sentence is TRUE of the command it names" | | C. `meta.ts` flag renamed `write` → `inPlace` (blob `c036012c63c9`→`29a8a9402284`) | 1 failed / 14 passed: "os migrate meta declares no `write` boolean flag" | Under the old corpora, mutation A would have stayed green, because driver-turso was in no corpus. ## One bounded fix on the same sentences: `CHATTER_POSITION_RETIRED` The three `record:chatter` / `record:discussion` `position` value prescriptions (`'sidebar'`, `'inline'`, `'drawer'`, in `ui/component.zod.ts`) told the author to run a bare `os migrate meta`. The command refuses that with `Missing required flag --from` (`meta.ts` `run()`, the `flags.from === undefined` branch). The conversion is `record-chatter-position-vocabulary`, `toMajor: 18`, so they now name `--from 17`. They therefore join the pin's judged set in house form, and the "(registered under protocol major 18)" aside goes. The fix qualifies as bounded: the same sentence class, a mechanical change to an already-pinned form, a file inside this claim's surface, and the same gate family. No test pinned the old text. ## Governed surface: `.claude/skills/spec-property-retirement/SKILL.md` (Tier S) The pin requires the retirement playbook to teach both shapes (`SKILL_HOUSE_TEMPLATE` and `SKILL_MIXED_TEMPLATE` must match its convention 5). So changing the sentence forces the playbook edit, and this PR lands as Tier S. Convention 5 now carries the two new templates. Its note that the command "never writes a source file" was made false by PR objectstack-ai#22108, so it is deleted. Line count 337 → 337 (ceiling 337), with every line within the 120-byte budget: `node scripts/pm/check-skill-line-ratchet.mjs` exits 0. ⛔ No published `skills/**` file changes: PR objectstack-ai#22122 owns `skills/objectstack-upgrade/SKILL.md`, and no published skill carries the sentence (`git grep` count 0). ## 维护者速读(草稿) **改了什么**:所有退役键报错末尾那句统一提示,从「运行 `os migrate meta --from N` 列出机械修改,然后手工改」改为「……列出机械修改;`--write` 会写入它能证明的那些,其余你手工改」。共 161 处被 pin 判定的报错文案(含 2 处两从句变体),外加生成的 registry 3 处、lint 模板字符串 1 处;其中 3 处原先写成不带 `--from` 的命令(该命令会直接拒绝),一并改正。守这句话的 pin 同步更新,并新增一条断言:CLI 必须真有 `--write` 且默认不写。 **为什么改**:`--write` 已随 PR objectstack-ai#22108 落地,旧句只说「手工改」,低估了工具;但 `--write` 只写能证明的站点,所以不能说「自动重写源文件」。新句两头都如实。 **风险与代价(含回滚)**:纯文案,不改任何 schema、键、类型、导出或错误码;解析结果不变。依赖旧整句原文匹配的调用方会失配(仓内 23 个测试已同步);前缀「…for existing sources;」不变。回滚即 revert 本 PR。在途的兄弟 PR 若新增处方仍用旧句,会被 pin 打红,后落地者改用新句。 **席位意见**: **你要做的**:无需操作;本 PR 触 `.claude/**`(Tier S),由席位按合同审查记录落地。 ## Verification (branch base `51290bca`; final head `f9ca14d548`) - `pnpm --filter @objectstack/spec build`: VERDICT command-exit 0. `check:generated --fix` regenerated the one stale artifact; `check:generated` then exited 0 (15 of 15 current), and again in the gate run at `f9ca14d548`. - spec `vitest run --project local`: Test Files 623 passed (623), Tests 18613 passed, 1 todo, at `017761f0`. - spec `vitest run --project repo` (53 files incl. the class pin): 53 passed (53), Tests 903 passed (903), at `017761f0`. - `@objectstack/driver-turso` `vitest run`: Test Files 88 passed (88), Tests 2373 passed, 33 skipped, at `017761f0` (after `pnpm --workspace-concurrency=2 --filter '@objectstack/lint...' --filter '@objectstack/driver-turso...' build`; the first run, before that build, could not resolve unbuilt dependencies and is NOT MEASURED, not red). - `typecheck` for spec (`tsc --noEmit` + `check:scripts-typecheck` + `check:test-typecheck`), lint and driver-turso: exit 0 at `017761f0`. - **After merging `main`** (`033e5c536d`: objectstack-ai#22122, objectstack-ai#22127, objectstack-ai#22106) as `f9ca14d548`, with no conflict (objectstack-ai#22127 also edits `validate-expressions.ts`): the class pin 15 passed (15); `@objectstack/lint` `vitest run`: Test Files 123 passed (123), Tests 5688 passed (5688); lint `typecheck` exit 0. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `f9ca14d548` derives 124 commands (the claim-time 79 plus 45). All 124 exit 0 at `f9ca14d548`. `--ran` reconciliation: 124 derived, 124 run, 0 NOT-MEASURED, a zero derived from the recorded exit codes. (At `017761f0`, five gates first answered exit 3, PREREQUISITE NOT MET: one shallow-clone fixture and four that need unbuilt dists. The clone was deepened as the gate asked, and all five are green in the `f9ca14d548` run.) - `node scripts/pm/check-skill-line-ratchet.mjs`: exit 0; the playbook is 337 lines (ceiling 337), and no line is over 120 bytes. - eslint, narrowed: `pnpm exec eslint --no-inline-config --format json` over the 66 changed `.ts` files reports 66 files, 0 errors and 0 warnings. The population is `eslint.config.mjs`'s `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` glob, which excludes the changed `.md` / `.mdx` files. The config never enables type-aware linting (its own comment at `:326`–`:328`), so this diff cannot move any untouched file's verdict. The repo-wide `pnpm lint` is CI's. ## Siblings in flight objectstack-ai#21982, PR objectstack-ai#22094 (objectstack-ai#13458) and PR objectstack-ai#22103 (objectstack-ai#5082) each add prescriptions with today's sentence. Whichever lands after this one carries the new sentence; the class pin reds it at that merge otherwise. Whichever of those lands first, this branch merges `main` before landing. ## Acceptance notes - **Hand-written docs still use the old sentence** (`content/docs/automation/flows.mdx` ×2, `protocol/objectql/query-syntax.mdx`, `data-modeling/queries.mdx`, `protocol/objectui/actions.mdx`, `ui/apps.mdx` ×2). Each is the page's own advice, not a quoted error, and still true of the default run; each undersells `--write`. They are `domain:devx` pages outside this claim, so they are not touched here. - **QA checklist item `cli.migrate-meta-codemod`** (`docs/qa/platform-checklist/areas/cli.json`). Its RESTART CHECK fired when PR objectstack-ai#22108 added `--write`, and the item still asserts a print-only command. Its step 1 greps for the objectstack-ai#9529 sentence verbatim and now finds none. Re-authoring the item belongs to the checklist author, not this PR. - **Comments that say the default run "lists the mechanical edits"** stay as they are, because they are still true: the `migrations/registry.ts` migration notes (outside the pin's scope by design) and the `conversions/registry.ts` comments. - **The lint `chartConfig.xAxis.field` hint** (`validate-widget-bindings.ts`) moved, but it remains invisible to the class pin: a template literal, with `suggestName(…)` and the suppress hint interpolated after the sentence. - **A published skill still claims an automatic strip.** `skills/objectstack-data/rules/indexing.md:31` says "run `os migrate meta --from 16` to strip them automatically", and `skills/objectstack-data/SKILL.md:377` says the command "strips them". The default run strips nothing from sources, and `--write` strips only what it can prove. `WITHDRAWN_CLAIM` has no strip spelling, so the pin cannot see this. Widening it here would red `main` on a Tier H file this PR may not touch, so it is reported to the PM for the skills lane. - **The `config.actionType` two-clause tail** ("the stub and marker values are removed") is unchanged in substance; only the `--write` clause was inserted before it. ## Patch round 1 (written by the PM seat from the dev's report `6052088494`) - **Merge:** `origin/main` `ef1fcb26a2` (PR objectstack-ai#22103) was merged through `os-regen-merge.sh` as `feca6b5ace`. The three reference pages both sides had changed (`api/metadata`, `data/object`, `system/migration`) were regenerated from the merged tree as `98e2f6e373`. That brings back objectstack-ai#22103's `unique?: false | 'global' | 'organization'` rows, which the driver had dropped. - **objectstack-ai#22103's sites:** the merge brought two non-test sites with the old tail and one test that asserts it verbatim. All three carry the new sentence at `b9d6e82619`: - `packages/spec/src/data/object.zod.ts` (`DECLARED_INDEX_BARE_TRUE_RETIRED`); - `packages/lint/src/data-model-rules.ts` (the `unique-unscoped-declared-index` fix text); - `unique-scope-message.test.ts`. The pin now judges 163 sentences: `spec` 158 (156 house + 2 two-clause), `lint` 2, `driver-turso` 3. - **The pin's blind spot:** the `data-model-rules.ts` sentence sat in a template literal, which the judge cannot read (escaped backticks), so it was never judged. It is now plain-quoted, as in `validate-expressions.ts`, and the pin's Mechanism paragraph records that template literals are invisible to the scan. Ablation D (that sentence back to the old tail) gives 3 failed / 12 passed, naming `lint:data-model-rules.ts:463`. `validate-widget-bindings.ts` stays the one template-literal site the pin cannot judge (an Acceptance note). - **Verification at `b9d6e82619`:** - spec `--project local`: 623 files / 18,619 tests; - spec `--project repo`: 53 / 903; - lint: 123 / 5,689; - driver-turso: 88 / 2,373; - typecheck: exit 0 for all three packages; - `dispatch-gates --ran`: 124 derived / 124 run / 0 NOT-MEASURED; - CI: 33 success, 2 expected skips. ## Patch round 2 (written by the PM seat from the dev's report `6053397952`; claim revised `6052335087`) - **Why:** PR objectstack-ai#22094 (objectstack-ai#13458, `fec87e7e07`) landed first with a two-clause prescription lacking the `--write` clause, which this PR's class pin refuses. The sibling rule here ("whichever lands later carries the new sentence") puts the edit in this PR. - **Merge:** `origin/main` `959c209d56` was merged through `os-regen-merge.sh` as `3b6335b9be`, with no hand-written conflict. `content/docs/references/api/protocol.mdx` was regenerated as `c40b3babd7`. - **The edit** (`fe3af5642c`, 4 files beyond the merge): - `packages/spec/src/kernel/manifest.zod.ts` `PLUGIN_PERMISSIONS_LIST_FORM` now closes with "Run `os migrate meta --from 17` to list the mechanical edits for the package manifest case; `--write` applies the ones it can prove, and a granted-permission record is not a source it reads." It keeps objectstack-ai#13458's own second clause and adds the house `--write` clause, the seat's wording. - Its verbatim pin `manifest-permissions-string-list.test.ts` moved with it. - The changeset's two-clause bullet now names three members and says "before their second clause". - **The two-clause variant now has three members:** dashboard `compareTo.offset`, the script node's `config.actionType`, and the package manifest `permissions` case. The pin judges 164 sentences (spec 159 = 156 house + 3 two-clause; lint 2; driver-turso 3) with 0 bad sites. - **Verification at `fe3af5642c`:** - spec `--project local`: 625 files / 18,661 tests; - spec `--project repo`: 53 / 903; - class pin: 15 / 15, and the manifest pin: 17 / 17; - `dispatch-gates --ran`: 124 / 124 / 0 NOT-MEASURED; - CI: 33 success, 2 expected skips. --- _Generated by [Claude Code](https://claude.ai/code/session_01LAi5BVvQNiYzepSAcsoFLK)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #22042
Clause-②: no (narrowing)
A
conditionalvalidation rule's nestedthen/otherwisepredicates now meet the samevalidateExpressionverdict as the rule's owncondition/when. That holds inos build(validateStackExpressions), and so at the object save door, which has given the build's verdict for validation predicates since #22032 pass 1 (PR #22041).What changes
packages/lint/src/validate-expressions.ts, the validation-rule loop.rulePredicatesnow tags each predicate with itsslot(conditionorwhen) and itsdepth(0 is the rule itself). Its labels are unchanged.check()calls stay as they were, with the same location and flags. The loop adds onecheck()for each nested yield (depth 1 and below) and skips depth 0, so no predicate is judged twice.rulePredicatesalready builds, which is the location the null-guard gate already gives that predicate:object 'OBJECT' · validation rule 'OUTER' then → 'INNER'. A nestedwhenappendswhen-predicate. The top-level findings keepobject 'OBJECT' · validation 'NAME'.traversalHydrationfollows the evaluator per slot, as at the top level. It is on for a nestedconditionand off for a nestedwhen(H2 below).saveMetaItem/publishMetaItem..changeset/22042-nested-validation-predicate-verdict.mdsets@objectstack/lintand@objectstack/metadata-protocoltominor. It carriesfix(lint)!,Clause-②: no (narrowing), a BREAKING section, the remedy, and the ADR-0087 dispositionnot-required (no-migration-prescription).Nothing else moves. There is no registry change in
authoring-rules.tsand no change inruntime-gate.ts.Measured before the change (the dispatch's H1–H6), at base
54ace18c6rulePredicatesis at:489and recurses intothen/otherwiseat:507.check(where, rule.condition, …, true)at:1891andcheck(`${where} when`, …)at:1896.rulePredicatesloop is at:1899and fedcheckNullGuardsalone.36ea1e8f8these are:499,:1904,:1909, and:1924(the new nestedcheck()loop) /:1930(the null-guard loop).checkConditional(rule-validator.ts:4226) evaluates itswhenagainstctx.merged, with noresolveTraversalScope. It then hands the chosen branch toevaluateRule(branch, ctx)(:3469).evaluateRulesends ascript/cross_fieldbranch tocheckPredicate(rule, ctx.merged, …, ctx.related, ctx.fields). That is the same call, with the samerelatedbinding, as a top-level rule.collectPredicateRelationships(:530) is what preloadsrelated, and it recurses into aconditional'sthen/otherwise(itsvisit). Theobjectqlpinreaches a predicate nested inside a conditional(rule-relationship-traversal.test.ts:86) covers that.conditionis hydrated, and the traversal checks are ON there. A nestedwhenis evaluated bycheckConditionalwithout hydration, so they are OFF, as at the top-levelwhensite.runAuthoringRules('build', …)give it identically:rule,where,path,messageandhintare compared key by key in the protocol(d)pin.conditiongives exactly one finding, atvalidation 'NAME'only. A faulting top-levelwhenbeside a faulting nestedthengives exactly two findings, one at each location.runtime-gate.tschange was needed.runtimeAuthoringRulesFor('object')already listsvalidateStackExpressions(pinned), and the loop is not fenced on an object write. The protocol pins below reach the door through the built@objectstack/lintdist/with no other edit.*.object.tsunderpackages/**andexamples/**, plus the twoapp-multi-packagesub-stacks: 111 files, 18 groups, 118 objects.conditional. One rule carries nested predicates:examples/app-showcaseshowcase_account.churn_reason_consistency, with 2 nestedconditions.ObjectSchema.parsed shapes, and throughrunAuthoringRules('build')). The object door gave 0 errors and 0 advisories.git grepfor aconditionalrule outside tests finds only that showcase rule and spec / skill doc examples. Those carry no object, so they are not a stored corpus. The siblingobjectuicheckout at9990f9ehas none.Tests (all at head
36ea1e8f8)@objectstack/lintpnpm test:Test Files 123 passed (123),Tests 5685 passed (5685).typecheckexit 0, includingcheck:test-typecheck(2 file(s) / 6 error(s) … held in test-typecheck-debt.json, unchanged).@objectstack/metadata-protocolpnpm test:Test Files 221 passed | 3 skipped (224),Tests 28245 passed | 19 skipped (28264).typecheckexit 0.tsc --listFilesOnlyputs each touched test file inside its program: lint'stsconfig.test.jsonand metadata-protocol'stsconfig.json.@objectstack/cli...and@objectstack/objectql...closure (59 tasks, 11 cached):@objectstack/objectqlsave-meta-response-conformance,publish-meta-response-conformance,publish-package-drafts-response-conformanceandengine-predicate-relationship: 4 files, 80 tests passed.@objectstack/cliauthoring-rule-command-parity,validate-field-predicate-traversalandverify-author-time-stage: 3 files, 16 passed.*.e2e.test.tsfiles that readexpression-invalidran underOS_TEST_TIERS=nightly: 3 files, 34 passed.packages/lint/src/runtime-gate.object-validation-writes.test.ts:ObjectSchema.safeParsegreen);thensqrt(record.amount) > 1andotherwiseamont > 1;conditional'swhenplus a rule below it;packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts:INVALID_METADATAwhosecode,status,pathandwherename the nested rule;active;Ablation (one-off, from committed head
b658c1a52, with trap restore)scripts/ablation-replace.mjsturnedif (p.depth === 0) continue;intoconst ablation22042 = true; if (ablation22042 || p.depth === 0) continue;.c9e828b47ceeto63a185602ce9.ablation-dist-preflightfound the marker in 4 built files.c9e828b47cee, andgit diff HEADis empty. After a rebuild,preflight --absentfound the marker absent from all 14 built files, with a clean tree. Back to green: 16/16 and 92/92.p.slot === 'condition'was set totrueand then tofalse. Each time exactly 1 test failed and 15 passed: "the traversal checks follow the evaluator per slot". Each restore was proven by blob equality.Gates (at head
36ea1e8f8)node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 63 commands. Each ran with its exit code captured before any pipe, and all 63 ended at exit 0.check:dual-build-cjs-loadsfirst exited 3 (PREREQUISITE NOT MET: some packages had nodist/). Afterturbo run build(72 tasks, 71 cached) it exited 0.--ranreconciliation: "63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN", exit 0.check-adr-0087-registration --base origin/mainexit 0 (not-required (no-migration-prescription)accepted),check-changeset-no-major --base origin/mainexit 0, andcheck-empty-changeset --base origin/mainexit 0.eslint --no-inline-config --format jsonover the 3 touched TS files gave 3 files, 0 errors and 0 warnings, with none ignored.--print-configmatches each file, and the config enables no type-aware linting (parserOptions.projectandprojectServiceunset). So this diff cannot move an untouched file's verdict.Acceptance notes
collectPredicateRelationshipsstops descending past depth 8 (depth > 8). So a nestedconditionnine or more levels down is not hydrated, while lint opts it into the traversal checks at every depth.checkPredicatejudges the conflict shapes (resolveTraversalScopestep 1) before, and independently of, hydration.content/docs/data-modeling/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 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, not a false line. Carrier: none.CONTRACT_REVIEW_TIERis the seat's.Generated by Claude Code