Skip to content

Commit eb52d76

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-20534-parse-date-cell-family
2 parents 76e7fb3 + 7a1faf1 commit eb52d76

27 files changed

Lines changed: 1346 additions & 161 deletions
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: `RealtimeEventType` names the realtime events the runtime emits — `data.record.*` and `data.records.*`; `record.created` / `record.updated` / `record.deleted` / `field.changed` retired (#20288)
6+
7+
Clause-②: yes (narrowing)
8+
9+
**BREAKING** — shipped as `minor` under the launch-window convention
10+
(`check-changeset-no-major` refuses `major` until GA; breaking-ness is carried by
11+
this banner, the `(narrowing)` arm above and the ADR-0087 disposition below,
12+
never by the level). The enum both gains and loses values: it gains the five
13+
names the runtime emits and loses the four it never emitted.
14+
15+
`RealtimeEventType` (`@objectstack/spec/api`) types `SubscriptionEvent.type`, so
16+
it is the event vocabulary of `Subscription.events[]` and
17+
`RealtimeConfig.subscriptions[].events[]`, and the generated API reference
18+
published it as such. None of its four values was ever emitted by anything. The
19+
ObjectQL engine publishes `data.record.created` / `data.record.updated` /
20+
`data.record.deleted` for each written record and `data.records.updated` /
21+
`data.records.deleted` for a predicate write (`multi: true`), and it parses every
22+
event through `DataEventSchema` / `BulkDataEventSchema` before publishing. A
23+
subscription written with the names the reference showed could never fire.
24+
The runtime's published event names do not change; the enum moves to them.
25+
26+
- **Accepted now:** exactly `DataEventType` + `BulkDataEventType` —
27+
`data.record.created`, `data.record.updated`, `data.record.deleted`,
28+
`data.records.updated`, `data.records.deleted`. Metadata change events
29+
(`metadata.{type}.{action}`) are not members: a subscription event is
30+
record-shaped, and metadata events keep their own `MetadataEventType` contract
31+
and `subscribeMetadata` client primitive.
32+
- **Refused now:** `record.created`, `record.updated`, `record.deleted` and
33+
`field.changed`, each with its own prescription. Any other unknown value keeps
34+
zod's own message, which lists the legal names.
35+
36+
```
37+
FROM SubscriptionSchema.parse({ id, transport: 'websocket', events: [{ type: 'record.created', object: 'account' }] })
38+
-> parsed; nothing ever published 'record.created', so the subscription never fired
39+
TO -> ZodError at events[0].type (invalid_value): `record.created` was removed from
40+
`RealtimeEventType` in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — … Write
41+
`data.record.created` — the same event, spelled the way the engine publishes it.
42+
43+
FROM { type: 'record.updated' } -> TO { type: 'data.record.updated' }
44+
(add { type: 'data.records.updated' } to also hear predicate writes, which carry a count, not records)
45+
FROM { type: 'record.deleted' } -> TO { type: 'data.record.deleted' }
46+
(add { type: 'data.records.deleted' } to also hear predicate writes)
47+
FROM { type: 'field.changed' } -> TO { type: 'data.record.updated' }
48+
(read the field from the DataEvent payload's `changes`; no event is published per field)
49+
```
50+
51+
**Fix.** Rename each value as mapped above. `tsc` refuses the old values at a
52+
`RealtimeEventType` / `SubscriptionEvent` position (the type no longer contains
53+
them), and a `SubscriptionSchema`, `SubscriptionEventSchema` or
54+
`RealtimeConfigSchema` parse refuses them with the prescription. A handler keyed
55+
on an old name never ran, so the rename changes behaviour only by making it fire.
56+
Nothing in the open framework parses a subscription today — it mounts no realtime
57+
transport — so no running deployment changes behaviour.
58+
59+
### The kit
60+
61+
- **Schema.** `RealtimeEventType` lists the five emitted names; its error map
62+
gives each retired value its own prescription (the `HookBodyCapability`
63+
precedent for a removed enum value). `realtime.test.ts` pins the enum equal to
64+
`DataEventType` + `BulkDataEventType`, and pins each refusal's code, path and
65+
first sentence through `SubscriptionSchema`.
66+
- **ADR-0087.** The D3 entry `realtime-event-type-unemitted-values-retired`
67+
carries the prescription to `os migrate meta` and the upgrade guide. No D2
68+
conversion and no `RETIRED_KEYS_BY_MAJOR` row: an enum value is not a key, and
69+
a subscription is neither a stack collection member nor a stored row, so no
70+
conversion seam would ever see one.
71+
- **Docs.** The `realtime` reference page is regenerated; the
72+
`RealtimeEventPayload.type` and `RealtimeEvent.type` examples name emitted
73+
events.
74+
75+
<!-- adr-0087: registered realtime-event-type-unemitted-values-retired -->
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/service-realtime': patch
3+
---
4+
5+
docs(service-realtime): the README and `InMemoryRealtimeAdapter` examples name the events the ObjectQL engine actually publishes (#20288)
6+
7+
The usage example subscribed to and published `record.created`, and the security
8+
posture section said the engine publishes `record.created` / `record.updated`.
9+
The engine publishes `data.record.created` / `data.record.updated` /
10+
`data.record.deleted` (and `data.records.updated` / `data.records.deleted` for a
11+
predicate write), with the spec's `DataEvent` as the envelope's `payload` — the row
12+
is `payload.after`, not the payload itself. Both examples now subscribe to
13+
`data.record.created` and publish that shape. No code changes: the adapter matches
14+
whatever event type a subscription names, exactly as before.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/cli": minor
4+
---
5+
6+
**`objectstack validate` and `objectstack build` now report the ADR-0087 conversions `defineStack` applied, and `objectstack validate --strict` fails on them.**
7+
8+
`defineStack` rewrites a deprecated metadata spelling to its canonical shape when the config loads, in either mode, and prints one `defineStack: PATH: 'OLD' → 'NEW' (converted at load; conversion 'ID', retires in protocol N)` line on stderr. The two commands received that already-converted stack, so their own conversion pass found nothing to convert: `--json` answered `conversions: []` for every `defineStack` config, and `objectstack validate --strict` exited 0 on a spelling that stops loading in a named protocol major. A CI job gating on either could not see the retirement coming.
9+
10+
- `@objectstack/spec`: `defineStack` (both modes) records every conversion notice it applied on the stack it returns, beside the provenance mark and stamped in the same act. The record is non-enumerable and frozen, so the schema, `Object.keys` and `JSON.stringify` never see it and no compiled artifact changes. `composeStacks` records its inputs' records in input order, counting the same built stack passed twice once. **New export:** `stackConversionsOf(value)` returns the `ConversionNotice[]` a producer recorded, the same element the commands' `conversions` field publishes, and `[]` for a value no producer returned. Like the mark, the record does not survive a spread or JSON copy.
11+
- `@objectstack/cli`: the config loader reads the record off the default export before it merges named exports into it (that merge is a spread, which drops the record as it drops the mark). `objectstack validate` and `objectstack build` add it to their `conversions` list right after the config loads. Their own conversion pass still runs, and still reports what it converts on a key merged in from a named export of the config module, which `defineStack` never saw. The `--json` envelope keeps its shape (`valid` / `success`, `errors`, `warnings`, `conversions`): `conversions` now lists each conversion once.
12+
13+
**What a CI job sees:** `objectstack validate --strict` and `objectstack validate --json --strict` now exit 1 for a config whose only advisory is a live conversion, which is what `--strict` ("treat warnings as errors") documents. Without `--strict` the exit stays 0. The fix is the one the notice names: author the canonical spelling it prints, for example `subtitle` instead of `description` on a `page:header` component. The text face lists the conversion in its warning block. The stderr line from `defineStack` is unchanged and still printed once per conversion.
14+
15+
Clause-②: yes (widening) — one new export, `stackConversionsOf`, on the spec package root. Nothing `objectstack build`, or `objectstack validate` without `--strict`, accepted before is refused. `--strict` now applies its documented meaning to the conversions a `defineStack` config carries. It does not add a new rule.

‎content/docs/references/api/realtime.mdx‎

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const result = RealtimeConfigSchema.parse(data);
3737
| Property | Type | Required | Description |
3838
| :--- | :--- | :--- | :--- |
3939
| **id** | `string` | ✅ | Unique subscription identifier |
40-
| **events** | `{ type: Enum<'record.created' \| 'record.updated' \| 'record.deleted' \| 'field.changed'>; object?: string; filters?: any }[]` | ✅ | Array of events to subscribe to |
40+
| **events** | `{ type: Enum<'data.record.created' \| 'data.record.updated' \| 'data.record.deleted' \| …>; object?: string; filters?: any }[]` | ✅ | Array of events to subscribe to |
4141
| **transport** | `Enum<'websocket' \| 'sse' \| 'polling'>` | ✅ | Transport protocol to use |
4242
| **channel** | `string` | optional | Optional channel name for grouping subscriptions |
4343

@@ -51,7 +51,7 @@ const result = RealtimeConfigSchema.parse(data);
5151
| Property | Type | Required | Description |
5252
| :--- | :--- | :--- | :--- |
5353
| **id** | `string` | ✅ | Unique event identifier |
54-
| **type** | `string` | ✅ | Event type (e.g., record.created, record.updated) |
54+
| **type** | `string` | ✅ | Event type (e.g., data.record.created, data.records.updated) |
5555
| **object** | `string` | optional | Object name the event relates to |
5656
| **action** | `Enum<'created' \| 'updated' \| 'deleted'>` | optional | Action performed |
5757
| **payload** | `Record<string, any>` | ✅ | Event payload data |
@@ -64,14 +64,15 @@ const result = RealtimeConfigSchema.parse(data);
6464

6565
## RealtimeEventType
6666

67-
Realtime event type
67+
Realtime event type: a record-change event name the runtime emits
6868

6969
### Allowed Values
7070

71-
* `record.created`
72-
* `record.updated`
73-
* `record.deleted`
74-
* `field.changed`
71+
* `data.record.created`
72+
* `data.record.updated`
73+
* `data.record.deleted`
74+
* `data.records.updated`
75+
* `data.records.deleted`
7576

7677

7778
---
@@ -97,15 +98,15 @@ Realtime event type
9798
| Property | Type | Required | Description |
9899
| :--- | :--- | :--- | :--- |
99100
| **id** | `string` | ✅ | Unique subscription identifier |
100-
| **events** | `{ type: Enum<'record.created' \| 'record.updated' \| 'record.deleted' \| 'field.changed'>; object?: string; filters?: any }[]` | ✅ | Array of events to subscribe to |
101+
| **events** | `{ type: Enum<'data.record.created' \| 'data.record.updated' \| 'data.record.deleted' \| …>; object?: string; filters?: any }[]` | ✅ | Array of events to subscribe to |
101102
| **transport** | `Enum<'websocket' \| 'sse' \| 'polling'>` | ✅ | Transport protocol to use |
102103
| **channel** | `string` | optional | Optional channel name for grouping subscriptions |
103104

104105
### Nested Shape: `Subscription.events[number]`
105106

106107
| Property | Type | Required | Description |
107108
| :--- | :--- | :--- | :--- |
108-
| **type** | `Enum<'record.created' \| 'record.updated' \| 'record.deleted' \| 'field.changed'>` | ✅ | Type of event to subscribe to |
109+
| **type** | `Enum<'data.record.created' \| 'data.record.updated' \| 'data.record.deleted' \| …>` | ✅ | Type of event to subscribe to |
109110
| **object** | `string` | optional | Object name to subscribe to |
110111
| **filters** | `any` | optional | Filter conditions |
111112

@@ -118,7 +119,7 @@ Realtime event type
118119

119120
| Property | Type | Required | Description |
120121
| :--- | :--- | :--- | :--- |
121-
| **type** | `Enum<'record.created' \| 'record.updated' \| 'record.deleted' \| 'field.changed'>` | ✅ | Type of event to subscribe to |
122+
| **type** | `Enum<'data.record.created' \| 'data.record.updated' \| 'data.record.deleted' \| 'data.records.updated' \| 'data.records.deleted'>` | ✅ | Type of event to subscribe to |
122123
| **object** | `string` | optional | Object name to subscribe to |
123124
| **filters** | `any` | optional | Filter conditions |
124125

‎packages/cli/src/commands/compile.ts‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,15 @@ export default class Compile extends Command {
262262
// catch-all below (`--json`: `error` + `code`, exit 1), the same
263263
// envelope a `defineStack` refusal raised at load reaches.
264264
refuseUnbuiltStack(loaded);
265+
// 1b. The ADR-0087 D2 conversions the PRODUCER applied — the same fold
266+
// `os validate` makes at its step 1b, for the same reason: `defineStack`
267+
// converts at load, so step 2's pass below finds nothing of the
268+
// default export left to convert, and `conversions` read `[]` on every
269+
// `defineStack` config. Read by `loadConfig` off the default export
270+
// before its named-export merge (`stackConversionsOf`). ⛔ Folded,
271+
// never recomputed. Rendered on the text face at step 2 with the
272+
// pass's own findings, in this one list.
273+
conversionNotices.push(...loaded.stackConversions);
265274

266275
if (!flags.json) {
267276
printKV('Config', path.relative(process.cwd(), absolutePath));
@@ -277,7 +286,10 @@ export default class Compile extends Command {
277286
// bites harder than it reads, because the notice is the ONLY warning an
278287
// old-shape author gets before the conversion retires and their metadata
279288
// stops loading. Five conversions are live today (protocol 11 and 15),
280-
// so the gap is real, not hypothetical.
289+
// so the gap is real, not hypothetical. After step 1b the pass can still
290+
// find what the producer never saw — a key `loadConfig` merged onto the
291+
// stack from a NAMED export of the config module — and appends it to
292+
// the same list.
281293
if (!flags.json) printStep('Normalizing stack definition...');
282294
// The sink is declared above the `try` (see its note there); the CALL that
283295
// fills it stays right here, at the step that owns it.

0 commit comments

Comments
 (0)