Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/retire-placeholder-metadata-kinds.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@objectstack/spec': minor
'@objectstack/objectql': minor
'@objectstack/metadata-core': minor
---

feat(spec)!: retire the placeholder metadata kinds `trigger`, `router`, `function`, `service` (ADR-0088).

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.
2 changes: 1 addition & 1 deletion content/docs/plugins/development.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ export const manifest = defineStudioPlugin({
key: 'automation',
label: 'Automation',
icon: 'workflow',
metadataTypes: ['flow', 'trigger', 'workflow'],
metadataTypes: ['flow', 'job'], // ADR-0020/0088: 'workflow' and 'trigger' are retired kinds
order: 30,
}],

Expand Down
4 changes: 2 additions & 2 deletions content/docs/references/api/metadata.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ Metadata query with filtering, sorting, and pagination

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **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 |
| **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 |
| **namespaces** | `string[]` | optional | Filter by namespaces |
| **packageId** | `string` | optional | Filter by owning package |
| **search** | `string` | optional | Full-text search query |
Expand Down Expand Up @@ -349,7 +349,7 @@ Metadata query with filtering, sorting, and pagination

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **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 |
| **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 |
| **name** | `string` | ✅ | Item name (snake_case) |
| **data** | `Record<string, any>` | ✅ | Metadata payload |
| **namespace** | `string` | optional | Optional namespace |
Expand Down
8 changes: 2 additions & 6 deletions content/docs/references/kernel/metadata-plugin.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ const result = MetadataBulkRegisterRequest.parse(data);
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **event** | `Enum<'metadata.registered' \| 'metadata.updated' \| 'metadata.unregistered' \| 'metadata.validated' \| 'metadata.deployed' \| 'metadata.overlay.applied' \| 'metadata.overlay.removed' \| 'metadata.imported' \| 'metadata.exported'>` | ✅ | Event type |
| **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 |
| **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 |
| **name** | `string` | ✅ | Metadata item name |
| **namespace** | `string` | optional | Namespace |
| **packageId** | `string` | optional | Owning package ID |
Expand All @@ -147,7 +147,7 @@ const result = MetadataBulkRegisterRequest.parse(data);

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **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 |
| **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 |
| **namespaces** | `string[]` | optional | Filter by namespaces |
| **packageId** | `string` | optional | Filter by owning package |
| **search** | `string` | optional | Full-text search query |
Expand Down Expand Up @@ -182,7 +182,6 @@ const result = MetadataBulkRegisterRequest.parse(data);

* `object`
* `field`
* `trigger`
* `validation`
* `hook`
* `seed`
Expand All @@ -198,9 +197,6 @@ const result = MetadataBulkRegisterRequest.parse(data);
* `datasource`
* `external_catalog`
* `translation`
* `router`
* `function`
* `service`
* `email_template`
* `doc`
* `book`
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/ui/component.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ Type: `string`
| :--- | :--- | :--- | :--- |
| **objectName** | `string` | ✅ | Related object name (e.g., "task", "opportunity") |
| **relationshipField** | `string` | ✅ | Field on related object that points to this record (e.g., "account_id") |
| **columns** | `string[]` | | Fields to display in the related list |
| **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. |
| **sort** | `string \| Object[]` | optional | Sort order for related records |
| **limit** | `integer` | ✅ | Number of records to display initially |
| **filter** | `Object[]` | optional | Additional filter criteria for related records |
Expand Down
3 changes: 2 additions & 1 deletion content/docs/references/ui/view.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,8 @@ List chart view configuration
| **view** | `string` | optional | Name of the form view to use for details (e.g. "summary_view", "edit_form") |
| **preventNavigation** | `boolean` | ✅ | Disable standard navigation entirely |
| **openNewTab** | `boolean` | ✅ | Force open in new tab (applies to page mode) |
| **width** | `string \| number` | optional | Width of the drawer/modal (e.g. "600px", "50%") |
| **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. |
| **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. |


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

The original ADR gates writes purely on `allowOrgOverride` (type-level).
This proved too coarse for one category of types: those that ship
executable code (`hook`, `trigger`, `validation`) declare
executable code (`hook`, `validation`) declare
`allowOrgOverride: false` AND `allowRuntimeCreate: true`. The intent —
documented in `metadata-plugin.zod.ts` — is "users may author brand-new
items of this type, but artifact-shipped items remain immutable".
Expand Down
5 changes: 3 additions & 2 deletions docs/adr/0010-metadata-protection-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,9 @@ The existing `allowOrgOverride` / `allowRuntimeCreate` flags on
`MetadataTypeRegistryEntry` continue to express **type-wide** policy:

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

Expand Down
56 changes: 56 additions & 0 deletions docs/adr/0088-metadata-kind-admission-and-retirement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# ADR-0088: Metadata-kind admission test; retire `trigger`, `router`, `function`, `service`

**Status**: Accepted (2026-07-05)
**Deciders**: ObjectStack Protocol Architects
**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)
**Closes**: framework#2613

**Premise**: pre-launch, no back-compat debt — remove wrong entries outright rather than deprecate them.

---

## TL;DR

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

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.

## The admission test

A metadata kind earns a registry entry only if ALL three hold:

1. **Independent lifecycle** — it is created, versioned, and invalidated as its own artifact (not merely a facet of another kind).
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.
3. **A real consumer** — runtime, Studio, or build reads artifacts of this kind and changes behavior.

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.

## Decisions

### 1. `trigger` — retired

"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.)

### 2. `router` / `function` / `service` — retired

Their real, consumed forms already live elsewhere, and all are **code contributions**, not declarative artifacts:

| Retired kind | Delivered form |
|---|---|
| `router` | plugin manifest `contributes.routes` (HttpDispatcher prefix routing) + app-authored declarative `apis:` (`ApiEndpoint`, executed by `handleApiEndpoint`) + imperative `http.server` mounts |
| `function` | `defineStack({ functions })` code values (hook-binder & flow `script` body runners) + plugin `contributes.functions` (QL query functions) |
| `service` | the plugin/service registry itself (`registerService`) |

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.

### 3. `external_catalog` — kept; runtime-created by design

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.

## Consequences

- `MetadataTypeSchema` and `DEFAULT_METADATA_TYPE_REGISTRY` shrink 30 → 26; every remaining entry is authorable, loadable, and consumed.
- `/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).
- `OPS_FILE_SUFFIX_REGEX` drops the four suffixes: `*.trigger.ts` / `*.router.ts` / `*.function.ts` / `*.service.ts` are no longer valid OPS metadata file names.
- 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.
- ADR-0005 / ADR-0010 prose tables no longer list the retired kinds.
24 changes: 1 addition & 23 deletions examples/app-showcase/src/coverage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,6 @@ export const KIND_COVERAGE: Record<MetadataType, KindCoverage> = {
notes:
'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.',
},
trigger: {
status: 'waived',
reason:
'No declarative authoring surface: no stack collection or defineTrigger helper. Record-change triggering is demonstrated behaviorally by the record-change flows (requires: ["triggers"]).',
issue: ISSUE.noAuthoringSurface,
},
validation: {
status: 'demonstrated',
files: [
Expand Down Expand Up @@ -102,26 +96,10 @@ export const KIND_COVERAGE: Record<MetadataType, KindCoverage> = {
external_catalog: {
status: 'waived',
reason:
'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.',
'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.',
issue: ISSUE.noAuthoringSurface,
},
translation: { status: 'demonstrated', files: ['src/system/translations/index.ts'] },
router: {
status: 'waived',
reason:
'Code-only (allowRuntimeCreate: false). The code-level equivalent is the imperative HTTP mount in src/system/server/recalc-endpoint.ts.',
issue: ISSUE.noAuthoringSurface,
},
function: {
status: 'waived',
reason: 'Code-only (allowRuntimeCreate: false); no declarative authoring surface.',
issue: ISSUE.noAuthoringSurface,
},
service: {
status: 'waived',
reason: 'Code-only (allowRuntimeCreate: false); no declarative authoring surface.',
issue: ISSUE.noAuthoringSurface,
},
email_template: { status: 'demonstrated', files: ['src/system/emails/index.ts'] },
doc: {
status: 'demonstrated',
Expand Down
6 changes: 2 additions & 4 deletions packages/metadata-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import { z } from 'zod';
export const MetadataTypeSchema = z.enum([
'object',
'field',
'trigger',
'validation',
'hook',
'view',
Expand All @@ -29,6 +28,8 @@ export const MetadataTypeSchema = z.enum([
'action',
'flow',
'workflow',
// ADR-0088: trigger/router/function/service retired (kept in lockstep
// with the spec kernel enum; see metadata-plugin.zod.ts).
// ADR-0019: `approval` is a flow node, not a metadata type.
'job',
'agent',
Expand All @@ -45,9 +46,6 @@ export const MetadataTypeSchema = z.enum([
'datasource',
'cube',
'settings',
'router',
'function',
'service',
'email_template',
]).describe('Canonical metadata type name');

Expand Down
Loading