Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .changeset/drop-require-auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ switch:
`RestApiConfigSchema` and the stack `api` block, so authoring it now fails with
a fix-it message rather than being silently stripped (the ADR-0104 / #3733
quiet-failure this whole line of work has been closing). `os migrate meta`
drops it via the protocol-18 conversion `stack-api-require-auth-removed`.
drops it via the protocol-17 conversion `stack-api-require-auth-removed`.
- `shouldDenyAnonymous` (@objectstack/core) no longer takes a `requireAuth`
input; it denies any anonymous, non-system caller outside the control-plane
allowlist.
Expand Down
2 changes: 1 addition & 1 deletion .changeset/query-ast-inert-request-surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ non-strict `BaseQuerySchema`, so authoring either fails `tsc` (input type
`never`) and a query still carrying one — even as an empty array — fails to
parse with the prescription itself. `QueryAST` is a request shape, never stored
in stack metadata, so there is no `os migrate meta` step: the removals are
registered as protocol-18 **semantic** migrations (`query-joins-retired`,
registered as protocol-17 **semantic** migrations (`query-joins-retired`,
`query-window-functions-retired`), the #4196 precedent.

Compat note for the REST boundary: both names remain **reserved** list-query
Expand Down
2 changes: 1 addition & 1 deletion .changeset/query-cursor-removed.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Mechanics: `retiredKey()` tombstones on both declaration sites
(`QuerySchema.cursor` and `EngineQueryOptionsSchema.cursor`, one shared
prescription), so authoring the key fails `tsc` and a query still carrying it
fails to parse with the fix. `QueryBuilder.cursor()` is deleted. Registered as
the protocol-18 semantic migration `query-cursor-retired` (request surface —
the protocol-17 semantic migration `query-cursor-retired` (request surface —
nothing stored to rewrite). The caller-built `Record<string, unknown>` shape
would not survive a real keyset design anyway: a first-class cursor, if ever
built, will be a response-minted opaque token (the pattern the
Expand Down
2 changes: 1 addition & 1 deletion .changeset/query-distinct-removed.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ The one-line fix: **delete the key**; deduplicate with `groupBy` /
Mechanics: `retiredKey()` tombstones on both declaration sites
(`QuerySchema.distinct` and `EngineQueryOptionsSchema.distinct`, one shared
prescription); `QueryBuilder.distinct()` is deleted; registered as the
protocol-18 semantic migration `query-distinct-retired`. **Observable REST
protocol-17 semantic migration `query-distinct-retired`. **Observable REST
change (`@objectstack/metadata-protocol`):** the count-suppression branch is
deleted — a list request that used to carry `distinct` now gets a real
`total`/`hasMore` again (that restoration is the point, not a side effect).
Expand Down
2 changes: 1 addition & 1 deletion .changeset/query-field-node-object-form-removed.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ depth 3).
There is no `os migrate meta` step, and deliberately so: `QueryAST` is a request
shape, never stored in stack metadata, so the chain has no source to rewrite. It
is registered as an ADR-0087 D3 **semantic** migration
(`query-field-node-object-form-retired`) on the protocol-18 step instead — the
(`query-field-node-object-form-retired`) on the protocol-17 step instead — the
`EnhancedApiError.fieldErrors` / `BatchOptions.validateOnly` precedent. Callers
move their own select lists, and both channels tell them how:

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

Expand Down
48 changes: 48 additions & 0 deletions .changeset/retirement-prescriptions-name-protocol-17.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
'@objectstack/spec': major
'@objectstack/metadata-protocol': patch
'@objectstack/client': patch
'@objectstack/service-automation': patch
---

fix(spec)!: the #3963 / #4052 / #4158 / #4196 / #4286 retirements land in protocol **17**, not a protocol 18 that this train cannot produce (#4350)

Ten tombstone prescriptions told authors a key "was removed in `@objectstack/spec` **18**",
and — worse — the machine agreed with them: a whole `step18` chain step and two
`toMajor: 18` conversions were wired for a major the release train does not reach.

**17 is what ships.** `latest` is 16.1.0 and `rc` is `17.0.0-rc.0` — 17.0.0 has never been
published. `.changeset/pre.json` records `@objectstack/spec` at initialVersion 16.1.0, and
changesets computes a pre-mode bump from the last *published* version: 16.1.0 + `major` =
**17.0.0**, released as `17.0.0-rc.N`. `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 `17.0.0-rc.0` 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 steps 11–17 and silently skipped 18. The same ceiling applies to `composeSpecChanges`,
so the generated `spec-changes.json`, `docs/protocol-upgrade-guide.md` and the `spec_changes`
MCP tool — the ADR-0087 D4 primary channel — carried **none** of these seven retirements:
`query.joins`, `query.windowFunctions` and `BatchOptions.validateOnly` appeared zero times in
the committed manifest, and the upgrade guide contained no "18" at all. Authors would have hit
the tombstones with no chain hop to run and no upgrade-guide row to read.

What changed:

- `step18` is folded into `step17` — its rationale, both `conversionIds`
(`stack-api-require-auth-removed`, `flow-node-wait-timeout-keys-removed`) and all six
semantic migrations move across, and `MIGRATIONS_BY_MAJOR[18]` is gone. Both conversions
become `toMajor: 17` (`migrations.test.ts` requires a conversion's `toMajor` to equal its
step's major), and `CONVERSIONS_BY_MAJOR[18]` merges into `[17]`.
- All 30 hand-written "18" references become "17": the ten tombstone prescriptions
(`query.zod.ts`, `flow.zod.ts`, `rest-server.zod.ts`, `stack.zod.ts`, `protocol.ts`), the
`query.test.ts` pin regex that was holding the wrong number in place, the internal comments,
the `liveness/query.json` + `liveness/README.md` notes, and the seven unconsumed changesets.
- The seven retirements are written into the v17 release notes and upgrade checklist, where
they had no entry at all — there is no `v18.mdx` for them to have landed in.

No behaviour is added or withdrawn: every key retired by #3963, #4052, #4158, #4196 and #4286
stays retired, on exactly the terms those changesets describe. What changes is that the
prescription now names the version that will actually carry it, and `os migrate meta` actually
applies the two stack conversions instead of stepping over them.
4 changes: 2 additions & 2 deletions .changeset/wait-timeout-keys-retired.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

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

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

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

One fixture interaction worth recording: the #4045 lift fixture used
`waitEventConfig.timeoutMs` to demonstrate its fourth ledger entry, and the fixture
harness replays the whole table — so its `after` described an end state protocol 18 makes
harness replays the whole table — so its `after` described an end state protocol 17 makes
unreachable. It now lifts `eventType` instead. The harness caught this itself.
2 changes: 1 addition & 1 deletion content/docs/protocol/objectql/query-syntax.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ interface AggregationNode {
// `expand`, not from inside this list.
//
// The `{ field, fields, alias }` nested-select member this union used to carry
// was REMOVED in protocol 18 (#4196): nothing produced it and nothing read
// was REMOVED in protocol 17 (#4196): nothing produced it and nothing read
// `.fields`/`.alias`, so it was dropped by the SQL and memory drivers,
// projected as a column named "[object Object]" by MongoDB, and refused as an
// unknown field by the REST ingress. `expand` is the one spelling.
Expand Down
8 changes: 4 additions & 4 deletions content/docs/references/api/contract.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -155,13 +155,13 @@ const result = ApiError.parse(data);
| **limit** | `number` | optional | Max records to return (LIMIT) |
| **offset** | `number` | optional | Records to skip (OFFSET) |
| **top** | `number` | optional | Alias for limit (OData compatibility) |
| **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. |
| **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']`). |
| **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. |
| **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']`). |
| **aggregations** | `{ function: Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct' \| 'array_agg' \| 'string_agg'>; field?: string; alias: string; distinct?: boolean; … }[]` | optional | Aggregation functions |
| **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) |
| **having** | `any` | optional | HAVING — filter over the AGGREGATED rows (aggregation aliases + groupBy projections); applied engine-side after aggregation |
| **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`. |
| **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. |
| **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`. |
| **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. |
| **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. |


Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/api/protocol.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -988,7 +988,7 @@ const result = AiAgentCapabilities.parse(data);
| **skip** | `number` | optional | Records to skip (offset). |
| **expand** | `string` | optional | Comma-separated list of lookup/master_detail field names to expand. Resolved to populate array and passed to the engine for batch $in expansion. |
| **search** | `string` | optional | Full-text search query. |
| **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. |
| **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. |
| **count** | `boolean` | optional | Include total count in response. |


Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/api/rest-server.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ const result = BatchEndpointsConfig.parse(data);
| **enableOpenApi** | `boolean` | ✅ | Enable OpenAPI 3.1 spec & docs viewer endpoints |
| **enableProjectScoping** | `boolean` | ✅ | Enable project-scoped routing for data/meta/AI APIs |
| **projectResolution** | `Enum<'required' \| 'optional' \| 'auto'>` | ✅ | Project ID resolution strategy |
| **requireAuth** | `any` | optional | [REMOVED] `api.requireAuth` was removed in @objectstack/spec 18 (#3963). Anonymous access to object data is now always denied — auth is a kernel concern, not a deployment posture. Delete the key. To publish something publicly, declare it: a public form view (`sharing.allowAnonymous`), a share link, or `book.audience: 'public'` — each derives its own narrow authorization instead of opening the whole data plane. |
| **requireAuth** | `any` | optional | [REMOVED] `api.requireAuth` was removed in @objectstack/spec 17 (#3963). Anonymous access to object data is now always denied — auth is a kernel concern, not a deployment posture. Delete the key. To publish something publicly, declare it: a public form view (`sharing.allowAnonymous`), a share link, or `book.audience: 'public'` — each derives its own narrow authorization instead of opening the whole data plane. |
| **documentation** | `{ enabled: boolean; title: string; description?: string; version?: string; … }` | optional | OpenAPI/Swagger documentation config |
| **responseFormat** | `{ envelope: boolean; includeMetadata: boolean; includePagination: boolean }` | optional | Response format options |

Expand Down
Loading
Loading