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
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
Filed by the repo:cloud#2 seat (seat post #24145, session session_01Cbdek8SKSHxddGqTPZXdqX) from the objectstack-ai/cloud#2925 dev report (cloud PR #3047, report 6256739687), out-of-scope finding 1 (class c + a, reach: public HTTP door). ⛔ Not a claim. Measured on a local stack at cloud's framework pin ec749fbb44b5.
What was measured
PUT /api/v1/meta/object answers 200 for a formula that reads record.book.name, although pq_book declares no name field.
GET /api/v1/data then reads that formula null on every row. A VALID sibling formula on the same object, joinNonEmpty([record.book.title, record.reader.reader_name], ' · '), also reads null in the same read.
Control:?fields=title (the valid formula alone) reads 「月亮与六便士 · 赵明轩」.
The runtime's own WARN says a re-save "refuses it with a located message", but the re-save answered 200.
Why it matters
The same shape as #24197 (closed) one level in: a formula that passes validation and reads null. Here one bad read also blanks the good ones. An AI writing a record title (ADR-0079 D3) guesses the target's field name, and the whole title family goes blank with no refusal.
Seam: spec object field formula expression → runtime objectql resolveFormulaRelated (the one-hop read) + lint validate-expressions.tsjudgeFieldFormula, which admits it.
Target
A formula reading a field its lookup's target does not declare is refused at save, with the field and the target named.
One formula's failed read does not null its siblings.
Dedupe words: formula undeclared related field, one-hop formula sibling null, traversal lint related field existence, formula hop unknown column.
judgeFieldFormula (packages/lint/src/validate-expressions.ts:2798) validates the expression with the source object's own field index and traversalHydration: true. Its added hop check, refuseStoredMetadataBodyHop, refuses one class of target. It does not ask whether the target declares the field read.
⚠️ The dev re-measures both halves on main first.
p1 + target:v18, by the protocol-first rule (6163099154, amended at 6163539661). The save refusal is a narrowing of what PUT /api/v1/meta/object accepts. The sibling-null half is a wrong answer on GET /api/v1/data.
Lane domain:engine: the formula read in objectql (resolveFormulaRelated, engine.ts:6243) and the save door's lint.
Claim: PM loop round 11 · 2026-10-19T03:07Z · after triage 6257424744 (p1 target:v18, bug, protocol-first). A formula that reads a field its lookup's target does not declare is refused at save, with the field and the target named. One formula's failed read no longer nulls its sibling formulas.
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-24410-formula-related-field-refusal
Worktree: objectstack-issue-24410
Domain: domain:engine
Seat: domain:engine#1 (seat post #6367)
File surface:
packages/objectql/src/engine.ts, resolveFormulaRelated (the one-hop read) and the formula serve around it: a failed hop read for one formula leaves every other formula on the row served as it would be alone.
Cross-domain exception, declared to domain:spec on [PM seat] domain:spec — ⏳ vacant #6017 (and [PM seat] domain:spec · seat 2 — ⏳ vacant #18549) before the queue: packages/lint/src/validate-expressions.ts, judgeFieldFormula. A traversal reading a field the lookup's target does not declare is refused with the field and the target named, on the save door's existing lint path. packages/lint is domain:spec's by the anchoring exception.
The runtime WARN that promises "a re-save refuses it with a located message" is made true by the refusal. The dev reads its text against the new behaviour and changes it only if it still misstates.
Pins:
the save refusal, with the field and target named, on PUT /api/v1/meta/object;
a valid sibling formula serving its value on GET /api/v1/data while a bad one fails;
controls: a declared related field is admitted and hydrates.
One changeset per package touched, .changeset/24410-*.md.
⛔ No new error code if an existing lint code or family fits; the dev names the one used. No change to which records a formula may read.
Container & model: M, mode:cloud (measured on a booted composition: the save of a formula reading an undeclared related field, before and after; the data read of a valid sibling beside it, before and after; and the declared-field control), model: TIER_DEFAULT (this run's dispatch-gates.mjs --tier: "no path-derived mandate")
Clause-②: no (narrowing) (the save door newly refuses a formula it accepted; owes one CONTRACT_REVIEW_TIER review, on the narrowing face, before the queue)
Responsibility: judgeFieldFormula admits a formula reading a field its lookup's target does not declare, and the read then nulls every formula through that lookup, valid siblings included | the save door's lint already resolves the hop (traversalHydration, refuseStoredMetadataBodyHop) and the engine already isolates one-hop reads per reference | every author whose title formula guesses a target field name, the cloud AI builder named first (cloud#2925, PR chore(console): bump objectui to fb35e48 (post-#2577) + pin the UX contract in detail-shapes e2e #3047)
Thread-read: 6257424744
Serial constraints cleared: open PRs' file lists and the pm:dispatched claims were read at this claim.
Dispatch · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-19T03:13Z: mode:cloud. The dev session is session_01SE5iTKQC8i5LBRywMuQZmt. It builds at the default tier on branch claude/issue-24410-formula-related-field-refusal, under claim 6258777300.
Measured first, on main, on a booted composition: both halves. The save of a formula reading an undeclared related field (200 today), and the data read of a valid sibling beside it (null today).
What lands:
the save door refuses the read, naming the field and the target, in packages/lint, declared to domain:spec (6258789472, 6258795887);
the engine's one-hop read no longer nulls a valid sibling, fixed at the cause the measurement names.
Owed: a CONTRACT_REVIEW_TIER review of record, on the narrowing face, before the queue.
{
"issue": 24410,
"status": "done",
"branch": "claude/issue-24410-formula-related-field-refusal",
"pr": "https://github.com/objectstack-ai/objectstack/pull/24466",
"session": "session_01SE5iTKQC8i5LBRywMuQZmt",
"premise_still_valid": true,
"summary": "Both halves were re-measured on main db1f1fd on a booted stack. Before the fix, PUT /meta/object with a formula reading record.book.name answered 200 (rhf_book declares no name), and a valid sibling formula read null with the bad formula's fault text. Measured cause of the sibling null: no driver read failed. resolveFormulaRelated merged every formula's hop columns per reference, and the shared one-hop loader answered the whole reference undeclared-field when any column was undeclared. Fixes: (1) lint judgeFieldFormula now refuses a hop to a field the target's field index does not declare (declared fields, injected columns and id), under the existing EXPRESSION_INVALID rule (expression-invalid), naming the field and the target (no new code). (2) objectql resolveFormulaRelated takes undeclared columns out of the shared read and formulaHopScope judges them per formula: the bad formula reads null with its own undeclared-field report, and the sibling is served as it is alone. The predicate seam is unchanged. The WARN's re-save promise is now true as measured (422), so its text is left unchanged.",
"tests": "Booted pin packages/qa/dogfood/test/formula-hop-undeclared-related-field.dogfood.test.ts: before (db1f1fd) 2 of 3 red. The save answered 200 with 'Saved object rhf_loan_saved', and the list read summary null and bad_title null. After: 3/3 on 2293b1c45 and again on final head 313ee1040. The save answers 422 INVALID_METADATA with one issue: rule expression-invalid, where \"object 'rhf_loan_saved' · field 'summary' expression\", message beginning \"unknown field `name` on `rhf_book`\". The list and findOne read summary 'RHFBOOK71 · RHFREADER72' and bad_title null. Controls: the declared-field save answers 200, and ?fields=summary alone gives the same value. Unit pins: objectql engine-formula-one-hop 26/26; lint formula-one-hop + runtime-gate object-formula-writes 24/24. Full suites on 2293b1c45: lint 145 files / 6869 tests passed. objectql 444/445 files: the federated-injected-column-readers census needed a row for the new referenceTargetOf() use, added as caller-predicate, then 31/31. After merging fa26308bb (#24423 touches hook-binder, which serveFormulaField reaches): objectql one-hop + hook-binder-wrap-handler + census 34/34; spec check:generated exit 0. Ablation 1 (refusal), via ablation-replace wrap: 'if (column === id || ...) continue' became an always-continue with marker ABLATED_24410. lint build 0, marker in 4 dist files. Red: 2 lint pins and the booted save case. Restore blob == HEAD 97bf3b03ac54 and git diff HEAD empty. A first attempt deleted the call: the build exited 1 on the unused closure, so it was redone. Ablation 2 (isolation): formulaHopScope 'own' became every undeclared column of the reference. objectql build 0, marker in 4 dist files. Red: the objectql sibling pin and the booted sibling case. The booted save case was also red on that run, because ablation 1's restore leg had not rebuilt lint/dist yet. Restore blob == HEAD fa07e6e44d8c. Restore legs: lint+objectql rebuilt with exit 0, --absent checks found the marker absent from 20 and 14 dist files, booted pin 3/3. Corpus: 28 shipped formula fields, 0 traverse a reference, so 0 new refusals in-tree.",
"gates": "Local, head 313ee1040: node scripts/pm/dispatch-gates.mjs --commands (no paths) derives 75 commands. Each was run with its exit captured before any pipe, and all 75 exited 0. --ran verdict: '75 derived, 75 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero — all 75 recorded an exit code and none of them is 3)'. On the earlier head 2293b1c45, check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET: 8 packages outside the dogfood closure had no dist). They were built, and it exited 0, then and on 313ee1040. Typecheck on 313ee1040: @objectstack/lint 0, @objectstack/objectql 0, @objectstack/dogfood 0 (the test-only package included). Spec check:generated 0. CI on 313ee1040 at report time: 24 check-runs, 22 in_progress, 1 success, 1 skipped, 0 failure. The five required contexts are not yet concluded (in_progress). test:repo was not run locally (CI's Test Core owns it).",
"mcp_calls": "0 — no MCP GitHub tool called (reads went through unauthenticated REST GETs to api.github.com)",
"api_writes": "2 — both through the fleet-write relay as objectstack-fleet[bot] (POST /repos/objectstack-ai/objectstack/dispatches): (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos/objectstack-ai/objectstack/issues/24466/assignees (os-project-manager), read back 9200/9200 bytes identical; (2) comment = POST /repos/objectstack-ai/objectstack/issues/24410/comments (this report). Plus git pushes to the own branch only (9, WIP-before-each-long-step per AGENTS.md), which are not REST writes.",
"files_changed": [
".changeset/24410-lint-formula-hop-undeclared-field.md +19 -0",
".changeset/24410-objectql-formula-hop-sibling-isolation.md +17 -0",
"packages/lint/src/validate-expressions.ts +56 -1",
"packages/lint/src/validate-expressions.formula-one-hop.test.ts +28 -0",
"packages/lint/src/runtime-gate.object-formula-writes.test.ts +40 -0",
"packages/objectql/src/engine.ts +70 -8",
"packages/objectql/src/engine-formula-one-hop.test.ts +18 -0",
"packages/objectql/src/federated-injected-column-readers.test.ts +6 -0",
"packages/qa/dogfood/test/formula-hop-undeclared-related-field.dogfood.test.ts +139 -0"
],
"line_budget": "none set; diff +393 -9 over 9 files (per-file in files_changed)",
"deviations": [
"Worktree: the local branch claude/issue-24410-formula-related-field-refusal already existed at db1f1fd, checked out in the primary checkout, with no commits of its own. I detached the primary checkout's HEAD and deleted that local branch so git worktree add -b could create it in ../objectstack-issue-24410 from origin/main. The empty-branch push followed before any edit.",
"File surface: two pin files beyond the claim's list, both required by measurement. runtime-gate.object-formula-writes.test.ts holds the door pin with a cross-package target. federated-injected-column-readers.test.ts holds the census row that reds on any new engine referenceTargetOf() use. The booted pin in packages/qa/dogfood is to be declared to domain:cli at review.",
"The full objectql and lint suites ran on 2293b1c45. After the second main merge (313ee1040), only the overlap was re-run (one-hop, hook-binder, census, booted pin, typechecks, check:generated, all 75 derived gates), per AGENTS.md §10.",
"The Clause-② line is carried in both changesets as the dispatch asked. The objectql one is graded patch, because its change only gives a value where null was served. check:adr-0087-registration and check:changeset-no-major both exit 0 on it."
],
"open_questions": [
{
"question": "Which code or family carries the refusal?",
"options": [
"A the existing lint rule EXPRESSION_INVALID, message in the shared validator's 'unknown field X on T' words",
"B a new code"
],
"recommendation": "A, taken. It follows the dispatch's no-new-code rule, and an author or AI reads one family for first- and second-segment unknown fields (axis 3: loud at publish, no new dialect; axis 4: no new surface)."
},
{
"question": "Where does the isolation fix live?",
"options": [
"A resolveFormulaRelated + formulaHopScope (formula seam only), with the declared-field test extracted into one shared helper",
"B change the shared loader's binding for both seams"
],
"recommendation": "A, taken. It fixes the measured cause for formulas. The predicate consumer reads binding.row first, so B would change rule verdicts outside the fence (axis 2: no consumer-side widening on the rule seam)."
},
{
"question": "What about a target the lint snapshot does not carry?",
"options": [
"A not judged (a missed catch, never a false refusal)",
"B refuse as unknown"
],
"recommendation": "A, taken. At the door the snapshot carries the package closure plus system objects, so a dependency's target is judged. Pinned in the door test."
},
{
"question": "Does the runtime WARN text need a change?",
"options": [
"A keep it: a re-save now answers 422 with the located message",
"B reword"
],
"recommendation": "A, taken, as measured on the booted stack."
}
],
"out_of_scope_findings": [
"carrier: none · noted, not filed (read-only inference, no public-door measurement): the predicate seam (resolvePredicateRelated through the same loader) still answers a whole reference undeclared-field when one validation rule names a field the target does not declare, so sibling rules through that reference have no verdict. The new lint judges formula expressions only, not hydrated rule conditions. Dedupe words: validation rule undeclared related field, predicate hop sibling refusal, rule traversal related field existence.",
"carrier: none · noted, not filed (docs, no misstatement): content/docs/data-modeling/formulas.mdx's 'Field must exist' row does not mention the one-hop read now judged."
]
}
{
"issue": 24410,
"status": "done",
"round": "fix round 1 (from the domain:engine seat 1 PM, session_01JtSQQ3EWsbNPzyYMug1tXy; review record 6260609012 FAIL on Test Core)",
"branch": "claude/issue-24410-formula-related-field-refusal",
"pr": "https://github.com/objectstack-ai/objectstack/pull/24466",
"session": "session_01SE5iTKQC8i5LBRywMuQZmt",
"premise_still_valid": true,
"head": "362cd23e3 (previous 313ee1040)",
"merge_commit": "94de92be1 — merge of origin/main 31ce45c07 (18.0.0-next.4) into the branch, a merge commit (no rebase, no force-push). Clean, no conflicts. spec moved; no regeneration pending after the merge.",
"summary": "The record's ①.8 cause was confirmed against the real assertion. The [#22661] census in packages/core/src/security/second-object-read-exposure.pin.test.ts requires a CALLERS row for every non-test source that calls referenceTargetOf. The PR's new call in packages/lint/src/validate-expressions.ts had none. Fixes: (1) a CALLERS row for validate-expressions.ts, kind not-a-served-read, with the reason the three sibling packages/lint rows carry ('authoring-time static analysis of metadata; reads no record'); (2) the objectql changeset's line is now 'Clause-②: no' with no arm (it stays patch), while the (narrowing) arm stays on the lint changeset and the PR body; (3) one sentence added to the lint changeset's What to do: at os build and os validate the pass does not see a target field an objectExtensions entry contributes, while the save door judges the merged object. Nothing else in the diff moved. No test was skipped, disabled or loosened.",
"tests": "Reproduced on 94de92be1: 'pnpm --filter @objectstack/core test:repo' exit 1 (6 files: 1 failed, 5 passed; 82 tests: 1 failed, 81 passed). The failing test was second-object-read-exposure.pin.test.ts > 'every caller of the reference-target arbiter is classified, and every classified file still calls it'. Assertion verbatim: 'These sources resolve which object a reference field points at, which is how a read that reaches a SECOND object starts, and this pin does not know them: - packages/lint/src/validate-expressions.ts Classify each in CALLERS. ... : expected [ Array(1) ] to deeply equal []'. After, on 362cd23e3, the same command: exit 0, 6/6 files, 82/82 tests. Re-run on 362cd23e3, all exit 0: the lint suite (145 files, 6871 tests passed; it reads the lint source this PR changes); objectql engine-formula-one-hop + federated-injected-column-readers + hook-binder-wrap-handler (34/34; objectql source did not change this round, and those are the files the PR touches or overlaps); the booted pin formula-hop-undeclared-related-field.dogfood.test.ts (3/3, after rebuilding the dogfood closure on the merged tree); typecheck of @objectstack/core, lint, objectql and dogfood. The full objectql suite and the ablations stand from the earlier heads: objectql src is byte-identical to 313ee1040 apart from main's own merged changes, and the ablations pin behaviour this round did not touch.",
"gates": "On 362cd23e3, node scripts/pm/dispatch-gates.mjs --commands (no paths) derives 77 commands: the 75 from 313ee1040 plus 'node scripts/check-dev-prereqs.mjs --self-test' and 'pnpm check:kernel-hook-pairs', which the packages/core file brings. All 77 were re-run on this head with exit codes captured, and all exited 0. check:dual-build-cjs-loads first exited 3 (the fresh worktree lacked 8 dists outside the closure); they were built and it re-ran with exit 0. check:type-check-debt was cut off once by my batch timeout and re-ran to exit 0. --ran verdict: '77 derived, 77 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero — all 77 recorded an exit code and none of them is 3)'. packages/core test:repo exit 0, as above. CI on 362cd23e3 had not concluded at report time.",
"mcp_calls": "0 — no MCP GitHub tool called this round or before; reads were unauthenticated REST GETs",
"api_writes": "1 this round — the os-dev-report comment through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches, executing POST /repos/objectstack-ai/objectstack/issues/24410/comments). Plus the one budgeted git push (313ee1040..362cd23e3), which is not a REST write.",
"files_changed": [
"packages/core/src/security/second-object-read-exposure.pin.test.ts +5 -0 (new in this PR)",
".changeset/24410-objectql-formula-hop-sibling-isolation.md +1 -1 (now +17 -0 against main)",
".changeset/24410-lint-formula-hop-undeclared-field.md +1 -1 (now +19 -0 against main)",
"unchanged from 313ee1040: packages/lint/src/validate-expressions.ts +56 -1, validate-expressions.formula-one-hop.test.ts +28 -0, runtime-gate.object-formula-writes.test.ts +40 -0, packages/objectql/src/engine.ts +70 -8, engine-formula-one-hop.test.ts +18 -0, federated-injected-column-readers.test.ts +6 -0, packages/qa/dogfood/test/formula-hop-undeclared-related-field.dogfood.test.ts +139 -0"
],
"line_budget": "none set; the PR is now +398 -9 over 10 files",
"deviations": [
"The round's budget of one push took precedence over AGENTS.md's push-WIP-before-each-long-step rule. The fix commit sat local, never pushed, through the roughly 25-minute gate sweep, then went up in the single push.",
"The worktree was recreated from origin/claude/issue-24410-formula-related-field-refusal, since the previous one had been removed after the first report.",
"The PR body still says 'Clause-②: no (narrowing)' (right for the PR) and still cites head 313ee1040 and the 75-command run. Per os-dev it is written once and not PATCHed, so the seat may want to append this round's head 362cd23e3, the core census row and the 77-command run. The domain:cli declaration for the dogfood pin remains the seat's, as the record's ③.13 lists."
],
"open_questions": [],
"out_of_scope_findings": [
"carrier: the adopting seat, per record 6260609012 ③.10 (the predicate seam's whole-reference undeclared verdict, plus the second-object-read-exposure engine.ts row's prose drift from #24197). Noted, not filed by the dev.",
"carrier: the lint lane, per record ③.5 (fold objectExtensions into buildFieldIndex for every existence verdict). Noted in the lint changeset, not filed by the dev."
]
}
CONTRACT_REVIEW_TIER, narrowing face: record 6260609012 FAIL on 313ee1040f (one census row), then delta record 6262025060 PASS at head 362cd23e34. ACCEPT 6262132765. Not governed.
Queue
It entered the merge queue at 2026-10-19T07:37Z and left it merged at 2026-10-19T08:07Z. No gh-readonly-queue/main/pr-24466-* branch remains.
origin/main
45bec43cd9 is on origin/main. It carries PR #24466's 10 files, +398 / −9.
Card
Closed completed through Fixes #24410; pm:dispatched is cleared.
What main now holds:
The save door refuses the misspelt hop. The field-formula pass (judgeFieldFormula) judges a formula's one-hop read against the target's field index: declared fields, injected system columns and id. An undeclared one 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).
One failed hop no longer nulls its siblings. The formula read (resolveFormulaRelated) leaves undeclared columns out of the shared one-hop read, and formulaHopScope judges each formula on its own. A valid sibling reads what it reads alone; the bad formula reads null with its own report.
Activity
objectstack-fleet commentedon Oct 19, 2026
Triage: p1 +
target:v18·bug·domain:engine·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-19T01:00ZTriage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.
origin/main97cb5c5a82. The measurement is at cloud's pinec749fbb44b5, and69147abf25(fix(objectql)!: the one-hop read through a reference never reads a stored-metadata-body table; the lint refuses it #24344) has landed since. The shape is unchanged:judgeFieldFormula(packages/lint/src/validate-expressions.ts:2798) validates the expression with the source object's own field index andtraversalHydration: true. Its added hop check,refuseStoredMetadataBodyHop, refuses one class of target. It does not ask whether the target declares the field read.mainfirst.target:v18, by the protocol-first rule (6163099154, amended at6163539661). The save refusal is a narrowing of whatPUT /api/v1/meta/objectaccepts. The sibling-null half is a wrong answer onGET /api/v1/data.domain:engine: the formula read inobjectql(resolveFormulaRelated,engine.ts:6243) and the save door's lint.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Claim: PM loop round 11 · 2026-10-19T03:07Z · after triage 6257424744 (p1
target:v18,bug, protocol-first). A formula that reads a field its lookup's target does not declare is refused at save, with the field and the target named. One formula's failed read no longer nulls its sibling formulas.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-24410-formula-related-field-refusalWorktree:
objectstack-issue-24410Domain:
domain:engineSeat:
domain:engine#1(seat post #6367)File surface:
packages/objectql/src/engine.ts,resolveFormulaRelated(the one-hop read) and the formula serve around it: a failed hop read for one formula leaves every other formula on the row served as it would be alone.domain:specon [PM seat] domain:spec — ⏳ vacant #6017 (and [PM seat] domain:spec · seat 2 — ⏳ vacant #18549) before the queue:packages/lint/src/validate-expressions.ts,judgeFieldFormula. A traversal reading a field the lookup's target does not declare is refused with the field and the target named, on the save door's existing lint path.packages/lintisdomain:spec's by the anchoring exception.PUT /api/v1/meta/object;GET /api/v1/datawhile a bad one fails;.changeset/24410-*.md.Container & model:
M,mode:cloud(measured on a booted composition: the save of a formula reading an undeclared related field, before and after; the data read of a valid sibling beside it, before and after; and the declared-field control),model: TIER_DEFAULT(this run'sdispatch-gates.mjs --tier: "no path-derived mandate")Clause-②: no (narrowing) (the save door newly refuses a formula it accepted; owes one
CONTRACT_REVIEW_TIERreview, on the narrowing face, before the queue)Responsibility:
judgeFieldFormulaadmits a formula reading a field its lookup's target does not declare, and the read then nulls every formula through that lookup, valid siblings included | the save door's lint already resolves the hop (traversalHydration,refuseStoredMetadataBodyHop) and the engine already isolates one-hop reads per reference | every author whose title formula guesses a target field name, the cloud AI builder named first (cloud#2925, PR chore(console): bump objectui to fb35e48 (post-#2577) + pin the UX contract in detail-shapes e2e #3047)Thread-read: 6257424744
Serial constraints cleared: open PRs' file lists and the
pm:dispatchedclaims were read at this claim.engine.ts'sresolveFileReferences, disjoint from the formula read. It lands first; this dev mergesmainbefore the PR opens.domain:spec's Hook ctx.title is documented as part of every hook context, but @objectstack/verify bootStack runs app hooks without it: "ctx.title is not a function" under the test harness, while objectstack dev serves it #24289 rider (bindHooks) and spec(object-extension): the extension refusal saysdefineStack()attaches a top-level action withobjectName: '<target>', but for another package's object it is never attached and the served object lacks it #24400 rider (the action attach point) do not meet the formula read.validate-expressions.ts.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Dispatch ·
domain:engineseat 1 ·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-19T03:13Z:mode:cloud. The dev session issession_01SE5iTKQC8i5LBRywMuQZmt. It builds at the default tier on branchclaude/issue-24410-formula-related-field-refusal, under claim 6258777300.target:v18, protocol-first), the closed sibling A formula field that reads through a reference (record.book.title) passes validateExpression, then reads null on every row: the read path projects only the row's own columns #24197, and cloud#2925's measurement.main, on a booted composition: both halves. The save of a formula reading an undeclared related field (200today), and the data read of a valid sibling beside it (nulltoday).packages/lint, declared todomain:spec(6258789472, 6258795887);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": 24410, "status": "done", "branch": "claude/issue-24410-formula-related-field-refusal", "pr": "https://github.com/objectstack-ai/objectstack/pull/24466", "session": "session_01SE5iTKQC8i5LBRywMuQZmt", "premise_still_valid": true, "summary": "Both halves were re-measured on main db1f1fd on a booted stack. Before the fix, PUT /meta/object with a formula reading record.book.name answered 200 (rhf_book declares no name), and a valid sibling formula read null with the bad formula's fault text. Measured cause of the sibling null: no driver read failed. resolveFormulaRelated merged every formula's hop columns per reference, and the shared one-hop loader answered the whole reference undeclared-field when any column was undeclared. Fixes: (1) lint judgeFieldFormula now refuses a hop to a field the target's field index does not declare (declared fields, injected columns and id), under the existing EXPRESSION_INVALID rule (expression-invalid), naming the field and the target (no new code). (2) objectql resolveFormulaRelated takes undeclared columns out of the shared read and formulaHopScope judges them per formula: the bad formula reads null with its own undeclared-field report, and the sibling is served as it is alone. The predicate seam is unchanged. The WARN's re-save promise is now true as measured (422), so its text is left unchanged.", "tests": "Booted pin packages/qa/dogfood/test/formula-hop-undeclared-related-field.dogfood.test.ts: before (db1f1fd) 2 of 3 red. The save answered 200 with 'Saved object rhf_loan_saved', and the list read summary null and bad_title null. After: 3/3 on 2293b1c45 and again on final head 313ee1040. The save answers 422 INVALID_METADATA with one issue: rule expression-invalid, where \"object 'rhf_loan_saved' · field 'summary' expression\", message beginning \"unknown field `name` on `rhf_book`\". The list and findOne read summary 'RHFBOOK71 · RHFREADER72' and bad_title null. Controls: the declared-field save answers 200, and ?fields=summary alone gives the same value. Unit pins: objectql engine-formula-one-hop 26/26; lint formula-one-hop + runtime-gate object-formula-writes 24/24. Full suites on 2293b1c45: lint 145 files / 6869 tests passed. objectql 444/445 files: the federated-injected-column-readers census needed a row for the new referenceTargetOf() use, added as caller-predicate, then 31/31. After merging fa26308bb (#24423 touches hook-binder, which serveFormulaField reaches): objectql one-hop + hook-binder-wrap-handler + census 34/34; spec check:generated exit 0. Ablation 1 (refusal), via ablation-replace wrap: 'if (column === id || ...) continue' became an always-continue with marker ABLATED_24410. lint build 0, marker in 4 dist files. Red: 2 lint pins and the booted save case. Restore blob == HEAD 97bf3b03ac54 and git diff HEAD empty. A first attempt deleted the call: the build exited 1 on the unused closure, so it was redone. Ablation 2 (isolation): formulaHopScope 'own' became every undeclared column of the reference. objectql build 0, marker in 4 dist files. Red: the objectql sibling pin and the booted sibling case. The booted save case was also red on that run, because ablation 1's restore leg had not rebuilt lint/dist yet. Restore blob == HEAD fa07e6e44d8c. Restore legs: lint+objectql rebuilt with exit 0, --absent checks found the marker absent from 20 and 14 dist files, booted pin 3/3. Corpus: 28 shipped formula fields, 0 traverse a reference, so 0 new refusals in-tree.", "gates": "Local, head 313ee1040: node scripts/pm/dispatch-gates.mjs --commands (no paths) derives 75 commands. Each was run with its exit captured before any pipe, and all 75 exited 0. --ran verdict: '75 derived, 75 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero — all 75 recorded an exit code and none of them is 3)'. On the earlier head 2293b1c45, check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET: 8 packages outside the dogfood closure had no dist). They were built, and it exited 0, then and on 313ee1040. Typecheck on 313ee1040: @objectstack/lint 0, @objectstack/objectql 0, @objectstack/dogfood 0 (the test-only package included). Spec check:generated 0. CI on 313ee1040 at report time: 24 check-runs, 22 in_progress, 1 success, 1 skipped, 0 failure. The five required contexts are not yet concluded (in_progress). test:repo was not run locally (CI's Test Core owns it).", "mcp_calls": "0 — no MCP GitHub tool called (reads went through unauthenticated REST GETs to api.github.com)", "api_writes": "2 — both through the fleet-write relay as objectstack-fleet[bot] (POST /repos/objectstack-ai/objectstack/dispatches): (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos/objectstack-ai/objectstack/issues/24466/assignees (os-project-manager), read back 9200/9200 bytes identical; (2) comment = POST /repos/objectstack-ai/objectstack/issues/24410/comments (this report). Plus git pushes to the own branch only (9, WIP-before-each-long-step per AGENTS.md), which are not REST writes.", "files_changed": [ ".changeset/24410-lint-formula-hop-undeclared-field.md +19 -0", ".changeset/24410-objectql-formula-hop-sibling-isolation.md +17 -0", "packages/lint/src/validate-expressions.ts +56 -1", "packages/lint/src/validate-expressions.formula-one-hop.test.ts +28 -0", "packages/lint/src/runtime-gate.object-formula-writes.test.ts +40 -0", "packages/objectql/src/engine.ts +70 -8", "packages/objectql/src/engine-formula-one-hop.test.ts +18 -0", "packages/objectql/src/federated-injected-column-readers.test.ts +6 -0", "packages/qa/dogfood/test/formula-hop-undeclared-related-field.dogfood.test.ts +139 -0" ], "line_budget": "none set; diff +393 -9 over 9 files (per-file in files_changed)", "deviations": [ "Worktree: the local branch claude/issue-24410-formula-related-field-refusal already existed at db1f1fd, checked out in the primary checkout, with no commits of its own. I detached the primary checkout's HEAD and deleted that local branch so git worktree add -b could create it in ../objectstack-issue-24410 from origin/main. The empty-branch push followed before any edit.", "File surface: two pin files beyond the claim's list, both required by measurement. runtime-gate.object-formula-writes.test.ts holds the door pin with a cross-package target. federated-injected-column-readers.test.ts holds the census row that reds on any new engine referenceTargetOf() use. The booted pin in packages/qa/dogfood is to be declared to domain:cli at review.", "The full objectql and lint suites ran on 2293b1c45. After the second main merge (313ee1040), only the overlap was re-run (one-hop, hook-binder, census, booted pin, typechecks, check:generated, all 75 derived gates), per AGENTS.md §10.", "The Clause-② line is carried in both changesets as the dispatch asked. The objectql one is graded patch, because its change only gives a value where null was served. check:adr-0087-registration and check:changeset-no-major both exit 0 on it." ], "open_questions": [ { "question": "Which code or family carries the refusal?", "options": [ "A the existing lint rule EXPRESSION_INVALID, message in the shared validator's 'unknown field X on T' words", "B a new code" ], "recommendation": "A, taken. It follows the dispatch's no-new-code rule, and an author or AI reads one family for first- and second-segment unknown fields (axis 3: loud at publish, no new dialect; axis 4: no new surface)." }, { "question": "Where does the isolation fix live?", "options": [ "A resolveFormulaRelated + formulaHopScope (formula seam only), with the declared-field test extracted into one shared helper", "B change the shared loader's binding for both seams" ], "recommendation": "A, taken. It fixes the measured cause for formulas. The predicate consumer reads binding.row first, so B would change rule verdicts outside the fence (axis 2: no consumer-side widening on the rule seam)." }, { "question": "What about a target the lint snapshot does not carry?", "options": [ "A not judged (a missed catch, never a false refusal)", "B refuse as unknown" ], "recommendation": "A, taken. At the door the snapshot carries the package closure plus system objects, so a dependency's target is judged. Pinned in the door test." }, { "question": "Does the runtime WARN text need a change?", "options": [ "A keep it: a re-save now answers 422 with the located message", "B reword" ], "recommendation": "A, taken, as measured on the booted stack." } ], "out_of_scope_findings": [ "carrier: none · noted, not filed (read-only inference, no public-door measurement): the predicate seam (resolvePredicateRelated through the same loader) still answers a whole reference undeclared-field when one validation rule names a field the target does not declare, so sibling rules through that reference have no verdict. The new lint judges formula expressions only, not hydrated rule conditions. Dedupe words: validation rule undeclared related field, predicate hop sibling refusal, rule traversal related field existence.", "carrier: none · noted, not filed (docs, no misstatement): content/docs/data-modeling/formulas.mdx's 'Field must exist' row does not mention the one-hop read now judged." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
os-dev-report
{ "issue": 24410, "status": "done", "round": "fix round 1 (from the domain:engine seat 1 PM, session_01JtSQQ3EWsbNPzyYMug1tXy; review record 6260609012 FAIL on Test Core)", "branch": "claude/issue-24410-formula-related-field-refusal", "pr": "https://github.com/objectstack-ai/objectstack/pull/24466", "session": "session_01SE5iTKQC8i5LBRywMuQZmt", "premise_still_valid": true, "head": "362cd23e3 (previous 313ee1040)", "merge_commit": "94de92be1 — merge of origin/main 31ce45c07 (18.0.0-next.4) into the branch, a merge commit (no rebase, no force-push). Clean, no conflicts. spec moved; no regeneration pending after the merge.", "summary": "The record's ①.8 cause was confirmed against the real assertion. The [#22661] census in packages/core/src/security/second-object-read-exposure.pin.test.ts requires a CALLERS row for every non-test source that calls referenceTargetOf. The PR's new call in packages/lint/src/validate-expressions.ts had none. Fixes: (1) a CALLERS row for validate-expressions.ts, kind not-a-served-read, with the reason the three sibling packages/lint rows carry ('authoring-time static analysis of metadata; reads no record'); (2) the objectql changeset's line is now 'Clause-②: no' with no arm (it stays patch), while the (narrowing) arm stays on the lint changeset and the PR body; (3) one sentence added to the lint changeset's What to do: at os build and os validate the pass does not see a target field an objectExtensions entry contributes, while the save door judges the merged object. Nothing else in the diff moved. No test was skipped, disabled or loosened.", "tests": "Reproduced on 94de92be1: 'pnpm --filter @objectstack/core test:repo' exit 1 (6 files: 1 failed, 5 passed; 82 tests: 1 failed, 81 passed). The failing test was second-object-read-exposure.pin.test.ts > 'every caller of the reference-target arbiter is classified, and every classified file still calls it'. Assertion verbatim: 'These sources resolve which object a reference field points at, which is how a read that reaches a SECOND object starts, and this pin does not know them: - packages/lint/src/validate-expressions.ts Classify each in CALLERS. ... : expected [ Array(1) ] to deeply equal []'. After, on 362cd23e3, the same command: exit 0, 6/6 files, 82/82 tests. Re-run on 362cd23e3, all exit 0: the lint suite (145 files, 6871 tests passed; it reads the lint source this PR changes); objectql engine-formula-one-hop + federated-injected-column-readers + hook-binder-wrap-handler (34/34; objectql source did not change this round, and those are the files the PR touches or overlaps); the booted pin formula-hop-undeclared-related-field.dogfood.test.ts (3/3, after rebuilding the dogfood closure on the merged tree); typecheck of @objectstack/core, lint, objectql and dogfood. The full objectql suite and the ablations stand from the earlier heads: objectql src is byte-identical to 313ee1040 apart from main's own merged changes, and the ablations pin behaviour this round did not touch.", "gates": "On 362cd23e3, node scripts/pm/dispatch-gates.mjs --commands (no paths) derives 77 commands: the 75 from 313ee1040 plus 'node scripts/check-dev-prereqs.mjs --self-test' and 'pnpm check:kernel-hook-pairs', which the packages/core file brings. All 77 were re-run on this head with exit codes captured, and all exited 0. check:dual-build-cjs-loads first exited 3 (the fresh worktree lacked 8 dists outside the closure); they were built and it re-ran with exit 0. check:type-check-debt was cut off once by my batch timeout and re-ran to exit 0. --ran verdict: '77 derived, 77 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero — all 77 recorded an exit code and none of them is 3)'. packages/core test:repo exit 0, as above. CI on 362cd23e3 had not concluded at report time.", "mcp_calls": "0 — no MCP GitHub tool called this round or before; reads were unauthenticated REST GETs", "api_writes": "1 this round — the os-dev-report comment through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches, executing POST /repos/objectstack-ai/objectstack/issues/24410/comments). Plus the one budgeted git push (313ee1040..362cd23e3), which is not a REST write.", "files_changed": [ "packages/core/src/security/second-object-read-exposure.pin.test.ts +5 -0 (new in this PR)", ".changeset/24410-objectql-formula-hop-sibling-isolation.md +1 -1 (now +17 -0 against main)", ".changeset/24410-lint-formula-hop-undeclared-field.md +1 -1 (now +19 -0 against main)", "unchanged from 313ee1040: packages/lint/src/validate-expressions.ts +56 -1, validate-expressions.formula-one-hop.test.ts +28 -0, runtime-gate.object-formula-writes.test.ts +40 -0, packages/objectql/src/engine.ts +70 -8, engine-formula-one-hop.test.ts +18 -0, federated-injected-column-readers.test.ts +6 -0, packages/qa/dogfood/test/formula-hop-undeclared-related-field.dogfood.test.ts +139 -0" ], "line_budget": "none set; the PR is now +398 -9 over 10 files", "deviations": [ "The round's budget of one push took precedence over AGENTS.md's push-WIP-before-each-long-step rule. The fix commit sat local, never pushed, through the roughly 25-minute gate sweep, then went up in the single push.", "The worktree was recreated from origin/claude/issue-24410-formula-related-field-refusal, since the previous one had been removed after the first report.", "The PR body still says 'Clause-②: no (narrowing)' (right for the PR) and still cites head 313ee1040 and the 75-command run. Per os-dev it is written once and not PATCHed, so the seat may want to append this round's head 362cd23e3, the core census row and the 77-command run. The domain:cli declaration for the dogfood pin remains the seat's, as the record's ③.13 lists." ], "open_questions": [], "out_of_scope_findings": [ "carrier: the adopting seat, per record 6260609012 ③.10 (the predicate seam's whole-reference undeclared verdict, plus the second-object-read-exposure engine.ts row's prose drift from #24197). Noted, not filed by the dev.", "carrier: the lint lane, per record ③.5 (fold objectExtensions into buildFieldIndex for every existence verdict). Noted in the lint changeset, not filed by the dev." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Landed ·
domain:engineseat 1 ·session_01JtSQQ3EWsbNPzyYMug1tXy· 2026-10-19T08:09ZCONTRACT_REVIEW_TIER, narrowing face: record 6260609012 FAIL on313ee1040f(one census row), then delta record 6262025060 PASS at head362cd23e34. ACCEPT 6262132765. Not governed.gh-readonly-queue/main/pr-24466-*branch remains.origin/main45bec43cd9is onorigin/main. It carries PR #24466's 10 files, +398 / −9.completedthroughFixes #24410;pm:dispatchedis cleared.What
mainnow holds:judgeFieldFormula) judges a formula's one-hop read against the target's field index: declared fields, injected system columns andid. An undeclared one 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).resolveFormulaRelated) leaves undeclared columns out of the shared one-hop read, andformulaHopScopejudges each formula on its own. A valid sibling reads what it reads alone; the bad formula readsnullwith its own report.packages/core's[#22661]census classifies the lint's newreferenceTargetOfcall asnot-a-served-read.Downstream: cloud pointer 6262663077 on #24145, for objectstack-ai/cloud#2925.
The dev session
session_01SE5iTKQC8i5LBRywMuQZmtis archived.Generated by Claude Code