Skip to content

fix(metadata-protocol): sort audit history and global search by order, not direction (#4674) - #4720

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-4674-orderby-direction
Aug 3, 2026
Merged

fix(metadata-protocol): sort audit history and global search by order, not direction (#4674)#4720
os-zhuang merged 1 commit into
mainfrom
claude/issue-4674-orderby-direction

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #4674.

Two internal engine.find calls in protocol.ts wrote their sort as { field, direction: 'desc' }. The QueryAST sort shape is SortNodeSchema = { field, order }, and both real drivers normalize off .order with no fallback — sql-driver maps item.order === 'desc', mongodb-driver the same. With order absent, undefined === 'desc' is false and both queries ran ascending.

Because both carry a limit, the wrong direction did not merely reorder a page — it changed which rows came back:

site intent actual
metadata audit history newest limit events the oldest — the beginning of an object's life, never its recent changes
global search most-recently-updated matches the stalest perObject, with recently-edited records truncated away

On a long-lived object the history panel would never show what an editor came for; in search, the recently-edited records are both the most wanted and the ones excluded.

Why the type erasure is most of the fix

EngineQueryOptions.orderBy is SortNodeSchema[] and would have rejected direction. Both sites opted out of exactly that check — one with } as any), the other with const opts: any. Dropping the erasure is what makes the mistake unrepeatable here, so this PR does that rather than only correcting the two literals.

That matters because for an internal caller tsc is the enforced channel, and nothing else on this path is:

  • INVALID_SORT lives in the protocol's inbound findData normalizer and in rest-server.ts. These two sites are the protocol calling this.engine.find directly — inside its own guard, not behind it.
  • That normalizer rejects bad values, not unknown keys. { field: 'x', direction: 'desc' } has the required field and no unreadable value, so it reads as a well-formed "sort by x, direction unspecified". The schema is not .strict(), so the key is dropped rather than flagged.

The test double was ratifying the bug — twice over

protocol-publish-rollback.test.ts destructured { field, direction } off opts.orderBy, which is an array. Both names read undefined, so the double spoke the vocabulary the engine does not read and sorted nothing at all. Either way, coverage built on it would have confirmed broken behaviour. It now reads { field, order } across the array, supporting multiple sort keys.

Regression tests assert identity, not sequence

protocol.orderby-vocabulary.test.ts (4 tests) sets limit below the fixture size, so sorting the wrong way returns a disjoint set — an order-only assertion would pass against a double that ignored orderBy entirely. The double honours orderBy + limit and nothing else; filtering is deliberately unimplemented so a sort bug cannot hide behind a where that happens to select the right rows.

Verified they fail against the old code:

× returns the NEWEST `limit` events, not the oldest
  AssertionError: expected [ 'a1', 'a2' ] to deeply equal [ 'a5', 'a4' ]
× returns the most recently updated matches, not the stalest
  AssertionError: expected [ 'c1', 'c2' ] to deeply equal [ 'c4', 'c3' ]

Swept for the same mistake

The three other direction sites in the tree are all correct and untouched:

  • report-service.ts:29IReportService.orderBy is { field, direction? }, a genuinely different contract
  • plugin-auth/objectql-adapter.ts:536 — the explicit directionorder translation, which is the proof the translation is known to be needed
  • sql-driver.ts:1429direction as the driver's post-normalization internal vocabulary, likely how the wrong key looked plausible in the first place

These two were the only wrong ones.

Item 4 is deliberately not here

The issue's fourth item — make the mismatch fail for callers too — is filed separately rather than folded in, because it protects a different population. Both proposals in the issue (.strict() on the sort schema, or a normalizer that recognizes direction) live on the inbound path, and the issue's own "why nothing caught it" section establishes that path does not run for these two sites. So the class fix would not have caught the bug that motivated it; it guards external callers, where tsc cannot reach. Worth doing, worth deciding on its own terms.

Verification

pnpm typecheck 122/122 · pnpm lint clean · metadata-protocol 220 tests (27 files) · objectql 1638 tests (103 files).

Refs: ADR-0049, #4363


Generated by Claude Code

…r`, not `direction` (#4674)

Two internal `engine.find` calls wrote their sort as
`{ field, direction: 'desc' }`. The QueryAST sort shape is `SortNodeSchema` =
`{ field, order }`, and both real drivers normalize off `.order` with no
fallback — `sql-driver` maps `item.order === 'desc'`, `mongodb-driver` the
same. With `order` absent, `undefined === 'desc'` is false and both queries ran
ASCENDING. `direction` is `IReportService`'s vocabulary, a genuinely different
contract, which is how the wrong spelling looked plausible.

Because both queries carry a `limit`, the wrong direction did not merely
reorder a page — it changed WHICH ROWS CAME BACK:

- Metadata audit history returned the OLDEST `limit` events: the beginning of
  an object's life and never its recent changes. On a long-lived object an
  editor would never see what they came for.
- Global search returned the STALEST `perObject` matches, truncating away the
  recently-edited records — the ones a searcher is most likely to want.

The `as any` / `: any` at both sites go with it. `EngineQueryOptions.orderBy` is
`SortNodeSchema[]` and would have rejected `direction`; the erasure is what let
this through, and for an internal caller `tsc` IS the enforced channel — the
protocol's `INVALID_SORT` normalizer does not run on calls the protocol makes
to `this.engine.find` directly, and it rejects bad VALUES rather than unknown
KEYS anyway (the schema is not `.strict()`, so `direction` was dropped, not
flagged).

Also fixes the `sys_metadata_history` double in
`protocol-publish-rollback.test.ts`, which destructured `{ field, direction }`
off `opts.orderBy` — an ARRAY. Both names read `undefined`, so it spoke the
vocabulary the engine does not read AND sorted nothing at all; a test built on
it would have ratified either failure.

The regression tests assert on row IDENTITY rather than sequence, with `limit`
below the fixture size, so a wrong direction returns a disjoint set — an
order-only assertion would pass against a double that ignored `orderBy`
entirely. Verified they fail against the old code: `['a1','a2']` instead of
`['a5','a4']` for audit history, `['c1','c2']` instead of `['c4','c3']` for
search.

Swept the tree for the same mistake: the three other `direction` sites are all
correct — `IReportService`'s own contract, plugin-auth's explicit
`direction` → `order` translation, and sql-driver's post-normalization internal
vocabulary. These two were the only wrong ones.

Item 4 of the issue (make the mismatch fail for EXTERNAL callers too) is
deliberately not here: it is a separate decision, and it protects a different
population — filed separately.

Closes #4674

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

vercel Bot commented Aug 2, 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 2, 2026 9:30pm

Request Review

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Aug 2, 2026
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol.

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

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/metadata-protocol)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/metadata-protocol)
  • content/docs/releases/v9.mdx (via @objectstack/metadata-protocol)

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.

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/m tests tooling

Projects

None yet

2 participants