fix(objectql)!: engine.find/findOne refuse a dotted projection instead of widening to every field (#7589) - #8327
Conversation
…d of widening to every field (#7589) The head-only known.has(head) filter kept dotted entries on the strength of a comment claiming the engine resolves them via populate; #7601 measured no populate step exists. A dotted entry is now 400 INVALID_FIELD at the engine boundary; the unknown-plain-column tolerance is explicitly kept. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RDTnVvsgA6cUZ4xFVtPZRy
…the kept plain tolerance, and the cross-door wording agreement Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RDTnVvsgA6cUZ4xFVtPZRy
…emantic entry + changeset (#7589) One entry file under entries/semantic/, registry region + spec-changes.json + upgrade guide regenerated. Breaking changeset per the #7095 precedent: same door (engine public API), same class (silent degradation becomes a refusal). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RDTnVvsgA6cUZ4xFVtPZRy
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 2 package(s): 109 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also reference the affected code. These are read-only:
|
…lookup's related column (#7589) types.mdx's lookup query example was the populate premise itself — the spelling both doors now refuse. Replaced with the expand form, reference column kept projected (#7537), refusal + remedy stated inline. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RDTnVvsgA6cUZ4xFVtPZRy
|
Patch round 1 (PM review):
Gates for the docs delta, re-derived via Generated by Claude Code Generated by Claude Code |
…8315) The merge with main was textually conflict-free, but a conflict-free merge of two independently-regenerated projections is not the generator's output. Measured on this tree: the plain merge result was MISSING two sibling PRs' entries from `spec-changes.json` and `docs/protocol-upgrade-guide.md` — `view-export-options-pdf-removed` (#8010 / PR #8324) and `engine-dotted-projection-refused` (#7589 / PR #8327). Both are present in origin/main's copies of those artifacts; git dropped them while reporting no conflict. `registry.ts` spliced correctly and regenerated byte-identical (78 semantic entries) — the loss was confined to the two prose projections. Not a silent class: against the un-regenerated merge, `check:spec-changes` and `check:upgrade-guide` both FAIL (exit 1) while `check:migration-registry` passes. So this would have been caught — in the merge queue, as an ejection. Regenerating before arming is what makes it cost nothing. Ran on the merged tree, merge committed first: pnpm --filter @objectstack/spec gen:migration-registry pnpm --filter @objectstack/spec gen:spec-changes pnpm --filter @objectstack/spec gen:upgrade-guide Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PEVB6w7D7uCszR9Mw1BL73
Fixes #7589
Implements the maintainer ruling recorded 2026-08-12 (issue comment 5266074163): Option B, verbatim — a dotted
fieldsentry the engine cannot resolve is refused loudly at the engine's head-only projection filter, and the false populate comment is deleted in the same change.What changed
engine.find()/engine.findOne()now throw400 INVALID_FIELD(ADR-0112 envelope:code+status, plusfield/fields/object) on anyfieldsentry carrying a dot. The head-onlyknown.has(head)keep — justified by a comment claiming the engine resolves relationship paths "via populate", which #7601 measured does not exist (it was the last such assertion in the repo after PR #7617) — is gone; the plain-name filter now matches whole names. The refusal sits before the formula planner, on the caller's own spellings, in both verbs.The measured chain (issue comment 5263464190) reaches both verbs: flow
get_recordconfig goes todata.find(...)whenlimitis greater than 1 and todata.findOne(...)otherwise — a flow authoringfields: ['name','account.name']now gets a loud node failure carrying the remedy instead of a silent 200 with every column.Explicitly KEPT (the ruling's carve-outs)
*. Pinned as guards so a future tighten cannot ride in on this card's precedent.packages/drivers/; the driver-side carve-out stays measured-need only per the ruling.validation/record-validator.ts(A number field's declaredscaleis never enforced — values with more decimals are accepted and stored verbatim (min/max on the same field are enforced) #7501),validation/rule-validator.ts(ApreserveAuditby-id update hands the primary key to the driver:isPreservableUnderAuditreinstates a platform object's ownid#8215).Every face reaching the filter, enumerated
find— refusal added beforeplanFormulaProjection.findOne— same refusal, same position (the inline head-split copy of the filter is rewritten too).this.find, so the refusal fires there — insideexpandRelatedRecords' pre-existing graceful-degradationcatch: outcome is an observable warning + retained FK ids, not a refusal. Pinned, with a plain-nested-fields control; same posture the sort axis (engine.find()still drops aformulaORDER BY silently — decide whether the engine refuses or keeps its internal-caller tolerance #7095) records for the same catch.id); judged before it runs.expandSearchOnAst) — writes onlywhere; no projection face.count/aggregate/update/delete— carry nofieldsoption (ENGINE_OPTION_KEY_SETS); no projection face.findData) — already refused at ingress since PR fix(metadata-protocol): refuse a dotted projection instead of widening the response to every field (#7532) #7588; the engine door is the second line behind it. All existing ingress pins stay green.plugin-reports— forwards a saved report'squery.fieldsverbatim intoengine.find; an author-reachable surface that now fails loudly with the remedy (recorded in the ADR-0087 entry).Semantics notes
no_such.name). Deliberate divergence from the ingress door'sunknownbeforedottedprecedence: the engine has no unknown-name refusal to defer to (unknown plains are tolerated by the kept carve-out), and a dotted entry with an unknown head still widened through the empty-projection fallback when it was the only entry. Pinned with the reasoning in the test.expand, denormalise onto a stored field) with the ingress door's dotted refusal — duplicated becausemetadata-protocolis assembled from an engine (importing would invert the layering), kept honest by a cross-door agreement pin, same mechanism as the sort axis' remedy pin.Changeset + ADR-0087
Breaking changeset (
minorbump + bang title percheck-changeset-no-majorconvention) with the disposition markerregistered engine-dotted-projection-refused, and the semantic migration entry added underpackages/spec/src/migrations/entries/semantic/— mirroring #7095'sengine-find-formula-order-by-refused(same door, same class: silent degradation becomes a refusal). Registry region,spec-changes.jsonand the upgrade guide regenerated;check:generatedreports all 13 artifacts current.Declared delta beyond the dispatch's file surface (engine.ts + its tests): the ADR-0087 registration kit above. It is the mechanical accompaniment
check:adr-0087-registrationrequires of a declared-breaking changeset — data-only, no spec schema or behavior change. Flagged here and in the dev report rather than silently included.Verification
@objectstack/objectql: full suite 3466/3466 green (196 files); conformance file 145/145;typecheckclean.origin/main'sengine.tsrestored and the new pins kept, exactly the 9 new refusal/agreement/expand pins go red and all 136 others — every control and every KEPT-tolerance guard — stay green. Fix restored from its commit, byte-identical (git diffagainst the fix commit: empty).@objectstack/service-automation955/955,@objectstack/metadata-protocol1117/1117 — green after building their dependency closures (the first lap's 4 file-level failures were unbuiltservice-jobdist, the known missing-build false red).check:adr-anchors,check:durability-log-level,check:engine-double-contract,check:stack-collection-maps,check-engine-split-ratioinformational,check:error-code-casing,check:nul-bytesre-run after the final tree), plus all 16 families re-derived from the actual changed paths (dispatch-gates.mjs) —check:i18nandcheck-dev-prereqswent green after a full workspace build; their first reds were unbuilt-dist prerequisites, named as such by the gates' own output.Out of scope, untouched, and staying open on their own terms: the driver-side ladder in
driver-sql(#3821 behaviour is unchanged; a carve-out there is measured-need only), and the dotted SORT leg on direct engine calls (a different axis; noted in the updated scope docblock). #7532 is the merged ingress half and is not modified here.Generated by Claude Code
Generated by Claude Code