Skip to content

Commit 36fa4ff

Browse files
fix(spec)!: an aggregation entry refuses an undeclared key by name on every door, REST matching MCP (#28609) (#28706)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XBiH7mWuVM4qYrH5Ba3EJPE)_ Fixes #28609 Clause-②: no (narrowing) Metadata-protocol: none Skills-docs: none — the published skills (`skills/objectstack-query`, `skills/objectstack-api`) and the docs teach only the declared entry members (`function`, `field`, `alias`, `filter`) and never an extra entry key, so nothing they teach changes; the refusal itself names the key and the spelling to send. ## 维护者速读 - **改了什么:** 查询里的聚合条目(`aggregations[]` 的每一项)现在在 spec 源头就关闭:写了 spec 没声明的键(比如拼错的 `bogusKey`,或把条目自己的 `filter` 写成 `where`)会被点名拒绝。REST、运行时分发器和 MCP 的 `aggregate_records` 读同一个 schema,拒绝文字一字不差。 - **为什么改:** 上一张卡(#28550)让 MCP 先拒绝了这种键,而 REST 仍然悄悄丢掉它照常返回 200,同一个条目在两个入口得到两种结论。 - **风险与回滚:** 发过多余条目键的调用方(REST、分发器、进程内)会从 200 变成 400 `VALIDATION_FAILED`;只含声明键的查询不变。回滚即还原 `query.zod.ts` 一处和 MCP 一处。 - **先看:** `packages/spec/src/data/query.zod.ts`(`AggregationNodeSchema` 变成 `strictObject`)、`packages/mcp/src/mcp-http-tools.ts`(删掉 MCP 自己那份条目副本,直接用 spec 节点)、`packages/rest/src/aggregation-entry-undeclared-key-door.test.ts`。 - **两分钟验证:** `pnpm --filter @objectstack/rest exec vitest run src/aggregation-entry-undeclared-key-door.test.ts`,预期 5 个用例全绿(3 个拒绝、2 个对照)。 - **需要你做的:** 无,席位落地。 ## What changes - **`AggregationNodeSchema` is closed at the producer** (`packages/spec/src/data/query.zod.ts`). It is now a `strictObject` with the unknown-key campaign's message (`Unrecognized key(s) on this aggregation entry: KEY.`), near-miss hints `where` / `filters` / `conditions` → `filter` and `as` → `alias`, and a history sentence. The retired `distinct` keeps its `retiredKey()` tombstone and answers that sentence, never the generic refusal (strictObject keeps tombstones out of suggestions). Every door that parses the node inherits it: `POST /api/v1/data/:object/query` on `RestServer` and on the runtime dispatcher (both parse `FindDataRequestSchema`), and `EngineAggregateOptionsSchema.aggregations`. - **MCP folds into it** (`packages/mcp/src/mcp-http-tools.ts`). `aggregate_records` took each entry as its own `strictToolInput` copy of the node's four members (#28550), a second closed key list. It now takes `AggregationNodeSchema` whole, so the refusal is the spec node's own sentence, the same one REST prints, and `distinct` answers its tombstone on both doors (MCP used to give it the generic unknown-key refusal). `filter` still reaches the bridge unchanged. `tools/list` now lists `distinct` as a schema nothing satisfies (`not: {}`) with its `[REMOVED]` text, and the member descriptions are the spec's. - **Strictness ledger**: `data/` strip count 64 → 63 (regenerated), and the hand prose rows for `query.zod.ts` re-read and updated to say the node is strict. - **Changeset** `.changeset/28609-aggregation-entry-strict.md`: `@objectstack/spec` and `@objectstack/mcp` patch, `fix(spec)!:`, **BREAKING** banner, both declaration lines, ADR-0087 `not-required (no-migration-prescription)` (a query aggregation entry is a request surface, never stored metadata). It carries one sentence superseding the pending #28550 changeset's "the REST query door is untouched". ## Tests - `packages/rest/src/aggregation-entry-undeclared-key-door.test.ts` (new): the real door (`RestServer` → `findData` → `ObjectQL.aggregate` → sqlite `SqlDriver`). `bogusKey`, entry `where`, entry `as`: 400 `VALIDATION_FAILED`, `fields[]` at `query.aggregations.1` carrying exactly `AggregationNodeSchema.safeParse(entry)`'s message, driver never read. Controls: `distinct` keeps its tombstone; declared-only entries (count, sum, filtered count) answer the same grouped numbers. - `packages/runtime/src/domains/data-write-request-gate.test.ts`: one row in `QUERY_REFUSED`, so both doors (RestServer and the dispatcher catch-all) refuse it and the cross-door parity block asserts they answer alike. - `packages/rest/src/request-schema-gate.conformance.test.ts`: one more violating body on the `/query` route. - `packages/mcp/src/mcp-http-tools.aggregation-entry.test.ts`: refusals now contain the spec node's message; `distinct` gives the tombstone sentence and no "Unrecognized key"; `tools/list` pin updated (`distinct` present, `not: {}`). - `packages/spec/src/data/query.test.ts`: new block for the closed node (slip, four hints, tombstone not suggested, closure through `QuerySchema` and `EngineAggregateOptionsSchema`, declared-only control); two stale comments that said the node is non-strict corrected. ## Acceptance notes - **Premise held.** Measured on base `0b6a52e8` through the REST door: all three entry slips answered 200 with the grouped numbers. - **Metadata-protocol measured `none`, not the order's expected `narrowed`.** AGENTS.md defines the line as the change to what a metadata author can write. `check:authorable-surface` stayed green (the node's key list is unchanged; strictness is not on that surface), the spec's own docblock calls QueryAST a request surface never stored in metadata, and the claim recorded `none` as expected. So the changeset is `patch` with the BREAKING banner, the shape AGENTS.md prescribes for a non-protocol breaking change. - **Analytics re-judged:** `service-analytics` builds its aggregations in the `method` dialect for `IDataEngine` and never imports or parses `AggregationNodeSchema`, so nothing there moves. No open PR touched `query.zod.ts` or the MCP data tools at branch time or before this PR. - **`dropped-refinements.baseline.json` did not move** (the order's census hypothesis): strictness is not a refinement; `data/AggregationNode` still records only `filter.lazy`. - **Fixture sweep:** every aggregation-entry literal in the tree (code, tests, skills, docs) was scanned for keys outside the five declared; the only hits are deliberate (`func` pins in driver tests that call drivers directly, `fn` in a plugin-security AST test, the analytics `method` dialect), none of which goes through this schema. ## Verification All readings at head `09f089de` unless named (the merge of `origin/main` plus the ledger regeneration; nothing of this card moved after the suites below). <details><summary>Red first, on base 0b6a52e</summary> `pnpm --filter @objectstack/rest exec vitest run src/aggregation-entry-undeclared-key-door.test.ts` with the spec unchanged: 3 failed / 2 passed. `bogusKey`, entry `where` and entry `as` each answered 200 (`expected undefined to be 400`, the handler wrote the grouped rows with no status); the `distinct` tombstone and the declared-only control passed. With the fix: 5/5. </details> <details><summary>Ablation (one-shot, not a kept test)</summary> `node scripts/ablation-replace.mjs` turned the node back into a plain `z.object(shape)` (anchor hit 1/1), `pnpm --filter @objectstack/spec build`, `ablation-dist-preflight spec ablationShape` found the marker in 2 built files. Reds: REST door 3 + REST gate table 1, runtime two-door rows 2, MCP entry file 3, spec block 4 (13 in all); every control stayed green. Restore leg: blob equal to HEAD, `git diff HEAD` empty, rebuilt, `--absent` preflight clean over 588 built files. </details> <details><summary>Package suites and dogfood</summary> - `@objectstack/spec`: 869 files, 24570 passed (before the last spec-test commit; that file then 96/96 on its own). - `@objectstack/rest`: 364 files, 6915 passed, 332 skipped. - `@objectstack/runtime` (after building `@objectstack/runtime...`): 504 files, 8189 passed, 19 skipped. - `@objectstack/mcp`: 48 files, 612 passed. - Full dogfood (owed by the `(narrowing)` arm), `pnpm --filter @objectstack/dogfood test` under the lock after building `@objectstack/dogfood^...`: VERDICT command-exit 0, 369 passed / 3 skipped files, 2981 passed / 28 skipped tests. </details> <details><summary>Derived gates</summary> `node scripts/pm/dispatch-gates.mjs --ran` on the final head: 85 derived families accounted for, all with exit codes, 0 NOT-MEASURED, every exit 0. Two needed a second run: `check:doc-authoring` first red on an issue id in the node's customer-facing `history` sentence (removed, then green), and `check-plugin-teardown-shape --self-test` exit 3 until its fixture commit was fetched (then green). Also green on the final head: `check:strictness-ledger`, `check:authorable-surface`, `check:api-surface` (public API surface unchanged), `check:docs`, `check:type-check-debt`, `check:dual-build-cjs-loads`, `check-adr-0087-registration --base origin/main`. Repo-wide lint and the `test:repo` project are CI's. </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes (CI). - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English (the maintainer summary block is the lane's required Chinese section). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01FEkQ5m158G1nvmxNYr7Jwq --- _Generated by [Claude Code](https://claude.ai/code/session_01FEkQ5m158G1nvmxNYr7Jwq)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6cbdef1 commit 36fa4ff

10 files changed

Lines changed: 325 additions & 75 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@objectstack/spec': patch
3+
'@objectstack/mcp': patch
4+
---
5+
6+
fix(spec)!: an aggregation entry refuses an undeclared key by name on every door, and MCP's `aggregate_records` reads the same refusal (#28609)
7+
8+
Clause-②: no (narrowing) — `AggregationNodeSchema` refuses an entry key it does not declare, which the REST query door stripped and ignored before; no key, value or export is added.
9+
Metadata-protocol: none
10+
11+
<!-- adr-0087: not-required (no-migration-prescription) A query's aggregation entry is a request surface, the client SDK builder's output and the `POST /data/:object/query` body, never stored in stack metadata, so there is no source for a conversion to rewrite (the disposition the spec already records for the retired `distinct` on this same node). No declared key is added, removed or renamed and no export or type changes: an undeclared key used to be dropped without effect and is now refused by name, with `where` / `filters` / `conditions` hinted to `filter` and `as` to `alias`. -->
12+
13+
**BREAKING** accept-set narrowing of a query's aggregation entry (`AggregationNodeSchema`, `@objectstack/spec`), at every door that takes one.
14+
15+
- **Before.** The entry was a plain object, so zod deleted a key it does not declare and the call answered: `POST /api/v1/data/:object/query` with `{"function":"sum","field":"amount","alias":"s","bogusKey":true}` returned 200 and the sum, and an entry `where` meant as the entry's own `filter` computed over every row of the group. The runtime dispatcher's `/data/:object/query` did the same. The MCP tool `aggregate_records` alone refused the key, from its own closed copy of the entry (#28550), so one entry shape got two verdicts depending on the door.
16+
- **Now.** The node is a `strictObject`. Every door that parses it (REST and the runtime dispatcher through `FindDataRequestSchema`, `EngineAggregateOptionsSchema.aggregations`, and `aggregate_records`, which now takes the node whole instead of its own copy) answers one refusal, word for word: `Unrecognized key(s) on this aggregation entry: KEY.` plus the near-miss hint, at `400 VALIDATION_FAILED` with `fields[]` on REST and as the tool's input error on MCP. Nothing reaches the engine. The retired `distinct` keeps its tombstone sentence on both doors (MCP used to answer it with the generic unknown-key refusal), and `tools/list` now lists `distinct` as a schema no value satisfies (`not: {}`) with its removal text.
17+
- **Unchanged.** Entries that carry only `function`, `field`, `alias` and `filter` answer as before on every door. This supersedes the #28550 entry's "the REST query door is untouched": it is closed here.
18+
- **Who sees it.** A REST, dispatcher or in-process caller that sent an extra key inside an aggregation entry: the query was answered with that key ignored, and it is now refused naming the key.
19+
20+
**The remedy.** Drop the key; for a per-aggregation condition send it as the entry's `filter`, and for the result column name use `alias`.

‎docs/audits/2026-07-unknown-key-strictness-ledger.counts/data.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ The `strict` column is the one the campaign schedules against; it counts both th
2121

2222
| Dir | Sites | strict | passthrough | catchall | strip |
2323
|---|---|---|---|---|---|
24-
| `data/` | 158 | 92 | 2 | 0 | 64 |
24+
| `data/` | 158 | 93 | 2 | 0 | 63 |
2525

2626
## `data/` — sites
2727

@@ -65,7 +65,7 @@ Per file, how many of its sites still silently discard unknown keys. The `Class`
6565
column that decides the bucket split is hand-written in the ledger; the arithmetic
6666
over it is here.
6767

68-
**64 strip of 158**, in 10 file(s).
68+
**63 strip of 158**, in 10 file(s).
6969

7070
| File | Strip | Sites |
7171
|---|---|---|
@@ -77,15 +77,15 @@ over it is here.
7777
| `field.zod.ts` | 1 | 15 |
7878
| `filter.zod.ts` | 11 | 13 |
7979
| `hook.zod.ts` | 5 | 8 |
80-
| `query.zod.ts` | 4 | 5 |
80+
| `query.zod.ts` | 3 | 5 |
8181
| `seed-loader.zod.ts` | 12 | 12 |
82-
| **total** | **64** | **158** |
82+
| **total** | **63** | **158** |
8383

8484
| Bucket | Sites |
8585
|---|---|
8686
| authorable — the ruling's forced scope | 0 |
8787
| unresolved — needs a per-schema verdict | 0 |
88-
| wire / open — out of forced scope | 63 |
88+
| wire / open — out of forced scope | 62 |
8989
| no door — no carrier, ADR-0049 territory | 1 |
9090
| no gate — carrier live, no parse | 0 |
9191
| covered — no carrier, no parse, guarded at every consumer | 0 |

‎docs/audits/2026-07-unknown-key-strictness-ledger.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -719,7 +719,7 @@ column does not move and the `strip` column falls by the count of what left.
719719
| `seed-loader.zod.ts` | wire | **re-verdicted 2026-08-14 (#4001 batch D)** — the `(p)` split's "authored" half dissolved under measurement. The seed FILE shape this file's old note meant is `SeedSchema` (`seed.zod.ts`, a registered type, strict since the registered-types batch) — it enters this file only as the `seeds[]` VALUE inside `SeedLoaderRequestSchema`, so the authored half was never one of these 12 sites. Every producer of the request/config halves is framework code, enumerated: `runtime/app-plugin.ts` ×3 (org replay / inline seed / hot-reload), `runtime/domains/packages.ts`, `metadata-protocol/protocol.ts` `applySeedBodies`, `metadata-protocol/seed-loader.ts` `validate()`, `cloud-connection` marketplace install — all `.parse()`/`.safeParse()` over config LITERALS written in code; no `defineStack` key, no metadata type, no Studio form, no CLI flag writes a loader config. `SeedIdentity` is server-constructed by design (`execution-context.zod.ts`: "never client-supplied"); graph/resolution/result/error shapes are built by the loader itself (`buildDependencyGraph`, `buildResult`). BFS from all 26 metadata-type roots + `ObjectStackSchema` (6689 nodes; `ObjectSchema` positive control REACHABLE, fresh uncarried shape negative control UNREACHABLE, synthetic-carrier flip green, same run): all 12 UNREACHABLE. Same class as `data-engine.zod.ts` — an internal service contract (`ISeedLoaderService`), machine-fed at every door |
720720
| `field.zod.ts` | authorable | partially strict |
721721
| `filter.zod.ts` | open | query dialect — user data flows through the predicate values; validated semantically elsewhere |
722-
| `query.zod.ts` | 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`) |
722+
| `query.zod.ts` | open, **except `SortNodeSchema` and `AggregationNodeSchema` → strict** | Blanket `open` was the imprecise verdict here, not the strictness. Four sites were the dialect proper (`BaseQuerySchema`, `AggregationNodeSchema`, `FullTextSearchSchema`, `GroupByNodeSchema`'s object arm) and kept the class; three still do. **`AggregationNodeSchema` is strict as of #28609** (`strictObject` + `aliases: { where, filters, conditions → filter; as → alias }`, the retired `distinct` keeping its tombstone): the MCP tool `aggregate_records` had closed its own copy of the entry (#28550) while `POST /data/:object/query` stripped the same undeclared key and answered, so one entry got two verdicts by door; closing the node at the producer gave every door the one refusal. `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`) |
723723
| `driver-nosql.zod.ts` / `driver.zod.ts` / `driver-sql.zod.ts` | wire | driver capability contracts |
724724
| `datasource.zod.ts` | 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
725725
| `driver/memory.zod.ts` / `driver/mongo.zod.ts` / `driver/postgres.zod.ts` | 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 |
@@ -1260,7 +1260,7 @@ triage row record which one was taken.
12601260
| `filter.zod.ts` | open | **out of scope** — query dialect; user data flows through, validated semantically elsewhere |
12611261
| `driver-nosql.zod.ts` | wire | **out of scope** |
12621262
| `driver.zod.ts` | wire | **out of scope** — driver capability contract |
1263-
| `query.zod.ts` | 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 |
1263+
| `query.zod.ts` | 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 kept `open`. Those 4 were 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. #28609 then closed `AggregationNodeSchema` (one entry, one refusal on REST, the dispatcher and MCP), leaving 3 |
12641264
| `external-catalog.zod.ts` | wire (p) | **out of scope** |
12651265
| `hook.zod.ts` | wire | **out of scope** — `HookContextSchema` + `.session`/`.provenance`/`.user` are the runtime shape handed to a handler; verified in the data step |
12661266
| `field.zod.ts` | no door | ⛔ **not strictness work — re-verdicted 2026-08-13 (#4001 data batch).** *(2026-10-28, #27182: `CurrencyValueSchema` and its `CurrencyValue` type are DELETED — registered as `RETIRED_DEFS_BY_MAJOR[18]` `data/CurrencyValue` with the D3 entry `currency-value-schema-retired`, ADR-0104 D1 carried out ahead of the multi-currency epic #8345. One strip site remains, `LocationCoordinatesSchema`, and the verdict below stands for it.)* This row's own instruction was "check whether they are record data (→ open) before closing", and the measured answer is the THIRD one: neither authorable nor open. Both remaining strip sites (`LocationCoordinatesSchema`, `CurrencyValueSchema`) are `@deprecated` DEAD EXPORTS that contradict the enforced value contract — `currency` stores a BARE NUMBER everywhere (validator, SQL driver `float` column, import coercion, field-zoo oracle), `location` stores `{lat, lng}` not `{latitude, longitude}`. Carrier: absent — no schema in the tree references either, so unreachability from every authoring root holds by construction and no BFS is needed (the only non-test references are two `type-alias-convention.pin.test.ts` rows). Parse: absent outside their own `field.test.ts` cases. Consumers' vocabulary: absent — zero references in objectui; `field-value.test.ts` pins from the other direction that the enforced contract REJECTS the retired `CurrencyValueSchema` object shape. The ADR-0049 answer this class prescribes already exists: ADR-0104's "Reality wins" section decides both removals ("an exported-but-unconsumed value schema is exactly the inert metadata ADR-0078 forbids"), the JSDoc deprecations are on `main` with "Removal rides the next spec major", and the removal is tracked at **#8562** — this row points there, never at a batch. Closing them instead would be #4583's precisely-validated dead slot, and worse than most instances of it: a `strictObject` `surface` name plus did-you-mean suggestions on a shape authors must NOT use is an invitation dressed as enforcement, on the exact spelling (`{value, currency}` / `{latitude, longitude}`) the real contract rejects. The third named shape the old row carried, `Address`, was never this file's site — `AddressSchema` is DECLARED in `field-value.zod.ts` since #7127 and only re-exported here |
@@ -1287,7 +1287,8 @@ until #8075 retired the whole file under ADR-0049 — its per-schema read arrive
12871287
zero-consumer verdict, and the strictness question died with the shapes.) The rest is wire/open and out of the
12881288
ruling's forced scope; that count fell by one when #4721 closed `query.zod.ts`'s
12891289
`SortNodeSchema`, the one row in this directory where the per-schema read moved a site OUT
1290-
of `open` rather than confirming it, and by one more when **#4001 batch B** closed
1290+
of `open` rather than confirming it, by one when #28609 closed the same file's
1291+
`AggregationNodeSchema` (a door-parity close: MCP already refused the entry key REST stripped), and by one more when **#4001 batch B** closed
12911292
`driver/memory.zod.ts`'s remaining 5 sites (the persistence-adapter union under
12921293
`datasource.config` — `PersistenceAdapterSchema`, `FilePersistenceConfigSchema`,
12931294
`LocalStoragePersistenceConfigSchema`, `CustomPersistenceConfigSchema`,

0 commit comments

Comments
 (0)