Skip to content

fix(objectql)!: engine.find/findOne refuse an unmaterializable formula ORDER BY (#7095) - #7337

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-7095-find-orderby-refusal
Aug 10, 2026
Merged

fix(objectql)!: engine.find/findOne refuse an unmaterializable formula ORDER BY (#7095)#7337
os-zhuang merged 2 commits into
mainfrom
claude/issue-7095-find-orderby-refusal

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #7095. Follows #6994 (the ingress half) and #6924 (the sort-hint prescription).

The defect, reproduced on current main first

#6994 refuses a non-dotted orderBy naming a formula field at ingress (assertSortFieldsExist, 400 INVALID_SORT), which covers everything reaching findData: the REST list route, POST /data/:object/query, the export route and the RPC dispatcher. It could not cover a caller reaching engine.find() / engine.findOne() directly.

Reproduced on 06be54ec3 before touching anything — the RECORD OF A KNOWN HOLE pin #6994 left behind still passed, i.e. the hole was still open:

engine.find(o, { orderBy: [{ field: <formula>, order: 'asc'  }] }) -> C A E B D
engine.find(o, { orderBy: [{ field: <formula>, order: 'desc' }] }) -> C A E B D
                                             asc === desc (byte-identical)

A formula value is computed on read, so no driver materialises a column for it: the ORDER BY reached the driver, found nothing, and the unknown-column backstop returned the rows unordered under a success — carrying the very values they were asked to be ordered by. With limit, "the latest N" was an arbitrary N nothing in the response could reveal.

The ruled direction

Per the maintainer ruling (2026-08-10 on #7095): refuse at the public boundary with guidance prose, never a silent drop. assertOrderByIsMaterializable refuses on both entry points with the same 400 INVALID_SORT and the same remedy sentence the two ingress verdicts emit.

findOne is covered as well as find — same public boundary, and the stakes are higher there: findOne applies limit: 1, so a dropped sort does not merely reorder the answer, it returns a different record that looks just as legitimate.

The measured verdict on internal reliance

The ruling made the internal-caller tolerance conditional on a measured internal call site relying on it. The sweep found none, so outcome 1 applies: refuse everywhere, no internal path shipped.

Sweep (151 non-test orderBy occurrences outside spec/metadata-protocol, narrowed to those reaching the engine directly — drivers are the receiving end, client/client-react/MCP go over the wire through ingress):

Internal call site Sort field(s) Formula?
plugin-approvals/approval-service.ts (×4) created_at no
plugin-reports/report-service.ts updated_at, next_run_at, q.orderBy author-driven
plugin-sharing (×4) created_at, name no
service-queue/db-queue-adapter.ts created_at, priority, scheduled_for no
service-job/db-job-adapter.ts started_at no
service-messaging/messaging-service.ts created_at no
plugin-email/outbox-sweep.ts created_at no
metadata/loaders/database-loader.ts recorded_at, version no
metadata/utils/history-cleanup.ts version no
runtime/domains/share-links.ts created_at no
plugin-auth/objectql-adapter.ts (×2) caller sortBy caller-driven
types/keyset-walk.ts keyset key (defaults id) no
engine.expandRelatedRecords nested expand sort author-driven

Two independent facts make the verdict solid rather than a shrug:

  1. Every hardcoded internal sort names a real stored column.
  2. No shipped object anywhere in the repo declares a formula field at all — the only non-test occurrences are the spec's own field builder (field.zod.ts) and one FLS contract fixture. So no hardcoded internal sort could name one.

No test relied on the drop either, except the deliberate RECORD OF A KNOWN HOLE pin — which said in as many words that it should go red the day this landed. It is now inverted in place.

The dynamic sites do not rely on the tolerance; they are the paths through which the silent drop was author-reachable, which is exactly why ingress-only was not tenable: a saved report's query.orderBy is forwarded verbatim into engine.find by plugin-reports, bypassing the ingress gate entirely.

One path deliberately does NOT become a refusal ⚠️

A nested expand sort raises this refusal inside expandRelatedRecords — but that sub-read sits inside a pre-existing graceful-degradation catch ("if expand fails, keep original IDs") which swallows every expand failure. Measured:

WARN Failed to expand relationship field; retaining foreign key IDs
     { field: 'parent_id', error: "ObjectQL.find('showcase_task') sorts by 'sort_key',
       a formula field … Denormalise the value onto … " }

So that path moves from silent to observable (a warning naming the field and the fix) rather than refusing. Reversing that backstop is the #3821-family swallow — a decision about all expand failure modes, not a rider on this card. It is measured and pinned as-is rather than left implied; flagged for follow-up.

Scope held deliberately narrow

  • The ingress gate is untouched — same message, same unknown > dotted > unmaterializable precedence, same param name the engine cannot know. Not weakened.
  • The engine door judges only the third verdict. Unknown and dotted sort names still reach the driver from a direct call, because refusing those is a posture change on two further axes, not a free extension of this one.
  • summary/rollup fields are unaffected and still sort in both directions — they get a real maintained column. A control pins that, so widening the set to the spec's COMPUTED_VALUE_TYPES (the write contract) goes red.
  • Reading a formula field, and the projection axis' SELECT * tolerance, are untouched — pinned.
  • The post-hoc-sort trap the card called out is written into the code comment: driver.find has already applied limit/offset, so sorting after applyFormulaPlan would reorder an arbitrary page.

Docblocks updated honestly

assertProjectionFieldsExist's documented internal-caller tolerance is the posture this narrows, so it now says the tolerance is per-axis and that the sort axis no longer has it — and that nothing equivalent has been measured for the projection axis, so the sentence is not a licence to assume it. assertSortFieldsExist's SCOPE paragraph no longer claims the direct path "still gets the silent drop".

Pins

  • engine.find refuses — asc, desc, and as the second of two sort keys — with asserted status + code + field + object, not merely "throws".
  • engine.findOne refuses, with a where present so it is the sort verdict and not requireFindOnePredicate answering first.
  • Guidance prose asserted: names the entry point, the field, the type, "computed on read", and never prescribes what it refuses.
  • Three doors agree word-for-word on the remedy (was two) — the anti-drift pin, and the reason the engine duplicates the prose instead of importing it (see below).
  • Negative pin: no internal path — four plausible opt-out flags are all refused by the public options shape, and the old silent-drop shape now throws.
  • Controls: a real column and a summary field still sort through engine.find; a formula field is still selectable and still computed.
  • The ingress pins from A non-dotted orderBy naming a formula field answers 200 in arbitrary order — the sort is silently dropped (measured on driver-sql + driver-memory) #6994 are untouched and still green.

Why the prose is duplicated rather than imported

metadata-protocol is assembled from an engine (assembleMetadataProtocol), so it is the layer above objectql; importing its error helper into engine.ts would invert the layering. The word-for-word equality pin is what keeps the duplication honest.

Changeset / ADR-0087

Declared major for @objectstack/objectql — a public API throws where it used to succeed. That triggers the ADR-0087 disposition requirement, so the trio is filled in following #7210 (708431313) as the worked example:

  • .changeset/engine-find-formula-orderby-refusal.md with <!-- adr-0087: registered engine-find-formula-order-by-refused -->
  • packages/spec/src/migrations/registry.ts — step-17 semantic entry
  • packages/spec/spec-changes.json + docs/protocol-upgrade-guide.md regenerated (gen:spec-changes, gen:upgrade-guide); both --check gates green

registered rather than one of the three not-required forms: @objectstack/objectql is published, no prior id covers this surface, and the change ships rewrite instructions — so no-migration-prescription would be self-contradictory.

⚠️ packages/spec is touched for the ADR-0087 disposition (ledger + generated artifacts only, no schema change) — cross-seat declaration per #6017.

Refs #7095, #6994, #6924, #4226, #4256, #3821, #7210, ADR-0087, ADR-0112


Generated by Claude Code

…rmula ORDER BY (#7095)

#6994 closed the SORT axis at the REST ingress (`assertSortFieldsExist`,
`400 INVALID_SORT`), covering everything reaching `findData`. A caller reaching
`engine.find()` / `engine.findOne()` directly passed through none of it, and a
`formula` ORDER BY there was dropped in silence. Measured on this change's base,
real `ObjectQL` over a driver that really sorts:

  engine.find(o, { orderBy: [{ field: <formula>, order: 'asc'  }] }) -> C A E B D
  engine.find(o, { orderBy: [{ field: <formula>, order: 'desc' }] }) -> C A E B D
                                               asc === desc (byte-identical)

Ruled 2026-08-10 on #7095: refuse at the public boundary with guidance prose,
never a silent drop. `assertOrderByIsMaterializable` refuses on both entry
points with the same `400 INVALID_SORT` and the same remedy sentence the two
ingress verdicts emit — pinned as an equality across all three doors, since
separate wordings is how #4256 and #6673 drifted apart.

The tolerance was to survive only behind a pinned internal path, and only if a
MEASURED internal call site relied on it. The sweep found none: every hardcoded
internal sort names a real stored column, and no shipped object declares a
`formula` field. So no internal path shipped, and a negative pin keeps one off
the public options shape.

The one author-reachable consumer is why ingress-only was not tenable: a saved
report's `query.orderBy` is forwarded verbatim into `engine.find` by
`plugin-reports`. One path deliberately does NOT become a refusal — a nested
`expand` sort raises it inside `expandRelatedRecords`, whose pre-existing
graceful-degradation catch swallows every expand failure, so that path moves
from silent to observable (a warning naming the field and the fix) rather than
refusing. Reversing that backstop is #3821's decision, not this card's; it is
measured and pinned as-is.

The ingress gate is untouched, and the engine door judges only the third verdict
— unknown and dotted names still reach the driver from a direct call, because
refusing those is a posture change on two further axes.

Registered in the ADR-0087 step-17 ledger as
`engine-find-formula-order-by-refused`; artifacts regenerated.

Refs #7095, #6994, #6924, #4226, #4256, #3821, ADR-0087, ADR-0112

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJgGahRqaYPRJ2oKmk9Czc
@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 10, 2026 9:04am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/metadata-protocol, @objectstack/objectql, @objectstack/spec.

109 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/metadata-protocol, @objectstack/objectql, packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql, @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via packages/objectql, @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/metadata-protocol, @objectstack/objectql, @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/permissions/system-context.mdx (via packages/objectql, packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/metadata-protocol, @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via packages/objectql, @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

7 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/metadata-protocol, @objectstack/spec)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Gates run locally

Enumerated with the prescribed command: 64 in lint.yml, 70 across all workflows.

⚠️ Enumeration caveat worth recording: the 70-union includes at least one name that is not a runnable gatecheck:variant-docs appears only inside a lint.yml comment (the #4177 post-mortem about unclassified scripts), not as a step. grep -oE 'check:[a-z0-9:-]+' cannot tell a step from a comment.

All green (exit 0):

check:adr-0087-registration (self-test: 142 assertions + real scan) · check:empty-changeset · check:changeset-no-major · check:changeset-fixed · check:changeset-gate-self-tests · check:spec-changes · check:upgrade-guide · check:generated --reconcile-only · check:docs · check:authorable-surface · check:export-origins · check:api-surface · check:exported-any · check:dual-source-exports · check:error-code-casing · check:query-options-erasure · check:engine-double-contract · check:route-envelope · check:adr-anchors · check:nul-bytes · check:strictness-ledger · check:type-check-coverage · check:type-check-debt · check:agent-model-declared · check:app-nav-i18n · check:authz-resolver · check:doc-authoring · check:doc-formula-expressions · check:docs-audit-scope · check:driver-conformance · check:driver-memory-census · check:durability-log-level · check:i18n · check:i18n-coverage · check:init-service-contract · check:kernel-hook-pairs · check:merge-driver · check:meta-type-normalized · check:node-version · check:org-identifier · check:published-files · check:quick-reference-counts · check:release-body · check:release-notes · check:required-contexts · check:resume-authority-declared · check:role-word · check:service-providers · check:shard-attestation · check:slot-lookup · check:spec-parsed-alias · check:stack-collection-maps · check:stall-guard · check:startup-registry-verdict · check:tenant-chokepoint · check:verify-stand-in · check:wildcard-fallthrough · check:workflow-status-functions · check:objectui-changeset · check:skill-compatibility · check:skill-frame-sync

Plus pnpm lint (eslint, --no-inline-config) and turbo run typecheck16/16 tasks successful across the touched packages and their dependents.

Pre-existing red, NOT caused by this PR:

  • check:platform-checklist — fails with coverage.json · qa: UNCLASSIFIED. Verified by running the same script in a clean worktree at origin/main (06be54ec3): identical single failure. Nothing in this diff touches docs/qa/.

Tests

Suite Result
@objectstack/objectql 2960 passed (169 files)
@objectstack/metadata-protocol 862 passed (67 files)
@objectstack/spec 9464 passed (362 files)
@objectstack/plugin-reports 68 passed
@objectstack/plugin-auth 996 passed
@objectstack/rest 1228 passed
@objectstack/runtime 1870 passed

plugin-reports and plugin-auth are the two packages that forward a caller-supplied sort into engine.find directly, so they are the regression surface that matters most here.

Note on a false alarm worth not repeating: these four consumer suites first appeared to fail wholesale. The cause was unbuilt workspace dist (Failed to resolve entry for package …), not the refusal — they are green after pnpm build.


Generated by Claude Code

@claude

claude Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

PM verification — the ESLint red IS this PR's to fix, and the ratchet's own message prescribes the remedy.

The session status said "hook flagged upstream commits, not my work". Measured: ESLint is green on main HEAD and green on every sibling PR; the failing step on this head (1ff0e37ed) is the query-options-erasure ratchet, and its message names this diff directly:

✗ query-options-erasure ratchet: test surface grew 249 → 253 site(s). Either type the options, or — if the input is DELIBERATELY off-contract (a test asserting the engine rejects an unknown option) — write as unknown as EngineQueryOptions, which names the contract being bypassed, keeps the rest of the call checked, and is not counted. Raising this number is a reviewed edit, not a remedy.

This PR adds four query-option call sites in test code (the refusal pins and the four opt-out-flag probes are exactly the shape the ratchet anticipates). The fix is the ratchet's second branch: spell the deliberately off-contract inputs as unknown as EngineQueryOptions; type the on-contract ones. ⛔ Do not raise the budget number.

Everything else about this PR reads well — the four-plausible-flags negative pin, the word-for-word three-door prose equality pin with the layering argument for duplication, and a complete ADR-0087 trio following 708431313. This one mechanical red is all that stands between it and the queue. The packages/spec touch (ledger + generated only) is noted; the PM will file the #6017 declaration once CI is green.


Generated by Claude Code

… them to `any`

The four call sites the #7095 pins added tripped the #4918 query-options-erasure
ratchet (test surface 249 -> 253). Fixed at the call sites, per the rule's own
prescription — the ceiling is unchanged and no pin is weakened.

Three were ON-contract and are now typed:

  - the `it.each` sort table is `Array<[string, NonNullable<EngineQueryOptions['orderBy']>]>`,
    so the three refused sorts are checked as the well-formed `SortNode[]` they
    are. It is the FIELD they name that the engine refuses, never their shape,
    and an `as any` there would have erased the one channel that enforces
    `{ field, order }` on a direct engine call — the `direction`-vs-`order`
    mistake #4674 is about.
  - both `expand` sites drop the assertion entirely: `EngineQueryOptions.expand`
    is `Record<string, QuerySchema>`, so the nested `{ orderBy }` was always
    assignable and the cast was never buying anything.

One is DELIBERATELY off-contract — the negative pin that smuggles an opt-out
flag onto the public options bag — and is now `as unknown as EngineQueryOptions`
rather than a bare `as any`: it names the contract being bypassed, keeps the
rest of the call type-checked, and greps as an intentional act. That is exactly
the case #4918 carved the spelling out for, since the assertion's whole subject
is that the engine rejects the unknown key.

Refs #7095, #4918, #4674, #4721

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJgGahRqaYPRJ2oKmk9Czc

Copy link
Copy Markdown
Contributor Author

Ratchet fixed at the call sites — and a correction to my earlier gate report

The correction first

My earlier "all gates green" comment was not trustworthy, and the fault was mine. Two of the three batches I ran captured the exit code through a pipeline:

out=$(timeout 600 pnpm $g 2>&1 | tail -3); code=$?   # ← $? is tail's status, always 0

So every gate in those batches reported ok regardless of its real result — including check:query-options-erasure, which was already red at that moment. The gate did not regress after I reported it; I never actually measured it. Apologies for the noise that caused.

Everything has now been re-run with the exit code captured directly (cmd >log 2>&1; code=$?). Corrected results are at the bottom.

The fix — ceiling unchanged, no pin weakened

before: test surface grew 249 → 253 site(s)
after:  test surface: 249 site(s) in 49 file(s) — at the ceiling

Four sites, fixed per the rule's own prescription. Three were on-contract and are now typed:

  • The it.each sort table is now Array<[string, NonNullable<EngineQueryOptions['orderBy']>]>. These three sorts are perfectly well-formed SortNode[] — it is the field they name that the engine refuses, never their shape. An as any there would have erased the one channel that enforces { field, order } on a direct engine call, which is precisely the direction-vs-order defect fix(metadata-protocol): two internal engine.find calls sort by direction, a key no driver reads — audit history and global search silently return ASCENDING #4674 is about. The cast was hiding the rule's whole purpose in a test about sort correctness.
  • Both expand sites drop the assertion entirely. EngineQueryOptions.expand is Record<string, QuerySchema>, so the nested { orderBy } was always assignable — the cast was never buying anything.

One is deliberately off-contract — the negative pin that smuggles an opt-out flag onto the public options bag — and is now as unknown as EngineQueryOptions. That is exactly the case #4918 carved the spelling out for: the assertion's entire subject is that the engine rejects the unknown key, so the input cannot be made to typecheck, and the cast now names the contract being bypassed while keeping the rest of the call checked.

Budget number untouched; no pin altered. All 118 conformance tests still pass, typecheck green.

Corrected gate results (exit codes captured directly)

  • 47 root check:* gates — all pass except one (below).
  • 15 package-filtered gates — all pass (@objectstack/spec × 14 incl. tsc --noEmit, plus @objectstack/lint check:doc-formula-expressions). These are the ones a bare pnpm check:* grep misses.
  • pnpm lint (eslint, --no-inline-config) — exit 0.
  • turbo run typecheck — green.

One pre-existing red, re-confirmed against the current main tip:

check:platform-checklistcoverage.json · qa: UNCLASSIFIED. Verified by running the same script in a clean worktree at origin/main db12b88 (main has moved since my first check at 06be54ec3; identical failure at both). Nothing in this diff touches docs/qa/.

CI is running on dfa2502; I will confirm the result rather than infer it.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 31376073539 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Temporal Conformance (live PG + MySQL) — 失败步骤: Install dependencies(日志不可读,点进 job 看)
  • Build Docs — 失败步骤: Install dependencies(日志不可读,点进 job 看)
  • Dogfood Verify CLI — 失败步骤: Install dependencies(日志不可读,点进 job 看)
  • Build Core — 失败步骤: Install dependencies(日志不可读,点进 job 看)

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 11 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 在其他 PR 的同类评论里搜同名测试;出现过 ⇒ flaky 实锤,开 issue 修/隔离那条测试。修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit 6908830 Aug 10, 2026
27 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-7095-find-orderby-refusal branch August 10, 2026 10:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

engine.find() still drops a formula ORDER BY silently — decide whether the engine refuses or keeps its internal-caller tolerance

2 participants