Skip to content

Commit 001c01e

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/plugin-boot-logs-visibility-12kkwh
# Conflicts: # scripts/check-wildcard-fallthrough.mjs
2 parents 257541d + 851a001 commit 001c01e

42 files changed

Lines changed: 1811 additions & 536 deletions

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/adapter-hono": patch
3+
---
4+
5+
fix(adapters/hono): the auth wildcard yields paths the auth service does not own (#4117)
6+
7+
`app.all('${prefix}/auth/*')` claimed a whole namespace and was **terminal**: it
8+
returned the auth service's response unconditionally, including better-auth's 404
9+
for a path it does not implement, and the legacy `handleAuth` bridge's own
10+
`handled: false` 404. That is the #4088 shape, found by #4116's enumeration after
11+
manual greps had missed it.
12+
13+
A 404 from better-auth, or `handled: false` from the dispatcher, now means "not
14+
this mount's path" and the handler yields. The predicate is the dispatcher's own
15+
`handled` flag wherever one exists — an explicit ownership answer beats inferring
16+
one from a status; only the better-auth hand-off lacks such a flag, and there the
17+
404 is the signal, as in #4092.
18+
19+
**What changes on the wire.** An unowned path under `${prefix}/auth/*` used to get
20+
a 404 built by this mount. It now continues to the `${prefix}/*` dispatcher
21+
catch-all and gets a real, gate-carrying `dispatch()` attempt, so a domain handler
22+
registered for such a path becomes reachable — this adapter's actual extension
23+
mechanism. When nothing anywhere claims the path the reply is still the same
24+
enveloped `{ success: false, error: { message: 'Not Found', code: 404 } }`. Paths
25+
the auth service does own are untouched, and a 401/403 from it is never treated as
26+
a disclaimer of ownership.
27+
28+
No configuration changes and no new routes.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/metadata-protocol': patch
3+
'@objectstack/runtime': patch
4+
---
5+
6+
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.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
---
3+
4+
Docs only — no package changes, nothing to release.
5+
6+
Records why `packages/*/CHANGELOG.md` is **not** added to the docs-drift mapper's test-file
7+
exclusion (#4091). It reads as the obvious next narrowing and is a provable no-op:
8+
9+
- `chore: version packages` is the only PR class that mass-touches those files, and it runs
10+
no GitHub Actions — `changesets/action` opens it with the repo `GITHUB_TOKEN`, which by
11+
design triggers no workflow runs. Measured on #3910: one check run, Vercel's own app.
12+
The version bump is still verified, just post-merge (`ci.yml` / `lint.yml` on `push: main`,
13+
and `release.yml` gates publish on a green build).
14+
- `changeset version` writes `package.json` beside every `CHANGELOG.md` it appends to
15+
(45 vs 46 on page 1 of #3910's diff), so excluding the CHANGELOGs would leave the derived
16+
package-root set bit-identical anyway.
17+
18+
Written down so the next reader does not spend a PR rediscovering it as a gap.
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/rest": patch
4+
"@objectstack/platform-objects": patch
5+
---
6+
7+
fix(spec,rest): the metadata forms save what they show — form ↔ Zod reconciliation (#3786)
8+
9+
Every entry in `METADATA_FORM_REGISTRY` is a hand-written `defineForm` layout
10+
that names keys of a Zod schema it never imports: two descriptions of one key
11+
set, a comment asking the next author to keep them in step, and nothing that
12+
fails when they don't. #3786 asked for a sweep of that shape across the repo.
13+
**Four of the seventeen forms had already drifted, every one of them silently.**
14+
15+
The silence is the point. `ObjectSchema` / `FieldSchema` are deliberately not
16+
`.strict()`, so a key the schema does not declare parses clean and is stripped
17+
on the way to storage — the same ADR-0104 failure class the `field.zod.ts`
18+
prune tombstone already describes in prose. An admin toggled a switch in
19+
Studio, got no error, and the value never landed.
20+
21+
**What was broken, from an author's seat:**
22+
23+
- **Object → Capabilities.** The block bound to `capabilities`; the
24+
`ObjectSchema` key is `enable`. All seven toggles (Track history, Searchable,
25+
API enabled, Files, Feeds, Activities, Clone) saved nothing.
26+
- **Object → Fields.** The inline column grid offered 16 keys `FieldSchema` has
27+
never declared. `PII`, `Encrypted`, `Indexed`, `Immutable`, `Filterable`,
28+
`Placeholder`, `Validation`/`Error message` and `Starting number` were
29+
controls with no storage behind them at all; the rest named keys the schema
30+
had **renamed** and the form never followed:
31+
`referenceFilter``lookupFilters`, `cascadeDelete``deleteBehavior`
32+
(a three-way enum, not a boolean), `formula``expression`,
33+
`displayFormat``autonumberFormat`, and the flat `summaryType` /
34+
`summaryField` pair → the single `summaryOperations` object, which also
35+
restores the `object` key the flat pair had no slot for. Roll-ups authored in
36+
that grid saved nothing.
37+
- **Report → Advanced.** `aria` and `performance` were pruned from
38+
`ReportSchema` by #3496; the form kept rendering both.
39+
- **Hook / Action → Body.** `memoryMb` was unauthorable — named in
40+
`hook.form.ts`'s own doc comment, absent from the list beneath it.
41+
- **Page → Interface.** `interfaceConfig.sort` was unauthorable, so a page's
42+
default sort order could not be set in Studio at all.
43+
44+
**No authored metadata changes and nothing you can write is removed.** These
45+
were UI controls that never persisted; every corrected key is one `FieldSchema`
46+
/ `ObjectSchema` already accepted. Metadata authored in YAML/TS was always
47+
validated against the real schema and is unaffected. If you had been filling
48+
those Studio controls expecting them to stick, they now either work (the
49+
renamed five) or are gone rather than lying to you.
50+
51+
The metadata-form translation bundles are derived from the registry, so all
52+
four locales are regenerated. Worth naming what they contained: translated
53+
labels, in four languages, for switches that saved nothing — the drift had
54+
propagated into a generated artifact and been dutifully translated there.
55+
56+
**The mechanism.** `metadata-form-zod-reconciliation.test.ts` walks every
57+
registered form and reconciles it against `getMetadataTypeSchema()`. The two
58+
directions are deliberately asymmetric: **form-only** (a control whose value is
59+
discarded) is always a defect and cannot be excused, because no design wants
60+
one; **zod-only** is ledgerable with a reason, for a deprecated key held back
61+
from new authoring or a curated quick-add subset that defers to a fuller
62+
editor. Ledger entries are checked for non-vacuity and for still resolving on
63+
both sides, per the #4045 / #4040 discipline. Verified by mutation — re-adding
64+
a stripped key, dropping a covered key, and offering a ledgered omission each
65+
turn the gate red.
66+
67+
**New export: `TRANSLATABLE_METADATA_TYPES`** (`@objectstack/spec/system`), the
68+
set of metadata types whose labels `translateMetadataDocument` localizes,
69+
derived from its dispatch table rather than restated. `@objectstack/rest` had
70+
been carrying a hand-copied literal set under a "keep in sync with the type
71+
dispatch" comment; it now reads this instead. Registering a translator in spec
72+
reaches the REST boundary with nothing else to remember — the second list is
73+
deleted rather than checked, which is the better half of derive-or-gate.
74+
75+
Also corrected: `ActionAiCategorySchema`'s comment claimed it mirrored
76+
`ToolCategorySchema` in `ai/tool.zod` and told the next author to update both
77+
sides — but #3896 deleted `ToolCategorySchema` along with the inert
78+
`tool.category` key it typed. The instruction had been pointing at a source
79+
that no longer exists. The enum is canonical now and says so.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
---
2+
'@objectstack/cli': patch
3+
'@objectstack/core': patch
4+
'@objectstack/metadata': patch
5+
'@objectstack/runtime': patch
6+
---
7+
8+
fix(cli,core,metadata,runtime): `os serve` boots with no compiled artifact — the platform does not need an application to start (#4085)
9+
10+
The artifact (`dist/objectstack.json`) defines an **application**. ObjectStack is
11+
a development platform, so it has to start without one — but `os serve
12+
objectstack.config.ts` died during boot whenever the artifact was absent:
13+
14+
```
15+
Loading objectstack.config.ts...
16+
[StandaloneStack] artifact read FAILED: path='…/dist/objectstack.json' error=ENOENT…
17+
18+
✗ Service 'manifest' is async - use await
19+
```
20+
21+
Exit 1 — on a **known-good app** (`examples/app-todo` fails the same way with
22+
only its `dist/objectstack.json` moved aside), and on every freshly authored
23+
project between `os init` and its first `os compile`. The message named neither
24+
the missing artifact nor a fix, so it read as an internal kernel fault.
25+
26+
Three separate faults, each of which alone was enough to refuse the boot:
27+
28+
- **`serve` registered the config-derived `AppPlugin` before the stack's own
29+
`plugins[]`.** Registration order *is* the kernel's init/start order, and that
30+
slot sits ahead of `ObjectQLPlugin` (which registers `manifest`/`objectql`) and
31+
`DefaultDatasourcePlugin` (which connects the database the app seeds through).
32+
The wrap is now **appended** to `plugins[]`, the same slot
33+
`createStandaloneStack` gives its artifact-derived `AppPlugin` — so config-boot
34+
and artifact-boot share one plugin order. The artifact path never hit this,
35+
which is exactly what made a plugin-**order** bug look artifact-related.
36+
37+
- **`ctx.getService()` reported a never-registered service as "is async".**
38+
`PluginLoader.getService` is an `async` method, so its return value is *always*
39+
a Promise and its internal "not found" rejection can never surface
40+
synchronously — the kernel read the answer off that Promise and told every
41+
caller to `await` a service that did not exist, while the `not found` branch
42+
below it was unreachable. It now decides from the registry: absent ⇒
43+
`[Kernel] Service 'x' not found`, registered-but-uninstantiated ⇒ the unchanged
44+
`Service 'x' is async - use await`. The same crash now reads
45+
`[Kernel] Service 'manifest' not found`, which points at the layer that is
46+
actually wrong.
47+
48+
- **`MetadataPlugin` treated an absent `local-file` artifact as fatal.**
49+
`createStandaloneStack` always points it at `dist/objectstack.json`, so a stack
50+
with no app at all could not boot. A **missing** local artifact is now "nothing
51+
compiled yet": it logs, starts empty, and leaves the artifact watcher armed, so
52+
a later `os compile` hydrates the running server. The tolerance is
53+
ENOENT-only — a malformed or unreadable artifact stays fatal — and
54+
`bootstrap: 'artifact-only'` (sealed runtime, where the artifact *is* the
55+
deployment) keeps failing loudly rather than silently serving an empty runtime.
56+
57+
`[StandaloneStack] artifact read FAILED … ENOENT` is likewise no longer shouted
58+
at callers for whom "no artifact" is a healthy state; a present-but-unusable
59+
artifact keeps the loud warning.
60+
61+
Pinned by an e2e pair that drives the real `os serve` with **no `os compile`
62+
anywhere**: an app defined only by `objectstack.config.ts` (asserting its object
63+
is in the started plugin set, not merely that boot survived) and a bare
64+
`export default {}` platform. The #4012 fixture drops the `os compile` this bug
65+
had forced on it.

content/docs/api/client-sdk.mdx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@ async function main() {
4040

4141
// 2. Check available services
4242
console.log('Services:', discovery.services);
43-
// → { metadata: { enabled: true, status: 'degraded' }, data: { enabled: true, status: 'available' }, auth: { enabled: false, ... } }
43+
// → { metadata: { enabled: true, status: 'degraded', message: 'In-memory metadata registry — …' },
44+
// data: { enabled: true, status: 'available' }, auth: { enabled: false, ... } }
45+
// `metadata` reports the implementation actually behind it: `degraded` plus a
46+
// message naming what is missing while the kernel's in-memory fallback fills
47+
// the slot, `available` once MetadataPlugin provides a persisted registry.
4448

4549
// 3. Query data
4650
const tasks = await client.data.find('todo_task', {

content/docs/api/index.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ Returns the full discovery manifest.
132132

133133
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).
134134

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+
135137
### `GET /.well-known/objectstack`
136138

137139
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`.

content/docs/kernel/services-checklist.mdx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,15 +113,19 @@ The metadata service is a **framework** — it works with an in-memory `SchemaRe
113113

114114
### Discovery: Per-Service Status
115115

116-
The discovery endpoint returns a `services` map so clients know what is available:
116+
The discovery endpoint returns a `services` map so clients know what is available.
117+
The `metadata` entry is **computed from the implementation that fills the slot**, not
118+
fixed (#4089): a stack running the kernel's in-memory fallback reports `degraded` with
119+
that fallback's own message, while a stack with `MetadataPlugin` reports `available` and
120+
no message. Below is the former — the fallback case:
117121

118122
```json
119123
{
120124
"services": {
121125
"metadata": {
122-
"enabled": true, "status": "degraded",
126+
"enabled": true, "status": "degraded", "handlerReady": true,
123127
"route": "/api/v1/meta", "provider": "kernel",
124-
"message": "In-memory registry; DB persistence pending"
128+
"message": "In-memory metadata registry — real reads and writes, no persistence (lost on restart). Register MetadataPlugin for a persisted registry."
125129
},
126130
"data": {
127131
"enabled": true, "status": "available",

content/docs/releases/implementation-status.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,7 @@ Implementation spans every layer of the platform. Core infrastructure, data mode
468468
| **Query Engine** | ⚠️ | Partial (depends on driver) |
469469
| **REST API** || Yes |
470470
| **Client SDKs** || Yes |
471-
| **Metadata System** | ⚠️ | Partial (in-memory only, DB persistence pending) |
471+
| **Metadata System** | ⚠️ | Partial (`sys_metadata` persistence via `MetadataPlugin`; without it the kernel's in-memory fallback fills the slot and discovery reports it `degraded`) |
472472
| **HTTP Caching** || Yes |
473473
| **Testing Tools** || Yes |
474474
| **UI Rendering** || No |

docs/adr/0076-objectql-core-tiering.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ Decision: each capability plugin registers its routes as a **normalized handler*
134134
**Inventory of current fakes / mis-reports:**
135135
- `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.)*
136136
- `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.)*
138138
- *(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.)*
139139

140140
**Decision — honest capabilities:**

0 commit comments

Comments
 (0)