Skip to content

fix(lint): descend into conditional then/otherwise when building the _validations universe - #14820

Merged
baozhoutao merged 3 commits into
mainfrom
claude/issue-14700-translation-refs-nested-conditional
Sep 3, 2026
Merged

fix(lint): descend into conditional then/otherwise when building the _validations universe#14820
baozhoutao merged 3 commits into
mainfrom
claude/issue-14700-translation-refs-nested-conditional

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #14700

What

validate-translation-references built its _validations universe with a flat walk of objects[].validations[]. A conditional rule's then / otherwise branch is itself a full rule carrying its own name, and that branch name — not the wrapper's — is the address checkConditional delegates to and authoredRuleMessage keys on at runtime (packages/objectql/src/validation/rule-validator.ts). The flat walk never saw a branch name, so a correct bundle entry for one was reported as an orphan translation-target-unknown, and the finding's own advice ("keeps its source locale in every refusal") was the opposite of the truth for that key — acting on it (deleting the entry) reintroduces the exact defect it fixed.

Fix

buildUniverse's walk over obj.validations now calls a small recursive collector (collectValidationRuleNames) instead of reading rule.name directly:

function collectValidationRuleNames(rule: AnyRec, validations: Set<string>): void {
  const ruleName = strName(rule.name);
  if (ruleName) validations.add(ruleName);
  if (isRec(rule.then)) collectValidationRuleNames(rule.then, validations);
  if (isRec(rule.otherwise)) collectValidationRuleNames(rule.otherwise, validations);
}

This mirrors evaluateRule's recursion (checkConditional dispatches the branch back through evaluateRule, which recurses into checkConditional again for a nested conditional) — depth is unbounded, matching the runtime.

The wrapper's own name stays in the universe, unchanged. Its message is structurally unreachable (checkConditional either returns nothing, returns unevaluableRuleError — which builds its own sentence — or delegates to the branch), so an entry for it is inert at runtime. #14518 keeps one anyway, deliberately, so the bundle mirrors the declared rule set 1:1 rather than re-deriving objectql's dispatch — this PR does not touch that decision.

packages/objectql/** is untouched: checkConditional / authoredRuleMessage are the addressing this mirrors, not changes.

Before / after (reproduces the card's own measurement)

Same fixture as the issue — demo_account with one conditional rule (churn_reason_consistency) whose then / otherwise are churn_reason_present / churn_reason_absent, and a bundle with entries for all three names:

Tree findings.length Findings
origin/main (e6ac0c6fd5, pre-fix) 2 translation-target-unknown on churn_reason_present and churn_reason_absent
this branch (post-fix) 0

Reproduced by importing validateTranslationReferences directly (via tsx) from a comparison worktree checked out at the pre-fix commit and from this branch, against the identical fixture object — not just asserted in a test, run both ways.

Fixtures added (validate-translation-references.test.ts, new describe block #14700)

Case Bundle names Expected findings
Wrapper + both branches named churn_reason_consistency, churn_reason_present, churn_reason_absent 0 (was 2 before the fix)
A name matching no rule at any depth the two real branch names + churn_reason_ghost 1 — churn_reason_ghost only (real orphans still caught)
Nested-in-nested (conditional whose then is itself a conditional) innermost_rule (2 levels deep) 0
Unnamed branch gate (wrapper only; branch has no name) 0, no crash (mirrors the existing unnamed-top-level-rule behaviour, #14253)

All 64 tests in the file pass, including the pre-existing object-branch coverage vs the schema (#13835) pin, which still asserts one working leg per ObjectTranslationDataSchema key including _validations.

Gates

  • pnpm --filter @objectstack/lint test — 93 test files / 2845 passed, 5 skipped (pre-existing skips, unrelated).
  • pnpm --filter @objectstack/lint typecheck — clean (tsc --noEmit + check:test-typecheck).
  • eslint --no-inline-config on both touched files — 0 errors / 0 warnings.
  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran <record> at 413167ed8c (post-merge-of-main) — 34 derived, 30 run (all green), 4 NOT-MEASURED, 0 UNRUN:
    • check-test-completeness — grades a saved turbo run test log; none exists locally (CI tees one).
    • check-half-states (plain, live sweep) — timed out at 180s reaching GitHub through the session proxy; report-only, never fails a build (its own header says so); the check:pm-half-states self-test alias ran and passed.
    • check:dual-build-cjs-loads — reads built dist/ across ~80 workspace packages; full pnpm build not run in this worktree.
    • check:type-check-debt--re-measure refuses without the full package build closure, by design (a number measured without it would count a different world, per its own message).

None of the 4 relate to this diff's file surface (packages/lint/src/validate-translation-references.{ts,test.ts}); all are repo-wide gates whose local prerequisite is a full monorepo build/test run, which CI performs.

Changeset

.changeset/nested-conditional-validation-refs.md@objectstack/lint patch.


🤖 Generated with Claude Code

https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV


Generated by Claude Code

…_validations universe

validate-translation-references built its `_validations` universe with a flat
walk of `objects[].validations[]`, so a `conditional` rule's `then` /
`otherwise` branch — a full rule carrying its own `name`, which IS the address
`checkConditional` delegates to and `authoredRuleMessage` keys on at runtime
(packages/objectql/src/validation/rule-validator.ts) — was reported as an
orphan `translation-target-unknown`, with inverted advice (acting on it
reintroduces the exact defect the entry fixed).

The walk now descends into `then` / `otherwise` via a small recursive
collector, mirroring `evaluateRule`'s recursion (a branch may itself be a
nested `conditional`, so depth is unbounded). The wrapper's own name stays in
the universe unchanged, since a bundle entry for it is kept deliberately
elsewhere.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV
…ditional

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV
@github-actions github-actions Bot added the size/m label Sep 3, 2026
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot 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
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 5 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 2d40f9146800dcf625fc54696f6435e676b1ed96packageMentionDocs.

Which tree this was computed on

This run read content/docs from 28e6348288a4174df92fb64f5fc8409ac0fcd2c6 — the merge of head c2095e53852f6702f3625c348f92067f254e1282 into base 2d40f9146800dcf625fc54696f6435e676b1ed96, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 28e6348288a4174df92fb64f5fc8409ac0fcd2c6 && git checkout 28e6348288a4174df92fb64f5fc8409ac0fcd2c6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2d40f9146800dcf625fc54696f6435e676b1ed96 c2095e53852f6702f3625c348f92067f254e1282 && git checkout -B drift-repro 2d40f9146800dcf625fc54696f6435e676b1ed96 && git merge --no-ff c2095e53852f6702f3625c348f92067f254e1282

node scripts/docs-audit/affected-docs.mjs --json 2d40f9146800dcf625fc54696f6435e676b1ed96

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@claude

claude Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Re-trigger via update-branch (domain:devx execution seat, session session_01LAwHpn4uVuf4N1geBcD5i3).

PR-side CI on 413167ed8 is red only on Test Core (1/6) (packages/cli pnpm run test exit 1 — check-run annotations carry the exit code, the log download is refused from this container; same shard/package as the #14648 signature, and this diff does not touch packages/cli test code). This head does not contain the merged fix accb9231c7 (PR #14715; verified git merge-base --is-ancestor), so this is the "base lacks a merged fix ⇒ merge main, push a new head" branch, not a re-run: GitHub merges main into the branch (a real merge commit, no rewrite). If Test Core (1/6) reds again on the new head with the same file, it parks under the #14648 post-fix reading like PR #14774; any other red is real. #14700 stays pm:dispatched; flip + arm on green.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants