Skip to content

Commit da807f7

Browse files
authored
ADR-0088: metadata-kind admission test — retire trigger/router/function/service (#2628)
1 parent 2bb193d commit da807f7

21 files changed

Lines changed: 167 additions & 138 deletions
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/objectql': minor
4+
'@objectstack/metadata-core': minor
5+
---
6+
7+
feat(spec)!: retire the placeholder metadata kinds `trigger`, `router`, `function`, `service` (ADR-0088).
8+
9+
The registry is the contract authors — human and AI — read to learn what can be authored, and these four kinds had no authoring surface, no loader, no schema, and no (or a dead) consumer. `MetadataTypeSchema` + `DEFAULT_METADATA_TYPE_REGISTRY` shrink 30 → 26; `OPS_FILE_SUFFIX_REGEX` drops the four suffixes; the dormant objectql load path that registered QL functions from `type: 'function'` metadata items is removed (`defineStack({ functions })` / plugin `contributes.functions` remain the delivered forms); the metadata-core lockstep enum follows. `external_catalog` stays and is now annotated RUNTIME-CREATED (ADR-0062): its lack of an authoring surface is correct design. The delivered replacements: `hook` / `record_change` flows (trigger), plugin `contributes.routes` + declarative `apis:` (router), `defineStack({ functions })` (function), the plugin/service registry (service). Persisted `sys_metadata` rows are unaffected — no production read path re-parses stored `type` values through the enum.

content/docs/plugins/development.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ export const manifest = defineStudioPlugin({
393393
key: 'automation',
394394
label: 'Automation',
395395
icon: 'workflow',
396-
metadataTypes: ['flow', 'trigger', 'workflow'],
396+
metadataTypes: ['flow', 'job'], // ADR-0020/0088: 'workflow' and 'trigger' are retired kinds
397397
order: 30,
398398
}],
399399

content/docs/references/api/metadata.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ Metadata query with filtering, sorting, and pagination
314314

315315
| Property | Type | Required | Description |
316316
| :--- | :--- | :--- | :--- |
317-
| **types** | `Enum<'object' \| 'field' \| 'trigger' \| 'validation' \| 'hook' \| 'seed' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| 'datasource' \| 'external_catalog' \| 'translation' \| 'router' \| 'function' \| 'service' \| 'email_template' \| 'doc' \| 'book' \| 'permission' \| 'profile' \| 'role' \| 'agent' \| 'tool' \| 'skill'>[]` | optional | Filter by metadata types |
317+
| **types** | `Enum<'object' \| 'field' \| 'validation' \| 'hook' \| 'seed' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| 'datasource' \| 'external_catalog' \| 'translation' \| 'email_template' \| 'doc' \| 'book' \| 'permission' \| 'profile' \| 'role' \| 'agent' \| 'tool' \| 'skill'>[]` | optional | Filter by metadata types |
318318
| **namespaces** | `string[]` | optional | Filter by namespaces |
319319
| **packageId** | `string` | optional | Filter by owning package |
320320
| **search** | `string` | optional | Full-text search query |
@@ -349,7 +349,7 @@ Metadata query with filtering, sorting, and pagination
349349

350350
| Property | Type | Required | Description |
351351
| :--- | :--- | :--- | :--- |
352-
| **type** | `Enum<'object' \| 'field' \| 'trigger' \| 'validation' \| 'hook' \| 'seed' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| 'datasource' \| 'external_catalog' \| 'translation' \| 'router' \| 'function' \| 'service' \| 'email_template' \| 'doc' \| 'book' \| 'permission' \| 'profile' \| 'role' \| 'agent' \| 'tool' \| 'skill'>` || Metadata type |
352+
| **type** | `Enum<'object' \| 'field' \| 'validation' \| 'hook' \| 'seed' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| 'datasource' \| 'external_catalog' \| 'translation' \| 'email_template' \| 'doc' \| 'book' \| 'permission' \| 'profile' \| 'role' \| 'agent' \| 'tool' \| 'skill'>` || Metadata type |
353353
| **name** | `string` || Item name (snake_case) |
354354
| **data** | `Record<string, any>` || Metadata payload |
355355
| **namespace** | `string` | optional | Optional namespace |

content/docs/references/kernel/metadata-plugin.mdx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ const result = MetadataBulkRegisterRequest.parse(data);
130130
| Property | Type | Required | Description |
131131
| :--- | :--- | :--- | :--- |
132132
| **event** | `Enum<'metadata.registered' \| 'metadata.updated' \| 'metadata.unregistered' \| 'metadata.validated' \| 'metadata.deployed' \| 'metadata.overlay.applied' \| 'metadata.overlay.removed' \| 'metadata.imported' \| 'metadata.exported'>` || Event type |
133-
| **metadataType** | `Enum<'object' \| 'field' \| 'trigger' \| 'validation' \| 'hook' \| 'seed' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| 'datasource' \| 'external_catalog' \| 'translation' \| 'router' \| 'function' \| 'service' \| 'email_template' \| 'doc' \| 'book' \| 'permission' \| 'profile' \| 'role' \| 'agent' \| 'tool' \| 'skill'>` || Metadata type |
133+
| **metadataType** | `Enum<'object' \| 'field' \| 'validation' \| 'hook' \| 'seed' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| 'datasource' \| 'external_catalog' \| 'translation' \| 'email_template' \| 'doc' \| 'book' \| 'permission' \| 'profile' \| 'role' \| 'agent' \| 'tool' \| 'skill'>` || Metadata type |
134134
| **name** | `string` || Metadata item name |
135135
| **namespace** | `string` | optional | Namespace |
136136
| **packageId** | `string` | optional | Owning package ID |
@@ -147,7 +147,7 @@ const result = MetadataBulkRegisterRequest.parse(data);
147147

148148
| Property | Type | Required | Description |
149149
| :--- | :--- | :--- | :--- |
150-
| **types** | `Enum<'object' \| 'field' \| 'trigger' \| 'validation' \| 'hook' \| 'seed' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| 'datasource' \| 'external_catalog' \| 'translation' \| 'router' \| 'function' \| 'service' \| 'email_template' \| 'doc' \| 'book' \| 'permission' \| 'profile' \| 'role' \| 'agent' \| 'tool' \| 'skill'>[]` | optional | Filter by metadata types |
150+
| **types** | `Enum<'object' \| 'field' \| 'validation' \| 'hook' \| 'seed' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| 'datasource' \| 'external_catalog' \| 'translation' \| 'email_template' \| 'doc' \| 'book' \| 'permission' \| 'profile' \| 'role' \| 'agent' \| 'tool' \| 'skill'>[]` | optional | Filter by metadata types |
151151
| **namespaces** | `string[]` | optional | Filter by namespaces |
152152
| **packageId** | `string` | optional | Filter by owning package |
153153
| **search** | `string` | optional | Full-text search query |
@@ -182,7 +182,6 @@ const result = MetadataBulkRegisterRequest.parse(data);
182182

183183
* `object`
184184
* `field`
185-
* `trigger`
186185
* `validation`
187186
* `hook`
188187
* `seed`
@@ -198,9 +197,6 @@ const result = MetadataBulkRegisterRequest.parse(data);
198197
* `datasource`
199198
* `external_catalog`
200199
* `translation`
201-
* `router`
202-
* `function`
203-
* `service`
204200
* `email_template`
205201
* `doc`
206202
* `book`

content/docs/references/ui/component.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ Type: `string`
346346
| :--- | :--- | :--- | :--- |
347347
| **objectName** | `string` || Related object name (e.g., "task", "opportunity") |
348348
| **relationshipField** | `string` || Field on related object that points to this record (e.g., "account_id") |
349-
| **columns** | `string[]` | | Fields to display in the related list |
349+
| **columns** | `string[]` | optional | Fields to display in the related list. Optional: when omitted, columns derive from the related object's highlightFields / default list columns (a related list is just another surface that lists that object). Override chain: child highlightFields → field-level relatedListColumns → this inline list. |
350350
| **sort** | `string \| Object[]` | optional | Sort order for related records |
351351
| **limit** | `integer` || Number of records to display initially |
352352
| **filter** | `Object[]` | optional | Additional filter criteria for related records |

content/docs/references/ui/view.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,8 @@ List chart view configuration
236236
| **view** | `string` | optional | Name of the form view to use for details (e.g. "summary_view", "edit_form") |
237237
| **preventNavigation** | `boolean` || Disable standard navigation entirely |
238238
| **openNewTab** | `boolean` || Force open in new tab (applies to page mode) |
239-
| **width** | `string \| number` | optional | Width of the drawer/modal (e.g. "600px", "50%") |
239+
| **size** | `Enum<'auto' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'>` || [#2578] Overlay size bucket for drawer/modal detail: 'auto' (default — renderer derives from field count + viewport; AI writes nothing) or a coarse override sm/md/lg/xl/full. Prefer this over the pixel `width`; page mode ignores it. |
240+
| **width** | `string \| number` | optional | [DEPRECATED → size] Pixel/percent width of the drawer/modal (e.g. "600px"). A pixel width cannot be chosen at authoring time without knowing the client viewport — use the `size` bucket. |
240241

241242

242243
---

docs/adr/0005-metadata-customization-overlay.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ derives its set from that flag — there is no parallel allowlist
5353
| Domain | Type | Per-org override? | Why |
5454
|:---|:---|:---:|:---|
5555
| data | `object`, `field` || A per-org overlay would diverge the table schema; shared DB cannot honour that. |
56-
| data | `trigger`, `validation`, `hook` || DB-side contracts. Per-org variants must ship as a separate package, not an overlay. |
56+
| data | `validation`, `hook` || DB-side contracts. Per-org variants must ship as a separate package, not an overlay. (`trigger` retired as a kind — ADR-0088.) |
5757
| automation | `flow`, `workflow`, `approval` || Carry execution side-effects (events, jobs, audit). Per-org variants are a deployment, not an overlay. |
5858
| security | `permission`, `profile`, `role` || Authorization correctness; overlays would create silent privilege drift. |
59-
| system | `datasource`, `router`, `function`, `service` || Wiring level; changes require code paths, not metadata. |
59+
| system | `datasource` || Wiring level; changes require code paths, not metadata. (`router`/`function`/`service` retired as kinds — ADR-0088; they are code contributions.) |
6060
| ai | `agent`, `tool`, `skill` || Behavioural contracts with model providers; treat like flows. |
6161
| **ui** | **`view`, `dashboard`, `report`** || **Pure presentation. Safe per-org override.** |
6262
| ui | `page`, `app`, `action` || Conservative default — these bind to routes and side-effects. Promote individually if a concrete need appears. |
@@ -534,7 +534,7 @@ entries derived from the prior baseline.
534534

535535
The original ADR gates writes purely on `allowOrgOverride` (type-level).
536536
This proved too coarse for one category of types: those that ship
537-
executable code (`hook`, `trigger`, `validation`) declare
537+
executable code (`hook`, `validation`) declare
538538
`allowOrgOverride: false` AND `allowRuntimeCreate: true`. The intent —
539539
documented in `metadata-plugin.zod.ts` — is "users may author brand-new
540540
items of this type, but artifact-shipped items remain immutable".

docs/adr/0010-metadata-protection-model.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,9 @@ The existing `allowOrgOverride` / `allowRuntimeCreate` flags on
120120
`MetadataTypeRegistryEntry` continue to express **type-wide** policy:
121121

122122
- `allowOrgOverride: false` → no overlay write to any item of this type
123-
is ever accepted from a tenant. (`datasource`, `router`, `function`,
124-
`service` keep this stance because they encode deployment topology.)
123+
is ever accepted from a tenant. (`datasource` keeps this stance because
124+
it encodes deployment topology; the former `router`/`function`/`service`
125+
kinds were retired outright — ADR-0088.)
125126
- `allowRuntimeCreate: false` → tenants cannot author brand-new items
126127
of this type at runtime.
127128

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# ADR-0088: Metadata-kind admission test; retire `trigger`, `router`, `function`, `service`
2+
3+
**Status**: Accepted (2026-07-05)
4+
**Deciders**: ObjectStack Protocol Architects
5+
**Builds on**: [ADR-0085](./0085-object-semantic-roles-over-surface-hint-blocks.md) (§2 authored-key admission test — this ADR is its kind-level analogue), [ADR-0018](./0018-unified-node-action-registry.md) / [ADR-0020](./0020-state-machine-converge-and-enforce.md) (prior kind retirements: `workflow`, `approval`), [ADR-0062](./0062-external-datasource-runtime.md) (`external_catalog` lifecycle)
6+
**Closes**: framework#2613
7+
8+
**Premise**: pre-launch, no back-compat debt — remove wrong entries outright rather than deprecate them.
9+
10+
---
11+
12+
## TL;DR
13+
14+
`DEFAULT_METADATA_TYPE_REGISTRY` is the contract AI and human authors read to learn *what can be authored*. Four of its kinds — `trigger`, `router`, `function`, `service` — have **no authoring surface, no loader, no schema, and no (or a dead) consumer**: every governance flag on their entries is already "no". They are Salesforce-inventory legacy slots, and keeping them violates the same principle that keeps rejected keys out of object schemas (ADR-0085 §2) and that retired `workflow`/`approval` (ADR-0019/0020): **never declare what the platform does not deliver** (Prime Directive #10, at the kind level).
15+
16+
This ADR states the admission test a kind must pass, retires the four kinds (30 → 26), and documents `external_catalog` as **runtime-created** — a kind whose *lack* of an authoring surface is correct design.
17+
18+
## The admission test
19+
20+
A metadata kind earns a registry entry only if ALL three hold:
21+
22+
1. **Independent lifecycle** — it is created, versioned, and invalidated as its own artifact (not merely a facet of another kind).
23+
2. **Declarative governability** — it can live in the metadata repository with meaningful versioning/overlay/audit semantics; at least one of `allowRuntimeCreate` / `supportsOverlay` / a file loader is genuinely true.
24+
3. **A real consumer** — runtime, Studio, or build reads artifacts of this kind and changes behavior.
25+
26+
A kind failing the test is removed, not annotated. If a future capability needs it, it returns with a full design (schema + loader + consumer + example), never as a placeholder — an empty kind actively misleads AI authors that enumerate the registry.
27+
28+
## Decisions
29+
30+
### 1. `trigger` — retired
31+
32+
"Data change → reaction" already has two delivered mechanisms with a clean seam: **`hook`** (synchronous, data-layer, in-transaction — 24 lifecycle events) and **`record_change` flows** (asynchronous, business-layer, observable/pausable — ADR-0018/0020). `trigger` had no stack collection, no `defineTrigger`, no FS loader consuming `**/*.trigger.ts`, no executor — and its enum comment referenced a `TriggerSchema` that never existed. Keeping the slot invites a third overlapping mechanism (the Apex-Trigger inheritance). *Authors: use `hook` for sync data-layer logic, a `record_change` flow for async automation.* (The `triggers` **capability token** in `requires:` — the FlowTrigger plugin family — is a different namespace and is unaffected.)
33+
34+
### 2. `router` / `function` / `service` — retired
35+
36+
Their real, consumed forms already live elsewhere, and all are **code contributions**, not declarative artifacts:
37+
38+
| Retired kind | Delivered form |
39+
|---|---|
40+
| `router` | plugin manifest `contributes.routes` (HttpDispatcher prefix routing) + app-authored declarative `apis:` (`ApiEndpoint`, executed by `handleApiEndpoint`) + imperative `http.server` mounts |
41+
| `function` | `defineStack({ functions })` code values (hook-binder & flow `script` body runners) + plugin `contributes.functions` (QL query functions) |
42+
| `service` | the plugin/service registry itself (`registerService`) |
43+
44+
Their registry entries had `allowRuntimeCreate: false`, no overlay, no loader — a kind whose every governance flag is "no" is declaring it does not belong in the registry. Studio visibility into registered routes/functions/services, if wanted, is **runtime introspection** (a read-only endpoint), not metadata. The dormant objectql load path that registered QL functions from `type: 'function'` metadata items (nothing ever produced them) is removed with this ADR; `defineStack({ functions })` remains the one way in.
45+
46+
### 3. `external_catalog` — kept; runtime-created by design
47+
48+
The cached remote-schema snapshot of a federated datasource (ADR-0062) has a real lifecycle (Sync refresh, mismatch policy) and a real consumer — it stays. But it is a **derived** artifact: a package shipping one would be stale on arrival, like shipping a build output as source. Its registry entry is annotated `runtime-created`; the absence of an authoring surface is the point, permanently.
49+
50+
## Consequences
51+
52+
- `MetadataTypeSchema` and `DEFAULT_METADATA_TYPE_REGISTRY` shrink 30 → 26; every remaining entry is authorable, loadable, and consumed.
53+
- `/api/v1/meta/types*` (dynamic over the registry) shrinks automatically. Persisted `sys_metadata` rows are unaffected — no production read path re-parses stored `type` values through the enum (verified; the metadata-core lockstep enum is edited in the same change).
54+
- `OPS_FILE_SUFFIX_REGEX` drops the four suffixes: `*.trigger.ts` / `*.router.ts` / `*.function.ts` / `*.service.ts` are no longer valid OPS metadata file names.
55+
- The showcase's registry-driven `KIND_COVERAGE` shrinks in lockstep (its coverage test enforces exact registry membership); the four waivers disappear and `external_catalog`'s waiver becomes a permanent, documented exclusion.
56+
- ADR-0005 / ADR-0010 prose tables no longer list the retired kinds.

examples/app-showcase/src/coverage.ts

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,6 @@ export const KIND_COVERAGE: Record<MetadataType, KindCoverage> = {
6161
notes:
6262
'FieldSchema is authored inline on objects (the stack DSL has no standalone `fields` collection); field-zoo exhausts every field type — see the variant-level test.',
6363
},
64-
trigger: {
65-
status: 'waived',
66-
reason:
67-
'No declarative authoring surface: no stack collection or defineTrigger helper. Record-change triggering is demonstrated behaviorally by the record-change flows (requires: ["triggers"]).',
68-
issue: ISSUE.noAuthoringSurface,
69-
},
7064
validation: {
7165
status: 'demonstrated',
7266
files: [
@@ -102,26 +96,10 @@ export const KIND_COVERAGE: Record<MetadataType, KindCoverage> = {
10296
external_catalog: {
10397
status: 'waived',
10498
reason:
105-
'Runtime-created via Setup → Datasources → Sync (ADR-0062); no declarative artifact an app package can ship. The showcase demos the federation flow that produces one.',
99+
'PERMANENT by design (ADR-0088): a runtime-created snapshot produced by Setup → Datasources → Sync (ADR-0062). A package shipping one would be stale on arrival; the showcase demos the federation flow that produces it.',
106100
issue: ISSUE.noAuthoringSurface,
107101
},
108102
translation: { status: 'demonstrated', files: ['src/system/translations/index.ts'] },
109-
router: {
110-
status: 'waived',
111-
reason:
112-
'Code-only (allowRuntimeCreate: false). The code-level equivalent is the imperative HTTP mount in src/system/server/recalc-endpoint.ts.',
113-
issue: ISSUE.noAuthoringSurface,
114-
},
115-
function: {
116-
status: 'waived',
117-
reason: 'Code-only (allowRuntimeCreate: false); no declarative authoring surface.',
118-
issue: ISSUE.noAuthoringSurface,
119-
},
120-
service: {
121-
status: 'waived',
122-
reason: 'Code-only (allowRuntimeCreate: false); no declarative authoring surface.',
123-
issue: ISSUE.noAuthoringSurface,
124-
},
125103
email_template: { status: 'demonstrated', files: ['src/system/emails/index.ts'] },
126104
doc: {
127105
status: 'demonstrated',

0 commit comments

Comments
 (0)