Skip to content

Commit 0df6726

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-20361-shard-liveness-counts
# Conflicts: # packages/spec/liveness/state-counts.md
2 parents ac64401 + fb38607 commit 0df6726

92 files changed

Lines changed: 4220 additions & 480 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): `os migrate meta` guidance for the `field-*`, `export-*`, `api-*`, `dataset-*`, `hook-*` and `metadata-*` migration entries states each lesson in words instead of citing tracker numbers
6+
7+
Clause-②: no
8+
9+
The ADR-0087 semantic entries of the `field-*` family (the runtime `field` write door, the
10+
`maxLength` / `minLength` / `scale` / `precision` refusals, `scale` on a currency field,
11+
`multiple` on a type that holds one value, and predicates that read through a reference),
12+
the `export-*` family (the export permission axis, the eight constraint keys retired from
13+
`ExportFieldMeta` and the retired export-job API family), the `api-*` family (the runtime `api` write door,
14+
the split API entry and two duration keys renamed with their unit), the `dataset-*` family
15+
(the aggregate × field-type refusals and the nested-relation list refused at save), the
16+
`hook-*` family (the retired hook-session `roles` and the two `registerHook` refusals) and
17+
the `metadata-*` family (the retired customization protocol, the re-partitioned endpoint
18+
switches, the metadata-manager cache keys and the retired `additionalTypes`) are printed by
19+
`os migrate meta` as the header, `why:` and `verify:` lines of a manual change. Their text
20+
sent the reader to issue-tracker, decision-batch and ruling-record numbers — some of which
21+
no longer resolve, and some in another repository — for what a ruling, measurement or fix
22+
had decided; it now says what was decided, in the sentence being read. ADR ids are kept.
23+
24+
Text only: no entry id, `from` / `to`, conversion or matching logic changes, and the chain
25+
rewrites exactly what it rewrote before. One entry's `surface` (the header line of
26+
`dataset-measure-aggregate-field-type-refused`) drops the two tracker numbers it carried and
27+
names nothing else differently. The generated migration registry, `spec-changes.json` and
28+
the protocol upgrade guide carry the same text.
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: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
'@objectstack/driver-sql': minor
3+
'@objectstack/driver-turso': minor
4+
'@objectstack/driver-memory': minor
5+
'@objectstack/driver-mongodb': minor
6+
'@objectstack/formula': minor
7+
'@objectstack/objectql': minor
8+
'@objectstack/spec': minor
9+
---
10+
11+
feat(drivers,formula,objectql): the engine's filter faces answer the staged `$empty` operator (#20444)
12+
13+
Clause-②: yes (widening)
14+
15+
`$empty: true | false` is declared by `@objectstack/spec` (`FieldOperatorsSchema`) with a per-type meaning: a text-like field is empty when it is null or `''`, a multi-value field (multiselect, checkboxes, tags, or a select / radio / lookup / user / file / image with `multiple: true`) when it is null or `[]`, and every other type only when it is null. `$empty: false` is the exact complement. Until now every face in this list refused it (`INVALID_FILTER` / 400), except `matchesFilterCondition`, which answered `false` for every record. **A driver or evaluator called directly now answers it:**
16+
17+
- **By the field's declared type**, through the spec's one expansion (`expandEmptyOperator`): `driver-sql`'s filter compiler (and so `driver-sqlite-wasm` and `driver-turso`'s local transport, which inherit it), `driver-turso`'s remote transport, `driver-memory`'s query path (`find` / `count` / `update` / `delete`) and `driver-mongodb`'s `translateFilter` (its `find`, its aggregate `$match`). The declaration is the one each driver already receives — `initObjects` / `registerObjectMetadata` / `registerExternalObject` on the SQL family, `syncSchema` on the others. On SQL a multi-value field's empty list is tested as stored JSON per dialect (SQLite `json_array_length` behind a `json_valid` guard, PostgreSQL a `jsonb` comparison, MySQL `JSON_LENGTH`), never as an equality comparand.
18+
- **By value** — null, a missing value, `''` and `[]` are empty (`isEmptyFilterValue`) — on the faces that read no field declaration: `@objectstack/formula`'s `matchesFilterCondition` (the RLS write-side `check`), `driver-memory`'s reference matcher, and `@objectstack/objectql`'s `having` and per-aggregation `filter`. In `having`, a `count` or `sum` holding `0` is not empty.
19+
20+
**Refused, never guessed** (`INVALID_FILTER` / 400): `$empty` on a field whose declaration the driver does not hold (a table built outside its registration, a builtin column such as `id`, a field with no `type`, or `translateFilter` / `RemoteTransport` used standalone without a declaration), a multi-value field on a SQL dialect the driver does not model, and a flag that is not a boolean. `driver-memory`'s analytics (cube) face refuses `$empty` as an operator it cannot compile, as it does `$null`.
21+
22+
New optional API: `translateFilter(where, temporalKind?, valueShape?)` in `@objectstack/driver-mongodb` takes a declared-value-shape resolver (type `ValueShapeResolver`), and `buildAggregationPipeline` a `valueShape` option; `RemoteTransport.setDeclaredValueShapeResolver` in `@objectstack/driver-turso`, which `TursoDriver` wires. `@objectstack/spec`'s shared `FILTER_LOGIC_CASES` table gains seven `$empty` cases: a backend that runs it answers `$empty` or goes red, and its harness must declare the fixture's columns.
23+
24+
`$empty` stays staged: it is not in `FILTER_OPERATORS`, so the engine's front door still refuses it until the flip card adds it, and the view operators `is_empty` / `is_not_empty` still lower to `$null`.
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.

0 commit comments

Comments
 (0)