diff --git a/.changeset/retire-placeholder-metadata-kinds.md b/.changeset/retire-placeholder-metadata-kinds.md new file mode 100644 index 0000000000..854751f599 --- /dev/null +++ b/.changeset/retire-placeholder-metadata-kinds.md @@ -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. diff --git a/content/docs/plugins/development.mdx b/content/docs/plugins/development.mdx index 373ce6c211..86fa279773 100644 --- a/content/docs/plugins/development.mdx +++ b/content/docs/plugins/development.mdx @@ -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, }], diff --git a/content/docs/references/api/metadata.mdx b/content/docs/references/api/metadata.mdx index ddba1f6c7f..6f4f80785e 100644 --- a/content/docs/references/api/metadata.mdx +++ b/content/docs/references/api/metadata.mdx @@ -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 | @@ -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` | ✅ | Metadata payload | | **namespace** | `string` | optional | Optional namespace | diff --git a/content/docs/references/kernel/metadata-plugin.mdx b/content/docs/references/kernel/metadata-plugin.mdx index 14ddc15375..3620d322fb 100644 --- a/content/docs/references/kernel/metadata-plugin.mdx +++ b/content/docs/references/kernel/metadata-plugin.mdx @@ -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 | @@ -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 | @@ -182,7 +182,6 @@ const result = MetadataBulkRegisterRequest.parse(data); * `object` * `field` -* `trigger` * `validation` * `hook` * `seed` @@ -198,9 +197,6 @@ const result = MetadataBulkRegisterRequest.parse(data); * `datasource` * `external_catalog` * `translation` -* `router` -* `function` -* `service` * `email_template` * `doc` * `book` diff --git a/content/docs/references/ui/component.mdx b/content/docs/references/ui/component.mdx index 71d6bf9f3c..7b5f0f053c 100644 --- a/content/docs/references/ui/component.mdx +++ b/content/docs/references/ui/component.mdx @@ -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 | diff --git a/content/docs/references/ui/view.mdx b/content/docs/references/ui/view.mdx index ab507dcefc..c78d8e2130 100644 --- a/content/docs/references/ui/view.mdx +++ b/content/docs/references/ui/view.mdx @@ -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. | --- diff --git a/docs/adr/0005-metadata-customization-overlay.md b/docs/adr/0005-metadata-customization-overlay.md index cbd2460893..c83d33a4d6 100644 --- a/docs/adr/0005-metadata-customization-overlay.md +++ b/docs/adr/0005-metadata-customization-overlay.md @@ -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. | @@ -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". diff --git a/docs/adr/0010-metadata-protection-model.md b/docs/adr/0010-metadata-protection-model.md index 0a1e923433..fdbc21d42d 100644 --- a/docs/adr/0010-metadata-protection-model.md +++ b/docs/adr/0010-metadata-protection-model.md @@ -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. diff --git a/docs/adr/0088-metadata-kind-admission-and-retirement.md b/docs/adr/0088-metadata-kind-admission-and-retirement.md new file mode 100644 index 0000000000..6865755408 --- /dev/null +++ b/docs/adr/0088-metadata-kind-admission-and-retirement.md @@ -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. diff --git a/examples/app-showcase/src/coverage.ts b/examples/app-showcase/src/coverage.ts index 428ab895a3..ef4a259bb3 100644 --- a/examples/app-showcase/src/coverage.ts +++ b/examples/app-showcase/src/coverage.ts @@ -61,12 +61,6 @@ export const KIND_COVERAGE: Record = { 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: [ @@ -102,26 +96,10 @@ export const KIND_COVERAGE: Record = { 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', diff --git a/packages/metadata-core/src/types.ts b/packages/metadata-core/src/types.ts index 04f20de294..ff32d7c5cf 100644 --- a/packages/metadata-core/src/types.ts +++ b/packages/metadata-core/src/types.ts @@ -19,7 +19,6 @@ import { z } from 'zod'; export const MetadataTypeSchema = z.enum([ 'object', 'field', - 'trigger', 'validation', 'hook', 'view', @@ -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', @@ -45,9 +46,6 @@ export const MetadataTypeSchema = z.enum([ 'datasource', 'cube', 'settings', - 'router', - 'function', - 'service', 'email_template', ]).describe('Canonical metadata type name'); diff --git a/packages/objectql/src/overlay-precedence.test.ts b/packages/objectql/src/overlay-precedence.test.ts index 3a8843da9b..8714eb5f54 100644 --- a/packages/objectql/src/overlay-precedence.test.ts +++ b/packages/objectql/src/overlay-precedence.test.ts @@ -163,12 +163,14 @@ describe('overlay whitelist enforcement (shared-DB invariant)', () => { // After PR-10d.7 introduced `allowRuntimeCreate`, "denied" now splits // into two cohorts: // - // 1. Types with `allowRuntimeCreate: true` (hook/trigger/validation) — + // 1. Types with `allowRuntimeCreate: true` (hook/validation) — // blocked only when overlaying an artifact-backed item. Brand-new // (artifact-free) names succeed. Tested separately below. // - // 2. Types with `allowRuntimeCreate: false` (router/function/service) — - // blocked for ANY write in project-kernel mode. + // 2. Types with `allowRuntimeCreate: false` — after ADR-0088 retired the + // router/function/service placeholder kinds, `agent` (platform-owned, + // ADR-0063) is the remaining member — blocked for ANY write in + // project-kernel mode. // // NOTE: `datasource` moved to cohort #1 with the ADR-0015 Addendum // (runtime-UI-creatable datasources). Brand-new runtime datasources @@ -181,19 +183,9 @@ describe('overlay whitelist enforcement (shared-DB invariant)', () => { describe('denied — must throw 403 (not_overridable or not_creatable)', () => { const deniedTypeWide: Array<{ type: string; reason: string; item: any }> = [ { - type: 'router', - reason: 'API routing must be deterministic; per-org divergence creates invisible conflicts', - item: { name: 'case_api', path: '/api/cases' }, - }, - { - type: 'function', - reason: 'serverless function definitions must be deployment-only, not per-org', - item: { name: 'process_payment', handler: 'index.ts' }, - }, - { - type: 'service', - reason: 'service definitions must be deployment-only, not per-org', - item: { name: 'notification_service' }, + type: 'agent', + reason: 'agents are platform-owned (ADR-0063); per-org agent forks are withdrawn', + item: { name: 'my_agent', label: 'My Agent' }, }, ]; @@ -275,16 +267,17 @@ describe('overlay whitelist enforcement (shared-DB invariant)', () => { // ── single-kernel deployments: gate disengaged ── describe('single-kernel mode (no environmentId) — gate bypassed', () => { - it('allows function overlay when environmentId is undefined (gate bypassed)', async () => { + it('allows agent overlay when environmentId is undefined (gate bypassed)', async () => { // No environmentId => not project-kernel mode => legacy "anything goes" // path used by control-plane bootstrap. ADR-0005 §"Whitelist". - // `function` is a definitively-denied type in project-kernel mode, - // so this case best demonstrates the bypass semantics. + // `agent` is a definitively-denied type in project-kernel mode + // (allowRuntimeCreate: false, ADR-0063), so this case best + // demonstrates the bypass semantics. const { protocol: localProto } = makeProtocol({ environmentId: undefined }); const result = await localProto.saveMetaItem({ - type: 'function', - name: 'my_fn', - item: { name: 'my_fn', handler: 'index.ts' }, + type: 'agent', + name: 'my_agent', + item: { name: 'my_agent', label: 'My Agent', role: 'assistant', instructions: 'Answer questions about test data.' }, }); expect(result.success).toBe(true); }); @@ -323,6 +316,8 @@ describe('overlay whitelist enforcement (shared-DB invariant)', () => { expect(allowedFromRegistry.has('profile')).toBe(true); // Execution/wiring-layer types must NOT be in the set. // Accepting them as overlays would corrupt runtime semantics. + // (trigger/router/function/service were retired outright by + // ADR-0088 — the asserts double as reintroduction guards.) expect(allowedFromRegistry.has('trigger')).toBe(false); expect(allowedFromRegistry.has('validation')).toBe(false); expect(allowedFromRegistry.has('hook')).toBe(false); diff --git a/packages/objectql/src/plugin.ts b/packages/objectql/src/plugin.ts index 672e4fce7f..66f9575878 100644 --- a/packages/objectql/src/plugin.ts +++ b/packages/objectql/src/plugin.ts @@ -1368,8 +1368,10 @@ export class ObjectQLPlugin implements Plugin { ctx.logger.info('Syncing metadata from external service into ObjectQL registry...'); // Metadata types to sync (ADR-0020: no `workflow` type — record state - // machines are a `state_machine` validation rule on the object) - const metadataTypes = ['object', 'view', 'app', 'flow', 'function', 'hook']; + // machines are a `state_machine` validation rule on the object; + // ADR-0088: no `function` kind — QL functions come from + // `defineStack({ functions })` / plugin contributions, never metadata rows) + const metadataTypes = ['object', 'view', 'app', 'flow', 'hook']; let totalLoaded = 0; for (const type of metadataTypes) { @@ -1379,19 +1381,6 @@ export class ObjectQLPlugin implements Plugin { const items = await metadataService.loadMany(type); if (items && items.length > 0) { - // Functions arrive as JSON-safe records ({name, handler}) - // where `handler` is a function reference or compiled code - // already attached by the metadata pipeline. Register them - // BEFORE binding hooks so string-named hook handlers can - // resolve. - if (type === 'function' && this.ql && typeof (this.ql as any).registerFunction === 'function') { - for (const item of items) { - if (item?.name && typeof item.handler === 'function') { - (this.ql as any).registerFunction(item.name, item.handler, 'metadata-service'); - } - } - } - items.forEach((item: any) => { // Determine key field (usually 'name' or 'id') const keyField = item.id ? 'id' : 'name'; diff --git a/packages/objectql/src/protocol-meta-types-rich.test.ts b/packages/objectql/src/protocol-meta-types-rich.test.ts index 33986fee33..dfa3aa1cea 100644 --- a/packages/objectql/src/protocol-meta-types-rich.test.ts +++ b/packages/objectql/src/protocol-meta-types-rich.test.ts @@ -30,13 +30,14 @@ describe('ObjectStackProtocolImplementation - getMetaTypes rich response', () => registry.registerItem('object', { name: 'sys_user', label: 'User' }, 'name'); registry.registerItem('view', { name: 'sys_user.grid', type: 'grid', object: 'sys_user' }, 'name'); registry.registerItem('app', { name: 'crm', label: 'CRM' }, 'name'); - // `function` is registry-default `allowOrgOverride: false` (system/wiring-layer) - // and is used by the "honours OS_METADATA_WRITABLE" test as a control type. registry.registerItem('flow', { name: 'crm.onboard', steps: [] }, 'name'); - // Register wiring-layer types so getMetaTypes() includes them in `entries` + // Register control types so getMetaTypes() includes them in `entries` // (getMetaTypes only returns types present in getRegisteredTypes()). - registry.registerItem('function', { name: 'process_payment' }, 'name'); - registry.registerItem('trigger', { name: 'on_insert' }, 'name'); + // `hook`/`validation`/`external_catalog` are registry-default + // `allowOrgOverride: false` — used by the OS_METADATA_WRITABLE tests. + registry.registerItem('hook', { name: 'audit_stamp' }, 'name'); + registry.registerItem('validation', { name: 'amount_positive' }, 'name'); + registry.registerItem('external_catalog', { name: 'warehouse_snapshot' }, 'name'); mockEngine = { registry, @@ -91,22 +92,23 @@ describe('ObjectStackProtocolImplementation - getMetaTypes rich response', () => }); it('honours OS_METADATA_WRITABLE to elevate allowOrgOverride', async () => { - // Use `function` and `service` — both are registry-default - // `allowOrgOverride: false` (wiring-layer types that must stay code-only). - process.env.OS_METADATA_WRITABLE = 'function,service'; + // Use `hook` and `validation` — both are registry-default + // `allowOrgOverride: false` (ADR-0088 retired the former code-only + // placeholder kinds this test used to lean on). + process.env.OS_METADATA_WRITABLE = 'hook,validation'; ObjectStackProtocolImplementation.resetEnvWritableCache(); const result: any = await protocol.getMetaTypes(); - const functionEntry = result.entries.find((e: any) => e.type === 'function'); - expect(functionEntry.allowOrgOverride).toBe(true); - expect(functionEntry.overrideSource).toBe('env'); + const hookEntry = result.entries.find((e: any) => e.type === 'hook'); + expect(hookEntry.allowOrgOverride).toBe(true); + expect(hookEntry.overrideSource).toBe('env'); // Types not listed AND not writable in the registry default retain - // `allowOrgOverride: false`. `trigger` is one such type — it is - // execution-pinned and not org-overridable. - const triggerEntry = result.entries.find((e: any) => e.type === 'trigger'); - expect(triggerEntry.allowOrgOverride).toBe(false); - expect(triggerEntry.overrideSource).toBe('registry'); + // `allowOrgOverride: false`. `external_catalog` is one such type — + // runtime-created by the Sync wizard, never org-overridable. + const catalogEntry = result.entries.find((e: any) => e.type === 'external_catalog'); + expect(catalogEntry.allowOrgOverride).toBe(false); + expect(catalogEntry.overrideSource).toBe('registry'); }); it('saveMetaItem honours the env-elevated allow list', async () => { @@ -114,27 +116,27 @@ describe('ObjectStackProtocolImplementation - getMetaTypes rich response', () => const scoped = new ObjectStackProtocolImplementation(mockEngine, undefined, undefined, 'env_alpha'); mockEngine.findOne.mockResolvedValue(null); - // Without env var: `function` writes blocked. Since the test - // registry has no artifact at this name and `function` has - // `allowRuntimeCreate: false`, the protocol returns - // `not_creatable` (the precise reason); for artifact-backed names - // the code would be `not_overridable`. Both indicate the gate + // Without env var: `agent` writes blocked — the one remaining + // `allowRuntimeCreate: false` kind (platform-owned, ADR-0063). Since + // the test registry has no artifact at this name, the protocol + // returns `not_creatable` (the precise reason); for artifact-backed + // names the code would be `not_overridable`. Both indicate the gate // fired with the same 403 status. delete process.env.OS_METADATA_WRITABLE; ObjectStackProtocolImplementation.resetEnvWritableCache(); resetEnvWritableMetadataTypes(); await expect( - scoped.saveMetaItem({ type: 'function', name: 'my_fn', item: { name: 'my_fn' } }) + scoped.saveMetaItem({ type: 'agent', name: 'my_agent', item: { name: 'my_agent' } }) ).rejects.toThrow(/not_(overridable|creatable)/); - // With env var: `function` writes allowed. - process.env.OS_METADATA_WRITABLE = 'function'; + // With env var: `agent` writes allowed. + process.env.OS_METADATA_WRITABLE = 'agent'; ObjectStackProtocolImplementation.resetEnvWritableCache(); resetEnvWritableMetadataTypes(); // Should no longer throw "not_overridable" / "not_creatable". (May still hit // unrelated persistence errors from the mock engine — we only assert the gate.) try { - await scoped.saveMetaItem({ type: 'function', name: 'my_fn', item: { name: 'my_fn' } }); + await scoped.saveMetaItem({ type: 'agent', name: 'my_agent', item: { name: 'my_agent' } }); } catch (err: any) { expect(err.code).not.toBe('not_overridable'); expect(err.code).not.toBe('not_creatable'); diff --git a/packages/objectql/src/protocol-meta.test.ts b/packages/objectql/src/protocol-meta.test.ts index 243d81296d..8047052805 100644 --- a/packages/objectql/src/protocol-meta.test.ts +++ b/packages/objectql/src/protocol-meta.test.ts @@ -1255,13 +1255,15 @@ describe('ObjectStackProtocolImplementation - Metadata Persistence', () => { expect(validationResult.success).toBe(true); }); - it('rejects brand-new function with not_creatable (allowRuntimeCreate:false)', async () => { - // `function` has BOTH flags false → no runtime authoring allowed. + it('rejects brand-new agent with not_creatable (allowRuntimeCreate:false)', async () => { + // `agent` has BOTH flags false (platform-owned, ADR-0063) → no + // runtime authoring allowed. (ADR-0088 retired the former + // `function` placeholder kind this test used to lean on.) await expect( scoped.saveMetaItem({ - type: 'function', - name: 'my_fn', - item: { name: 'my_fn', handler: 'index.ts' }, + type: 'agent', + name: 'my_agent', + item: { name: 'my_agent', label: 'My Agent' }, organizationId: 'org_alpha', }), ).rejects.toMatchObject({ diff --git a/packages/objectql/src/sys-metadata-repository.test.ts b/packages/objectql/src/sys-metadata-repository.test.ts index f918b54d68..36d12ad481 100644 --- a/packages/objectql/src/sys-metadata-repository.test.ts +++ b/packages/objectql/src/sys-metadata-repository.test.ts @@ -317,13 +317,15 @@ describe('SysMetadataRepository', () => { ).rejects.toMatchObject({ code: 'not_overridable', status: 403 }); }); - it('put refuses statically-registered type with allowRuntimeCreate:false (function)', async () => { - // `function` IS in the static registry with both flags false. + it('put refuses statically-registered type with allowRuntimeCreate:false (agent)', async () => { + // `agent` IS in the static registry with both flags false + // (platform-owned, ADR-0063; ADR-0088 retired the former `function` + // placeholder this test used to lean on). // The new "unknown type ⇒ permissive" branch must NOT relax it. await expect( repo.put( - { org: 'org_alpha', type: 'function', name: 'my_fn' }, - { name: 'my_fn', handler: 'index.ts' }, + { org: 'org_alpha', type: 'agent', name: 'my_agent' }, + { name: 'my_agent', label: 'My Agent' }, { parentVersion: null, actor: 'studio', intent: 'runtime-only' }, ), ).rejects.toMatchObject({ code: 'not_creatable', status: 403 }); diff --git a/packages/spec/src/api/metadata.test.ts b/packages/spec/src/api/metadata.test.ts index 7947b2539c..853b57320b 100644 --- a/packages/spec/src/api/metadata.test.ts +++ b/packages/spec/src/api/metadata.test.ts @@ -237,7 +237,7 @@ describe('MetadataRegisterRequestSchema', () => { }); it('should accept all valid metadata types', () => { - const validTypes = ['object', 'field', 'trigger', 'view', 'page', 'dashboard', 'app', 'flow', 'agent']; + const validTypes = ['object', 'field', 'view', 'page', 'dashboard', 'app', 'flow', 'agent']; // ADR-0088: 'trigger' retired for (const type of validTypes) { const result = MetadataRegisterRequestSchema.parse({ type, name: 'test_item', data: {} }); expect(result.type).toBe(type); @@ -998,7 +998,7 @@ describe('Cross-Framework Metadata API Contracts', () => { it('returns all registered types', () => { const response = MetadataTypesResponseSchema.parse({ success: true, - data: ['object', 'field', 'trigger', 'view', 'page', 'dashboard', 'app', 'flow', 'agent'], + data: ['object', 'field', 'view', 'page', 'dashboard', 'app', 'flow', 'agent'], }); expect(response.data).toContain('object'); expect(response.data).toContain('agent'); diff --git a/packages/spec/src/kernel/metadata-plugin.test.ts b/packages/spec/src/kernel/metadata-plugin.test.ts index 9dd4196f8d..5e3e6633b1 100644 --- a/packages/spec/src/kernel/metadata-plugin.test.ts +++ b/packages/spec/src/kernel/metadata-plugin.test.ts @@ -18,10 +18,11 @@ describe('MetadataPluginProtocol', () => { describe('MetadataTypeSchema', () => { it('should accept all built-in metadata types', () => { const types = [ - 'object', 'field', 'trigger', 'validation', 'hook', 'seed', + 'object', 'field', 'validation', 'hook', 'seed', 'view', 'page', 'dashboard', 'app', 'action', 'report', 'flow', // ADR-0020: `workflow` retired as a metadata type - 'datasource', 'translation', 'router', 'function', 'service', + // ADR-0088: `trigger`/`router`/`function`/`service` retired as kinds + 'datasource', 'external_catalog', 'translation', 'permission', 'profile', 'role', 'agent', 'tool', 'skill', ]; diff --git a/packages/spec/src/kernel/metadata-plugin.zod.ts b/packages/spec/src/kernel/metadata-plugin.zod.ts index cd0461286b..35d24433b7 100644 --- a/packages/spec/src/kernel/metadata-plugin.zod.ts +++ b/packages/spec/src/kernel/metadata-plugin.zod.ts @@ -73,7 +73,9 @@ export const MetadataTypeSchema = lazySchema(() => z.enum([ // Data Protocol 'object', // Business entity definition (ObjectSchema) 'field', // Standalone field definition (FieldSchema) - 'trigger', // Data-layer event triggers (TriggerSchema) + // ADR-0088: there is no `trigger` metadata type — sync data-layer logic is a + // `hook` (24 lifecycle events); async automation is a `record_change` flow. + // (The `triggers` capability token in `requires:` is a different namespace.) 'validation', // Validation rules (ValidationSchema) 'hook', // Data hooks (HookSchema) 'seed', // Seed/fixture data — runtime-draftable; publishing applies it (SeedSchema) @@ -98,11 +100,12 @@ export const MetadataTypeSchema = lazySchema(() => z.enum([ // System Protocol 'datasource', // Data connections (DatasourceSchema) - 'external_catalog', // Cached remote schema snapshot for federated datasources (ADR-0015) + 'external_catalog', // Cached remote schema snapshot for federated datasources (ADR-0015) — RUNTIME-CREATED by the Sync wizard (ADR-0062/0088); packages never ship one 'translation', // i18n resources (TranslationSchema) - 'router', // API routes - 'function', // Serverless functions - 'service', // Service definitions + // ADR-0088: `router`/`function`/`service` are NOT metadata kinds — they are + // code contributions: plugin `contributes.routes` + declarative `apis:` + // (router), `defineStack({ functions })` + `contributes.functions` + // (function), and the plugin/service registry itself (service). 'email_template', // Outbound email templates (EmailTemplateSchema) 'doc', // Package documentation — flat Markdown items (DocSchema, ADR-0046) 'book', // Documentation navigation spine (BookSchema, ADR-0046 §6) @@ -595,7 +598,6 @@ export const DEFAULT_METADATA_TYPE_REGISTRY: MetadataTypeRegistryEntry[] = [ // are not modifiable per-org beyond layout/label; custom objects are full). { type: 'object', label: 'Object', filePatterns: ['**/*.object.ts', '**/*.object.yml', '**/*.object.json'], supportsOverlay: false, allowOrgOverride: false, allowRuntimeCreate: true, supportsVersioning: true, executionPinned: false, loadOrder: 10, domain: 'data' }, { type: 'field', label: 'Field', filePatterns: ['**/*.field.ts', '**/*.field.yml'], supportsOverlay: false, allowOrgOverride: false, allowRuntimeCreate: true, supportsVersioning: false, executionPinned: false, loadOrder: 20, domain: 'data' }, - { type: 'trigger', label: 'Trigger', filePatterns: ['**/*.trigger.ts', '**/*.trigger.yml'], supportsOverlay: false, allowOrgOverride: false, allowRuntimeCreate: true, supportsVersioning: false, executionPinned: false, loadOrder: 30, domain: 'data' }, { type: 'validation', label: 'Validation Rule', filePatterns: ['**/*.validation.ts', '**/*.validation.yml'], supportsOverlay: false, allowOrgOverride: false, allowRuntimeCreate: true, supportsVersioning: false, executionPinned: false, loadOrder: 30, domain: 'data' }, { type: 'hook', label: 'Hook', filePatterns: ['**/*.hook.ts', '**/*.hook.yml'], supportsOverlay: false, allowOrgOverride: false, allowRuntimeCreate: true, supportsVersioning: false, executionPinned: false, loadOrder: 30, domain: 'data' }, // `seed`: fixture / initialization data (SeedSchema = object + records + mode + @@ -655,11 +657,10 @@ export const DEFAULT_METADATA_TYPE_REGISTRY: MetadataTypeRegistryEntry[] = [ // framework from advertising a button whose backend it doesn't ship: // the button is emitted iff the plugin that serves it is installed. }, + // RUNTIME-CREATED (ADR-0062/0088): produced by the datasource Sync wizard — + // a derived snapshot; packages never ship one (it would be stale on arrival). { type: 'external_catalog', label: 'External Catalog', filePatterns: ['**/*.external-catalog.ts', '**/*.external-catalog.yml', '**/*.external-catalog.json'], supportsOverlay: false, allowOrgOverride: false, allowRuntimeCreate: true, supportsVersioning: false, executionPinned: false, loadOrder: 6, domain: 'system' }, { type: 'translation', label: 'Translation', filePatterns: ['**/*.translation.ts', '**/*.translation.yml', '**/*.translation.json'], supportsOverlay: true, allowOrgOverride: true, allowRuntimeCreate: true, supportsVersioning: false, executionPinned: false, loadOrder: 90, domain: 'system' }, - { type: 'router', label: 'Router', filePatterns: ['**/*.router.ts'], supportsOverlay: false, allowOrgOverride: false, allowRuntimeCreate: false, supportsVersioning: false, executionPinned: false, loadOrder: 40, domain: 'system' }, - { type: 'function', label: 'Function', filePatterns: ['**/*.function.ts'], supportsOverlay: false, allowOrgOverride: false, allowRuntimeCreate: false, supportsVersioning: false, executionPinned: false, loadOrder: 40, domain: 'system' }, - { type: 'service', label: 'Service', filePatterns: ['**/*.service.ts'], supportsOverlay: false, allowOrgOverride: false, allowRuntimeCreate: false, supportsVersioning: false, executionPinned: false, loadOrder: 40, domain: 'system' }, { type: 'email_template', label: 'Email Template', filePatterns: ['**/*.email-template.ts', '**/*.email-template.yml', '**/*.email-template.json'], supportsOverlay: true, allowOrgOverride: true, allowRuntimeCreate: true, supportsVersioning: false, executionPinned: false, loadOrder: 85, domain: 'system' }, // ADR-0046: package documentation. Inert data — no runtime behavior, no // overlay (a manual is the publisher's voice; tenants don't patch it). diff --git a/packages/spec/src/kernel/metadata-type-schemas.ts b/packages/spec/src/kernel/metadata-type-schemas.ts index 9fbf0f13fd..6cd1c47ff6 100644 --- a/packages/spec/src/kernel/metadata-type-schemas.ts +++ b/packages/spec/src/kernel/metadata-type-schemas.ts @@ -14,10 +14,9 @@ * writable metadata type now requires **zero** Studio-side code. * * The map intentionally only contains types that meaningfully round-trip - * through the runtime metadata API. Code-only types whose entries cannot - * be created via REST (`function`, `service`, `router`) are excluded — - * `DEFAULT_METADATA_TYPE_REGISTRY` already marks them `allowRuntimeCreate: - * false`, so the engine never tries to render a form for them. + * through the runtime metadata API. (The former code-only placeholder kinds + * `function`/`service`/`router` — and `trigger` — were retired from the + * registry entirely by ADR-0088.) * * Profile shares `PermissionSetSchema` (a profile is a permission set * with `isProfile: true`); `validation` exposes the discriminated union @@ -73,8 +72,6 @@ const BUILTIN_METADATA_TYPE_SCHEMAS: Partial> = hook: HookSchema, validation: ValidationRuleSchema, seed: SeedSchema, // fixture/init data; runtime-draftable, applied on publish - // `trigger` — no standalone Zod schema yet; falls back to raw-JSON - // editor until the data-trigger spec lands. // UI Protocol view: ViewSchema, @@ -99,7 +96,6 @@ const BUILTIN_METADATA_TYPE_SCHEMAS: Partial> = email_template: EmailTemplateDefinitionSchema, doc: DocSchema, // ADR-0046: flat Markdown package documentation book: BookSchema as unknown as z.ZodType, // ADR-0046 §6: documentation navigation spine - // `router` / `function` / `service` are code-only (allowRuntimeCreate: false). // Security Protocol permission: PermissionSetSchema, @@ -119,8 +115,8 @@ const EXTRA_METADATA_TYPE_SCHEMAS = new Map(); * Look up the canonical Zod schema for a metadata type. * * Returns the user-registered override if any, otherwise the built-in - * schema. Returns `undefined` for types with no schema (e.g. `trigger`, - * `function`, `service`, `router`). + * schema. Returns `undefined` for types with no schema (e.g. `external_catalog`, + * which is runtime-created by the datasource Sync wizard — ADR-0062/0088). */ export function getMetadataTypeSchema(type: string): z.ZodType | undefined { return EXTRA_METADATA_TYPE_SCHEMAS.get(type) ?? BUILTIN_METADATA_TYPE_SCHEMAS[type as MetadataType]; diff --git a/packages/spec/src/kernel/plugin-structure.zod.ts b/packages/spec/src/kernel/plugin-structure.zod.ts index 33dbe377ad..bf510d4b6a 100644 --- a/packages/spec/src/kernel/plugin-structure.zod.ts +++ b/packages/spec/src/kernel/plugin-structure.zod.ts @@ -16,14 +16,16 @@ import { lazySchema } from '../shared/lazy-schema'; const SNAKE_CASE_REGEX = /^[a-z][a-z0-9_]*$/; // REGEX: Standard File Suffixes -const OPS_FILE_SUFFIX_REGEX = /\.(object|field|trigger|function|view|page|dashboard|flow|app|router|service)\.ts$/; +// ADR-0088: trigger/function/router/service retired from the kind registry — +// their file suffixes are no longer valid OPS metadata names. +const OPS_FILE_SUFFIX_REGEX = /\.(object|field|view|page|dashboard|flow|app)\.ts$/; /** * Validates a single file path against OPS Naming Conventions. * * @example Valid Paths * - "src/crm/lead.object.ts" - * - "src/finance/invoice_payment.trigger.ts" + * - "src/finance/invoice_payment.flow.ts" * - "src/index.ts" * * @example Invalid Paths