Skip to content

Commit f6b94a3

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-4456-datasource-key-alias-conversion
# Conflicts: # docs/protocol-upgrade-guide.md # packages/spec/spec-changes.json # packages/spec/src/conversions/registry.ts
2 parents c47f854 + 0a936ea commit f6b94a3

53 files changed

Lines changed: 1904 additions & 926 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: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
"@objectstack/metadata-protocol": patch
3+
---
4+
5+
fix(metadata-protocol): boot hydration grafts each overlay row's protection envelope from ITS OWN package (#4624)
6+
7+
`loadMetaFromDb` (boot hydration) kept a **third** inline copy of the
8+
overlay→SchemaRegistry registration rule, and its artifact lookup was
9+
**unscoped** — the exact pre-#1828 shape ADR-0048 removed from `getMetaItems`:
10+
with two installed packages shipping the same `type`/`name`, a name-colliding
11+
overlay row grafted the **first-registered** package's
12+
`_lock`/`_lockReason`/`_packageId`/`_provenance` onto another package's row at
13+
every kernel boot. A row customized under package B could come up wearing
14+
package A's identity and lock.
15+
16+
The non-object branch now delegates to the ONE shared
17+
`hydrateOverlayIntoRegistry` (introduced by #4521 for the read-side hydration
18+
and the write-through), passing the row's own `package_id` — one rule, one
19+
implementation, and the ADR-0048 package-scoped lookup applies at boot exactly
20+
as it does on read and write.
21+
22+
No other boot behaviour changes:
23+
24+
- **Boot order** — when packaged artifacts have not loaded yet at hydration
25+
time, the scoped lookup finds nothing, exactly like the unscoped one did,
26+
and the row registers unchanged.
27+
- **Package-less (global) rows**`package_id IS NULL` keeps the legacy
28+
best-effort first-match graft, identical to the read-side hydration.
29+
- **Row selection** — the helper carries no environment gate; which rows
30+
`loadMetaFromDb` loads is decided by its query, unchanged here.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
"@objectstack/spec": major
3+
"@objectstack/example-showcase": patch
4+
---
5+
6+
feat(spec)!: retire `external.label` and `external.requirePermission` (#4583 batch D)
7+
8+
Two keys on the federation block, both read by nothing.
9+
10+
**`external.label`** — nothing rendered the federation block's own label. Setup →
11+
Datasources renders the datasource's **top-level** `label`, which every datasource already
12+
has, so this was a second display name that never displayed. The showcase example declared
13+
both; it now declares only the one that shows.
14+
15+
**`external.requirePermission`** — no authorization check ever consulted it. A permission
16+
named here gated nothing: access to a federated datasource's data is governed by the
17+
ordinary object permission sets and RLS, exactly as for a managed datasource. Naming a
18+
permission that is never required is the false-compliance shape ADR-0049 exists to remove
19+
— it reads like an access control and is one only in the author's head.
20+
21+
FROM → TO: delete `external.label` (use the top-level `label`); delete
22+
`external.requirePermission` and grant or withhold the object permissions instead.
23+
`os migrate meta --from 16` removes both automatically (conversion
24+
`datasource-inert-blocks-removed`).
25+
26+
With these, the `datasource` liveness ledger reaches **zero dead properties** — down from
27+
the 20 it was seeded with in #4487, the highest dead ratio of any governed type.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
"@objectstack/spec": major
3+
---
4+
5+
feat(spec)!: retire `datasource.healthCheck` — no probe loop ever existed (#4583 batch C)
6+
7+
Three keys — `enabled`, `intervalMs`, `timeoutMs` — declared, strict-guarded, read by
8+
nothing. No health-check loop was ever scheduled, so `enabled: true` enabled nothing and
9+
the two timeouts bounded nothing.
10+
11+
Connection liveness is probed **on demand** through the driver handle's `ping()` /
12+
`checkHealth()`, which the datasource admin service calls for "Test connection". That is
13+
the mechanism — it needs no configuration here and never read this block.
14+
15+
Note what it is NOT to be confused with: `external.validation.checkIntervalMs` is the one
16+
recurring datasource timer, and it checks **schema drift** on a federated datasource, not
17+
connection liveness. It is unaffected.
18+
19+
FROM → TO: delete the block. `os migrate meta --from 16` removes it automatically
20+
(conversion `datasource-inert-blocks-removed`).
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
"@objectstack/spec": major
3+
---
4+
5+
feat(spec)!: retire `datasource.retryPolicy` — nothing ever retried on it (#4583 batch B)
6+
7+
Four keys — `maxRetries`, `baseDelayMs`, `maxDelayMs`, `backoffMultiplier` — declared,
8+
strict-guarded, and read by no connect or query path. Connection failure is handled by
9+
the boot policy in the datasource connection service (degraded boot, or `bootCritical`
10+
fail-fast); nothing retries on a schedule, so setting `maxRetries: 5` changed nothing.
11+
12+
**Do not "fix" this by renaming keys.** `hook.retryPolicy` and `job.retryPolicy` ARE
13+
enforced — but they are a different key on a different type, and they spell the delay
14+
`backoffMs`, not `baseDelayMs`. That very inconsistency is the evidence nothing read the
15+
datasource one: no code in the repo reads both spellings. Moving these values onto a hook
16+
or a job only makes sense if you actually want that hook or job retried.
17+
18+
FROM → TO: delete the block. `os migrate meta --from 16` removes it automatically
19+
(conversion `datasource-inert-blocks-removed`). `DatasourceSchema` is `.strict()`, so a
20+
leftover `retryPolicy` is a loud rejection carrying this prescription — never a silent
21+
strip.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/metadata': patch
3+
'@objectstack/rest': patch
4+
'@objectstack/cli': patch
5+
'@objectstack/service-analytics': patch
6+
'@objectstack/service-datasource': patch
7+
'@objectstack/service-job': patch
8+
'@objectstack/service-knowledge': patch
9+
'@objectstack/service-queue': patch
10+
'@objectstack/service-settings': patch
11+
'@objectstack/service-storage': patch
12+
---
13+
14+
Init-time service consumption is now declared everywhere, and the declaration is enforced (#4471, ADR-0116). A new CI gate (`check:init-service-contract`) walks every plugin's `init()` call graph — including private helpers, the shape that shipped #4420 — and errors on any init-reachable `getService('X')` of a workspace-provided service that is not covered by `dependencies`, `optionalDependencies`, or `requiresServices`. Eleven previously undeclared init-time consumers (metadata, rest, cli serve plugins, and seven services) now declare `optionalDependencies` on their providers, so the kernel orders them deterministically instead of by registration luck; each still degrades on purpose when the provider is not composed. Plugin authors: a best-effort init-time `getService` must declare its provider in `optionalDependencies` (declared tolerance) — the checker never exempts it.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
"@objectstack/metadata": patch
3+
"@objectstack/client": patch
4+
"@objectstack/spec": minor
5+
---
6+
7+
fix(metadata,client): `subscribeMetadata` callbacks receive real `MetadataEvent`s — the producer now fulfils the declared contract (#4602)
8+
9+
`@objectstack/spec/api`'s `MetadataEvent` declares top-level `id` (uuid,
10+
required), `metadataType`, `name`, `definition?`, `userId?` — and after
11+
#4587's convergence it is the **only** declared contract for realtime
12+
metadata-change events. But the producer (`MetadataManager`) published a raw
13+
`RealtimeEventPayload` envelope with everything nested under `payload` and no
14+
`id`/`userId`, while the client SDK force-cast that envelope into the callback
15+
(`callback(event as any as MetadataEvent)`). Subscribers who wrote
16+
`event.name` / `event.metadataType` — exactly what the types promised —
17+
compiled green and read `undefined` at runtime.
18+
19+
Producer now fulfils the contract:
20+
21+
- `MetadataManager.register()` / `unregister()` build a true `MetadataEvent`
22+
(generated uuid `id`, flattened top-level fields, `userId` when the write
23+
declares an actor) and validate it with `MetadataEventSchema.parse` before
24+
publishing. The transport envelope is unchanged (`RealtimeEventPayload`,
25+
with `payload` carrying the complete `MetadataEvent`).
26+
- A `register()` **overwrite now publishes `metadata.{type}.updated`** instead
27+
of a second `.created`, mirroring the existing `added`/`changed` watcher
28+
split. Previously `.updated` was declared with no producer at all.
29+
- `MetadataEventType` is a closed enum: metadata types outside it (e.g.
30+
`translation`) have no declared realtime event, so nothing is published for
31+
them (debug-logged) instead of emitting an event every schema-compliant
32+
consumer must reject.
33+
34+
Consumer validates instead of casting:
35+
36+
- `@objectstack/client`'s `subscribeMetadata` (and therefore
37+
`@objectstack/client-react`'s metadata hooks, which delegate to it) unwraps
38+
the envelope and runs `MetadataEventSchema.safeParse` at the boundary. An
39+
off-contract payload is rejected loudly (handler error, callback never
40+
invoked) — never coerced or passed through. The `as any as MetadataEvent`
41+
double-cast is gone.
42+
43+
New seam: `MetadataWriteOptions.userId` (`@objectstack/spec/contracts`) lets
44+
write paths that know the acting user carry it into the published event's
45+
`userId`. Existing callers are unaffected — the field is optional and absence
46+
means "no human actor".
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
"@objectstack/spec": major
3+
---
4+
5+
feat(spec)!: the notification vocabulary has one owner per name — `@objectstack/spec/ui` no longer exports `Notification(Schema)` / `NotificationConfig(Schema)`, and `@objectstack/spec/system` no longer exports `NotificationConfig(Schema)` (#4610)
6+
7+
The names `Notification` / `NotificationSchema` (`./api` vs `./ui`) and
8+
`NotificationConfig` / `NotificationConfigSchema` (`./system` vs `./ui`)
9+
each resolved to **two different declarations** depending on the import
10+
path — the #4411 dual-source trap. Resolution (three-repo,
11+
import-statement-level consumer scan: framework, cloud, objectui):
12+
13+
- **Removed** `NotificationSchema` / `Notification` from
14+
`@objectstack/spec/ui`. This was a toast/banner "notification instance"
15+
shape (`type`/`severity`/`message`/`duration`/`actions`/`position` + ARIA
16+
props) with **zero importers** in all three repos — objectui's toaster
17+
never adopted it. The live contract is `./api`'s `Notification(Schema)`:
18+
the REST inbox row (`id`/`type`/`title`/`body`/`read`/`data`/`actionUrl`/
19+
`createdAt`) embedded in `ListNotificationsResponseSchema`, served by
20+
`/api/v1/notifications`, implemented by `@objectstack/client`, and
21+
mirrored by `InboxNotification` in `@objectstack/spec/contracts`
22+
(ADR-0030: the bell reads this shape).
23+
- FROM `import { NotificationSchema, type Notification } from '@objectstack/spec/ui'`
24+
TO `import { NotificationSchema, type Notification } from '@objectstack/spec/api'`.
25+
**Shape change**: the api row is an inbox record, not a presentation
26+
config — the ui shape's `severity` / `duration` / `dismissible` /
27+
`actions` / `position` / ARIA fields do not exist there. For the
28+
presentation vocabulary keep using the ui enums, which are unchanged:
29+
`NotificationTypeSchema`, `NotificationSeveritySchema`,
30+
`NotificationPositionSchema`, `NotificationActionSchema` (+ their
31+
types) still live in `@objectstack/spec/ui`.
32+
- **Removed** `NotificationConfigSchema` / `NotificationConfig` from **both**
33+
`@objectstack/spec/system` and `@objectstack/spec/ui` — the bare name left
34+
the spec export surface entirely. Both declarations had zero importers in
35+
all three repos and were wired into no parent schema. The system side (a
36+
channel + template + recipients + schedule + retryPolicy + tracking
37+
"unified notification management protocol") predates ADR-0030's accepted
38+
delivery architecture and advertised capability the runtime does not
39+
deliver (its channel enum's `push`/`slack`/`teams`/`webhook` dead-letter,
40+
#3197; nothing reads `schedule`/`retryPolicy`/`tracking`). The ui side (a
41+
toaster global config: `defaultPosition`/`defaultDuration`/`maxVisible`/
42+
`stackDirection`/`pauseOnHover`) was never adopted by objectui.
43+
- FROM `import { NotificationConfigSchema } from '@objectstack/spec/system'` (or `.../ui`) →
44+
TO: no direct replacement. The live delivery vocabulary is
45+
`NotificationService.emit` (`INotificationService`,
46+
`@objectstack/spec/contracts`), the `notify` flow node
47+
(`NotifyConfigSchema`, `@objectstack/spec/automation`) and the
48+
`sys_notification*` platform objects; per-user delivery preferences are
49+
`NotificationPreferences(Schema)` in `@objectstack/spec/api`.
50+
- `@objectstack/spec/api`'s `Notification(Schema)` and
51+
`NotificationPreferences(Schema)` are **unchanged**; `./api` is now the
52+
sole owner of the bare `Notification(Schema)` names. Imports from `./api`
53+
need no migration. `@objectstack/spec/system`'s `NotificationChannel(Schema)`,
54+
`EmailTemplate(Schema)`, `SMSTemplate(Schema)`, `PushNotification(Schema)`
55+
and `InAppNotification(Schema)` are **unchanged**.
56+
57+
`dual-source-exports.baseline.json` shrinks by exactly these 4 rows (28 → 24,
58+
#4535 C3).

.github/workflows/lint.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,21 @@ jobs:
173173
- name: Wildcard fall-through guard
174174
run: pnpm check:wildcard-fallthrough
175175

176+
# Init-service declaration guard (#4471, ADR-0116). The kernel's ordering
177+
# contract (dependencies / optionalDependencies / requiresServices /
178+
# providesServices) was complete but VOLUNTARY: a plugin that resolves
179+
# getService('X') during init() and declares nothing fails only under
180+
# unlucky composition orders, usually inside a best-effort try/catch that
181+
# downgrades the miss to a warn. That silence shipped #4085 and #4420 (the
182+
# latter losing every in-flight approval on restart). This scan walks each
183+
# plugin's init() call graph from the AST — the #4420 call sat in a private
184+
# helper, not init()'s own body — and errors on any init-reachable
185+
# getService of a workspace-provided service that no declaration covers.
186+
# Declared tolerance stays in the plugin (optionalDependencies), never in a
187+
# checker-side ledger. Runs its own --self-test first.
188+
- name: Init-service declaration guard
189+
run: pnpm check:init-service-contract
190+
176191
# Release-notes drift guard: the platform is one version-locked train, so
177192
# every released @objectstack/spec major must have a curated, navigable
178193
# release page at content/docs/releases/v<major>.mdx. Catches the gap that

content/docs/references/api/meta.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
"export",
3333
"identity",
3434
"metadata",
35-
"notification",
3635
"package-api",
3736
"package-registry",
3837
"plugin-rest-api",

content/docs/references/api/notification.mdx

Lines changed: 0 additions & 37 deletions
This file was deleted.

0 commit comments

Comments
 (0)