Skip to content

Commit 0d76b88

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-20516-scaffold-probe-port
2 parents ffa48ba + 397572e commit 0d76b88

37 files changed

Lines changed: 1885 additions & 198 deletions
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/rest': minor
4+
---
5+
6+
feat(spec,rest)!: the served OpenAPI `info` carries the publisher's `api.documentation` identity; `api.documentation.version` retired (#20294)
7+
8+
Clause-②: yes (narrowing)
9+
10+
**BREAKING** — shipped as `minor` under the launch-window convention
11+
(`check-changeset-no-major` refuses `major` until GA; breaking-ness is carried by
12+
this banner, the `(narrowing)` arm above and the ADR-0087 disposition below,
13+
never by the level). The breaking half is one key: `api.documentation.version`.
14+
15+
`RestServerConfig.api.documentation` (`RestApiConfigSchema`) declared nine
16+
members, and `RestServer` parsed them, copied them into its config — and never
17+
read them back. Measured before this change, with every member authored: both
18+
doors that serve the OpenAPI document (`{apiPath}/openapi.json` and its
19+
environment-scoped twin) answered the bundled artifact's `info` unchanged, 0 of 9
20+
honoured. ADR-0049 enforce-or-remove, split by who owns each field:
21+
22+
- **Enforced — the publisher's identity.** `title`, `description`,
23+
`termsOfService`, `contact` (`name` / `url` / `email`) and `license` (`name` /
24+
`url`) now overlay the served `info` on both doors. A member you leave unset
25+
keeps the bundled value, and a config with nothing authored — no block,
26+
`documentation: {}` — serves `info` byte-identical to
27+
`@objectstack/spec/openapi.json`, exactly as before. `contact` and `license`
28+
replace the bundled object **whole**: `license: { name: 'MIT' }` serves
29+
`{ name: 'MIT' }` with no URL, never MIT at the bundled Apache-2.0 URL, and a
30+
partial `contact` never keeps ObjectStack's name or URL.
31+
- **Retired — `documentation.version`.** The served `info.version` is the
32+
protocol version, the version of the `@objectstack/spec` package that generated
33+
the document, with no configured override: an earlier ruling made it equal the
34+
published artifact's so an integrator can read which protocol version they are
35+
talking to. A publisher-set version would give the field a third meaning, so
36+
the key is now refused.
37+
38+
```
39+
FROM new RestServer(server, protocol, { api: { documentation: { title: 'Acme Orders API', version: '2.3.0' } } })
40+
-> constructed; GET /api/v1/openapi.json served info.title 'ObjectStack REST API'
41+
and info.version = the spec version — both authored values ignored
42+
TO -> throws: REST API configuration is invalid: `api` does not satisfy
43+
`RestApiConfigSchema` …
44+
- api.documentation.version: `api.documentation.version` was removed in
45+
@objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — … Delete the key. To publish
46+
your app's own release number, write it into `api.documentation.description`, …
47+
48+
FROM new RestServer(server, protocol, { api: { documentation: { title: 'Acme Orders API' } } })
49+
-> GET /api/v1/openapi.json: info.title 'ObjectStack REST API'
50+
TO -> GET /api/v1/openapi.json: info.title 'Acme Orders API' (and on the environment-scoped door)
51+
52+
FROM RestApiConfigSchema.parse({ documentation: { description: 'd' } }).documentation
53+
-> { title: 'ObjectStack API', description: 'd' } // a default no document ever served
54+
TO -> { description: 'd' }
55+
```
56+
57+
**Fix.** `api.documentation.version` → delete the key. The served
58+
`info.version` is always the protocol version; to publish your app's own release
59+
number, write it into `api.documentation.description`. `tsc` refuses the key at
60+
the authoring site (its input type is `never`), and `RestServer` construction and
61+
the REST plugin's `start` refuse it with that prescription.
62+
63+
**What else changes.** `documentation.title` is `.optional()` instead of
64+
`.default('ObjectStack API')`: that default was materialized into every present
65+
block and never served, so the parsed block now carries exactly what was
66+
authored (the parsed `title` is typed `string | undefined` now). `api.version` (the route identifier) and the runtime version still
67+
never reach `info.version`. A host that authors none of these keys — every
68+
CLI-started deployment, since `os serve` forwards only `enableProjectScoping`
69+
and `projectResolution` — serves the same document as before.
70+
71+
### The kit
72+
73+
- **Schema.** The eight identity members carry describes naming the served
74+
`info` field; `version` is a `retiredKey()` tombstone inside the live
75+
`documentation` block (a non-strict `z.object()`, so a bare deletion would have
76+
stripped it in silence), next to the `enabled` tombstone.
77+
- **REST server.** `registerOpenApiEndpoints` builds `info` through a pure
78+
helper that returns a NEW object — the cached artifact's own `info` is never
79+
written — and the same handler serves both doors.
80+
- **ADR-0087.** `RETIRED_KEYS_BY_MAJOR[18]` gains
81+
`api/RestApiConfig:documentation.version`; the D3 entry
82+
`rest-api-documentation-version-retired` carries the prescription to
83+
`os migrate meta` and the upgrade guide. No D2 conversion: a `RestServerConfig`
84+
is plugin TS configuration, never a stack collection member or a stored row.
85+
- **Ledger and docs.** `liveness/rest_api.json`: the eight identity leaves and
86+
the `contact` / `license` containers flip to `live` with the overlay as
87+
evidence; the `version` row stays `dead` with a REMOVED note. The generated
88+
`state-counts.md` moves `rest_api` from 12 live / 12 dead to 20 / 4; the
89+
`rest-server` reference page is regenerated.
90+
91+
<!-- adr-0087: registered rest-api-documentation-version-retired -->

‎.changeset/20397-diff-default-range-labels.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ fix(metadata-protocol): `diffMetaItem`'s default range labels its to side with t
66

77
With no `toVersion`, the to side is the current active `sys_metadata` row. Its body was compared, but `toVersion` came from the newest `sys_metadata_history` row, which is a draft save whenever a draft is pending: every draft save appends a history row. The labels and the bodies then named different rows. Measured on the real REST stack, an app with one active save and two draft saves answered `fromVersion 2 → toVersion 3` over its version-1 body, and a view with one active save and one draft save answered "no changes" labelled `1 → 2` while version 2 differs.
88

9-
- **Now:** `toVersion` is the active row's own `version`, read in the same read as its body. The default `fromVersion` is still the history version immediately before that label. An item whose active row is version 2 with a draft pending answers `1 → 2`, the same answer as `?from=1&to=2`.
9+
- **Now:** `toVersion` is the active row's own `version`, read in the same read as its body. The default `fromVersion` rule is not changed by this entry (#20451, in the same release, then moves it to the nearest earlier version whose body differs from the to side's). An item whose active row is version 2 with a draft pending answers `1 → 2`, the same answer as `?from=1&to=2`.
1010
- **No active row** (a draft-only item, or a deleted one): the to side is absent, and both labels are `null` with empty buckets, as the response schema declares for an absent side. Before, a draft-only item was labelled with its newest draft save, and its from side could be an earlier draft save's body. A deleted item was labelled `N-1 → N` up to its tombstone. That deletion is still read by naming its versions (`?from=N-1&to=N`).
1111
- Unchanged: the response shape, explicit `from` / `to` ranges, and the default range of an item with no draft pending.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
'@objectstack/metadata-protocol': patch
3+
'@objectstack/rest': patch
4+
---
5+
6+
fix(metadata-protocol): `GET /meta/:type/:name/diff` with no `from` compares against the nearest earlier version whose body differs, so the default diff right after a publish shows what the publish changed (#20451)
7+
8+
Clause-②: no — no key, export, route, parameter or response field moves; only which version the default `from` side names.
9+
10+
Every draft save appends a `sys_metadata_history` row, and publishing the draft appends the same body again as the next row. The default `from` side was the history row immediately before the `to` side, so right after a publish it was the draft save the publish came from, and the default diff answered "no changes". The change the publish carried was reachable only by naming `?from=`.
11+
12+
- **Now:** with no `from`, `diffMetaItem` walks back from the `to` side over the history rows it already reads and takes the nearest earlier row whose body differs, by the diff's own equality (all three buckets empty means equal). A body-less row, a delete's, compares as an empty body, so the walk stops on it and the answer names the deletion. With no earlier row that differs, the `from` side is absent: `fromVersion: null`, everything added.
13+
- **Measured on the real REST stack**, before → after:
14+
15+
| history | default range before | default range now |
16+
|:--|:--|:--|
17+
| v1 active, v2 draft save, v3 publish | `2 → 3`, no changes | `1 → 3`, the change the publish carried |
18+
| the same with a v4 draft pending | `2 → 3`, no changes | `1 → 3` |
19+
| create, delete, draft save, publish | `3 → 4`, no changes | `2 → 4`, everything added |
20+
| create, delete, active recreate | `2 → 3`, everything added | unchanged |
21+
| a new item draft-saved, then published | `1 → 2`, no changes | `null → 2`, everything added |
22+
| a single version | `null → 1`, everything added | unchanged |
23+
24+
- **Unchanged:** an explicit `?from=` / `?to=` names exactly its versions (`?from=2&to=3` over the first row still answers "no changes"); the default `to` side is the active version; the response shape; the one history read, with no cap. The walk compares the stored bodies before redaction, as the diff itself does, so a credential-only change still stops it and its values are still not served.
25+
- `@objectstack/rest`: the route's OpenAPI summary states the new default.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/runtime': patch
3+
'@objectstack/spec': patch
4+
---
5+
6+
fix(runtime): `DELETE /packages/:id` refuses an uninstall that names no organization before it touches the running registry (#20492)
7+
8+
Clause-②: no
9+
10+
A caller holding `manage_metadata` with no active organization — a member removed from an organization whose session still names it, or a caller who never selected one — sent `DELETE /api/v1/packages/:id` and was answered `400 TENANT_SCOPE_REQUIRED`. The dispatcher had already run the registry uninstall by then, so the package and every object it registers had left the running process for everyone it serves, while its stored rows still said it was installed. The state lasted until a restart re-seeded the registry.
11+
12+
The door now asks the persisted delete's organization-scope question first, from the same organization value it hands `deletePackage`, and only when a persisted delete will run. The same refusal (`400 TENANT_SCOPE_REQUIRED`) now arrives before anything changes: the package stays served, listed and registered, and its stored rows are untouched. The refusal's message names what an HTTP caller can do, which is to select an organization they are a member of and retry.
13+
14+
- **Unchanged:** a caller acting in an organization uninstalls exactly as before. A read-only package is still refused `422 WRITABLE_PACKAGE_REQUIRED` first. A host with no persisted delete (no `deletePackage` on its `protocol` service) still uninstalls from the registry alone, because there is no refusal to mirror there. The protocol keeps its own refusal as a second line.
15+
16+
- **`@objectstack/spec`:** `PROVENANCE_WAIVERS` (the error-code ledger) gains one entry: `@objectstack/runtime` stamps `TENANT_SCOPE_REQUIRED`, which stays registered under `@objectstack/metadata-protocol`. The door mirrors `deletePackage`'s refusal and does not emit a second vocabulary. The registered code union and `ErrorCode` are unchanged.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
`activityMilestones[].type`'s `.describe()` now states the real default: an unset `type` keeps the update row's kind, `updated` (#20494)
6+
7+
Clause-②: no
8+
9+
No behaviour changes and no schema shape change. `object.zod.ts`'s `activityMilestones[].type` field described its default as `"completed"`; the runtime never wrote that. `audit-writers.ts` starts `activityType` from `activityTypeFor(action)`, and a milestone can only fire on the UPDATE branch (`create` / `delete` return their own summary before the milestone match ever runs), so an unset `type` has always emitted `updated`. `milestone.type` overrides it only when the author actually sets it — that half of the describe was correct and is unchanged.
10+
11+
The corrected string is the published half: it ships in `packages/spec/dist/*.d.ts`, in the JSON Schema under `packages/spec/json-schema/`, and in the generated `content/docs/references/data/object.mdx` (regenerated with `gen:docs`, never hand-edited). A repo-wide search for the old wording found no other hand-written copy; `object.form.ts`'s `activityMilestones.type` help text ("Unset: updated.", shipped with PR #20485) already stated the real default and is unchanged.
12+
13+
`packages/plugins/plugin-audit/src/activity-type-vocabulary-enforcement.test.ts` already measured the runtime's real answer — its title and docblock are corrected in the same PR to stop describing a divergence and stop saying the finding was "filed separately" (this card, #20494, is where it was filed). Its assertions are byte-for-byte unchanged.

‎content/docs/references/api/rest-server.mdx‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -250,20 +250,20 @@ const result = BatchEndpointsConfigSchema.parse(data);
250250
| **enableProjectScoping** | `boolean` | optional (default: `false`) | Enable project-scoped routing for data/meta/AI APIs |
251251
| **projectResolution** | `Enum<'required' \| 'optional' \| 'auto'>` | optional (default: `"auto"`) | Project ID resolution strategy |
252252
| **requireAuth** | `never` | optional | [REMOVED] `api.requireAuth` was removed in @objectstack/spec 17. 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. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. |
253-
| **documentation** | `{ title: string; description?: string; version?: string; termsOfService?: string; … }` | optional | OpenAPI/Swagger documentation config |
253+
| **documentation** | `{ title?: string; description?: string; termsOfService?: string; contact?: object; … }` | optional | Publisher identity of the served OpenAPI document: each member set here overlays its `info` on both /openapi.json doors, and nothing set serves the bundled `info` unchanged. `info.version` is always the protocol version |
254254
| **responseFormat** | `never` | optional | [REMOVED] `api.responseFormat` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — nothing ever read it: `envelope`, `includeMetadata` and `includePagination` were parsed, defaulted and copied into the REST server's config and never consulted, so `envelope: false` unwrapped no response. Delete the key. Response shapes are fixed, not a server-wide option: each route answers in the response schema `@objectstack/spec/api` declares for it, which is what the client SDK parses and the served /openapi.json describes, so no configuration changes them. |
255255

256256
### Nested Shape: `RestApiConfig.documentation`
257257

258258
| Property | Type | Required | Description |
259259
| :--- | :--- | :--- | :--- |
260260
| **enabled** | `never` | optional | [REMOVED] `api.documentation.enabled` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — nothing ever read it: whether the server publishes its OpenAPI document is decided by the sibling `api.enableOpenApi` at the mount, so `enabled: false` turned nothing off. Delete the key; `api.enableOpenApi: false` is the switch that leaves the `/openapi.json` document and its `/docs` viewer unmounted. |
261-
| **title** | `string` | optional (default: `"ObjectStack API"`) | API documentation title |
262-
| **description** | `string` | optional | API description |
263-
| **version** | `string` | optional | Documentation version |
264-
| **termsOfService** | `string` | optional | Terms of service URL |
265-
| **contact** | `{ name?: string; url?: string; email?: string }` | optional | |
266-
| **license** | `{ name: string; url?: string }` | optional | |
261+
| **title** | `string` | optional | Title of the served OpenAPI document (`info.title`); unset keeps the bundled title |
262+
| **description** | `string` | optional | Description of the served OpenAPI document (`info.description`); unset keeps the bundled description. Your app's own release number belongs here |
263+
| **version** | `never` | optional | [REMOVED] `api.documentation.version` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — nothing ever read it, and the served OpenAPI document's `info.version` has one source: the protocol version, i.e. the version of the `@objectstack/spec` package that generated the document, which no deployment configuration overrides. Delete the key. To publish your app's own release number, write it into `api.documentation.description`, which the served `info.description` carries. |
264+
| **termsOfService** | `string` | optional | Terms-of-service URL of the served OpenAPI document (`info.termsOfService`); unset serves none |
265+
| **contact** | `{ name?: string; url?: string; email?: string }` | optional | Contact of the served OpenAPI document; replaces the bundled `info.contact` whole, so a member left out is absent rather than inherited. Unset keeps the bundled contact |
266+
| **license** | `{ name: string; url?: string }` | optional | License of the served OpenAPI document; replaces the bundled `info.license` whole, so a license without `url` serves no URL. Unset keeps the bundled license |
267267

268268

269269
---
@@ -298,7 +298,7 @@ const result = BatchEndpointsConfigSchema.parse(data);
298298
| **enableProjectScoping** | `boolean` | optional (default: `false`) | Enable project-scoped routing for data/meta/AI APIs |
299299
| **projectResolution** | `Enum<'required' \| 'optional' \| 'auto'>` | optional (default: `"auto"`) | Project ID resolution strategy |
300300
| **requireAuth** | `never` | optional | [REMOVED] `api.requireAuth` was removed in @objectstack/spec 17. 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. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. |
301-
| **documentation** | `{ title: string; description?: string; version?: string; termsOfService?: string; … }` | optional | OpenAPI/Swagger documentation config |
301+
| **documentation** | `{ title?: string; description?: string; termsOfService?: string; contact?: object; … }` | optional | Publisher identity of the served OpenAPI document: each member set here overlays its `info` on both /openapi.json doors, and nothing set serves the bundled `info` unchanged. `info.version` is always the protocol version |
302302
| **responseFormat** | `never` | optional | [REMOVED] `api.responseFormat` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — nothing ever read it: `envelope`, `includeMetadata` and `includePagination` were parsed, defaulted and copied into the REST server's config and never consulted, so `envelope: false` unwrapped no response. Delete the key. Response shapes are fixed, not a server-wide option: each route answers in the response schema `@objectstack/spec/api` declares for it, which is what the client SDK parses and the served /openapi.json describes, so no configuration changes them. |
303303

304304
### Nested Shape: `RestServerConfig.crud`

0 commit comments

Comments
 (0)