fix(metadata-protocol): sort audit history and global search by order, not direction (#4674) - #4720
Merged
Merged
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #4674.
Two internal
engine.findcalls inprotocol.tswrote their sort as{ field, direction: 'desc' }. The QueryAST sort shape isSortNodeSchema={ field, order }, and both real drivers normalize off.orderwith no fallback —sql-drivermapsitem.order === 'desc',mongodb-driverthe same. Withorderabsent,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:limiteventsperObject, with recently-edited records truncated awayOn 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.orderByisSortNodeSchema[]and would have rejecteddirection. Both sites opted out of exactly that check — one with} as any), the other withconst 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
tscis the enforced channel, and nothing else on this path is:INVALID_SORTlives in the protocol's inboundfindDatanormalizer and inrest-server.ts. These two sites are the protocol callingthis.engine.finddirectly — inside its own guard, not behind it.{ field: 'x', direction: 'desc' }has the requiredfieldand 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.tsdestructured{ field, direction }offopts.orderBy, which is an array. Both names readundefined, 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) setslimitbelow the fixture size, so sorting the wrong way returns a disjoint set — an order-only assertion would pass against a double that ignoredorderByentirely. The double honoursorderBy+limitand nothing else; filtering is deliberately unimplemented so a sort bug cannot hide behind awherethat happens to select the right rows.Verified they fail against the old code:
Swept for the same mistake
The three other
directionsites in the tree are all correct and untouched:report-service.ts:29—IReportService.orderByis{ field, direction? }, a genuinely different contractplugin-auth/objectql-adapter.ts:536— the explicitdirection→ordertranslation, which is the proof the translation is known to be neededsql-driver.ts:1429—directionas the driver's post-normalization internal vocabulary, likely how the wrong key looked plausible in the first placeThese 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 recognizesdirection) 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, wheretsccannot reach. Worth doing, worth deciding on its own terms.Verification
pnpm typecheck122/122 ·pnpm lintclean · metadata-protocol 220 tests (27 files) · objectql 1638 tests (103 files).Refs: ADR-0049, #4363
Generated by Claude Code