You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(spec,metadata-protocol)!: 排序节点写 direction 不再静默排反方向,两扇门同一个 change 关 (#4721) (#4922)
* feat(spec,metadata-protocol)!: reject a sort node spelling its direction `direction` (#4721)
`SortNodeSchema` was a plain `z.object`, so zod's `.strip` default applied:
SortNodeSchema.parse({ field: 'updated_at', direction: 'desc' })
→ { field: 'updated_at', order: 'asc' }
The foreign key was discarded, `order` fell back to `asc`, and the sort ran the
OPPOSITE way under an ordinary success — with `limit`, a different set of rows,
with no signal anywhere in the response.
`direction` is not a typo: it is `IReportService.orderBy`'s live vocabulary,
which plugin-auth's objectql adapter already translates by hand. A translation
known to be necessary and enforced nowhere is the ADR-0049 shape.
Both doors onto that shape are closed here, in one change:
- `SortNodeSchema` (spec/src/data/query.zod.ts) → `strictObject` with
`aliases: { direction: 'order' }`, so the rejection carries the translation.
Edit distance can never bridge `direction` → `order`, so a bare
"unrecognized key" would leave the caller where the silent strip did.
- `normalizeSortNodes` (metadata-protocol/src/protocol.ts) — the ingress every
external `orderBy` funnels through — refuses `{ field, direction }` with
400 INVALID_SORT naming `order` and quoting the corrected node.
Closing only the schema would repeat the #1535/#4522 door asymmetry:
`SortNodeSchema.parse` is reachable by three paths the REST normalizer never
sees, and the normalizer runs ahead of any QueryAST parse.
Deliberately NOT in scope: `QuerySchema`'s top level stays non-strict
(`QuerySchema.safeParse({object:'sales', nonsenseKey:1}).success === true`) —
tracked in the #4001 campaign map for its own batch.
The `{field: direction}` map form is untouched: there `direction` is an ordinary
column name, and refusing it would be the mirror-image bug.
Strictness ledger: `query.zod.ts` keeps its `open` class for the four dialect
sites; `SortNodeSchema` is carved out as authorable (4 strip of 5), which also
resolves the recorded classification conflict — the FILE was the wrong unit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9
* docs(spec): keep SortNodeSchema's page description short; rationale moves to line comments
`build-docs.ts` takes the FIRST `/** */` block in a `.zod.ts` file as the
reference page's description, and joins every line of it with a blank line. The
long #4721 rationale therefore rendered as a 56-line wall at the top of
`content/docs/references/data/query.mdx`, where a customer reads what a sort
node IS — not why one schema in the file is strict.
The prose is unchanged, it is just `//` instead of `/** */` so the generator
cannot pick it up, with a note at the top saying why it must stay that way.
Regenerated: query.mdx (two lines), and the two skill reference indexes, which
grew transitive entries because query.zod.ts now imports shared/strict-object.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9
---------
Co-authored-by: Claude <noreply@anthropic.com>
|`filter.zod.ts` / `query.zod.ts`| 11+5 | open | query dialect — user data flows through; validated semantically elsewhere. `query.zod.ts` dropped one site in #4196: `FieldNodeSchema`'s nested-select object form was declared-but-inert and narrowed to `z.string()`, so the union's second member is gone. Four more left in #4286 with the `joins`/`windowFunctions` removals: `JoinNodeBaseSchema`, `WindowFunctionNodeSchema`, and `WindowSpecSchema`'s two blocks (outer + `frame`) were deleted with their clusters. Class unchanged |
513
+
|`filter.zod.ts`| 11 | open | query dialect — user data flows through the predicate values; validated semantically elsewhere |
514
+
| `query.zod.ts` | 5 | open, **except `SortNodeSchema` → authorable** | Blanket `open` was the imprecise verdict here, not the strictness. Four sites are the dialect proper (`BaseQuerySchema`, `AggregationNodeSchema`, `FullTextSearchSchema`, `GroupByNodeSchema`'s object arm) and keep the class. `SortNodeSchema` is not dialect: a closed two-key tuple `{field, order}` with **no user-data face at all** — so #4721 carved it out and it is **strict as of #4721** (`strictObject` + `aliases: { direction: 'order' }`). What that bought, measured on `main` first: `SortNodeSchema.parse({field, direction:'desc'})` → `{field, order:'asc'}` — the sort ran the OTHER WAY, and with `limit` that is a different set of rows under an ordinary 200. Per the 11:41Z ruling on #4721 this is a NEW door, not the completion of #4371's: that check is a hand-written top-level allowlist in `objectql/src/engine.ts` (`rejectUnknownEngineOptions`) that never recurses into `orderBy[]`, and `QuerySchema` itself is **not** strict (probe: `QuerySchema.safeParse({object:'sales', nonsenseKey:1}).success === true`) — top-level strictness is #4001's, tracked separately. Site history: one site dropped in #4196 (`FieldNodeSchema`'s nested-select object form narrowed to `z.string()`); four more in #4286 with the `joins`/`windowFunctions` removals (`JoinNodeBaseSchema`, `WindowFunctionNodeSchema`, `WindowSpecSchema`'s outer + `frame`) |
| `datasource.zod.ts` | 6 | authorable | **strict as of #4001 data step** — all 6: `DatasourceSchema` (+ `pool` / `ssl`), `ExternalDatasourceSettingsSchema` (+ `validation`), `DriverDefinitionSchema`. **#4583 B/C dropped two more sites**: the `healthCheck` and `retryPolicy` blocks are gone — nothing scheduled a probe and nothing retried, so their strictness was validating a shape no code consumed. `config` stays `z.record` **at this level** by construction (per-driver shapes), but is no longer unchecked: **#4410** made `DatasourceSchema`'s refinement parse it against the contract for the declared driver (`driver/config-registry.zod.ts`), so the openness here is a shape this level cannot express rather than the absence of one. This row used to add "the driver's own `configSchema` validates them", which was false until #4410 landed the parse site it names. #4410 extended the same parse to each `readReplicas` entry; **#4468 retired that key** — no driver ever opened a replica connection and no query path splits reads from writes, so the entries were being checked against a contract nothing would apply. Strictness makes a dropped key loud; it cannot make a slot live, and a *precisely validated* dead slot is the more convincing lie | **#4583 dropped the ninth site**: `DatasourceCapabilities` is gone — eleven flags no code read, on a block whose strictness was the clearest case of this row's own closing sentence. `readOnly` in particular was *precisely validated* and completely inert, and had been relocated twice (#4410, #4465) toward somewhere it might be enforced; the shipped CRM example called a datasource a read replica on the strength of it while writes went through. Class unchanged
516
517
| `driver/memory.zod.ts` / `driver/mongo.zod.ts` / `driver/postgres.zod.ts` | 6+1+1 | authorable | The per-driver shapes for the `config` slot — what an author actually writes under `datasource.config` (`host`, `port`, `filename`). **Undeclared here until the coverage walk went recursive** (see below): a subdirectory was invisible to the gate, so these sites sat outside the map while the map reported full coverage. **Strict as of #4410**, which is also what unblocked them: this row previously read "strictness here would enforce nothing" because nothing parsed `datasource.config` against these schemas and both `*DriverSpec.configSchema` literals were `{}`. Now `DatasourceSchema` parses `config` against them, and the same schemas project onto `configSchema` and onto the Studio connection form. (#4410 also ran the parse over each `readReplicas` entry; #4468 retired that key outright — see the row above.) `postgres.zod.ts` drops a site: its `ssl` was a `boolean | {ca, cert, key, …}` union, and the object arm is gone — certificates now live in the datasource-level `ssl` block (declared, strict, and until #4410 read by nobody), leaving `config.ssl` as the on/off shorthand. That narrowing is forced by the same projection: the Studio form renders anything that is not boolean/enum/number as a TEXT INPUT, so a union here would have produced a wizard whose every `ssl` value the new gate rejects. `memory.zod.ts` keeps 6 but loses two KEYS — `indexes` / `maxRecordsPerObject`, which `InMemoryDriverConfig` has no field for, removed under ADR-0049 rather than blessed by the new gate |
@@ -670,7 +671,7 @@ reverse pin above). Worth noting for the next batch that "resolve a row" has two
670
671
exits, and the reverse pin cannot tell them apart — only the changeset and the
671
672
triage row record which one was taken.
672
673
673
-
#### `data/` — 121 strip of 162
674
+
#### `data/` — 120 strip of 162
674
675
675
676
| File | Strip | Sites | Class | Batch |
676
677
|---|---|---|---|---|
@@ -684,14 +685,14 @@ triage row record which one was taken.
|`document.zod.ts`| 8 | 8 | wire (p) |`DocumentTemplate` / `ESignatureConfig` read authorable on their face — the `(p)` is unresolved, verify before scheduling either way |
686
687
|`driver/memory.zod.ts`| 5 | 6 | authorable | The persistence-adapter union under `datasource.config`; `datasource.config` HAS been parsed against these since #4410, so strictness here now binds |
687
-
|`query.zod.ts`|5| 5 | open | ⚠️ **classification conflict — see #4721.** The row calls the query dialect `open`; #4721 asks for `SortNodeSchema.strict()`. Both cannot be right. Resolve the class before writing code|
688
+
|`query.zod.ts`|4| 5 | open |~~⚠️ classification conflict — see #4721~~**RESOLVED (11:41Z ruling, closed by #4721).** The conflict was real and the answer was that per-FILE classification was the imprecise instrument: `SortNodeSchema` was carved out as `authorable` and closed (`strictObject` + `aliases: { direction: 'order' }`), the other 4 sites keep `open`. Those 4 are the dialect proper — `BaseQuerySchema`, `AggregationNodeSchema`, `FullTextSearchSchema`, `GroupByNodeSchema`'s object arm — and `BaseQuerySchema`'s own top-level strictness is #4001's to schedule, deliberately **not** taken by #4721|
688
689
|`external-catalog.zod.ts`| 4 | 4 | wire (p) |**out of scope**|
689
690
|`hook.zod.ts`| 4 | 6 | wire |**out of scope** — `HookContextSchema` + `.session`/`.provenance`/`.user` are the runtime shape handed to a handler; verified in the data step |
690
691
|`field.zod.ts`| 3 | 11 | authorable |`LocationCoordinates` / `CurrencyValue` / `Address` — field VALUE shapes, not field config; check whether they are record data (→ open) before closing |
691
692
|`driver-sql.zod.ts`| 2 | 2 | wire |**out of scope**|
692
693
|`field-value.zod.ts`| 1 | 2 | mixed (p) |`LocationValueSchema` — record data, very likely **open**; its sibling `FileValueSchema` is already `z.looseObject`|
693
694
694
-
**Authorable strip in `data/`: ~22 firm** (`object` 14 + `driver/memory` 5 + `field` 3), **plus ~33 needing a per-schema verdict** (`external-lookup` 12, `seed-loader` 12, `analytics` 8, `field-value` 1). 66 are wire/open and out of the ruling's forced scope.
695
+
**Authorable strip in `data/`: ~22 firm** (`object` 14 + `driver/memory` 5 + `field` 3), **plus ~33 needing a per-schema verdict** (`external-lookup` 12, `seed-loader` 12, `analytics` 8, `field-value` 1). 65 are wire/open and out of the ruling's forced scope — 66 until #4721 closed `query.zod.ts`'s `SortNodeSchema`, which is the one row in this directory where the per-schema read moved a site OUT of `open` rather than confirming it.
695
696
696
697
#### `security/` — 13 strip of 20
697
698
@@ -789,25 +790,39 @@ is the confirmation the campaign's own progress log was missing.
789
790
estimated — read it, do not re-derive it, and do not plan off `strictObject(`
790
791
occurrence counts (finding 19 explains what that undercounts).
791
792
792
-
Two things in that map need a decision before any code is written, and both
793
-
are classification questions rather than implementation ones:
794
-
795
-
-**`data/query.zod.ts` is classed `open`, and #4721 asks for
796
-
`SortNodeSchema.strict()`.** Both cannot be right. Measured, so the decision
797
-
is made against facts rather than recollection: `SortNodeSchema.parse({
0 commit comments