Skip to content

Commit f4d8990

Browse files
committed
docs: date the v17 query-surface removals to 17, not 18 (#4476)
Seventeen passages dated v17 removals to `@objectstack/spec` 18. They ship in 17 -- this train. The number is the actionable half of a removal notice: a reader on 16 asking whether upgrading to 17 breaks their cursor-paginated loop was told the removal is a major away, so they plan for it later and the upgrade breaks. Evidence that 17 is correct: `spec-changes.json` carries `toMajor: 17` for data.query.{cursor,distinct,joins,windowFunctions} and stack.api.requireAuth; this tree is 17.0.0-rc.1 with `PROTOCOL_VERSION = '17.0.0'`; and the keys are already `[RETIRED]` in `authorable-surface.json`. A removal cannot be retired in a 17 build and also ship in 18. #4476 fingerprinted nine locations. Grepping the bare pattern -- which the issue itself recommended over working the list file-by-file -- found eight more: - The nine listed: query-syntax.mdx (4), queries.mdx (4), troubleshooting.mdx. - query-syntax.mdx:98, the #4286 sweep summary paragraph, same error. - skills/objectstack-query/ (5): SKILL.md and the aggregation/pagination rules. These are agent-facing and the highest-leverage of the set -- an agent authoring a query reads them as ground truth. Body prose only, so the frontmatter-derived listings that build-skill-docs.ts generates are unchanged. - implementation-status.mdx (2): the same error shape on a different change, `api.requireAuth` (#3963), which spec-changes.json also puts at toMajor 17. Also records the fingerprint in the sweep run log, as #4476 asks. Runs 1-2 matched on surface names and so read past passages that named the right surface and the wrong release; the new row tells the next run to check the number, with spec-changes.json `toMajor` as the arbiter. Fixes #4476
1 parent 4f078fc commit f4d8990

8 files changed

Lines changed: 41 additions & 17 deletions

File tree

content/docs/data-modeling/queries.mdx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ backend chooses, exactly as before.
256256

257257
### Keyset Pagination — a `where` predicate on the sort key
258258

259-
`query.cursor` was **removed in `@objectstack/spec` 18** (#4286): nothing on the server
259+
`query.cursor` was **removed in `@objectstack/spec` 17** (#4286): nothing on the server
260260
ever read it, so a cursor query silently returned the same first page every time. The
261261
key is tombstoned and `QueryBuilder.cursor()` was removed with it. Express the keyset
262262
directly — seek past the last row instead of offsetting:
@@ -422,7 +422,7 @@ come later behind a driver capability flag without changing these semantics.
422422
## Joins — removed
423423

424424
<Callout type="warn">
425-
`query.joins` was **removed in `@objectstack/spec` 18** (#4286, ADR-0049
425+
`query.joins` was **removed in `@objectstack/spec` 17** (#4286, ADR-0049
426426
enforce-or-remove): no driver's `find()` ever executed a join — the SQL, in-memory, and
427427
MongoDB drivers all ignored the array, so it only ever declared a capability that did
428428
not run. The key is tombstoned: authoring it is a `tsc` error, and a query carrying it
@@ -532,7 +532,7 @@ by `@objectstack/plugin-pinyin-search`) recomputes the column on demand.
532532
## Window Functions — removed from the request surface
533533

534534
<Callout type="warn">
535-
`query.windowFunctions` was **removed in `@objectstack/spec` 18** (#4286):
535+
`query.windowFunctions` was **removed in `@objectstack/spec` 17** (#4286):
536536
`ObjectQL.find()` / `.aggregate()` and the `POST /api/v1/data/:object/query` route never
537537
routed it anywhere, so sending it had no effect. The key is tombstoned — a query carrying
538538
it fails to parse with the upgrade prescription — and the
@@ -565,7 +565,7 @@ report/dashboard metadata.
565565

566566
### Distinct Records — removed flag, three live spellings
567567

568-
The top-level `query.distinct` flag was **removed in `@objectstack/spec` 18** (#4286):
568+
The top-level `query.distinct` flag was **removed in `@objectstack/spec` 17** (#4286):
569569
no driver's `find()` ever applied it, and its only observable effect was mis-wired —
570570
it silently suppressed the REST list count while still returning duplicate rows (the
571571
count is truthful again). The key is tombstoned and `QueryBuilder.distinct()` was

content/docs/deployment/troubleshooting.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ console.log(field.maxLength?.toString() ?? 'no limit');
280280
4. **Avoid deep nesting** — Limit nested `$and`/`$or` depth
281281
5. **Use keyset pagination** — For large datasets, seeking past the last row is
282282
faster than a deep `offset`. Express the keyset as a `where` predicate on the
283-
sort key (the `cursor` query property was removed in `@objectstack/spec` 18,
283+
sort key (the `cursor` query property was removed in `@objectstack/spec` 17,
284284
#4286 — nothing ever read it)
285285

286286
```typescript

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ on the `find()` path:
9595
`top` is the exception that *is* honored — the engine normalises it to `limit`.
9696

9797
The #4286 sweep (ADR-0049 enforce-or-remove) settled every other declared-but-inert
98-
member. **Removed** — tombstoned in `@objectstack/spec` 18, so a query carrying one
98+
member. **Removed** — tombstoned in `@objectstack/spec` 17, so a query carrying one
9999
fails to parse with the upgrade prescription and authoring it is a `tsc` error:
100100
`joins` (related records are read through `expand`), `windowFunctions` (a SQL-driver
101101
door remains: `SqlDriver.findWithWindowFunctions()`), `cursor` (express the keyset as
@@ -723,7 +723,7 @@ the driver's raw rows.
723723

724724
### Distinct
725725

726-
`query.distinct` was **removed in `@objectstack/spec` 18** (#4286): no driver ever
726+
`query.distinct` was **removed in `@objectstack/spec` 17** (#4286): no driver ever
727727
rendered `SELECT DISTINCT`, and the flag's only observable effect was mis-wired — it
728728
silently suppressed the REST list count (`total`/`hasMore` degraded to a page-local
729729
estimate) while still returning duplicate rows. The key is tombstoned and
@@ -793,7 +793,7 @@ expansion ignores them.
793793

794794
### Joins — removed (#4286)
795795

796-
`query.joins` was **removed in `@objectstack/spec` 18** (#4286, ADR-0049
796+
`query.joins` was **removed in `@objectstack/spec` 17** (#4286, ADR-0049
797797
enforce-or-remove): no driver ever read it, so a query carrying `joins` silently ran
798798
as a single-table query. The key is tombstoned — authoring it is a `tsc` error, and a
799799
query that still carries it (even as an empty array) fails to parse with the upgrade
@@ -805,7 +805,7 @@ joined in application code.
805805

806806
### Window Functions — removed from the request surface (#4286)
807807

808-
`query.windowFunctions` was **removed in `@objectstack/spec` 18** (#4286): `find()`
808+
`query.windowFunctions` was **removed in `@objectstack/spec` 17** (#4286): `find()`
809809
never applied it, so every OVER clause it declared was silently dropped. The key is
810810
tombstoned, and the `WindowFunction` / `WindowSpec` / `WindowFunctionNode` exports
811811
left with it — they declared `field` / `over` / `frame` members that no executor ever
@@ -871,7 +871,7 @@ every page full and every row real (objectui#3106, #4363). A query with no
871871
### Keyset Pagination
872872

873873
<Callout type="warn">
874-
`query.cursor` was **removed in `@objectstack/spec` 18** (#4286): no driver ever
874+
`query.cursor` was **removed in `@objectstack/spec` 17** (#4286): no driver ever
875875
implemented keyset pagination, so a cursor was accepted and ignored and every page came
876876
back identical — a caller looping "until `hasMore` is false" never terminated. The key
877877
is tombstoned (on `EngineQueryOptions` too) and `QueryBuilder.cursor()` was removed

content/docs/releases/implementation-status.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ Every route carries the `/api/v1` prefix. When project scoping is enabled each r
291291
- Client SDK supports bearer token header — but token validation requires the auth plugin
292292
- Auth route (`/auth/*`) only appears in Discovery when the auth plugin is registered
293293
- Fine-grained authorization (RLS, sharing) lives in `plugin-security` / `plugin-sharing`, not in the auth plugin. Territory-style access is expressed as an RLS dynamic-membership set (`ExecutionContext.rlsMembership`, e.g. `id in current_user.territory_account_ids`) rather than as a dedicated territory module
294-
- **Phase-1 RBAC enforcement is live end-to-end**: REST → ObjectQL → SecurityPlugin middleware now receives a populated `ExecutionContext` (userId, tenantId, positions, permissions). Tenant isolation is enforced as a Layer 0 tenant wall (`plugin-security/tenant-layer.ts`, ADR-0095 D1) that AND-composes `organization_id == current_user.organization_id` ahead of and independently of business RLS — the earlier wildcard `tenant_isolation` RLS policy on `member_default` was retired (an OR-merged business policy could widen it). The default `member_default` set still ships per-object overrides `sys_organization_self` (`id == current_user.organization_id`) and `sys_user_self` (`id == current_user.id`) for the global tables that lack an `organization_id` column. The earlier `tenantField` indirection (RLS expressions written against an abstract `tenant_id` column then rewritten to the configured physical column at compile time) was removed — the placeholder, the column name, and `RLSUserContext.organization_id` are now the same name end-to-end. The legacy `objectql.registerTenantMiddleware` (hardcoded `where.tenant_id` injection that pre-dated SecurityPlugin) has been removed; SecurityPlugin is the sole authority for tenant isolation. Analytics now uses the same reusable read scope via `security.getReadFilter`, so dataset-bound dashboards/reports do not bypass RLS. Verified cross-organization isolation on `pnpm dev:crm` across `sys_organization`, `sys_member`, `sys_user`, `sys_user_permission_set`, `sys_position_permission_set`. **Anonymous traffic is always denied** (ADR-0056 D2). The deployment-wide opt-out is gone: `api.requireAuth` was retired in `@objectstack/spec` 18 (#3963) and is now a tombstoned key that fails validation rather than reopening the data plane. The single decision lives in `@objectstack/core` (`security/anonymous-deny.ts`, 401 `UNAUTHENTICATED`), and every surface that legitimately serves a session-less caller derives its own narrow authorization from a declaration instead: control-plane paths via the auth-gate allowlist, public forms via `publicFormGrant` (ADR-0056 Option A), share links via a capability token validated then read as SYSTEM, `book.audience: 'public'` reads via the audience gate, and MCP via an OAuth token or API key.
294+
- **Phase-1 RBAC enforcement is live end-to-end**: REST → ObjectQL → SecurityPlugin middleware now receives a populated `ExecutionContext` (userId, tenantId, positions, permissions). Tenant isolation is enforced as a Layer 0 tenant wall (`plugin-security/tenant-layer.ts`, ADR-0095 D1) that AND-composes `organization_id == current_user.organization_id` ahead of and independently of business RLS — the earlier wildcard `tenant_isolation` RLS policy on `member_default` was retired (an OR-merged business policy could widen it). The default `member_default` set still ships per-object overrides `sys_organization_self` (`id == current_user.organization_id`) and `sys_user_self` (`id == current_user.id`) for the global tables that lack an `organization_id` column. The earlier `tenantField` indirection (RLS expressions written against an abstract `tenant_id` column then rewritten to the configured physical column at compile time) was removed — the placeholder, the column name, and `RLSUserContext.organization_id` are now the same name end-to-end. The legacy `objectql.registerTenantMiddleware` (hardcoded `where.tenant_id` injection that pre-dated SecurityPlugin) has been removed; SecurityPlugin is the sole authority for tenant isolation. Analytics now uses the same reusable read scope via `security.getReadFilter`, so dataset-bound dashboards/reports do not bypass RLS. Verified cross-organization isolation on `pnpm dev:crm` across `sys_organization`, `sys_member`, `sys_user`, `sys_user_permission_set`, `sys_position_permission_set`. **Anonymous traffic is always denied** (ADR-0056 D2). The deployment-wide opt-out is gone: `api.requireAuth` was retired in `@objectstack/spec` 17 (#3963) and is now a tombstoned key that fails validation rather than reopening the data plane. The single decision lives in `@objectstack/core` (`security/anonymous-deny.ts`, 401 `UNAUTHENTICATED`), and every surface that legitimately serves a session-less caller derives its own narrow authorization from a declaration instead: control-plane paths via the auth-gate allowlist, public forms via `publicFormGrant` (ADR-0056 Option A), share links via a capability token validated then read as SYSTEM, `book.audience: 'public'` reads via the audience gate, and MCP via an OAuth token or API key.
295295
- **OWD / sharing-model enforcement is live and proven end-to-end (ADR-0056)**: `private`, `public_read`, `public_read_write`, and `controlled_by_parent` are enforced through `plugin-sharing` + `plugin-security` and verified by dogfood proofs over the real HTTP stack. `object.sharingModel` accepts the canonical OWD vocabulary only (`private` / `public_read` / `public_read_write` / `controlled_by_parent`) — the legacy `read` / `read_write` / `full` aliases were removed from the enum (ADR-0090 D4), and an unset `sharingModel` on a custom object resolves to `private` (ADR-0090 D1). RLS owner policies resolve `current_user.email` in addition to `id` / `organization_id` / `positions` (#2054). Permission sets may declare `isDefault: true` as the install-time suggestion to bind the set to the built-in `everyone` position (ADR-0090 D5, superseding the ADR-0056 D7 fallback-profile mechanism). **A sharing rule must state its criteria** (#3896): all three write paths reject a match-all shape, a stored criteria-less rule matches nothing, and its materialised grants are revoked on the next reconcile.
296296

297297
---
@@ -439,7 +439,7 @@ There is no MSW package in this repo — browser API mocking is a devDependency
439439
- [x] Organization-Wide Defaults / sharing model — `private`, `public_read`, `public_read_write`, and `controlled_by_parent` enforced via `plugin-sharing` + `plugin-security`, proven by dogfood over the real HTTP stack (ADR-0056). Canonical vocabulary only — legacy aliases removed from the enum (ADR-0090 D4); unset custom-object OWD resolves to `private` (ADR-0090 D1)
440440
- [x] Sharing Rule evaluator — criteria rules re-evaluated on `afterInsert` / `afterUpdate` (`plugin-sharing/rule-hooks.ts`); every authorable recipient maps 1:1 onto an enforced `expandRecipient` branch (`plugin-sharing/sharing-rule-service.ts`) — `user`, `team`, `position`, `business_unit`, and `unit_and_subordinates` (business-unit-subtree widening, ADR-0057 D5 / ADR-0090 D3). Under ADR-0078 enforce-or-remove, `criteria` is now the only rule *type* (owner-type rules were removed from the authoring surface because the static materialiser cannot track live membership), the `group` recipient was renamed to `team`, `guest` was removed, and `queue` stays reserved in the runtime contract but deliberately non-authorable
441441
- [x] Everyone-baseline suggestion — a permission set may set `isDefault: true` as the install-time suggestion to bind it to the built-in `everyone` position; resolved per-request as an additive baseline, no fallback cliff (ADR-0090 D5)
442-
- [x] Default-deny for anonymous traffic — the global default-deny landed (ADR-0056 D2) and the `api.requireAuth` opt-out was then **removed** in `@objectstack/spec` 18 (#3963): the key is tombstoned and rejected at parse time, the deny decision is centralised in `@objectstack/core` `security/anonymous-deny.ts`, and public forms self-authorize via `publicFormGrant` (Option A)
442+
- [x] Default-deny for anonymous traffic — the global default-deny landed (ADR-0056 D2) and the `api.requireAuth` opt-out was then **removed** in `@objectstack/spec` 17 (#3963): the key is tombstoned and rejected at parse time, the deny decision is centralised in `@objectstack/core` `security/anonymous-deny.ts`, and public forms self-authorize via `publicFormGrant` (Option A)
443443
- [ ] Studio RLS visual editor
444444
- [ ] Per-user×org permission cache
445445
- [ ] Audit UI / denied-access logging

docs/v17-docs-sweep.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ merely *references* changed code, use the `docs-accuracy-audit` workflow the
5858
| Node `18` as a floor | states an out-of-date prerequisite | engines-node-22 |
5959
| `PortalSchema`, `AuditConfig`, Capabilities-descriptor cluster, `FeatureFlagSchema`, `DEFAULT_*_ROUTES`, report `aria`/`performance`, `ReportColumn/GroupingSchema` | teaches a pruned cluster | prune-* family |
6060
| `GetTranslationsRequest` `namespace`/`keys` filters | teaches the dropped filters | i18n-translations-request-drop-phantom-filters |
61+
| `` `@objectstack/spec` 18 `` (any v17 removal dated to **18**) | dates a removal that ships in **17** to the next major — the reader plans for it a release late and their upgrade breaks. Check the *number*, not just the surface name: `spec-changes.json` `toMajor` is the arbiter | #4286, #3963 |
6162

6263
## Run log
6364

@@ -108,4 +109,27 @@ merely *references* changed code, use the `docs-accuracy-audit` workflow the
108109
- **Not yet swept:** `examples/**` inline prose and `docs/**` (internal);
109110
lower-priority — user-facing `content/docs` + `skills` covered first.
110111

112+
### 2026-08-01 — run 3 (version-number pass, #4476)
113+
114+
- **Watermark:** framework `0f9faa2` (origin/main, post-#4489).
115+
- **Fingerprint added:** the `@objectstack/spec` 18 row above. Runs 1-2 matched on
116+
*surface names* and so read straight past a passage that named the right surface
117+
and the wrong release. The number is the actionable half of a removal notice.
118+
- **Fixed (drift → corrected):** 17 passages dating v17 removals to 18 → 17.
119+
`spec-changes.json` gives `toMajor: 17` for all five surfaces involved, and this
120+
tree is `17.0.0-rc.1` / `PROTOCOL_VERSION = '17.0.0'` with the keys already
121+
`[RETIRED]` in `authorable-surface.json` — a removal cannot already be retired in
122+
a 17 build and also ship in 18.
123+
- `protocol/objectql/query-syntax.mdx` (5) · `data-modeling/queries.mdx` (4) ·
124+
`deployment/troubleshooting.mdx` (1) — the #4286 query surfaces.
125+
- `skills/objectstack-query/SKILL.md` (3), `rules/aggregation.md` (1),
126+
`rules/pagination.md` (1) — same wrong number in the **agent-facing** skill,
127+
which #4476's fingerprint list did not cover. Highest-leverage of the set: an
128+
agent authoring queries reads these as ground truth.
129+
- `releases/implementation-status.mdx` (2) — same error shape on a *different*
130+
change, `api.requireAuth` (#3963), also `toMajor: 17`.
131+
- **Method note for the next run:** #4476 listed nine locations; a bare-pattern grep
132+
found seventeen. Grep the pattern repo-wide (including `skills/`), do not work a
133+
fingerprint list file-by-file.
134+
111135
<!-- Append the next run above this line, newest last. -->

skills/objectstack-query/SKILL.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ Sort with `orderBy` — an array of sort nodes:
278278

279279
### Keyset Pagination (Performant)
280280

281-
> **`query.cursor` was REMOVED in `@objectstack/spec` 18 (#4286).** No
281+
> **`query.cursor` was REMOVED in `@objectstack/spec` 17 (#4286).** No
282282
> engine or driver ever read it — a query carrying `cursor` silently returned
283283
> **page 1 forever**. The key is tombstoned (a query carrying it fails to
284284
> parse with the prescription) and `QueryBuilder.cursor()` is gone. Do keyset
@@ -443,7 +443,7 @@ Load related records through lookup/master_detail fields:
443443

444444
## Joins
445445

446-
> **REMOVED in `@objectstack/spec` 18 (#4286, ADR-0049).** `query.joins`
446+
> **REMOVED in `@objectstack/spec` 17 (#4286, ADR-0049).** `query.joins`
447447
> (and the `JoinNode`/`JoinType`/`JoinStrategy` vocabulary) is gone from the
448448
> `QueryAST` schema — no engine or driver ever consumed it, so it only ever
449449
> declared a capability that did not run. The key is tombstoned: authoring it
@@ -504,7 +504,7 @@ auto-default of name/title + short-text fields), resolved server-side.
504504

505505
## Window Functions (Analytics)
506506

507-
> **REMOVED from the request surface in `@objectstack/spec` 18 (#4286).**
507+
> **REMOVED from the request surface in `@objectstack/spec` 17 (#4286).**
508508
> `query.windowFunctions` is gone from the `QueryAST` schema — the engine
509509
> never routed it to any driver, so every OVER clause it declared was
510510
> silently dropped. The key is tombstoned (a query carrying it fails to

skills/objectstack-query/rules/aggregation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ const [active] = await engine.aggregate('user', {
157157

158158
## Window Functions
159159

160-
> **REMOVED in `@objectstack/spec` 18 (#4286, ADR-0049).** The `QueryAST`
160+
> **REMOVED in `@objectstack/spec` 17 (#4286, ADR-0049).** The `QueryAST`
161161
> schema no longer declares `windowFunctions` — the engine never routed the
162162
> property to any driver, so it was silently dropped. The key is tombstoned:
163163
> a query carrying it fails to parse with the upgrade prescription. The one

skills/objectstack-query/rules/pagination.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Guide for implementing pagination in ObjectStack queries.
99
| Offset | UI page navigation, small datasets | Simple, random page access | Slow on large offsets, drift on inserts |
1010
| Keyset (manual `where`) | Infinite scroll, real-time feeds | Consistent results, O(1) performance | No random page access |
1111

12-
> **The `cursor` query property was REMOVED in `@objectstack/spec` 18
12+
> **The `cursor` query property was REMOVED in `@objectstack/spec` 17
1313
> (#4286).** No engine or driver ever read it: a query carrying `cursor`
1414
> silently returned **page 1 forever**. The key is tombstoned — a query
1515
> carrying it fails to parse with the prescription — and

0 commit comments

Comments
 (0)