Skip to content

Commit 2d52025

Browse files
os-zhuangclaude
andcommitted
fix(spec)!: the retirement prescriptions name protocol 17, the major this train actually ships (#4350)
Ten tombstones told authors a key "was removed in `@objectstack/spec` 18", and the machine agreed: a whole `step18` chain step plus two `toMajor: 18` conversions were wired for a major the release train cannot reach. 17 is what ships. `latest` is 16.1.0, `rc` is 17.0.0-rc.0 (17.0.0 never published), and `.changeset/pre.json` records spec at initialVersion 16.1.0 — changesets computes a pre-mode bump from the last *published* version, so 16.1.0 + major = 17.0.0. `changeset status` confirms 17.0.0-rc.0 -> 17.0.0-rc.1. `PROTOCOL_VERSION` is '17.0.0' and `protocol-version.test.ts` pins it to the package major, so it cannot unilaterally become 18 either. The "18" came from counting up from the in-flight rc instead of from 16.1.0. The prose was the smaller half. `composeMigrationChain(from, to = PROTOCOL_MAJOR)` filters `m <= toMajor`, so a step keyed 18 was unreachable: `os migrate meta --from 16` walked 11-17 and stepped over it. The same ceiling applies to `composeSpecChanges`, so the committed `spec-changes.json` carried none of these seven retirements — `query.joins`, `windowFunctions` and `validateOnly` appeared zero times, and the upgrade guide contained no "18" at all. Authors would have hit a tombstone with no chain hop to run and no guide row to read. - Fold `step18` into `step17` (rationale, both `conversionIds`, all six semantic migrations); drop `MIGRATIONS_BY_MAJOR[18]`; retarget both conversions to `toMajor: 17` (`migrations.test.ts` requires conversion major == step major) and merge `CONVERSIONS_BY_MAJOR[18]` into `[17]`. - Retarget all 30 hand-written "18" references: the ten prescriptions, the `query.test.ts` regex that pinned the wrong number, internal comments, the liveness ledger + README, and the seven unconsumed changesets. - Regenerate `spec-changes.json` (+108), `protocol-upgrade-guide.md` (+32) and the five reference pages. - Write the seven retirements into the v17 release notes and upgrade checklist, where they had no entry at all — there is no v18.mdx for them to land in. No behaviour is added or withdrawn: every key retired by #3963, #4052, #4158, prescription names the version that will carry it, and `os migrate meta` applies the two stack conversions instead of stepping over them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 0449c63 commit 2d52025

29 files changed

Lines changed: 367 additions & 118 deletions

.changeset/drop-require-auth.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ switch:
3232
`RestApiConfigSchema` and the stack `api` block, so authoring it now fails with
3333
a fix-it message rather than being silently stripped (the ADR-0104 / #3733
3434
quiet-failure this whole line of work has been closing). `os migrate meta`
35-
drops it via the protocol-18 conversion `stack-api-require-auth-removed`.
35+
drops it via the protocol-17 conversion `stack-api-require-auth-removed`.
3636
- `shouldDenyAnonymous` (@objectstack/core) no longer takes a `requireAuth`
3737
input; it denies any anonymous, non-system caller outside the control-plane
3838
allowlist.

.changeset/query-ast-inert-request-surface.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ non-strict `BaseQuerySchema`, so authoring either fails `tsc` (input type
4141
`never`) and a query still carrying one — even as an empty array — fails to
4242
parse with the prescription itself. `QueryAST` is a request shape, never stored
4343
in stack metadata, so there is no `os migrate meta` step: the removals are
44-
registered as protocol-18 **semantic** migrations (`query-joins-retired`,
44+
registered as protocol-17 **semantic** migrations (`query-joins-retired`,
4545
`query-window-functions-retired`), the #4196 precedent.
4646

4747
Compat note for the REST boundary: both names remain **reserved** list-query

.changeset/query-cursor-removed.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ Mechanics: `retiredKey()` tombstones on both declaration sites
2525
(`QuerySchema.cursor` and `EngineQueryOptionsSchema.cursor`, one shared
2626
prescription), so authoring the key fails `tsc` and a query still carrying it
2727
fails to parse with the fix. `QueryBuilder.cursor()` is deleted. Registered as
28-
the protocol-18 semantic migration `query-cursor-retired` (request surface —
28+
the protocol-17 semantic migration `query-cursor-retired` (request surface —
2929
nothing stored to rewrite). The caller-built `Record<string, unknown>` shape
3030
would not survive a real keyset design anyway: a first-class cursor, if ever
3131
built, will be a response-minted opaque token (the pattern the

.changeset/query-distinct-removed.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ The one-line fix: **delete the key**; deduplicate with `groupBy` /
2929
Mechanics: `retiredKey()` tombstones on both declaration sites
3030
(`QuerySchema.distinct` and `EngineQueryOptionsSchema.distinct`, one shared
3131
prescription); `QueryBuilder.distinct()` is deleted; registered as the
32-
protocol-18 semantic migration `query-distinct-retired`. **Observable REST
32+
protocol-17 semantic migration `query-distinct-retired`. **Observable REST
3333
change (`@objectstack/metadata-protocol`):** the count-suppression branch is
3434
deleted — a list request that used to carry `distinct` now gets a real
3535
`total`/`hasMore` again (that restoration is the point, not a side effect).

.changeset/query-field-node-object-form-removed.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ depth 3).
5050
There is no `os migrate meta` step, and deliberately so: `QueryAST` is a request
5151
shape, never stored in stack metadata, so the chain has no source to rewrite. It
5252
is registered as an ADR-0087 D3 **semantic** migration
53-
(`query-field-node-object-form-retired`) on the protocol-18 step instead — the
53+
(`query-field-node-object-form-retired`) on the protocol-17 step instead — the
5454
`EnhancedApiError.fieldErrors` / `BatchOptions.validateOnly` precedent. Callers
5555
move their own select lists, and both channels tell them how:
5656

.changeset/retire-batch-validate-only.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ deliberately when there is a real need.
2424
rather than being silently stripped (the ADR-0104 / #3733 quiet-failure class).
2525
The `BatchOptions` type's `validateOnly` becomes `never`.
2626
- The retirement is HTTP-only (the key never appeared in stored stack metadata),
27-
so it is recorded as a semantic migration on the protocol-18 chain step
27+
so it is recorded as a semantic migration on the protocol-17 chain step
2828
(`batch-options-validate-only-retired`) — a TODO for API callers, not a stack
2929
conversion.
3030

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
'@objectstack/spec': major
3+
'@objectstack/metadata-protocol': patch
4+
'@objectstack/client': patch
5+
'@objectstack/service-automation': patch
6+
---
7+
8+
fix(spec)!: the #3963 / #4052 / #4158 / #4196 / #4286 retirements land in protocol **17**, not a protocol 18 that this train cannot produce (#4350)
9+
10+
Ten tombstone prescriptions told authors a key "was removed in `@objectstack/spec` **18**",
11+
and — worse — the machine agreed with them: a whole `step18` chain step and two
12+
`toMajor: 18` conversions were wired for a major the release train does not reach.
13+
14+
**17 is what ships.** `latest` is 16.1.0 and `rc` is `17.0.0-rc.0` — 17.0.0 has never been
15+
published. `.changeset/pre.json` records `@objectstack/spec` at initialVersion 16.1.0, and
16+
changesets computes a pre-mode bump from the last *published* version: 16.1.0 + `major` =
17+
**17.0.0**, released as `17.0.0-rc.N`. `PROTOCOL_VERSION` is `'17.0.0'`, and
18+
`protocol-version.test.ts` pins it to the package major, so it cannot unilaterally become 18
19+
either. The "18" came from counting up from the in-flight `17.0.0-rc.0` instead of from
20+
16.1.0.
21+
22+
**The prose was the smaller half.** `composeMigrationChain(from, to = PROTOCOL_MAJOR)`
23+
filters `m <= toMajor`, so a step keyed 18 was **unreachable**: `os migrate meta --from 16`
24+
walked steps 11–17 and silently skipped 18. The same ceiling applies to `composeSpecChanges`,
25+
so the generated `spec-changes.json`, `docs/protocol-upgrade-guide.md` and the `spec_changes`
26+
MCP tool — the ADR-0087 D4 primary channel — carried **none** of these seven retirements:
27+
`query.joins`, `query.windowFunctions` and `BatchOptions.validateOnly` appeared zero times in
28+
the committed manifest, and the upgrade guide contained no "18" at all. Authors would have hit
29+
the tombstones with no chain hop to run and no upgrade-guide row to read.
30+
31+
What changed:
32+
33+
- `step18` is folded into `step17` — its rationale, both `conversionIds`
34+
(`stack-api-require-auth-removed`, `flow-node-wait-timeout-keys-removed`) and all six
35+
semantic migrations move across, and `MIGRATIONS_BY_MAJOR[18]` is gone. Both conversions
36+
become `toMajor: 17` (`migrations.test.ts` requires a conversion's `toMajor` to equal its
37+
step's major), and `CONVERSIONS_BY_MAJOR[18]` merges into `[17]`.
38+
- All 30 hand-written "18" references become "17": the ten tombstone prescriptions
39+
(`query.zod.ts`, `flow.zod.ts`, `rest-server.zod.ts`, `stack.zod.ts`, `protocol.ts`), the
40+
`query.test.ts` pin regex that was holding the wrong number in place, the internal comments,
41+
the `liveness/query.json` + `liveness/README.md` notes, and the seven unconsumed changesets.
42+
- The seven retirements are written into the v17 release notes and upgrade checklist, where
43+
they had no entry at all — there is no `v18.mdx` for them to have landed in.
44+
45+
No behaviour is added or withdrawn: every key retired by #3963, #4052, #4158, #4196 and #4286
46+
stays retired, on exactly the terms those changesets describe. What changes is that the
47+
prescription now names the version that will actually carry it, and `os migrate meta` actually
48+
applies the two stack conversions instead of stepping over them.

.changeset/wait-timeout-keys-retired.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
feat(spec)!: retire `waitEventConfig.timeoutMs` / `.onTimeout``wait` never had a timeout (#4158)
77

8-
Both keys described a timeout and neither delivered one, so protocol 18 removes the pair
8+
Both keys described a timeout and neither delivered one, so protocol 17 removes the pair
99
rather than leaving a promise the runtime does not keep (PD #10).
1010

1111
- **`onTimeout`** had **zero** readers. No path ever inspected it, so neither `'fail'` nor
@@ -38,5 +38,5 @@ tombstones name the replacement.
3838

3939
One fixture interaction worth recording: the #4045 lift fixture used
4040
`waitEventConfig.timeoutMs` to demonstrate its fourth ledger entry, and the fixture
41-
harness replays the whole table — so its `after` described an end state protocol 18 makes
41+
harness replays the whole table — so its `after` described an end state protocol 17 makes
4242
unreachable. It now lifts `eventType` instead. The harness caught this itself.

content/docs/protocol/objectql/query-syntax.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ interface AggregationNode {
130130
// `expand`, not from inside this list.
131131
//
132132
// The `{ field, fields, alias }` nested-select member this union used to carry
133-
// was REMOVED in protocol 18 (#4196): nothing produced it and nothing read
133+
// was REMOVED in protocol 17 (#4196): nothing produced it and nothing read
134134
// `.fields`/`.alias`, so it was dropped by the SQL and memory drivers,
135135
// projected as a column named "[object Object]" by MongoDB, and refused as an
136136
// unknown field by the REST ingress. `expand` is the one spelling.

content/docs/references/api/contract.mdx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,13 +155,13 @@ const result = ApiError.parse(data);
155155
| **limit** | `number` | optional | Max records to return (LIMIT) |
156156
| **offset** | `number` | optional | Records to skip (OFFSET) |
157157
| **top** | `number` | optional | Alias for limit (OData compatibility) |
158-
| **cursor** | `any` | optional | [REMOVED] `query.cursor` was removed in @objectstack/spec 18 (#4286, ADR-0049) — no driver ever implemented keyset pagination, so the cursor was accepted and ignored and every page came back identical (a caller looping "until hasMore is false" never terminates). Delete the key; `QueryBuilder.cursor()` was removed with it. Express the keyset as an ordinary `where` predicate on your sort key — `where: { created_at: { $gt: last.created_at } }` with the matching `orderBy` — which every driver executes with canonicalised comparands. A first-class cursor, if ever built, will be a response-minted opaque token, not this caller-built record. |
159-
| **joins** | `any` | optional | [REMOVED] `query.joins` was removed in @objectstack/spec 18 (#4286, ADR-0049) — no engine or driver ever read it: a query carrying `joins` behaved exactly as if the key were absent, while its name squatted on the reserved REST parameter set. Delete the key. Related records are read through `expand``expand: { owner: { object: 'user', fields: ['name'] } }` — which the engine resolves via batch $in queries, and a single related column is a dotted `fields` path (`fields: ['owner.name']`). |
158+
| **cursor** | `any` | optional | [REMOVED] `query.cursor` was removed in @objectstack/spec 17 (#4286, ADR-0049) — no driver ever implemented keyset pagination, so the cursor was accepted and ignored and every page came back identical (a caller looping "until hasMore is false" never terminates). Delete the key; `QueryBuilder.cursor()` was removed with it. Express the keyset as an ordinary `where` predicate on your sort key — `where: { created_at: { $gt: last.created_at } }` with the matching `orderBy` — which every driver executes with canonicalised comparands. A first-class cursor, if ever built, will be a response-minted opaque token, not this caller-built record. |
159+
| **joins** | `any` | optional | [REMOVED] `query.joins` was removed in @objectstack/spec 17 (#4286, ADR-0049) — no engine or driver ever read it: a query carrying `joins` behaved exactly as if the key were absent, while its name squatted on the reserved REST parameter set. Delete the key. Related records are read through `expand``expand: { owner: { object: 'user', fields: ['name'] } }` — which the engine resolves via batch $in queries, and a single related column is a dotted `fields` path (`fields: ['owner.name']`). |
160160
| **aggregations** | `{ function: Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct' \| 'array_agg' \| 'string_agg'>; field?: string; alias: string; distinct?: boolean; … }[]` | optional | Aggregation functions |
161161
| **groupBy** | `string \| { field: string; dateGranularity?: Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>; alias?: string }[]` | optional | GROUP BY targets (strings or `{field, dateGranularity?}` objects for date bucketing) |
162162
| **having** | `any` | optional | HAVING — filter over the AGGREGATED rows (aggregation aliases + groupBy projections); applied engine-side after aggregation |
163-
| **windowFunctions** | `any` | optional | [REMOVED] `query.windowFunctions` was removed in @objectstack/spec 18 (#4286, ADR-0049) — `find()` never applied it: no engine or driver read the key on the query path, so every OVER clause it declared was silently dropped. Delete the key. Window functions are a SQL-driver capability behind `SqlDriver.findWithWindowFunctions(object, query)` (embedder-level; not on the `IDataDriver` contract or the REST surface); request-level analytics are `aggregations` + `groupBy`. |
164-
| **distinct** | `any` | optional | [REMOVED] `query.distinct` was removed in @objectstack/spec 18 (#4286, ADR-0049 / ADR-0078) — no driver ever rendered SELECT DISTINCT; the flag's only observable effect was MIS-WIRED: the REST list path treated a distinct query as not countable and silently degraded `total`/`hasMore` to a page-local estimate while still returning duplicate rows. Delete the key; `QueryBuilder.distinct()` was removed with it, and the count suppression is gone (`total` is truthful again). For unique values of one column use the SQL/memory drivers' `distinct(object, field)` door; for unique combinations, `groupBy`; for a deduplicated count, the `count_distinct` aggregation. |
163+
| **windowFunctions** | `any` | optional | [REMOVED] `query.windowFunctions` was removed in @objectstack/spec 17 (#4286, ADR-0049) — `find()` never applied it: no engine or driver read the key on the query path, so every OVER clause it declared was silently dropped. Delete the key. Window functions are a SQL-driver capability behind `SqlDriver.findWithWindowFunctions(object, query)` (embedder-level; not on the `IDataDriver` contract or the REST surface); request-level analytics are `aggregations` + `groupBy`. |
164+
| **distinct** | `any` | optional | [REMOVED] `query.distinct` was removed in @objectstack/spec 17 (#4286, ADR-0049 / ADR-0078) — no driver ever rendered SELECT DISTINCT; the flag's only observable effect was MIS-WIRED: the REST list path treated a distinct query as not countable and silently degraded `total`/`hasMore` to a page-local estimate while still returning duplicate rows. Delete the key; `QueryBuilder.distinct()` was removed with it, and the count suppression is gone (`total` is truthful again). For unique values of one column use the SQL/memory drivers' `distinct(object, field)` door; for unique combinations, `groupBy`; for a deduplicated count, the `count_distinct` aggregation. |
165165
| **expand** | `Record<string, { object: string; fields?: string[]; where?: any; search?: string \| { query: string; fields?: string[]; fuzzy: boolean; operator: Enum<'and' \| 'or'>; … }; … }>` | optional | Recursive relation loading map. Keys are lookup/master_detail field names; values are nested QueryAST objects that control select (`fields`) and filter (`where`, AND-merged with the batch $in), plus further expansion on the related object. The engine resolves expand via batch $in queries (driver-agnostic) with a default max depth of 3; per-parent `limit`/`offset`/`orderBy` are NOT applied on this path. |
166166

167167

0 commit comments

Comments
 (0)