You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(runtime,metadata-protocol): the two discovery builders compute the metadata slot instead of contradicting each other (#4089) (#4114)
ADR-0076 D12's rule is that a service self-identifies (`__serviceInfo`) and the
discovery builder reports what it declares. #4058 step 1 gave the kernel's
`createMemoryMetadata` that marker. It went unread anyway: `metadata` was not in
the loop that reads markers, but in a "kernel-provided (always available)" block
above it — hardcoded separately in each builder, and the two hardcodes said
opposite things about the same slot.
- `runtime/http-dispatcher.ts`: permanently `degraded` + "In-memory registry; DB
persistence pending". A stack with MetadataPlugin and a real `sys_metadata`
table was reported as having no persistence.
- `metadata-protocol/protocol.ts`: permanently `available`. A registry that never
reaches disk was reported as indistinguishable from the persisted one — the
over-declaration #4089 filed.
So each builder was wrong for half the stacks, in opposite directions, and a
consumer reading `discovery.services.metadata` got a different answer depending
on which host answered.
Both now read the registered implementation's self-description: `degraded` plus
that implementation's own message (which names what is missing and what to
install) for the kernel fallback and plugin-dev's dev registry; `available` with
no message for MetadataPlugin, or any implementation carrying no marker — the
absent-service case included, since the protocol serves `/meta` regardless.
`handlerReady: true` is stated unconditionally on both sides, deliberately NOT
taken from the self-description: it answers "is `/api/v1/meta` mounted?", and
that route is served by the protocol, so a degraded service in the slot does not
unmount it. (Contrast `realtime`, where D12 pins `handlerReady: false` because no
surface exists at all.) `data` keeps its hardcode — ObjectQL is its only producer
and plugin-dev always loads ObjectQLPlugin as a child — but that truth rests on a
load-order convention rather than construction, filed as #4130.
No routing, gating or dispatch behavior changes; `svcAvailable` and every domain
predicate are untouched. Tests pin both directions per builder, the real
kernel-boot path end-to-end (ObjectQLPlugin with no MetadataPlugin ⇒ `degraded`),
and — the invariant this closes — that the two builders now answer the same
service instance identically.
Docs: ADR-0076 D12 inventory, the `services-checklist` / `api/index` discovery
samples, and the two docs that still repeated the removed hardcode
(`api/client-sdk`, `releases/implementation-status`).
Closes#4089
Both discovery builders now compute the `metadata` service entry from the implementation that fills the slot, instead of hardcoding opposite verdicts for it (#4089).
7
+
8
+
`metadata` sat in a "kernel-provided (always available)" block above the loop that reads `__serviceInfo`, hardcoded separately in each builder — and the two disagreed about the same slot:
9
+
10
+
-`@objectstack/runtime`'s dispatcher declared it permanently `status: 'degraded'` with `message: 'In-memory registry; DB persistence pending'`, so a stack with `MetadataPlugin` and a real `sys_metadata` table was still reported as having no persistence.
11
+
-`@objectstack/metadata-protocol` declared the same slot permanently `status: 'available'`, so the kernel's in-memory fallback (`createMemoryMetadata`, auto-registered when no metadata plugin is present) read exactly like a persisted registry — the `__serviceInfo` marker #4058 gave it went unread here.
12
+
13
+
Both now read the registered service's `__serviceInfo` (via `readServiceSelfInfo`) and report what it declares:
14
+
15
+
- kernel in-memory fallback, or plugin-dev's dev registry → `status: 'degraded'` plus that implementation's own `message`, which names what is missing and what to install.
16
+
-`MetadataPlugin` (or any implementation carrying no marker) → `status: 'available'` with no message.
17
+
18
+
`handlerReady: true` is now stated unconditionally on both sides: it answers "is `/api/v1/meta` mounted?", and that route is served by the protocol whichever implementation occupies the slot — a degraded service in it does not unmount the route. Nothing about routing, gating, or dispatch changes; consumers that treat `status` as a capability claim (AI agents, the console) simply stop being told two different things by two hosts.
Copy file name to clipboardExpand all lines: content/docs/api/index.mdx
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -132,6 +132,8 @@ Returns the full discovery manifest.
132
132
133
133
Disabled/uninstalled route keys (e.g. `auth`, `analytics`, `workflow`) are omitted from `routes` entirely rather than set to `null`; check `services` to tell "not installed" apart from "installed but not yet mounted here." The sample above shows a minimal install: `analytics` reports `unavailable` and advertises no route until `@objectstack/service-analytics` registers the engine — the dispatcher then also mounts `/api/v1/analytics/*` (the routes are capability-conditional; an uninstalled capability answers 404 for every method).
134
134
135
+
`metadata` is reported from whatever implementation fills its slot, so the sample's `available` is the `MetadataPlugin` case (a persisted `sys_metadata` registry). A stack running the kernel's in-memory fallback instead reports `status: "degraded"` with a `message` naming what is missing and what to install. `handlerReady` is `true` either way: `/api/v1/meta` is served by the protocol, so the route is mounted whichever registry sits behind it.
136
+
135
137
### `GET /.well-known/objectstack`
136
138
137
139
Served by the runtime dispatcher (`@objectstack/runtime`), not `@objectstack/rest` — its body is wrapped as `{ "data": { ... } }` and includes fields (`name`, `environment`, `features`, `locale`) that the `@objectstack/rest`-served `/api/v1` response above does not. The client SDK's `connect()` tries `/api/v1/discovery` first and falls back to this endpoint, unwrapping either `body.data` or the bare `body`.
|**Metadata System**| ⚠️ | Partial (`sys_metadata` persistence via `MetadataPlugin`; without it the kernel's in-memory fallback fills the slot and discovery reports it `degraded`) |
Copy file name to clipboardExpand all lines: docs/adr/0076-objectql-core-tiering.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -134,7 +134,7 @@ Decision: each capability plugin registers its routes as a **normalized handler*
134
134
**Inventory of current fakes / mis-reports:**
135
135
- `plugin-dev` registers ~8 dev stubs — `storage` / `search` / `automation` / `graphql` / `analytics` / `realtime` / `notification` / `ai` (they already carry a `_dev: true` marker that nothing respects). *(Update #4000: the `analytics` one is **gone** — after the fallback's retirement (#3891/#3989) it was the last thing refilling that slot, and refilling it re-created the retired shape in dev: the dispatcher gated on service presence, so the stub was called like an engine and answered 200 with fabricated rows. The remaining stubs are unchanged, and the class-wide question they raise is tracked separately — see conclusion 3 below.)* *(Update #4058 step 1: the blanket `_dev: true` is **gone** too. Every dev implementation now carries the standard `__serviceInfo`, classified in one reviewable table (`DEV_STUB_SELF_INFO`) by what it actually is — `degraded` for the ones that really work with reduced capability (`file-storage` / `search` / `metadata` / `workflow` / `realtime`, plus the wrapped kernel fallbacks), `stub` for the ones whose answer is fabricated (`ai` / `automation` / `notification` / `data` / `auth` / `security.*`). The single marker made those two indistinguishable and declared the working ones fake, which is why "adopt the dispatcher gate everywhere?" could not be answered. The gates themselves are untouched — that is #4058 step 2.)*
136
136
-`ObjectQLPlugin` registers a ~66-line `analytics`**fallback** (the D10 note — deliberate, but it reports as fully available).
137
-
-`http-dispatcher.ts``svcAvailable` — the hardcode above.
137
+
- `http-dispatcher.ts` `svcAvailable` — the hardcode above. *(Update #4089: `svcAvailable` respects the marker, but two entries never went through it at all — a "kernel-provided (always available)" block above the loop, hardcoded per builder and **contradicting itself across them**: the dispatcher declared `metadata` permanently `degraded` with "In-memory registry; DB persistence pending", while metadata-protocol declared the same slot permanently `available`. So one host called a `sys_metadata`-backed registry degraded and the other called the in-memory fallback fully real — the marker #4058 had just added to `createMemoryMetadata` went unread on both sides, because neither side read anything. Both now compute the slot from the registered implementation's `__serviceInfo` and agree; `handlerReady: true` stays unconditional on both, since `/api/v1/meta` is served by the protocol whichever implementation occupies the slot. `data` keeps its hardcode: ObjectQL is the only producer that ever fills it, and `ObjectQLPlugin` — which plugin-dev always loads as a child — registers the real engine, so plugin-dev's `data` stub is unreachable in a stack that has a discovery builder at all.)*
138
138
-*(Added by #4058: the **kernel's own** fallbacks — `createMemoryCache` / `Queue` / `Job` / `I18n` / `Metadata`, auto-registered by ObjectKernel — were missing from this inventory and were the worst case in it: they carried `_fallback: true`, a marker **no** reader recognized (not even `readServiceSelfInfo`), so both discovery builders reported them as fully `available`. They now self-describe as `degraded` with `handlerReady: false` where no HTTP surface exists (`cache` / `queue` / `job`). The lone duck-typed consumer of `_fallback`/`_dev` — an i18n diagnostic in `app-plugin.ts` — reads `readServiceSelfInfo` instead.)*
0 commit comments