Skip to content

Commit d0d7464

Browse files
os-zhuangclaude
andauthored
feat(plugin-dev)!: retire the stub table — DevPlugin assembles real plugins, empty slots stay empty (ADR-0115) (#4137)
Executes ADR-0115 in full, absorbing the #4112 and #4126 subsets that landed mid-flight (their choices are canonical where they overlap: OS_ALLOW_DEV_PLUGIN, assertNotProduction(), the loud security warn). The entire stub table is deleted across Tiers A+B+C; service-storage and service-realtime are auto-wired as optional child plugins; docs converge on the assembly reality; the breaking changeset carries the full FROM→TO table per retired slot. Closes #4104. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BEM82RNKyyyTNPYHK1VJJS
1 parent 1e38158 commit d0d7464

9 files changed

Lines changed: 289 additions & 877 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
'@objectstack/plugin-dev': minor
3+
---
4+
5+
feat(plugin-dev)!: the stub table is retired — DevPlugin assembles real plugins and registers no service implementations of its own (ADR-0115, #4093, #4104).
6+
7+
DevPlugin used to fill every core-service slot no real plugin occupied with a dev stub. Every one of those stubs is gone. A slot nothing fills now stays EMPTY, exactly as in production: routes answer 404/501, discovery reports `unavailable`, and in-process consumers must handle absence — which production already required of them. FROM → TO per retired slot:
8+
9+
| Slot | The stub did | Instead |
10+
|:---|:---|:---|
11+
| `security.permissions` | allow-all `checkObjectPermission()` | install `@objectstack/plugin-security` (already part of the default assembly) |
12+
| `security.rls` | compiled no row filter | same — `plugin-security` |
13+
| `security.fieldMasker` | returned results unmasked | same — `plugin-security` |
14+
| `auth` | `verify()` accepted everyone as admin | install `@objectstack/plugin-auth` (already part of the default assembly) |
15+
| `data` | accepted writes, stored nothing | install `@objectstack/objectql` (already part of the default assembly) |
16+
| `ui` | shapeless `{}` placeholder | nothing consumed it; handle the absent slot |
17+
| `ai` | placeholder chat/complete answers | install a real AI service |
18+
| `automation` | `execute()` reported success without running | install an automation engine plugin |
19+
| `notification` | claimed "sent", delivered nothing | install a notification service |
20+
| `file-storage` | in-memory files lost on restart | `@objectstack/service-storage` — now auto-wired by DevPlugin when installed (local-disk adapter) |
21+
| `realtime` | in-process pub/sub copy | `@objectstack/service-realtime` — now auto-wired by DevPlugin when installed (its default in-memory adapter) |
22+
| `search` | in-memory substring index | no consumer resolves this slot; a future search service ships its own dev strategy |
23+
| `workflow` | unvalidated state transitions | no consumer resolves this slot; a future workflow service ships its own dev strategy |
24+
| `metadata` | a second hand-written copy of core's `createMemoryMetadata` | no behavior change — the kernel pre-injects core's fallback for empty core slots (`CORE_FALLBACK_FACTORIES`), and ObjectQL registers the real metadata service in the default assembly |
25+
| `cache` / `queue` / `job` / `i18n` | re-registered core's `createMemory*` fallbacks | no behavior change — the kernel pre-injects the same core fallbacks automatically; install `@objectstack/service-cache` / `service-queue` / `service-job` for real engines, and i18n auto-wires from the stack's translations (unchanged) |
26+
27+
Also new, from the same ADR:
28+
29+
- **Production guard** (first shipped with the security-trio subset): `DevPlugin.init()` throws when `NODE_ENV === 'production'` — the assembly is built around a well-known default auth secret and a seeded dev admin. Escape hatch: `OS_ALLOW_DEV_PLUGIN=1`.
30+
- **Assembly auto-wire**: `@objectstack/service-storage` and `@objectstack/service-realtime` are wired as optional child plugins when installed (both ship with DevPlugin's dependencies), so dev keeps working file storage and realtime through real implementations.
31+
- `options.services` keys for the retired stubs are accepted and ignored; `'file-storage'` / `'realtime'` now toggle the real service wiring.
32+
33+
One-line fix for an upgrading stack: if something you called in dev now throws "service not found" or 404s, that call was consuming a fabricated answer — install the real service for that slot (table above), or make the caller tolerate absence the way it already must in production.

content/docs/kernel/services-checklist.mdx

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -310,30 +310,24 @@ methods, so this entry describes a contract with no implementer on either side.
310310
`getLocales`, `getTranslations`, `getFieldLabels`
311311

312312
**Service Name**: `i18n` · **Criticality**: `core`
313-
**Implementations**: `@objectstack/service-i18n` (production — file-based) · Built-in in-memory fallback · Dev Plugin (in-memory stub)
313+
**Implementations**: `@objectstack/service-i18n` (production — file-based) · In-memory fallback (`createMemoryI18n` from `@objectstack/core`)
314314
**Route Mount**: `/api/v1/i18n`
315315
**Contract**: `II18nService` in `@objectstack/spec/contracts`
316316

317317
#### Service Registration
318318

319-
The i18n service is a **core built-in service** with automatic in-memory fallback. If no plugin registers the service, the kernel automatically injects an in-memory implementation during startup:
320-
321319
| Environment | Provider | Registration |
322320
|:------------|:---------|:-------------|
323321
| **Production** | `I18nServicePlugin` | File-based `FileI18nAdapter` loads JSON locale files from disk |
324-
| **Built-in Fallback** | Kernel | In-memory Map-backed stub, auto-injected when no plugin provides `i18n` |
325-
| **Development** | `DevPlugin` | In-memory Map-backed stub, supports `loadTranslations()` |
322+
| **In-memory fallback** | `@objectstack/core` (`createMemoryI18n`) | Registered by `AppPlugin` when the stack declares translation bundles and no i18n service is present; self-describes as `degraded` (ADR-0076 D12) |
323+
| **Development** | `DevPlugin` | Auto-wires `I18nServicePlugin` when the stack declares translations; otherwise the AppPlugin fallback above applies. DevPlugin registers no stub of its own (ADR-0115) |
326324

327325
```typescript
328-
// Production (overrides built-in fallback)
326+
// Production (real service)
329327
kernel.use(new I18nServicePlugin({ defaultLocale: 'en', localesDir: './i18n' }));
330328

331-
// Development (automatic — DevPlugin registers i18n stub for all 16 core services)
329+
// Development (automatic — DevPlugin wires I18nServicePlugin when translations are declared)
332330
kernel.use(new DevPlugin());
333-
334-
// No plugin required — kernel auto-injects in-memory fallback if none registered
335-
const kernel = new ObjectKernel();
336-
await kernel.bootstrap(); // i18n service available via built-in fallback
337331
```
338332

339333
#### Discovery & Handler Consistency

content/docs/plugins/packages.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -344,10 +344,10 @@ All services implement contracts from `@objectstack/spec/contracts` and are kern
344344

345345
### @objectstack/plugin-dev
346346

347-
**Local Development Plugin** — one-line local stack: wires ObjectQL, the in-memory driver, auth, security, the HTTP server, REST and the dispatcher, then fills any still-unclaimed core service slot with an in-memory dev implementation.
347+
**Development Assembly Plugin** — one plugin that wires the real platform stack for zero-config local development.
348348

349-
- **Features**: Composes the real plugins above; registers dev implementations for unclaimed slots, each declaring what kind of fake it is (`__serviceInfo``degraded` when it really does the work in memory, `stub` when it fabricates). Slots that would fabricate an answer over HTTP get no implementation at all: `analytics` and the three `security.*` handles stay empty on purpose.
350-
- **When to use**: Local development only. **`init()` throws under `NODE_ENV=production`** — set `OS_ALLOW_DEV_PLUGIN=1` only if you deliberately want the dev slate under a production `NODE_ENV`.
349+
- **Features**: Auto-assembles ObjectQL + in-memory driver + auth + security + Hono server + REST + dispatcher + app metadata, plus optional real services when installed (storage, realtime, i18n); registers no stubs — a slot no plugin fills stays empty, as in production (ADR-0115); refuses to boot with `NODE_ENV=production` (`OS_ALLOW_DEV_PLUGIN` escape hatch)
350+
- **When to use**: Zero-config local development and playgrounds
351351
- **README**: [View README](https://github.com/objectstack-ai/objectstack/blob/main/packages/plugins/plugin-dev/README.md)
352352

353353
### @objectstack/plugin-approvals

docs/adr/0115-plugin-dev-assembly-not-stub-table.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ Precision, for the record: the `auth` stub is **dead code on the real identity p
8888

8989
**D5 — Direct cut inside the 17.x rc window; the changeset carries the FROM → TO per slot.** No deprecation window and no stub-preserving grace release: the honest `stub`/`degraded` self-descriptions shipped by #4082/#4086 already are the deprecation notice (discovery has been telling every consumer these are not capabilities), and keeping an allow-all security fake alive one extra release "to be polite" is exactly what D2 forbids. External rc consumers get the standard breaking-changeset prescription (Post-Task Checklist rule: FROM → TO and the one-line fix — *install the corresponding real service, or handle the absent slot as production already requires*). Resolves open question 1.
9090

91-
**D6 — plugin-dev grows a production guard, in the same change as Tier A.** `DevPlugin.init` throws when `NODE_ENV === 'production'`, escape hatch `OS_ALLOW_DEV_PLUGIN_IN_PRODUCTION=1` (Prime Directive #9 `OS_ALLOW_*` shape — deliberately scary). Deleting the fakes removes the security-semantics hazard; the guard covers what deletion cannot: the plugin still assembles a stack around a well-known default auth secret and a seeded dev admin, which no production deployment should get by accident. It does not ship as a separate earlier fix because Tier A itself waits for nothing (resolves open question 3 — the "maybe it shouldn't wait" concern was about the security stubs, and D2 removes them rather than gating them).
91+
**D6 — plugin-dev grows a production guard, in the same change as Tier A.** `DevPlugin.init` throws when `NODE_ENV === 'production'`, escape hatch `OS_ALLOW_DEV_PLUGIN=1` (Prime Directive #9 `OS_ALLOW_*` shape — deliberately scary; the shipped name, landed by #4126's subset). Deleting the fakes removes the security-semantics hazard; the guard covers what deletion cannot: the plugin still assembles a stack around a well-known default auth secret and a seeded dev admin, which no production deployment should get by accident. It does not ship as a separate earlier fix because Tier A itself waits for nothing (resolves open question 3 — the "maybe it shouldn't wait" concern was about the security stubs, and D2 removes them rather than gating them).
9292

9393
**D7 — The descriptions converge on what the plugin is.** `content/docs/plugins/packages.mdx` and the plugin README/class doc describe an assembly plugin ("auto-wires ObjectQL + driver-memory + auth + security + HTTP server + REST + dispatcher + app metadata for local development"); the "Stub services" section and the "simulating all 17+ kernel services" claim are removed. Docs describing the retired design are ADR-0078's silent lie in prose form.
9494

@@ -108,6 +108,15 @@ Sequenced to avoid editing `dev-plugin.ts` under an in-flight PR. Ratified by th
108108

109109
End state, mechanically checkable: `DEV_STUB_FACTORIES`, `DEV_STUB_SELF_INFO`, `SHAPELESS_STUB_SELF_INFO`, `NO_DEV_STUB_SERVICES`, and the fill loop no longer exist in `dev-plugin.ts`; `grep -r "createMemoryMetadata\|registerInMemory" packages/plugins/plugin-dev` is empty. The two-PR split is the ratified shape; the tier boundaries remain the decision.
110110

111+
### Update (2026-07-30, post-#4086 reconciliation): executed as ONE PR
112+
113+
Three facts changed between ratification and execution, all shrinking the work:
114+
115+
1. **#4086's merged form deleted nothing.** It gated the six dispatcher domains on `handlerReady` and explicitly deferred "should the fabricators keep occupying slots" to this ADR's Tier A. So `ai` / `automation` / `notification` join Tier A's deletion list here — the Context table's "retired" row described their HTTP surface (gated to empty-slot answers since #4086), not their registration.
116+
2. **#4089 closed at the source.** #4082 had already given core's five fallbacks their `__serviceInfo` self-descriptions, so Tier B's core half was done before PR-2 existed; what remained of Tier B was only plugin-dev's `metadata` copy and the four wrappers.
117+
3. **With the core half gone, the PR-1/PR-2 boundary lost its reason** (Tier B no longer reached into core), and the maintainer directed completing all remaining work at once. The implementation therefore landed as one PR: Tiers A+B+C, the D6 guard, the D4 assembly auto-wire, and the D7 docs convergence — the full end state above, under a single FROM → TO changeset narrative.
118+
4. **#4126 landed the D2 security trio + D6 guard as a first subset while the full PR was in flight.** Its choices are canonical where they overlap: the escape hatch is `OS_ALLOW_DEV_PLUGIN` (not the longer name this ADR first wrote), the guard is a module-level `assertNotProduction()`, and an empty security slot gets one loud boot-log warn ("RBAC, row-level security and field masking are NOT enforced") instead of silence. It also filed #4113 — the dispatcher's `/auth` domain carries its own mock fallback in `packages/runtime` — as the remaining fabricator OUTSIDE plugin-dev; retiring plugin-dev's `auth` stub neither worsens nor fixes that path (both the stub and the runtime mock fabricate a 200; neither yields a session the identity resolver accepts), so #4113 stays the one place that class of fake survives.
119+
111120
## Consequences
112121

113122
- **+** "The capability is present" has one meaning across dev and production; the D12 producer inventory for plugin-dev closes completely instead of shrinking by one per issue.

packages/plugins/plugin-dev/README.md

Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
11
# @objectstack/plugin-dev
22

3-
> Development Mode Plugin for ObjectStack — auto-enables **all 17+ kernel services** for a full-featured API development environment.
3+
> Development Assembly Plugin for ObjectStack — wires the **real** platform stack for zero-config local development.
44
55
## Overview
66

77
Instead of manually wiring up ObjectQL, drivers, auth, HTTP server, REST endpoints, dispatcher, security, and metadata for local development, use `DevPlugin` to get a fully functional stack in one line.
88

9-
The dev environment simulates **all kernel services** so you can:
10-
- CRUD business objects via REST API
11-
- Read, modify, and save views/apps/dashboards via metadata API (`PUT /api/v1/meta/:type/:name`)
12-
- Use GraphQL, analytics, storage, and automation endpoints
13-
- Authenticate with dev credentials (no real auth provider needed)
14-
- Test UI permissions, workflows, and notifications with dev stubs
9+
Everything it wires is a **real implementation** — there are no simulated services. A capability whose package is not installed is simply absent, exactly as in production: its routes answer 404/501 and discovery reports it `unavailable` (ADR-0115). That keeps "the capability is present" meaning the same thing in dev and production.
1510

1611
## Usage
1712

@@ -53,16 +48,14 @@ plugins: [
5348
port: 4000,
5449
seedAdminUser: true,
5550
services: {
56-
auth: false, // Skip auth for quick prototyping
57-
dispatcher: false, // Skip extended API routes
51+
dispatcher: false, // Skip extended API routes
52+
'file-storage': false, // Skip the storage service
5853
},
5954
}),
6055
]
6156
```
6257

63-
## What it auto-configures
64-
65-
### Real plugin implementations
58+
## What it assembles (all real implementations)
6659

6760
| Service | Package | Description |
6861
|---------|---------|-------------|
@@ -73,27 +66,22 @@ plugins: [
7366
| Security | `@objectstack/plugin-security` | RBAC, RLS, field-level masking |
7467
| Hono Server | `@objectstack/plugin-hono-server` | HTTP server on configured port |
7568
| REST API | `@objectstack/rest` | Auto-generated CRUD + metadata endpoints |
76-
| Dispatcher | `@objectstack/runtime` | Auth routes, GraphQL, analytics, packages, storage |
77-
78-
### ⛔ Local development only
79-
80-
`DevPlugin.init()` **throws under `NODE_ENV=production`**. It fills unclaimed service slots with fakes — some of which report success for work they never did — so a production process must not load it. Remove it from that deployment's plugin list and install the real services. `OS_ALLOW_DEV_PLUGIN=1` overrides the refusal if you deliberately want the dev slate under a production `NODE_ENV` (a staging box mimicking prod, a smoke test that pins the variable).
69+
| Dispatcher | `@objectstack/runtime` | Auth routes, GraphQL, packages, storage bridges |
70+
| Storage | `@objectstack/service-storage` | `file-storage` service (local-disk adapter, files under `./storage`) |
71+
| Realtime | `@objectstack/service-realtime` | `realtime` service (in-memory adapter) |
72+
| I18n | `@objectstack/service-i18n` | Auto-registered when the stack declares translations |
8173

82-
### Dev stubs (in-memory / no-op)
74+
Every part is loaded via dynamic import and skipped with a log line when its package is not installed, and each can be disabled via `options.services`.
8375

84-
Most core kernel services not provided by a real plugin are registered as a dev stub, so the kernel service map is populated and callers get correct return types instead of `undefined`. Each one declares what kind of fake it is (`__serviceInfo`, ADR-0076 D12) — consumers, the dispatcher included, gate on that:
76+
## Empty slots stay empty
8577

86-
| Class | Slots | Meaning |
87-
|:---|:---|:---|
88-
| `degraded` | `cache`, `queue`, `job`, `file-storage`, `search`, `realtime`, `i18n`, `workflow`, `metadata` | Really does the work, in memory only. Served normally over HTTP. |
89-
| `stub` | `data`, `auth`, plus `ui` (placeholder with no implementation) | Fabricates its answer. Reported as a stub in discovery, and every dispatcher-owned domain answers it exactly as it answers an empty slot. |
78+
This plugin registers **no service implementations of its own**. The earlier design filled every unoccupied kernel-service slot with a dev stub — fabricated answers such as allow-all permission checks and "sent" notifications that were never delivered. That design is retired (ADR-0115): a slot no real plugin fills stays empty, and consumers handle absence exactly as they already must in production.
9079

91-
**Never stubbed** — these slots stay empty on purpose, which is what production has when the real plugin isn't installed:
80+
To use a capability locally, install its real service — e.g. `@objectstack/service-analytics` for `/analytics` (it runs an InMemory strategy), `@objectstack/service-cache` / `service-queue` / `service-job` for cache/queue/job.
9281

93-
- `analytics` (#4000). Install `@objectstack/service-analytics` (it runs an InMemory strategy).
94-
- `security.permissions`, `security.rls`, `security.fieldMasker` (#4093). The former stubs answered "allowed" for every permission check, compiled no row-level filter, and returned rows unmasked — inverting the decisions they stood in for. ADR-0076 D12's rule is that a fallback may degrade features, **never security semantics**. Without `@objectstack/plugin-security` nothing enforces RBAC, RLS or field masking, and the boot log says so rather than a fake quietly saying yes.
82+
## Production guard
9583

96-
All services are **optional** — if a peer package isn't installed it is skipped, and for the slots above a stub takes its place.
84+
`init()` throws when `NODE_ENV === 'production'`: the assembly is built around a well-known default auth secret and a seeded dev admin. If you really mean it, set `OS_ALLOW_DEV_PLUGIN=1`.
9785

9886
## API Endpoints (when all services enabled)
9987

@@ -120,7 +108,7 @@ All services are **optional** — if a peer package isn't installed it is skippe
120108
| `authSecret` | `string` | dev default | JWT secret for auth sessions |
121109
| `authBaseUrl` | `string` | `http://localhost:{port}` | Auth callback URL |
122110
| `verbose` | `boolean` | `true` | Enable verbose logging |
123-
| `services` | `Record<string, boolean>` | all `true` | Enable/disable individual services |
111+
| `services` | `Record<string, boolean>` | all `true` | Enable/disable individual parts of the assembly |
124112
| `extraPlugins` | `Plugin[]` | `[]` | Additional plugins to load |
125113
| `stack` | `object` || Stack definition to load as project metadata |
126114

packages/plugins/plugin-dev/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "@objectstack/plugin-dev",
33
"version": "17.0.0-rc.0",
44
"license": "Apache-2.0",
5-
"description": "Development Mode Plugin for ObjectStack — auto-enables all services with in-memory implementations",
5+
"description": "Development Assembly Plugin for ObjectStack — wires the real platform stack for zero-config local development",
66
"main": "dist/index.js",
77
"types": "dist/index.d.ts",
88
"exports": {
@@ -27,6 +27,8 @@
2727
"@objectstack/rest": "workspace:^",
2828
"@objectstack/runtime": "workspace:^",
2929
"@objectstack/service-i18n": "workspace:^",
30+
"@objectstack/service-realtime": "workspace:^",
31+
"@objectstack/service-storage": "workspace:^",
3032
"@objectstack/setup": "workspace:^",
3133
"@objectstack/spec": "workspace:*",
3234
"@objectstack/types": "workspace:*"

0 commit comments

Comments
 (0)