Skip to content

A conditional validation rule's when that reads through a lookup (record.account.status) saves at every door, but checkConditional evaluates it unhydrated, so every write the rule guards is refused as unevaluable #24512

Closed

Description

@objectstack-fleet

Filing gate ①, a metadata-authoring trap: the authoring doors accept a predicate the runtime can only refuse. It is the when twin of #20078, which covered the field-level slots. Found by #24468's dev (os-dev-report 6263711750, out-of-scope finding 1), and read right by PR #24508's CONTRACT_REVIEW_TIER record 6264118179 (③.10). Filed by domain:engine seat 1 (seat post #6367) · os-project-manager · session_01JtSQQ3EWsbNPzyYMug1tXy. ⛔ Not a claim. Grade and lane are triage's.

The defect (read on origin/main ca426cb65d; read in the code, not probed on a booted stack)

Where it could land (triage picks)

Pin: a conditional rule whose when reads record.account.status is refused at save, naming the slot. Or, if hydrated, it evaluates on a real row and picks its branch.

Dedupe

Two MCP search_issues queries on this repository, closed included:

None covers the when slot. #20078 (closed) is the field-level twin. #22042 (closed) brought nested then / otherwise predicates under the lint and kept when opted out. #20007 (closed) named the when: record.REF != null guard.

Dedupe words: conditional when traversal lookup unhydrated; checkConditional relationship hop; when-predicate unevaluable lookup.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: p2 · bug · domain:engine · area:records · pm:queue · route: hydrate when · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-19T10:53Z

    Triage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 3 · 2026-10-19T19:46Z
    Session: session_01AHeUBbe6S3JRawcpbLejXD (the dispatching seat; the dev runs as this session's subagent, identified by the branch)
    Account: os-warren (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-24512-conditional-when-hydration
    Worktree: objectstack-issue-24512
    Domain: domain:engine
    Seat: domain:engine#2 (seat post #20966)
    File surface:


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 24512,
    "status": "done",
    "branch": "claude/issue-24512-conditional-when-hydration",
    "pr": "#24650",
    "session": "session_01AHeUBbe6S3JRawcpbLejXD — the dispatching seat's session; this dev ran as its subagent (Claude-Session trailer on every commit)",
    "premise_still_valid": true,
    "summary": "Premise reproduced at base 1e8b628. A conditional's when reading record.account.type was refused on every write as unevaluable ('runtime: No such key: type ... The when-predicate reads 'type', which this object does not declare'). The save door/build accepted a when with a second hop, an undeclared related column or a both-ways read with 0 errors. Engine (packages/objectql rule-validator.ts only; engine.ts untouched): collectPredicateRelationships now collects a conditional's when at every depth, so the engine's existing resolvePredicateRelated preloads its hop (same loader, system authority, projection, and the per-rule undeclared-column isolation from PR 24508). checkConditional evaluates when on resolveTraversalScope's scope, as checkPredicate does; the branch keeps the untouched context. Scope refusals go through a shared traversalScopeRefusal builder that checkPredicate now also uses (its text and log are byte-identical). referentialClearRefusal applies to a when fault on a delete's reference cleanup. Lint (packages/lint validate-expressions.ts, the domain:spec rider): the top-level and nested when pass traversalHydration, so the existing hop checks judge them under EXPRESSION_INVALID. No new rule, code or message. The now-unread RulePredicate.slot is removed. MEASURED NARROWING beyond the claim: a real-engine probe (base vs head; probe not committed) shows a null-safe when (.? / has() / inline record.account != null test) on an EMPTY reference was judged by otherwise before and is refused now ('no single related record' / 'both ways'), the same as a condition. The same null-safe spellings on a SET reference silently took the wrong branch (otherwise) before and are now judged on the row. So the objectql changeset is minor BREAKING (narrowing), not patch; see open_questions. Shipped-tree exposure at 1e8b628: 0 when reads through a reference outside tests (control regex with condition finds 8 lines).",
    "tests": "Repro before the fix (rule-validator.ts / validate-expressions.ts at base): new objectql unit pins 10 failed / 38 passed, the then-pin showing constraint.fault 'runtime: No such key: type'. New lint pins: 3 failed (each 'expected [] to deeply equal [ "object 'hop_order' · validation 'gate' when" ]'). Final head a8614e4: pnpm --filter @objectstack/objectql exec vitest run --project local --maxWorkers=2 gave 447 files, 8432 tests passed, exit 0. pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 gave 148 files, 6932 tests passed, exit 0. Typecheck: objectql exit 0, lint exit 0 (each includes check:test-typecheck). Ablations, each via scripts/ablation-replace.mjs (anchor hit x1, blob changed, restored to the HEAD blob with git diff HEAD empty; subjects imported by relative src path, so no dist leg). (A) delete addFrom(r.when): 5 red, including real-engine pin (a) showing the card's 'No such key: type' refusal. (B) checkConditional evaluates ctx.merged: 6 red. (C) when scope refusal replaced by return null: 4 red. (D) when cleanup text replaced by null: 1 red. (E1/E2) lint opt-in reverted at the top-level / nested when: 5 / 4 red. Direction as expected (turn red) on every leg. test:repo not run: CI-owned, and no repo-project test is touched.",
    "gates": "Local: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) derived 70 commands, the same list at e5a2eec and at the final head a8614e4. On a8614e4 all 70 ran, each exit captured before any pipe: 70 x exit 0. --ran reports '70 derived, 70 run, 0 NOT-MEASURED, 0 UNRUN' (exit 0). The earlier battery on e5a2eec had three non-zero exits, each resolved before the final run. check:error-code-casing exit 1: new test literals lacked the field key; fixed in 9b80c99. check:engine-split-ratio exit 2: shallow-clone refusal; deepened with --shallow-since=2026-07-14, then exit 0. check:dual-build-cjs-loads exit 3: PREREQUISITE NOT MET; full turbo build 77/77 exit 0, then exit 0. check-adr-0087-registration: accepted 'not-required (no-migration-prescription)' on the BREAKING+bang+clause-②-narrowing changeset. Local-only families check:dev-prereqs and check:skill-frame-freshness: not owed, since packages/spec and the frame files are untouched. CI on PR 24650 head a8614e4 at report time: 'No other open PR may claim the same issue' success; 'Packed-tarball smoke (opt-in)' skipped; every other run in_progress (Check Changeset, Governed Surface Queue Guard, Lint · CI, release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped x3, Type Check · consumer gates / debt ledger / source gates / workspace, the card-claims-branch and single-writer-path checks, filter). Not awaited, per contract.",
    "mcp_calls": "0",
    "api_writes": "3 REST writes in 2 fleet-write relay strokes, each a POST /repos/objectstack-ai/objectstack/dispatches under the session token, executed as objectstack-fleet[bot]. Stroke 1 (fw-20261019T210614Z-372b85, run 39324819197, success, read-back identical 12749/12749 bytes, assignees os-warren): POST /repos/objectstack-ai/objectstack/pulls (draft PR 24650) and POST /repos//issues/24650/assignees. Stroke 2: POST /repos//issues/24512/comments (this os-dev-report). git push (not a REST write): the empty-branch probe plus 5 pushes of the branch.",
    "open_questions": [
    {
    "question": "The dispatch planned @objectstack/objectql as patch. The write-time probe measured a narrowing on that face: a null-safe when on an empty reference was judged by otherwise and is now refused. The changeset therefore ships objectql minor BREAKING with Clause-② no (narrowing), alongside lint minor BREAKING. Does the seat accept that classification?",
    "options": [
    "A: keep objectql minor BREAKING (narrowing), as shipped. It follows the measured behaviour and the 8d8f965 precedent for an objectql write-time narrowing.",
    "B: re-grade objectql to patch. That under-declares a measured write-time narrowing, which the Clause-② arm exists to make visible.",
    "C: special-case when to skip the empty-reference / both-ways refusal so nothing narrows at write time. This diverges when from condition, makes the verdict depend on the CEL operator written (what the hydration design forbids), and departs from the triage route 'as checkPredicate does'."
    ],
    "recommendation": "A, because the narrowing is measured, the ruled route requires when to behave as a condition does, and the changeset and PR body already carry the FROM/TO and the guard prescription for it."
    }
    ],
    "out_of_scope_findings": [],
    "deviations": [
    "objectql changeset is minor BREAKING with Clause-② no (narrowing), not the planned patch. One changeset file covers both packages (.changeset/24512-conditional-when-hydration.md). Evidence: the write-time probe table in the PR body.",
    "Triage premise 'hydration admits nothing new ... makes a write refused today evaluate' holds only for the plain spelling. Null-safe spellings were silently mis-branched before (now corrected), and on an empty reference they are now refused.",
    "referentialClearRefusal was extended to the when fault path for parity with checkPredicate (text only, pinned by ablation D).",
    "RulePredicate.slot was removed from lint: its only reader went away with the opt-out.",
    "The existing lint pin '#22042 ... OFF for a nested when' was rewritten to assert the opt-in, since it pinned exactly the branch removed.",
    "No booted dogfood pin (packages/qa/dogfood is outside the claim's file surface). The HTTP statuses 400 VALIDATION_FAILED / 422 INVALID_METADATA are the existing mappings and were NOT MEASURED on a booted stack here. The envelope is asserted at the engine (code VALIDATION_FAILED plus per-field code and constraint) and at the save-door pass (rule expression-invalid).",
    "Merged origin/main 14ccb1d before opening the PR (AGENTS §10). The incoming files (service-automation, spec/ui export) do not overlap this diff. Full suites and the 70-gate battery were re-run on the merged head.",
    "The shared clone was deepened (git fetch --shallow-since=2026-07-14 origin) for check:engine-split-ratio. It is additive to the shared object store."
    ],
    "files_changed": [
    ".changeset/24512-conditional-when-hydration.md",
    "packages/objectql/src/validation/rule-validator.ts",
    "packages/objectql/src/validation/rule-relationship-traversal.test.ts",
    "packages/objectql/src/engine-predicate-relationship.test.ts",
    "packages/lint/src/validate-expressions.ts",
    "packages/lint/src/validate-expressions.rule-one-hop.test.ts",
    "packages/lint/src/runtime-gate.object-validation-writes.test.ts"
    ],
    "line_budget": "not applicable: no skills/** or ratcheted line ledger touched; diff +498/-55 over 7 files, under the 3000-line human-merge threshold",
    "cleanup": "Worktree /home/user/objectstack-issue-24512 removed (node_modules deleted first; git worktree remove exit 0, status was clean). No dev server or monitor was started. Background runners were waited out with tail --pid. The probe test file was removed by its trap."
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    待裁:#24512 的修复带两处收窄,是否提前进 18.0 · domain:engine seat 2 · session_01AHeUBbe6S3JRawcpbLejXD · 2026-10-19T21:13Z

    ⛔ Not a release: claim 6275835421 and PR #24650 stay this seat's. The card moves pm:dispatched → needs-user-decision in the same act as this comment. PR #24650 stays draft until the answer is recorded here.

    一句话问题: 修好「when 经关联读值永远求不了值」之后,有两类写法会从「存得进去、运行时出错或悄悄走错分支」变成「保存或写入时直接被拒」。这算协议类收窄,按你定的口径要问:进不进 18.0?

    为什么现在要问

    • 分诊选「补全 when」这条路,前提是「不在门上收紧」(6265543836:"Hydration admits nothing new at the door")。dev 实测证伪了这个前提(报告 6277364966,PR 正文的两张测量表)。
    • 依据的口径:你的原话「后续的任务如果有涉及到spec元数据协议相关的修改,也可以和我沟通是否提前」(分诊常设规则 6261447528,"a change … to what a metadata door accepts or answers")。
    • 时点:18.0.0 GA 还没发。npm 上 latest 是 17.7.0,next 是 18.0.0-next.5,.changeset/pre.json 已是 mode: exit,版本 PR chore: version packages #24524 挂着。fix(objectql,lint)!: a conditional rule's when reads one hop through a reference, hydrated like a condition #24650 若在它之前合入,就随 18.0.0 发出。

    两处收窄(实测)

    1. 保存门(os validate / os build / 对象保存):when 读两跳、读关联对象没声明的列、或同时把关联当值和当对象读,今天存得进去、之后每次写入都被拒;改后保存时就拒,指出位置。
    2. 写入门:形如 record.account.?type… / has(record.account.type) && … 的空判断写法,关联为空时以前走 otherwise,改后写入被拒,拒绝文字指出正确写法(把 record.account != null 单独做外层 when)。同样的写法关联非空时,以前悄悄走错分支,改后判对。

    暴露面: 已发布代码树里 when 经关联读取的写法为 0(对照:同一正则换成 condition 命中 8 行)。

    选项

    选项 做什么 客户后果 工作量
    A 进 18.0:#24650 契约复核通过、CI 全绿后按现状落地,两处收窄随 GA 一起发(changeset 已写 BREAKING 与迁移说明) 新装用户从 18.0 起就拿到一条规则:when 与 condition 同判;坏写法保存时就被拒 已完成;剩契约复核与 CI
    B 不进 18.0:PR 停放,收窄等 19.0;或 18.x 先改成只告警 18.x 期间 when 经关联读值仍然每次写入都失败(本卡的缺陷不修),或者告警版要另做一轮 停放 0;告警版需另起 1 个 PR(无先例)

    业务含义直译: A = 新车出厂就装好正确的仪表;B = 先按旧仪表出厂,下一代车型再换。

    四轴

    • 项目长远合理性: A 让 when 和 condition 走同一套补全与同一套拒绝,特例减少;B 让 18.x 整个周期保留一个已知的「能存不能跑」缺口。
    • 实际业务拉动: 已发布代码里没有受影响的 when;拉动来自作者照平台自己的 when: record.REF != null 提示往下写时会踩到的坑。
    • 防 AI 犯错: A 在保存时响亮拒绝并给出写法;今天是「存进去、运行时每次失败或悄悄走错分支」。
    • 创业阶段不扩散: A 不新增任何规则码、门禁或键;B 的告警版要多维护一条过渡路径。

    推荐:A

    只看「项目长远合理性」选 A:趁 18.0 是新装起点,一次把 when 与 condition 对齐。B 不取:它让本卡缺陷在 18.x 里不修,或者为过渡多做一版。
    置信缺口:看不见 18.0 之外是否有人已在私有元数据里写了这类空判断式 when。
    自检:只看①选 A;②③④ 是否翻转:否。

    裁后执行

    os-decision-facets

    • ① 项目长远合理性:A 缩小特例(when 与 condition 同判);B 在 18.x 保留已知缺口。
    • ② 实际业务拉动:已发布代码 0 处受影响;作者顺着平台提示写下一步时会撞上。
    • ③ 防 AI 犯错:A 保存时响亮拒绝;现状是运行时失败或静默走错分支。
    • ④ 创业阶段不扩散:A 不增规则、码、门禁;B 的告警版多一条过渡路径。

    Prior rulings read: when,conditional,hydration,narrowing → standing rule 6261447528 (pull-forward); triage 6265543836 (route); thread: 2


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruled: A — land into 18.0 · recorded by domain:engine seat 2 · session_01AHeUBbe6S3JRawcpbLejXD · 2026-10-19T22:35Z


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Landing held · domain:engine seat 2 · session_01AHeUBbe6S3JRawcpbLejXD · 2026-10-19T22:39Z. ⛔ Not a release: claim 6275835421 and ruling 6278876096 stand.

    • What happened: the seat flipped PR fix(objectql,lint)!: a conditional rule's when reads one hop through a reference, hydrated like a condition #24650 to ready and armed auto-merge, then read main's hourly full battery red. The scheduled CI run 39331938913 on cc95f5e63 failed Dogfood Regression Gate (3/3) at "Boot example apps and exercise real user flows" (packages/qa/dogfood, pnpm run test exit 1). Its job log is behind a host this container cannot reach (403), so the failing flow is not attributed to a lane yet.
    • Why held: AGENTS.md (Multi-agent discipline §7, Main-red) says no lane PR arms the queue while a full-battery red is open. An unattributed red cannot be ruled out as this lane's. Auto-merge is disabled and the PR is back in draft. The timeline shows it never entered the merge queue.
    • Re-arm when: the red is attributed to another lane's change, or a later full battery on main is green. Nothing on the branch changes. The PASS record 6278025857 is on the head, which does not move.

    Generated by Claude Code

  7. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:engine seat 2 · session_01AHeUBbe6S3JRawcpbLejXD · 2026-10-20T01:46Z

    reading result
    PR #24650 merged through the queue 2026-10-20T01:41Z, merge 073b1b2480
    origin/main carries 073b1b2480 (git merge-base --is-ancestor); git grep -c resolveTraversalScope in packages/objectql/src/validation/rule-validator.ts = 9

    State: closed completed by the PR's Fixes; pm:dispatched removed in this act. Ruled A, into 18.0 (6278876096); the objectql and lint minor changesets ride the next Version Packages PR.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:enginepriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions