Repository navigation
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
Conversation
…ot declare (wip, red on main) Claude-Session: https://claude.ai/code/session_01SE5iTKQC8i5LBRywMuQZmt Co-authored-by: Claude <noreply@anthropic.com>
… its siblings The formula read merged every formula's hop columns per reference and the shared loader answered the whole reference undeclared-field when any column was undeclared, so a valid sibling reading through the same lookup read null with the bad formula's fault. The undeclared columns now leave the shared read and are judged per formula. Claude-Session: https://claude.ai/code/session_01SE5iTKQC8i5LBRywMuQZmt Co-authored-by: Claude <noreply@anthropic.com>
…ot declare judgeFieldFormula validated the expression against the source object only, so record.book.name saved with a 200 when the book declares no name, and the formula then read null on every row. The field-formula pass now refuses it under EXPRESSION_INVALID, naming the field and the target, at the build and at the object save door alike. Claude-Session: https://claude.ai/code/session_01SE5iTKQC8i5LBRywMuQZmt Co-authored-by: Claude <noreply@anthropic.com>
…ormula hop field Claude-Session: https://claude.ai/code/session_01SE5iTKQC8i5LBRywMuQZmt Co-authored-by: Claude <noreply@anthropic.com>
…ormula hop to an undeclared field Claude-Session: https://claude.ai/code/session_01SE5iTKQC8i5LBRywMuQZmt Co-authored-by: Claude <noreply@anthropic.com>
…rmula-related-field-refusal
… receivers The #5017 receiver scan reads every member access in the rule's source; the closure now walks the object's fields through the receivers the scan already tables instead of adding new ones. Claude-Session: https://claude.ai/code/session_01SE5iTKQC8i5LBRywMuQZmt Co-authored-by: Claude <noreply@anthropic.com>
…ederated disposition Claude-Session: https://claude.ai/code/session_01SE5iTKQC8i5LBRywMuQZmt Co-authored-by: Claude <noreply@anthropic.com>
…rmula-related-field-refusal
📓 Docs Drift CheckThis PR changes 2 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 21 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 868f4c236c858e577478b59d290e864683de64d9 && git checkout 868f4c236c858e577478b59d290e864683de64d9
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fb9c22aa0929e19ae76707cb55b5f24fead2d9fe 362cd23e347656b16b605a7af60b5aeb88706e3e && git checkout -B drift-repro fb9c22aa0929e19ae76707cb55b5f24fead2d9fe && git merge --no-ff 362cd23e347656b16b605a7af60b5aeb88706e3e
node scripts/docs-audit/affected-docs.mjs --json fb9c22aa0929e19ae76707cb55b5f24fead2d9fe
|
|
CI red on
Generated by Claude Code |
Contract reviewServed-tier: Inputs: card #24410 (body; triage 6257424744, claim 6258777300, dispatch 6258848224, the Check-runs on the head, final read 2026-10-19T05:42:58Z: 41 runs, none in progress. 33 success, among them Check Changeset, Governed Surface Queue Guard, Build Core, every Lint job, Type Check source/consumer/debt/workspace, Spec property liveness, Dogfood Regression Gate, the three claim and card guards, and Test Core shards 1, 3, 4, 5 and 6. 6 skipped by design. 2 failure: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL Generated by Claude Code |
…rmula-related-field-refusal
…the objectql changeset declares no narrowing The [#22661] census requires a CALLERS row for every source that calls the reference-target arbiter; validate-expressions.ts now does, as authoring-time static analysis. The objectql changeset is a patch and narrows nothing, so its Clause-② line carries no arm; the lint changeset names the objectExtensions blind spot at build time. Claude-Session: https://claude.ai/code/session_01SE5iTKQC8i5LBRywMuQZmt Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: This is a DELTA on record 6260609012 (FAIL on head The delta, measured. Between the heads: one merge of Check-runs on the head. First read 07:16:27Z: 40 runs, 28 success, 6 skipped by design, 6 in progress (the six ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 ·
|
| Item | Reading |
|---|---|
| Shape | Draft PR → main. The body opens Fixes #24410. 10 files, +398 / −9. Not governed. |
| Scope | packages/lint's judgeFieldFormula refuses a one-hop read of a field the target does not declare, under the existing expression-invalid rule. objectql's resolveFormulaRelated leaves undeclared columns out of the shared read, and formulaHopScope judges each formula on its own. The predicate seam is untouched. Pins in lint, objectql and a booted dogfood test; one census row in packages/core's [#22661] pin. |
| Seat's own read | Between the two heads, only three paths' net patches moved: the census row, the objectql changeset's line and the lint changeset's sentence. Every other file's patch is byte-identical (measured per file). |
| Measured | Booted, on main: the save answered 200 and a valid sibling read null. After: the save answers 422 INVALID_METADATA naming the field and the target, and the sibling reads its value (equal to ?fields= alone). |
| Ablation | Both mechanisms ablated, each red, each restore checked. |
| Review | CONTRACT_REVIEW_TIER, narrowing face: record 6260609012 FAIL on 313ee1040f (the census row), then delta record 6262025060 PASS on this head. |
| Declaration | Clause-②: no (narrowing) on the claim, the PR body and the lint changeset. The objectql patch changeset says Clause-②: no. |
| Cross-lane | packages/lint declared to domain:spec (6258789472, 6258795887); the dogfood pin to domain:cli (6260339978, 6260346158). No objection on the card. |
| Gates | Dev, on 362cd23e34: 77 derived, 77 run, 77 exit 0; packages/core test:repo 82/82. |
| CI | At this read on 362cd23e34: every check completed, none failed. |
Disposition:
open_questions: all four accepted (record 6260609012 ③.6–③.9).deviations: all accepted (records ③). The PR body still cites the earlier head; the records name this one.out_of_scope_findings: the predicate seam's whole-reference verdict is filed as 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 (p1, triage 6260730682). Theformulas.mdxrow is below the gate.
Landing: ordinary queue (pr_ready + automerge_enable through the relay). Fixes #24410 then closes the card, and #24468 is claimed behind it.
Generated by Claude Code
Fixes #24410
Clause-②: no (narrowing) (the save door newly refuses a formula it accepted; owes one
CONTRACT_REVIEW_TIERreview, on the narrowing face, before the queue)Dispatched by the
domain:engineseat 1 PM (claim 6258777300). Developer session:session_01SE5iTKQC8i5LBRywMuQZmt.What changes
Save refusal (lint,
judgeFieldFormula). The field-formula pass now judges the field a formula reads one hop through a reference against the target object's field index: its declared fields, the system columns the platform injects on it, andid. A read of a field the target does not declare is refused under the existingEXPRESSION_INVALIDrule (expression-invalid). No new code. The message opens in the shared validator'sunknown fieldwords and names the field and the target: "unknown fieldnameonrhf_book: this formula reads it through the referencerecord.book...". The same pass runs foros validate,os buildand the object save door.refuseStoredMetadataBodyHopand is not judged twice.Sibling isolation (objectql,
resolveFormulaRelated). Measured cause: no driver read failed. The formula read merged every formula's hop columns per reference. The shared one-hop loader then answered the whole referenceundeclared-fieldwhen any column was undeclared. So the valid sibling readnulland reported the bad formula's field: "the formula reads 'name' throughbook". The fix:resolveFormulaRelatedtakes the undeclared columns out of the shared read.formulaHopScopejudges them per formula.nullwith its existingundeclared-fieldfault report, which now names only its own columns.undeclaredRelatedFields, that both seams share.The WARN text. "a re-save of the object, refuses it with a located message" now holds for this fault: a re-save of the object answers 422 with the located refusal (measured below). Left unchanged.
⛔ No change to which records a formula may read: the #24316 / #24358 family guards are untouched.
Verification (M, booted composition:
bootStack, real ObjectQL, SQL driver, REST, auth, metadata)Pin:
packages/qa/dogfood/test/formula-hop-undeclared-related-field.dogfood.test.ts. The fixture isrhf_book(title),rhf_reader(reader_name) andrhf_loan. The loan has a validsummaryformula (joinNonEmpty([record.book.title, record.reader.reader_name], ' · ')) and a code-declared badbad_title(record.book.name).maindb1f1fd)PUT /meta/object/rhf_loan_savedwithrecord.book.nameSaved object 'rhf_loan_saved'INVALID_METADATA, one issue: ruleexpression-invalid, whereobject 'rhf_loan_saved' · field 'summary' expression, messageunknown field \name` on `rhf_book` ...`record.book.titleGET /data/rhf_loan?fields=summary(valid alone)RHFBOOK71 · RHFREADER72RHFBOOK71 · RHFREADER72GET /data/rhf_loan:summary/bad_titlenull/null, and WARN onsummary: "reads 'name' throughbook"RHFBOOK71 · RHFREADER72/null, with WARN onbad_titleonlyGET /data/rhf_loan/:id:summary/bad_titlenull/nullnullUnit pins:
engine-formula-one-hop.test.ts: the sibling is served as alone, ONE book read with fieldsid,title, and only the bad formula reports.validate-expressions.formula-one-hop.test.ts: the refusal names the field and the target.idand an injected column (created_at) are admitted. A target the stack does not carry is not judged.runtime-gate.object-formula-writes.test.ts: at the door, the target comes from another package the written one depends on. The refusal is LIT and the declared-field read is the CONTROL.Ablations (
scripts/ablation-replace.mjswrap form, thenscripts/ablation-dist-preflight.mjs). Each restore was checked against the HEAD blob with an emptygit diff HEAD.refuseUndeclaredHopField,if (column === 'id' || ...) continue;became an always-continue carrying the markerABLATED_24410.dist/files.refuses a read of a field the target does not declare, the doorLIT), and the booted save case.formulaHopScope,ownbecame every undeclared column of the reference, not this formula's own.dist/files.lint/distyet. That redness belongs to ablation 1.--absentchecks: marker absent from all 20 lint and all 14 objectql built files. Booted pin 3/3.Corpus. No formula field the repository ships reads through a reference: 28 formula fields across examples (app-crm, app-showcase, app-todo) and platform display titles. So the pass refuses nothing it accepted in-tree.
Tests and gates
The full package suites ran on
2293b1c45, after mergingorigin/main9112bcd88:pnpm --filter @objectstack/lint exec vitest run: 145 files, 6869 tests passed.pnpm --filter @objectstack/objectql exec vitest run: 444/445 files passed. The one red was the federated-reader census, which needed a row for the newreferenceTargetOf()use. Row added (caller-predicate); that file plus the one-hop file then passed 31/31.Then
origin/mainfa26308bbwas merged in (head313ee1040). It brings #24423, which touchesobjectql/hook-binder.ts: the hook body'sctx.title, whichserveFormulaField→resolveFormulaRelatedserves. It also movespackages/spec. On313ee1040:@objectstack/lint0,@objectstack/objectql0,@objectstack/dogfood0.pnpm --filter @objectstack/spec check:generated: exit 0.Derived gates:
node scripts/pm/dispatch-gates.mjs --commands(no paths) on313ee1040gives 75 commands. Each was run with its exit code recorded, and all 75 exited 0.check:dual-build-cjs-loadsexited 3 on the earlier head (8 packages outside the dogfood closure had nodist/). They were built, and it exits 0.--ranverdict: "75 derived, 75 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero — all 75 recorded an exit code and none of them is 3)".Diff (+/−)
.changeset/24410-lint-formula-hop-undeclared-field.md.changeset/24410-objectql-formula-hop-sibling-isolation.mdpackages/lint/src/validate-expressions.tspackages/lint/src/validate-expressions.formula-one-hop.test.tspackages/lint/src/runtime-gate.object-formula-writes.test.tspackages/objectql/src/engine.tspackages/objectql/src/engine-formula-one-hop.test.tspackages/objectql/src/federated-injected-column-readers.test.tspackages/qa/dogfood/test/formula-hop-undeclared-related-field.dogfood.test.tsFile surface: within the claim, plus two pin files the measurement required.
runtime-gate.object-formula-writes.test.tsholds the door pin with a cross-package target.federated-injected-column-readers.test.tsholds the census row. It reds on any new enginereferenceTargetOf()use.packages/qa/dogfood, to be declared todomain:cliat review.Acceptance notes
resolvePredicateRelatedhands the shared loader every rule's hop columns per reference. So one validation rule naming a field its target does not declare leaves every rule through that reference without a verdict. The lint added here judges formulaexpressions only, not hydrated ruleconditions. The fence leaves that seam unchanged. Dedupe words: validation rule undeclared related field, predicate hop sibling refusal, rule traversal related field existence.content/docs/data-modeling/formulas.mdx's "Field must exist" row does not mention the one-hop read. It does not misstate the new behaviour, so it was left alone.Generated by Claude Code