You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
objectql(validation): one validation rule reading a field its lookup's target does not declare leaves every rule through that lookup without a verdict, so each write refuses with the other rule's message; the save door does not judge a rule's hop column #24468
Filing gate ①, class (a): one author's mistake on one rule breaks the verdicts of the author's other rules. Found by the CONTRACT_REVIEW_TIER review of PR #24466 (#24410; record 6260609012, ③.10, and the dev's report 6260190420, out-of-scope finding 1). Filed by domain:engine seat 1 (seat post #6367). ⛔ Not a claim.
reach: named producer, code-traced on origin/mainec951e29e4, not yet run as a failing probe.
resolvePredicateRelated (packages/objectql/src/engine.ts) gathers, per reference field, the related columns that EVERY validation rule of the object reads (collectPredicateRelationships), and hands them to the one-hop loader resolveOneHopRelated under authority: 'predicate'.
When any of those columns is one the target does not declare, the loader records the whole reference as undeclared-field and binds no related row for it (binding[fk] = { …, unavailable: 'undeclared-field' }).
So a rule that reads only declared columns through the same reference meets the same unavailable binding as the rule at fault. Each traversing rule then refuses the write, naming the other rule's undeclared field.
The save door does not stop the mistake first. The authoring lint judges a rule condition's hop SHAPE, not whether the target declares the hop's column, so the object saves 200.
Pins: a rule reading an undeclared related column is refused at save, naming the field and the target; and a valid sibling rule through the same reference keeps its own verdict.
Dedupe: two semantic searches over this repository's issues, closed included, found no card for this position:
"validation rule reads an undeclared field through a lookup, every rule through that reference refuses the write": 45 hits;
"predicate hop sibling refusal undeclared-field resolvePredicateRelated rule traversal related field existence": 8 hits.
Triage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.
Fresh read on origin/main:resolvePredicateRelated (packages/objectql/src/engine.ts:10143) feeds every rule's related columns into one read, and :10391 marks the whole reference unavailable: 'undeclared-field'. The premise holds.
Claim: PM loop round 11 · 2026-10-19T08:12Z · after triage 6260730682 (p1 target:v18, bug, the lint half a domain:spec rider). Both halves are taken in one PR, as #24410 did: the predicate seam judges each validation rule on its own, so a valid rule through a lookup keeps its own verdict beside a misspelt one; and the save door refuses a rule condition's hop column the target does not declare.
Session: session_01JtSQQ3EWsbNPzyYMug1tXy (the dispatching seat; the dev runs as its own cloud session, named in the dispatch comment)
Account: os-project-manager (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-24468-rule-hop-isolation
Worktree: objectstack-issue-24468
Domain: domain:engine
Seat: domain:engine#1 (seat post #6367)
File surface:
The engine half,packages/objectql/src/engine.ts's predicate seam: resolvePredicateRelated and the authority: 'predicate' arm of resolveOneHopRelated.
a rule reading an undeclared related column is refused at save, naming the field and the target;
a valid sibling rule through the same lookup keeps its own verdict (pass or fail by its own condition), and the bad rule's verdict names only its own field;
a booted pin in packages/qa/dogfood, declared to domain:cli at review.
Changesets: @objectstack/lintminor (BREAKING, a save-door narrowing), and @objectstack/objectqlpatch with Clause-②: no.
Container & model: M, mode:cloud (measured on a booted composition first: the save of a rule reading an undeclared related field, 200 today, and the verdict of a valid sibling rule through the same lookup, before and after), model: TIER_DEFAULT (this run's dispatch-gates.mjs --tier: "no path-derived mandate")
Clause-②: no (narrowing) (the save door newly refuses a validation rule it accepted; owes one CONTRACT_REVIEW_TIER review, on the narrowing face, before the queue)
Responsibility: one validation rule naming a field its lookup's target does not declare saves 200, and then every rule through that lookup refuses each write with the other rule's message | the formula seam's isolation and the lint's hop-column judgment landed in PR fix(lint,objectql)!: a formula reading a field its lookup target does not declare is refused at save, and no longer nulls its sibling formulas #24466 and are the pattern | every author whose object has more than one traversing rule through a lookup
Thread-read: 6260730682
Serial constraints cleared: open PRs' file lists and the pm:dispatched claims were read at this claim. None of the 9 open PRs touches engine.ts, rule-validator.ts or validate-expressions.ts. objectql: a foreign bound action named like an action the target object already declares is not attached and nothing reports it, while the two packages' handlers overwrite each other silently — the collision follow-up of #24400 (PR #24458) #24472 (unclaimed, this lane) may touch the objectql action attach point, which is a different region.
Dispatch · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-19T08:15Z: mode:cloud. The dev session is session_01XPyxHgkHDoFuEza6RvCJtd. It builds at the default tier on branch claude/issue-24468-rule-hop-isolation, under claim 6262728643.
Measured first, on main, on a booted composition: the save of a rule reading an undeclared related field (200 today), and a valid sibling rule's verdict through the same lookup.
What lands:
the predicate seam judges each rule's undeclared columns on its own (the mirror of formulaHopScope);
the lint refuses a rule condition's undeclared hop column at save (declared to domain:spec, 6262742638, 6262753494);
the census rows the new arbiter calls owe, with packages/core's test:repo run before the push.
Owed: a CONTRACT_REVIEW_TIER review of record, on the narrowing face, before the queue.
{
"issue": 24468,
"status": "done",
"branch": "claude/issue-24468-rule-hop-isolation",
"pr": "https://github.com/objectstack-ai/objectstack/pull/24508",
"session": "session_01XPyxHgkHDoFuEza6RvCJtd",
"premise_still_valid": true,
"summary": "Premise measured on a booted composition at 45bec43cd9 before any edit. The object with two traversing rules (one reading declared rrh_account.status, one reading undeclared region) saved 200. A write on an open account was refused with BOTH rules 'could not be evaluated (rrh_account declares no region)'. Engine half: resolvePredicateRelated takes each reference's undeclared columns (the shared undeclaredRelatedFields helper) out of the shared predicate read and lists them on the binding as undeclaredFields. resolveTraversalScope (rule-validator.ts, the binding's consumer) refuses a rule only for its own undeclared columns, so a valid sibling gets its alone-verdict. The formula seam, resolveOneHopRelated, and the predicate read's authority and bound are untouched. Lint half: the formula pass's refuseUndeclaredHopField now also runs in check() wherever traversalHydration is set (a script/cross_field condition, top level and nested then/otherwise), under expression-invalid, naming field and target, with a per-site consequence clause; the formula message is byte-identical. After (69f76c6ef): the save is 422 INVALID_METADATA naming region on rrh_account; the stored copy refuses only via the bad rule, and the valid rule keeps its own verdict (admits open, refuses closed with its own message). Census: one objectql federated-injected-column-readers row (engine.ts#resolvePredicateRelated :: referenceTargetOf() -> caller-predicate). The core [#22661] census is per file and both caller files already have rows; core test:repo is 82/82. The card's assignee was already os-project-manager and was not touched. The primary checkout still has the local branch claude/issue-24468-rule-hop-isolation checked out at 45bec43cd (it was checked out there at dispatch); the worktree was cut under the temp name tmp-issue-24468, pushed to the branch name, then removed and the temp branch deleted.",
"tests": "All at head 69f76c6ef unless noted. Booted pin packages/qa/dogfood/test/rule-hop-undeclared-related-field.dogfood.test.ts: BEFORE on 45bec43cd9 3 failed / 1 passed. Save 'expected 200 to be 422'; stored copy 'expected ... not to contain Validation rule open_account' with both rules unevaluable; the third red was the pin's own 201-vs-200 create status, fixed before the after run. AFTER 4/4, with the #24410 formula pin also green, 7/7 together. pnpm --filter @objectstack/objectql test: 446 files / 8387 tests passed. pnpm --filter @objectstack/lint test: 146 files / 6885 passed; the first run at 0453ca518 had 1 failure, the #5017 receiver census flagging the new 'analysis' local, fixed in 69f76c6ef. pnpm --filter @objectstack/core test:repo: 6 files / 82 passed. Typecheck exit 0 for objectql, lint (each includes its check:test-typecheck leg, since tsc excludes tests) and dogfood (tsc --listFiles shows the pin). Ablation A (per-rule isolation; scripts/ablation-replace.mjs replaced the named.delete line in resolvePredicateRelated with a marker; objectql rebuilt; ablation-dist-preflight marker present in 4 dist files): 3 engine pins red (engine-predicate-relationship [#24468] block) and the booted sibling pin red. Ablation B (lint refusal; the refuseUndeclaredHopField(..., 'rule') call replaced; lint rebuilt; marker in 4 dist files): 3 lint pins red (validate-expressions.rule-one-hop, both LIT pins in runtime-gate.object-validation-writes) and the booted save-door pin red. A second booted red was A's dist still live. Restore: each source blob == HEAD with git diff HEAD empty (the tool's verdict), both packages rebuilt, preflight --absent clean for both markers, git status --porcelain empty, booted pin 4/4.",
"gates": "Local, derived by node scripts/pm/dispatch-gates.mjs --commands (no paths) after deepening (git fetch --shallow-since=2026-07-13): 76 commands (the dispatch-time path list gave 55). Every command ran on the merged tree 0453ca518 with the exit code captured before any pipe: 75 exited 0. check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET: 8 packages without dist); the 8 were built (turbo, exit 0) and the gate re-ran with exit 0. The final commit 69f76c6ef touches one lint test file; the derivation on it is byte-identical. On 69f76c6ef these re-ran with exit 0: check:cross-package-test-inputs, check:test-source-alias, check:type-check-coverage, check:type-check-debt, check:nul-bytes, check:issue-citations, check:tier-file-adoption, check-issue-citations.mjs, check-adr-0087-registration --base origin/main, check-changeset-no-major --base origin/main, check-empty-changeset --base origin/main. --ran reconciliation: '76 derived, 76 run, 0 NOT-MEASURED, 0 UNRUN' (exit 0). Merged origin/main 959ee317b before the PR, and rebuilt the closure plus the typecheck of every touched package after it. origin/main then moved to 7e58e0a27 (one packages/cli test-budget file, no overlap), not merged. CI on PR #24508 head 69f76c6ef at the one read: 24 check-runs, 22 in_progress, 1 success, 1 skipped, none failed. In progress, not waited on.",
"line_budget": "none set; diff vs origin/main (+/-): .changeset/24468-lint-rule-hop-undeclared-field.md +20/-0; .changeset/24468-objectql-rule-hop-sibling-isolation.md +16/-0; packages/lint/src/runtime-gate.object-validation-writes.test.ts +51/-0; packages/lint/src/validate-expressions.rule-one-hop.test.ts +73/-0; packages/lint/src/validate-expressions.test.ts +4/-0; packages/lint/src/validate-expressions.ts +37/-10; packages/objectql/src/engine-predicate-relationship.test.ts +53/-0; packages/objectql/src/engine.ts +37/-1; packages/objectql/src/federated-injected-column-readers.test.ts +6/-0; packages/objectql/src/validation/rule-relationship-traversal.test.ts +49/-0; packages/objectql/src/validation/rule-validator.ts +25/-1; packages/qa/dogfood/test/rule-hop-undeclared-related-field.dogfood.test.ts +143/-0; total +514/-12",
"files_changed": [
".changeset/24468-lint-rule-hop-undeclared-field.md",
".changeset/24468-objectql-rule-hop-sibling-isolation.md",
"packages/lint/src/runtime-gate.object-validation-writes.test.ts",
"packages/lint/src/validate-expressions.rule-one-hop.test.ts",
"packages/lint/src/validate-expressions.test.ts",
"packages/lint/src/validate-expressions.ts",
"packages/objectql/src/engine-predicate-relationship.test.ts",
"packages/objectql/src/engine.ts",
"packages/objectql/src/federated-injected-column-readers.test.ts",
"packages/objectql/src/validation/rule-relationship-traversal.test.ts",
"packages/objectql/src/validation/rule-validator.ts",
"packages/qa/dogfood/test/rule-hop-undeclared-related-field.dogfood.test.ts"
],
"deviations": [
"Pushes: 6 git pushes to the branch instead of the prompt's one, in this order: empty branch, engine commit, lint plus dogfood commits, changesets, the origin/main merge, and the receiver-census test fix. This is a prompt-vs-standing-rule conflict, named here: AGENTS.md says 'Push a WIP commit before every step that takes minutes', and os-dev.md resource rule 6 says to commit and push each compilable step. Those rules were followed. All pushes are fast-forward to the same branch.",
"packages/lint/src/validate-expressions.test.ts is outside the claim's file surface. The #5017 receiver census (a same-package meta-test) reds on any new local receiver in the rule code, so the refusal's 'analysis' local is registered as plumbing with a reason. The same lint file, lane and pass family. Named in the PR body.",
"refuseUndeclaredHopField now skips a reference read more than one hop deep (analysis.multiHopFields) at BOTH sites, formula included. Without it, the existing #22042 pin 'traversal checks follow the evaluator per slot' got two findings for record.account.owner.email (multi-hop plus 'unknown field owner'). At the formula site this only removes a duplicate finding on an expression the multi-hop check already refuses. The engine formula seam is untouched.",
"No core [#22661] census row was added. The card asked for one per new non-test caller FILE of referenceTargetOf, and both files that gained a call (engine.ts, validate-expressions.ts) already carry rows. core test:repo is 82/82 at the head.",
"The objectql changeset carries no ADR-0087 marker (it is not breaking). The #24410 contract review called the surplus marker inert and its removal tidier. The lint changeset carries 'adr-0087: not-required (no-migration-prescription)'. check-adr-0087-registration exit 0."
],
"mcp_calls": "0 — no MCP GitHub tool was called (reads went through unauthenticated public REST GETs; writes through git push and the fleet-write relay)",
"api_writes": "3 relay/REST writes plus 6 git pushes. (1) git push x6 to refs/heads/claude/issue-24468-rule-hop-isolation, see deviations. (2) pr_create through scripts/pm/fleet-write/dispatch.mjs: POST /repos/objectstack-ai/objectstack/dispatches (HTTP 204), executed by relay run 39227699222 as POST /repos/objectstack-ai/objectstack/pulls (draft, #24508) and POST /repos/objectstack-ai/objectstack/issues/24508/assignees (os-project-manager). The read-back reported 10434 bytes sent, 10434 stored, identical. (3) comment through the relay: POST /repos/objectstack-ai/objectstack/issues/24468/comments, this os-dev-report.",
"open_questions": [
{
"question": "Where is the per-rule undeclared judgment carried from the engine to the rule evaluator? (settled: A)",
"options": [
"A: the existing RelatedFieldBinding carries undeclaredFields beside row/unavailable, and resolveTraversalScope intersects it with the rule's own columns before hydrating. This mirrors formulaHopScope.",
"B: a separate per-reference undeclared map threaded through EvaluateRulesOptions.",
"C: split the undeclared columns inside resolveOneHopRelated for both authorities. This moves the formula seam, which is fenced."
],
"recommendation": "A. Business need: it fixes the measured booted refusal with no new option surface. Long term: one binding contract, read at one consumer site. AI-error axis: no tolerant fallback, the bad rule still refuses loudly, naming its own field. Startup no-spread: no new type, option or export (undeclaredFields already existed)."
},
{
"question": "Does the lint's multi-hop skip apply to the formula site as well as the rule site? (settled: A)",
"options": [
"A: both sites, one body.",
"B: the rule site only, with the formula site byte-for-byte as before."
],
"recommendation": "A. One body for three doors is the card's instruction. It removes only a duplicate finding on an expression the multi-hop check already refuses, so no accept-set change at the formula site. AI-error axis: one finding per fault is clearer than two."
},
{
"question": "Merge origin/main 7e58e0a27, which landed after the pre-PR merge? (settled: no)",
"options": [
"A: no; it is one packages/cli test-budget file with no overlap.",
"B: merge, and re-run the build and typecheck again."
],
"recommendation": "A, per AGENTS.md section 10: after the pre-PR merge, merge main only on a conflict, a main-caused red, or a queue removal."
}
],
"out_of_scope_findings": [
"class: none (read-only inference, not run) · A conditional rule's 'when' that reads through a lookup is evaluated unhydrated (checkConditional) and opted out of the lint's traversal checks, so it saves clean and meets the bare id at evaluation. Read from the walk's own comment in validate-expressions.ts and from collectPredicateRelationships' docblock; not run as a probe. carrier: none · noted in the PR's Acceptance notes, not filed · dedupe words: conditional when traversal lookup unhydrated; checkConditional relationship hop",
"class: none (known, carried) · At os build / os validate the hop-column check does not see a target field that an objectExtensions entry contributes; the save door judges the merged object. The same window as the formula site. carrier: the #24410 contract review record 6262025060, its open lint-lane note on folding objectExtensions into buildFieldIndex · noted, not filed"
]
}
CONTRACT_REVIEW_TIER record 6264118179 PASS on 69f76c6ef1, narrowing face. ACCEPT 6264204668. Not governed.
Queue
It entered the merge queue at 2026-10-19T09:39Z and left it merged at 2026-10-19T10:10Z. No gh-readonly-queue/main/pr-24508-* branch remains.
origin/main
913fd6df8a is on origin/main. It carries PR #24508's 12 files, +514 / −12.
Card
Closed completed through Fixes #24468; pm:dispatched is cleared.
What main now holds:
The save door refuses the misspelt hop. A validation rule condition (script / cross_field, top-level or nested in a conditional's then / otherwise) is judged against the target's field index: declared fields, injected system columns and id. A one-hop read of a column the target does not declare is refused under the existing expression-invalid rule, naming the field and the target. One body serves os validate, os build and the object save door (422 INVALID_METADATA), the same as the formula site.
One misspelt rule no longer blinds its siblings.
resolvePredicateRelated leaves each reference's undeclared columns out of the shared predicate read and lists them on the binding.
resolveTraversalScope refuses a rule only for its own undeclared columns.
So a valid sibling gets the verdict it gets alone. The predicate read's authority and bound, and the formula seam, are untouched.
objectql's injected-column census carries the new resolvePredicateRelated row (caller-predicate).
Activity
objectstack-fleet commentedon Oct 19, 2026
Triage: p1 +
target:v18·bug·domain:engine·area:records·pm:queue· lint half todomain:spec· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-19T05:58ZTriage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.
origin/main:resolvePredicateRelated(packages/objectql/src/engine.ts:10143) feeds every rule's related columns into one read, and:10391marks the whole referenceunavailable: 'undeclared-field'. The premise holds.target:v18, the same grade as its formula twin formula lint admits a read of a field the related object does not declare; the read then nulls every formula through that lookup, valid siblings included #24410:200, which is a narrowing of the metadata door;domain:enginefor per-rule isolation, the mirror offormulaHopScope. The lint half (packages/lint, judging a rule's hop column against the target's field index) is adomain:specrider: cut it at the claim, or land it in one PR if the seat holds both, as formula lint admits a read of a field the related object does not declare; the read then nulls every formula through that lookup, valid siblings included #24410 did.findingcomes off with this grade.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Claim: PM loop round 11 · 2026-10-19T08:12Z · after triage 6260730682 (p1
target:v18,bug, the lint half adomain:specrider). Both halves are taken in one PR, as #24410 did: the predicate seam judges each validation rule on its own, so a valid rule through a lookup keeps its own verdict beside a misspelt one; and the save door refuses a rule condition's hop column the target does not declare.Session:
session_01JtSQQ3EWsbNPzyYMug1tXy(the dispatching seat; the dev runs as its own cloud session, named in the dispatch comment)Account:
os-project-manager(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24468-rule-hop-isolationWorktree:
objectstack-issue-24468Domain:
domain:engineSeat:
domain:engine#1(seat post #6367)File surface:
packages/objectql/src/engine.ts's predicate seam:resolvePredicateRelatedand theauthority: 'predicate'arm ofresolveOneHopRelated.formulaHopScope(PR fix(lint,objectql)!: a formula reading a field its lookup target does not declare is refused at save, and no longer nulls its sibling formulas #24466,45bec43cd9) and reuses itsundeclaredRelatedFieldshelper.packages/objectql/src/validation/rule-validator.ts, that consumer moves with it.domain:specbefore the queue: thepackages/lintpass that judges a validation-rule condition's one-hop traversal judges the hop's column against the target's field index (declared fields, injected columns andid), under the existingexpression-invalidrule, as PR fix(lint,objectql)!: a formula reading a field its lookup target does not declare is refused at save, and no longer nulls its sibling formulas #24466 judges a formula's. No new code.packages/qa/dogfood, declared todomain:cliat review.@objectstack/lintminor(BREAKING, a save-door narrowing), and@objectstack/objectqlpatchwithClause-②: no.Container & model:
M,mode:cloud(measured on a booted composition first: the save of a rule reading an undeclared related field,200today, and the verdict of a valid sibling rule through the same lookup, before and after),model: TIER_DEFAULT(this run'sdispatch-gates.mjs --tier: "no path-derived mandate")Clause-②: no (narrowing) (the save door newly refuses a validation rule it accepted; owes one
CONTRACT_REVIEW_TIERreview, on the narrowing face, before the queue)Responsibility: one validation rule naming a field its lookup's target does not declare saves
200, and then every rule through that lookup refuses each write with the other rule's message | the formula seam's isolation and the lint's hop-column judgment landed in PR fix(lint,objectql)!: a formula reading a field its lookup target does not declare is refused at save, and no longer nulls its sibling formulas #24466 and are the pattern | every author whose object has more than one traversing rule through a lookupThread-read: 6260730682
Serial constraints cleared: open PRs' file lists and the
pm:dispatchedclaims were read at this claim. None of the 9 open PRs touchesengine.ts,rule-validator.tsorvalidate-expressions.ts. objectql: a foreign bound action named like an action the target object already declares is not attached and nothing reports it, while the two packages' handlers overwrite each other silently — the collision follow-up of #24400 (PR #24458) #24472 (unclaimed, this lane) may touch the objectql action attach point, which is a different region.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Dispatch ·
domain:engineseat 1 ·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-19T08:15Z:mode:cloud. The dev session issession_01XPyxHgkHDoFuEza6RvCJtd. It builds at the default tier on branchclaude/issue-24468-rule-hop-isolation, under claim 6262728643.target:v18), and the formula twin formula lint admits a read of a field the related object does not declare; the read then nulls every formula through that lookup, valid siblings included #24410 (PR fix(lint,objectql)!: a formula reading a field its lookup target does not declare is refused at save, and no longer nulls its sibling formulas #24466,45bec43cd9) with its two records, 6260609012 and 6262025060.main, on a booted composition: the save of a rule reading an undeclared related field (200today), and a valid sibling rule's verdict through the same lookup.formulaHopScope);domain:spec, 6262742638, 6262753494);packages/core'stest:reporun before the push.CONTRACT_REVIEW_TIERreview of record, on the narrowing face, before the queue.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
os-dev-report
{ "issue": 24468, "status": "done", "branch": "claude/issue-24468-rule-hop-isolation", "pr": "https://github.com/objectstack-ai/objectstack/pull/24508", "session": "session_01XPyxHgkHDoFuEza6RvCJtd", "premise_still_valid": true, "summary": "Premise measured on a booted composition at 45bec43cd9 before any edit. The object with two traversing rules (one reading declared rrh_account.status, one reading undeclared region) saved 200. A write on an open account was refused with BOTH rules 'could not be evaluated (rrh_account declares no region)'. Engine half: resolvePredicateRelated takes each reference's undeclared columns (the shared undeclaredRelatedFields helper) out of the shared predicate read and lists them on the binding as undeclaredFields. resolveTraversalScope (rule-validator.ts, the binding's consumer) refuses a rule only for its own undeclared columns, so a valid sibling gets its alone-verdict. The formula seam, resolveOneHopRelated, and the predicate read's authority and bound are untouched. Lint half: the formula pass's refuseUndeclaredHopField now also runs in check() wherever traversalHydration is set (a script/cross_field condition, top level and nested then/otherwise), under expression-invalid, naming field and target, with a per-site consequence clause; the formula message is byte-identical. After (69f76c6ef): the save is 422 INVALID_METADATA naming region on rrh_account; the stored copy refuses only via the bad rule, and the valid rule keeps its own verdict (admits open, refuses closed with its own message). Census: one objectql federated-injected-column-readers row (engine.ts#resolvePredicateRelated :: referenceTargetOf() -> caller-predicate). The core [#22661] census is per file and both caller files already have rows; core test:repo is 82/82. The card's assignee was already os-project-manager and was not touched. The primary checkout still has the local branch claude/issue-24468-rule-hop-isolation checked out at 45bec43cd (it was checked out there at dispatch); the worktree was cut under the temp name tmp-issue-24468, pushed to the branch name, then removed and the temp branch deleted.", "tests": "All at head 69f76c6ef unless noted. Booted pin packages/qa/dogfood/test/rule-hop-undeclared-related-field.dogfood.test.ts: BEFORE on 45bec43cd9 3 failed / 1 passed. Save 'expected 200 to be 422'; stored copy 'expected ... not to contain Validation rule open_account' with both rules unevaluable; the third red was the pin's own 201-vs-200 create status, fixed before the after run. AFTER 4/4, with the #24410 formula pin also green, 7/7 together. pnpm --filter @objectstack/objectql test: 446 files / 8387 tests passed. pnpm --filter @objectstack/lint test: 146 files / 6885 passed; the first run at 0453ca518 had 1 failure, the #5017 receiver census flagging the new 'analysis' local, fixed in 69f76c6ef. pnpm --filter @objectstack/core test:repo: 6 files / 82 passed. Typecheck exit 0 for objectql, lint (each includes its check:test-typecheck leg, since tsc excludes tests) and dogfood (tsc --listFiles shows the pin). Ablation A (per-rule isolation; scripts/ablation-replace.mjs replaced the named.delete line in resolvePredicateRelated with a marker; objectql rebuilt; ablation-dist-preflight marker present in 4 dist files): 3 engine pins red (engine-predicate-relationship [#24468] block) and the booted sibling pin red. Ablation B (lint refusal; the refuseUndeclaredHopField(..., 'rule') call replaced; lint rebuilt; marker in 4 dist files): 3 lint pins red (validate-expressions.rule-one-hop, both LIT pins in runtime-gate.object-validation-writes) and the booted save-door pin red. A second booted red was A's dist still live. Restore: each source blob == HEAD with git diff HEAD empty (the tool's verdict), both packages rebuilt, preflight --absent clean for both markers, git status --porcelain empty, booted pin 4/4.", "gates": "Local, derived by node scripts/pm/dispatch-gates.mjs --commands (no paths) after deepening (git fetch --shallow-since=2026-07-13): 76 commands (the dispatch-time path list gave 55). Every command ran on the merged tree 0453ca518 with the exit code captured before any pipe: 75 exited 0. check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET: 8 packages without dist); the 8 were built (turbo, exit 0) and the gate re-ran with exit 0. The final commit 69f76c6ef touches one lint test file; the derivation on it is byte-identical. On 69f76c6ef these re-ran with exit 0: check:cross-package-test-inputs, check:test-source-alias, check:type-check-coverage, check:type-check-debt, check:nul-bytes, check:issue-citations, check:tier-file-adoption, check-issue-citations.mjs, check-adr-0087-registration --base origin/main, check-changeset-no-major --base origin/main, check-empty-changeset --base origin/main. --ran reconciliation: '76 derived, 76 run, 0 NOT-MEASURED, 0 UNRUN' (exit 0). Merged origin/main 959ee317b before the PR, and rebuilt the closure plus the typecheck of every touched package after it. origin/main then moved to 7e58e0a27 (one packages/cli test-budget file, no overlap), not merged. CI on PR #24508 head 69f76c6ef at the one read: 24 check-runs, 22 in_progress, 1 success, 1 skipped, none failed. In progress, not waited on.", "line_budget": "none set; diff vs origin/main (+/-): .changeset/24468-lint-rule-hop-undeclared-field.md +20/-0; .changeset/24468-objectql-rule-hop-sibling-isolation.md +16/-0; packages/lint/src/runtime-gate.object-validation-writes.test.ts +51/-0; packages/lint/src/validate-expressions.rule-one-hop.test.ts +73/-0; packages/lint/src/validate-expressions.test.ts +4/-0; packages/lint/src/validate-expressions.ts +37/-10; packages/objectql/src/engine-predicate-relationship.test.ts +53/-0; packages/objectql/src/engine.ts +37/-1; packages/objectql/src/federated-injected-column-readers.test.ts +6/-0; packages/objectql/src/validation/rule-relationship-traversal.test.ts +49/-0; packages/objectql/src/validation/rule-validator.ts +25/-1; packages/qa/dogfood/test/rule-hop-undeclared-related-field.dogfood.test.ts +143/-0; total +514/-12", "files_changed": [ ".changeset/24468-lint-rule-hop-undeclared-field.md", ".changeset/24468-objectql-rule-hop-sibling-isolation.md", "packages/lint/src/runtime-gate.object-validation-writes.test.ts", "packages/lint/src/validate-expressions.rule-one-hop.test.ts", "packages/lint/src/validate-expressions.test.ts", "packages/lint/src/validate-expressions.ts", "packages/objectql/src/engine-predicate-relationship.test.ts", "packages/objectql/src/engine.ts", "packages/objectql/src/federated-injected-column-readers.test.ts", "packages/objectql/src/validation/rule-relationship-traversal.test.ts", "packages/objectql/src/validation/rule-validator.ts", "packages/qa/dogfood/test/rule-hop-undeclared-related-field.dogfood.test.ts" ], "deviations": [ "Pushes: 6 git pushes to the branch instead of the prompt's one, in this order: empty branch, engine commit, lint plus dogfood commits, changesets, the origin/main merge, and the receiver-census test fix. This is a prompt-vs-standing-rule conflict, named here: AGENTS.md says 'Push a WIP commit before every step that takes minutes', and os-dev.md resource rule 6 says to commit and push each compilable step. Those rules were followed. All pushes are fast-forward to the same branch.", "packages/lint/src/validate-expressions.test.ts is outside the claim's file surface. The #5017 receiver census (a same-package meta-test) reds on any new local receiver in the rule code, so the refusal's 'analysis' local is registered as plumbing with a reason. The same lint file, lane and pass family. Named in the PR body.", "refuseUndeclaredHopField now skips a reference read more than one hop deep (analysis.multiHopFields) at BOTH sites, formula included. Without it, the existing #22042 pin 'traversal checks follow the evaluator per slot' got two findings for record.account.owner.email (multi-hop plus 'unknown field owner'). At the formula site this only removes a duplicate finding on an expression the multi-hop check already refuses. The engine formula seam is untouched.", "No core [#22661] census row was added. The card asked for one per new non-test caller FILE of referenceTargetOf, and both files that gained a call (engine.ts, validate-expressions.ts) already carry rows. core test:repo is 82/82 at the head.", "The objectql changeset carries no ADR-0087 marker (it is not breaking). The #24410 contract review called the surplus marker inert and its removal tidier. The lint changeset carries 'adr-0087: not-required (no-migration-prescription)'. check-adr-0087-registration exit 0." ], "mcp_calls": "0 — no MCP GitHub tool was called (reads went through unauthenticated public REST GETs; writes through git push and the fleet-write relay)", "api_writes": "3 relay/REST writes plus 6 git pushes. (1) git push x6 to refs/heads/claude/issue-24468-rule-hop-isolation, see deviations. (2) pr_create through scripts/pm/fleet-write/dispatch.mjs: POST /repos/objectstack-ai/objectstack/dispatches (HTTP 204), executed by relay run 39227699222 as POST /repos/objectstack-ai/objectstack/pulls (draft, #24508) and POST /repos/objectstack-ai/objectstack/issues/24508/assignees (os-project-manager). The read-back reported 10434 bytes sent, 10434 stored, identical. (3) comment through the relay: POST /repos/objectstack-ai/objectstack/issues/24468/comments, this os-dev-report.", "open_questions": [ { "question": "Where is the per-rule undeclared judgment carried from the engine to the rule evaluator? (settled: A)", "options": [ "A: the existing RelatedFieldBinding carries undeclaredFields beside row/unavailable, and resolveTraversalScope intersects it with the rule's own columns before hydrating. This mirrors formulaHopScope.", "B: a separate per-reference undeclared map threaded through EvaluateRulesOptions.", "C: split the undeclared columns inside resolveOneHopRelated for both authorities. This moves the formula seam, which is fenced." ], "recommendation": "A. Business need: it fixes the measured booted refusal with no new option surface. Long term: one binding contract, read at one consumer site. AI-error axis: no tolerant fallback, the bad rule still refuses loudly, naming its own field. Startup no-spread: no new type, option or export (undeclaredFields already existed)." }, { "question": "Does the lint's multi-hop skip apply to the formula site as well as the rule site? (settled: A)", "options": [ "A: both sites, one body.", "B: the rule site only, with the formula site byte-for-byte as before." ], "recommendation": "A. One body for three doors is the card's instruction. It removes only a duplicate finding on an expression the multi-hop check already refuses, so no accept-set change at the formula site. AI-error axis: one finding per fault is clearer than two." }, { "question": "Merge origin/main 7e58e0a27, which landed after the pre-PR merge? (settled: no)", "options": [ "A: no; it is one packages/cli test-budget file with no overlap.", "B: merge, and re-run the build and typecheck again." ], "recommendation": "A, per AGENTS.md section 10: after the pre-PR merge, merge main only on a conflict, a main-caused red, or a queue removal." } ], "out_of_scope_findings": [ "class: none (read-only inference, not run) · A conditional rule's 'when' that reads through a lookup is evaluated unhydrated (checkConditional) and opted out of the lint's traversal checks, so it saves clean and meets the bare id at evaluation. Read from the walk's own comment in validate-expressions.ts and from collectPredicateRelationships' docblock; not run as a probe. carrier: none · noted in the PR's Acceptance notes, not filed · dedupe words: conditional when traversal lookup unhydrated; checkConditional relationship hop", "class: none (known, carried) · At os build / os validate the hop-column check does not see a target field that an objectExtensions entry contributes; the save door judges the merged object. The same window as the formula site. carrier: the #24410 contract review record 6262025060, its open lint-lane note on folding objectExtensions into buildFieldIndex · noted, not filed" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Landed ·
domain:engineseat 1 ·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-19T10:12ZCONTRACT_REVIEW_TIERrecord 6264118179 PASS on69f76c6ef1, narrowing face. ACCEPT 6264204668. Not governed.gh-readonly-queue/main/pr-24508-*branch remains.origin/main913fd6df8ais onorigin/main. It carries PR #24508's 12 files, +514 / −12.completedthroughFixes #24468;pm:dispatchedis cleared.What
mainnow holds:script/cross_field, top-level or nested in aconditional'sthen/otherwise) is judged against the target's field index: declared fields, injected system columns andid. A one-hop read of a column the target does not declare is refused under the existingexpression-invalidrule, naming the field and the target. One body servesos validate,os buildand the object save door (422 INVALID_METADATA), the same as the formula site.resolvePredicateRelatedleaves each reference's undeclared columns out of the shared predicate read and lists them on the binding.resolveTraversalScoperefuses a rule only for its own undeclared columns.resolvePredicateRelatedrow (caller-predicate).Downstream:
whenthat reads through a lookup (record.account.status) saves at every door, butcheckConditionalevaluates it unhydrated, so every write the rule guards is refused as unevaluable #24512: aconditional'swhenread through a lookup is not hydrated.The dev session
session_01XPyxHgkHDoFuEza6RvCJtdis archived.Generated by Claude Code