Uh oh!
There was an error while loading. Please reload this page.
feat(objectql,metadata-protocol): refuse a dotted filter key whose head is a relation, a formula, or a plain scalar — at both doors (#8371) - #8936
Conversation
…ad is a relation, a formula, or a plain scalar (#8371) Implements the #8371 maintainer ruling (Option 2, delegated adjudication): a dotted filter key whose head segment is a relation, a virtual formula, or a plain scalar is refused at BOTH doors — the REST ingress (assertFilterFieldsExist) and the engine's filter seam (assertFilterIsMaterializable) — with the existing INVALID_FIELD/400 identity and the SORT #4256 denormalise remedy sentence. The structured/JSON head (address.city) stays deliberately unjudged, per the ruling's carve-out: it is live on two of three backends. Both doors judge the head by one shared classification (classifyDottedFilterHead, new @objectstack/spec/data export), the same one-source move #8296 made with isVirtualSearchField. Precedence mirrors the sort axis: unknown > dotted > unmaterializable. ADR-0087: semantic entry engine-dotted-filter-refused (protocol major 18). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fgvh1iEJfxetei7aNVdtJt
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 3 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:
|
…#8371 verdict The GUARD pinned exactly the limb the ruling deletes ('a dotted path on a REAL head passes through — nothing may refuse it after'). owner_id is a registry-injected lookup, so 'owner_id.name' is now refused with the whole key named; the surviving guarded properties (nested-relation OBJECT form not descended into, unknown-head precedence) stay pinned by their own tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fgvh1iEJfxetei7aNVdtJt
check:api-surface red at fd8f1eb: DottedFilterHeadClass, DottedFilterHeadMeta, SCALAR_FILTER_HEAD_TYPES and classifyDottedFilterHead are intentional additions (the shared dotted-head classification both filter doors read); gen:api-surface run on a freshly built spec. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fgvh1iEJfxetei7aNVdtJt
…relay) gen:migration-registry (96 semantic — both semantic:18 entries stack: engine-dotted-filter-refused + filter-preset-ordering-comparand-refused), spec build, gen:export-origins, gen:api-surface, gen:spec-changes, gen:upgrade-guide, gen:openapi restore. check:generated: all 13 artifacts up to date. Survival asserted on the merged tree: #8935's entry and its implementation body (isDateRangePresetName), #8932's ApiKeySchema still absent, this branch's entry and classifyDottedFilterHead at all three consuming files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fgvh1iEJfxetei7aNVdtJt
…ter-dotted-path-verdict
…8939 relay) gen:migration-registry, spec build, gen:export-origins, gen:api-surface, gen:spec-changes, gen:upgrade-guide, gen:openapi restore. check:generated: all 13 artifacts up to date. Survival asserted on the merged tree, one hit each in the regenerated registry: engine-dotted-filter-refused (this branch), driver-sql-unresolvable-where-column-refused (#8927), filter-preset-ordering-comparand-refused (#8935), identity-api-key-schema-retired (#8932) - plus implementation bodies: sql-driver.ts INVALID_FIELD refusal + envelope test (#8927), isDateRangePresetName (#8935), ApiKeySchema still absent (#8932), classifyDottedFilterHead at both doors (this branch). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fgvh1iEJfxetei7aNVdtJt
Uh oh!
There was an error while loading. Please reload this page.
…merge (#8807) Both entries stack: #8936's engine-dotted-filter-refused and this card's driver-sql-upsert-cross-row-identity-merge-refused. 97 semantic entries. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XeQRiAa7vYRVX5Fog7Zby8
Fixes#8371
Implements the maintainer ruling (Option 2, delegated adjudication, comment
5300373151): a type-directed verdict on the head segment of a dotted filter key, at both doors. The measurement phase was already complete (dev report5299949142) and is built on, not re-run.The ruled shape, as landed
lookup/master_detail/user/tree), a virtualformula, or a plain scalar:400 INVALID_FIELD, naming the whole offending key exactly as the caller wrote it, closing with the SORTsort的点号路径(?sort=account.company_name)仍然静默降级为「不排序」——#4226 收口后唯一漏网的 sort 形态 #4256 remedy sentence ("Denormalise the value onto '…' (a stored field, written when the source changes) and filter that."). No new mechanism, no new error class.assertFilterFieldsExist,packages/metadata-protocol/src/protocol.ts) and the engine's own filter seam (assertFilterIsMaterializable,packages/objectql/src/filter-comparand-shape.ts) — the same two doors The FILTER axis has no unmaterializable verdict: awhereon a virtual formula field returns 0 rows silently, while sort and search refuse the same field with a 400 #8296 wired its virtual verdict into, structure mirrored exactly. Precedence mirrors the sort axis, verdict for verdict:unknownbeforedottedbefore unmaterializable.{'address.city': …}) keeps today's behaviour — live on two of three backends, so a refusal would delete a working capability. Array-valued heads (multiple: true, tag types) and file heads are also unjudged, for the same measured fail-open reason the collectors document (a hole, never a false 400). One deliberate reading of the ruling's letter: a multi-valued relation head (lookupwithmultiple: true) is still refused — the ruling names the relation class flat, andtag_ids.nameis the same traversal error whether the head stores one id or many.{ owner: { region: 'NA' } }keeps working — the collectors never descend into a field key's value, and the distinction is pinned as a control at both doors.One shared classification, not two ladders
Both doors judge the head via a new
@objectstack/spec/dataexport,classifyDottedFilterHead(packages/spec/src/data/filter-dotted-head.ts) — the same one-source move #8296 made withisVirtualSearchField, so the doors cannot drift into answering one spelling two ways. The scalar set is derived from the ADR-0104 value-shape classes, never a list minted at a door; a disjointness pin keeps the classification order-independent.ADR-0087
Semantic entry
engine-dotted-filter-refused(protocol major 18), following the sibling precedents (engine-find-formula-filter-refused,engine-dotted-projection-refused). Registry block regenerated withgen:migration-registry;export-originsandapi-surfaceregenerated for the new spec exports. Changeset carries the disposition marker and the FROM → TO prescription.os-regen relay — PR #8935 landed under this branch
scripts/pm/os-regen-merge.shsequence run: merge committed first (one hand-resolved conflict inregistry.ts— both branches added a semantic:18 entry; resolved by taking main's side and regenerating from the merged entry FILES), then the regen chain (gen:migration-registry→ 96 semantic entries, spec build,gen:export-origins,gen:api-surface,gen:spec-changes,gen:upgrade-guide,gen:openapirestore).check:generated: all 13 artifacts up to date. Asserted surviving on the merged tree: #8935's entryfilter-preset-ordering-comparand-refusedAND its implementation body (isDateRangePresetNameindata/date-range-presets.ts), #8932'sApiKeySchemastill absent, this branch's entry andclassifyDottedFilterHeadat all three consuming files.Out of scope, deliberately
find()silently returns [] whilecount()throws a raw dialect error with no ADR-0112 envelope #8790 (driver-sql find/count divergence on an unresolvable WHERE column) remains open — independent card, not addressed here.context.now, and the fixture pins a calendar date #8937 remains open — filed from this run:engine-temporal-comparand-door.test.tswent permanently red at UTC midnight after 2026-08-15 (its fixture pinsnow = 2026-08-15while the{30_days_ago}resolution reads the real clock). The failing file and code path are byte-identical toorigin/mainin this branch; it will redden every objectql CI run repo-wide from 2026-08-16 onward until fixed onmain.assertQueryParamsAreFields(the bare-?field=xquery-param door): its head-segment tolerance is documented as deliberate, and a params-derived dotted filter that rides through it is caught at the engine seam anyway, so no silent path remains. Widening that third door's wording is a separate posture decision.Verification — union re-run after the final commit, at
20e3132, clean treeReverse verification (direction decided in advance: refusal pins go red, resolving 200/empty): fix committed first, both door files reverted to their
origin/mainversions viagit checkout origin/main -- packages/metadata-protocol/src/protocol.ts packages/objectql/src/filter-comparand-shape.ts, both packages rebuilt, conformance suite run: exactly the 20 new refusal pins failed (queries resolved with empty lists), all 175 controls and pre-existing pins stayed green — including the OBJECT-form and structured-head controls. Restored from the committed branch state viagit checkoutof the same two paths (byte-identical,git statusclean), rebuilt, re-run green.Suites at
20e3132, fully built workspace closure (turbo run build --filter=./packages/* --filter=./packages/*/*):@objectstack/spec— 406 files / 10,719 tests passed; typecheck clean (incl.check:test-typecheck).@objectstack/metadata-protocol(edited) — 105 files / 1,508 tests passed; typecheck clean.@objectstack/objectql(edited) — 210 of 211 files / 3,718 of 3,719 tests passed — the single red is the pre-existing calendar-armed engine-temporal-comparand-door.test.ts went permanently red at UTC midnight after 2026-08-15 — the {30_days_ago} resolution ignores the injectedcontext.now, and the fixture pins a calendar date #8937 (byte-identical toorigin/main, reproduced twice, mechanism in the card), not this diff; the conformance file with all [finding] The FILTER axis has no DOTTED-path verdict —where: { project_id.name: 'x' }rides its head segment past both doors, where SORT refuses the same spelling (#4256) #8371 pins (195 tests) is green.@objectstack/rest(consumer of the ingress door, downstream direction) — 119 files / 1,962 tests passed.objectstack validate×3 (example-crm,example-showcase,example-todo) — all exit 0, pre-existing advisories only.20e3132: all 27 check runs green (Test Core 1–3, TypeScript Type Check incl.check:api-surface, ESLint, Temporal Conformance live PG+MySQL, Dogfood gates, changeset gates). The two earlier reds were: the stale Data query: an unknown field insidewhere/$filteranswers 200/0 instead of400 INVALID_FIELD— the bare-key door disagrees (#4134's uncovered sibling) #7534 pass-through GUARD atb7f9b89(re-judged infd8f1eb— the fixture pinned exactly the limb the ruling deletes) and the api-surface snapshot atfd8f1eb(regenerated in62ff302).Gates at
20e3132, re-derived from actual changed paths vianode scripts/pm/dispatch-gates.mjs, all PASS:check:nul-bytes,check:filter-alias-parity,check:merge-driver,check:objectui-changeset,check:durability-log-level,check:spec-parsed-alias,check:changeset-gate-self-tests,check:cross-package-test-inputs,check:type-source-resolution,check:test-source-alias,check:query-options-erasure,check:type-check-coverage,check:type-check-debt --re-measure(33 entries, 1,926 raw errors, none above recorded; the lint-1surplus pre-dates this branch — also measured by PR #8933 — and is deliberately not--lowered here to avoid contending that shared ledger across live branches),check-adr-0087-registration,check-changeset-no-major,check-empty-changeset,check-dev-prereqs,check-engine-split-ratio,check:doc-formula-expressions. Fullpackages/speccheck:*battery green exceptcheck:react-declaration-parity, which needs an objectui sdui manifest this container cannot produce (needs the vendored console build + a browser) — environmental, unrelated to this diff; CI's own TypeScript job (which covers the spec surface gates) is green.Beyond the dispatch prompt's gate list, the re-derivation added
check:objectui-changeset,check-changeset-no-major,check-empty-changeset,check-dev-prereqs,check-engine-split-ratio,check:durability-log-level,check:spec-parsed-alias,check:doc-formula-expressionsand (post-CI-red)check:api-surface— all run.Generated by Claude Code