diff --git a/.changeset/workflow-slot-retired.md b/.changeset/workflow-slot-retired.md new file mode 100644 index 0000000000..cc5b590639 --- /dev/null +++ b/.changeset/workflow-slot-retired.md @@ -0,0 +1,58 @@ +--- +"@objectstack/spec": major +"@objectstack/client": major +"@objectstack/metadata-protocol": minor +"@objectstack/runtime": minor +--- + +refactor(spec,client,metadata-protocol,runtime)!: retire the workflow service slot — declared end to end, implemented nowhere (#4451) + +The `workflow` slot was ADR-0078's silently-inert declaration at every layer at +once: a `CoreServiceName` nothing ever registered or resolved (ADR-0115 +Evidence 5 — "no code in this repository resolves either slot", verified across +both repositories), an `IWorkflowService` contract with zero implementations, a +`WorkflowProtocol` whose three methods no code ever provided, a discovery +`routes.workflow` field no builder could truthfully populate, and a +`/api/v1/workflow` advertisement for a path no host ever mounted (the +pre-#3586 `DEFAULT_DISPATCHER_ROUTES` already listed it among routes that +never existed). The capability it promised is live elsewhere and has been for +majors: record state machines are enforced by the `state_machine` validation +rule, approvals are first-class flow nodes on the approvals runtime +(ADR-0019), and record-triggered automation is lifecycle hooks + +`record_change` flows (`service-automation`). + +FROM → TO: + +- `CoreServiceName 'workflow'` / `ServiceRequirementDef.workflow` / + `CORE_SERVICE_PROVIDER['workflow']` → removed; there is no slot to fill. +- `IWorkflowService` (`@objectstack/spec/contracts`) → removed; no + implementation ever existed. Register nothing — use the mechanisms above. +- `WorkflowProtocol` + `GetWorkflowConfigRequest/Response`, + `WorkflowState`, `GetWorkflowStateRequest/Response`, + `WorkflowTransitionRequest/Response` (`@objectstack/spec/api`) → removed, + along with the seven published JSON schemas. Delete the import; nothing + ever answered these shapes. +- Discovery `routes.workflow` / `services.workflow` / `features.workflow` + (metadata-protocol + runtime builders) → absent. A reader keying on them + only ever saw `unavailable` / `false`; delete the read. +- `RouterConfig.mounts.workflow` → removed; there was never a surface to + mount at it. +- `RestApiRouteCategory 'workflow'` → removed; categorize automation-adjacent + routes as `'automation'`. +- `@objectstack/client` re-exports of the four workflow types → removed with + their source. (The `client.workflow.*` methods were already removed earlier + in the v17 cycle — this retires the types they returned.) +- Also removed: the stray `graphql` entry in `CORE_SERVICE_PROVIDER` and the + `graphql: { route: '/graphql' }` discovery entry — `graphql` was never a + `CoreServiceName`, and the dispatcher had already dropped `/graphql` as out + of the product plan (#2462 follow-on). + +The retirement kit: the `workflow-service-slot-retired` semantic migration +(major 17) carries this prescription into `spec-changes.json`, the generated +upgrade guide and the `spec_changes` MCP tool. These are TS/API surfaces and a +discovery response field — never stored in stack metadata — so there is no +load-path conversion and nothing for `os migrate meta` to rewrite; the +21 `authorable-surface.json` baseline lines and 7 `json-schema.manifest.json` +entries for the deleted schemas are dropped deliberately in the same change +(the plugin-runtime precedent: a prescription nobody can receive is noise — +nothing parses these shapes any more). diff --git a/content/docs/api/plugin-endpoints.mdx b/content/docs/api/plugin-endpoints.mdx index 4463d58791..d48504a35a 100644 --- a/content/docs/api/plugin-endpoints.mdx +++ b/content/docs/api/plugin-endpoints.mdx @@ -26,19 +26,21 @@ Authenticate with email and password (better-auth's email sign-in route, mounted The following endpoints become available when the corresponding plugin is installed and registered with the kernel. Use the discovery `services` map to check availability. -### Workflow (`/workflow`) — Plugin Required +### Workflow (`/workflow`) — removed in v17 -Not yet mounted. These routes are declared in the API protocol but the core dispatcher registers no `/workflow` handler and no bundled plugin provides a `workflow` service (only an in-memory dev stub), so they return **404** today. `discovery.services.workflow` reports `unavailable` in a standard install. +There is no workflow endpoint, and there is no `workflow` service slot. The +three routes documented here were declared in the API protocol and served by +nothing — no dispatcher handler, no plugin — so they 404'd for the whole life +of the declaration. The slot, the `WorkflowProtocol` methods behind it and the +discovery fields that reported it were all retired in v17 ([#4451](https://github.com/objectstack-ai/objectstack/issues/4451)). +Use the live mechanisms instead: an object validation rule of type +`state_machine` for lifecycle transitions, an `approval` flow node for human +approval pauses (ADR-0019), and lifecycle hooks / `record_change` flows for +record-triggered automation. -| Method | Endpoint | Description | -|:-------|:---------|:------------| -| GET | `/workflow/:object/config` | Get workflow configuration | -| GET | `/workflow/:object/:recordId/state` | Get record's workflow state | -| POST | `/workflow/:object/:recordId/transition` | Execute state transition | - -Approve/reject are **not** workflow routes (ADR-0019): approval is a flow node, and decisions are recorded on the approvals runtime via `POST /approvals/requests/:id/approve` and `POST /approvals/requests/:id/reject`. +Approve/reject were never workflow routes (ADR-0019): approval is a flow node, and decisions are recorded on the approvals runtime via `POST /approvals/requests/:id/approve` and `POST /approvals/requests/:id/reject`. ### Automation (`/automation`) — Plugin Required diff --git a/content/docs/kernel/services-checklist.mdx b/content/docs/kernel/services-checklist.mdx index 67f18463a2..763f88e664 100644 --- a/content/docs/kernel/services-checklist.mdx +++ b/content/docs/kernel/services-checklist.mdx @@ -24,12 +24,14 @@ The ObjectStack protocol defines **16 kernel services** registered via the `Core - ✅ Implemented — the 17 kernel-provided protocol methods (`DataProtocol` 9 + `MetadataProtocol` 8) - ⚠️ Framework — the slot is filled by the kernel's in-memory fallback: real reads and writes, no persistence (self-declares `degraded`, ADR-0076 D12) -- ❌ Plugin Required — the remaining 36 methods declared across the other per-domain +- ❌ Plugin Required — the remaining 33 methods declared across the other per-domain contracts (`analytics` 2, `automation` 1, `packages` 6, `views` 5, `permissions` 3, - `workflow` 3, `realtime` 6, `notification` 7, `i18n` 3). *Declared* is not *routed*: + `realtime` 6, `notification` 7, `i18n` 3). *Declared* is not *routed*: `packages` is answered kernel-side by the `/packages` dispatcher domain over the - ObjectQL registry, `i18n` has a kernel in-memory fallback, and `views` / `permissions` / - `workflow` have no implementation anywhere — the rest wait on whatever fills the slot + ObjectQL registry, `i18n` has a kernel in-memory fallback, and `views` / + `permissions` have no implementation anywhere — the rest wait on whatever fills + the slot. (`workflow`'s 3 methods were the fourth such group; the whole slot + retired in v17, [#4451](https://github.com/objectstack-ai/objectstack/issues/4451).) --- @@ -90,10 +92,12 @@ installable optional package, so they need no remedy (`NO_REMEDY_SLOTS` in the guard script). And `null` there does **not** always mean "nothing ships" — it means "no name belongs in an `Install X` sentence", which covers two cases: -- **Nothing provides the slot at all** — `search`, `workflow` (and the retired - `graphql`). Discovery says exactly that rather than naming a plausible - package: `No implementation ships for the '' slot — register a service - under it to enable`. +- **Nothing provides the slot at all** — `search`. Discovery says exactly that + rather than naming a plausible package: `No implementation ships for the + '' slot — register a service under it to enable`. (`workflow` and the + never-real `graphql` sat here too until both were retired outright in v17, + [#4451](https://github.com/objectstack-ai/objectstack/issues/4451) — a slot + nothing fills and nothing consumes is better removed than explained.) - **A provider exists but cannot be installed** — `ai`. `@objectstack/service-ai` registers the slot in `objectstack-ai/cloud` and is `private: true`. @@ -333,13 +337,18 @@ the domain answers **501** with that remedy spelled out, not a generic "install plugin". -### 6. workflow Service — 3 methods ❌ Nothing ships -`getWorkflowConfig`, `getWorkflowState`, `workflowTransition` -State machine transitions. No package registers the `workflow` slot -(`CORE_SERVICE_PROVIDER.workflow` is `null`). Approve/reject are not workflow -methods — per ADR-0019 they moved to the request-id-based approvals API under -`/api/v1/approvals` (`POST /requests/:id/{approve,reject,recall}`, served by -`@objectstack/plugin-approvals`). +### 6. workflow Service — retired in v17 +The slot, its `IWorkflowService` contract and the three `WorkflowProtocol` +methods (`getWorkflowConfig`, `getWorkflowState`, `workflowTransition`) were +removed in [#4451](https://github.com/objectstack-ai/objectstack/issues/4451): +nothing ever registered or resolved the slot (ADR-0115 Evidence 5), no method +ever had an implementation, and no host ever mounted `/api/v1/workflow`. The +three capabilities it named are live elsewhere — state-machine transitions are +an object validation rule of type `state_machine`, approvals are `approval` +flow nodes on the approvals runtime (ADR-0019 — decisions via +`POST /api/v1/approvals/requests/:id/{approve,reject,recall}`, served by +`@objectstack/plugin-approvals`), and record-triggered automation is lifecycle +hooks + `record_change` flows. ### 7. automation Service — 1 method ✅ `@objectstack/service-automation` `triggerAutomation` @@ -502,11 +511,16 @@ a package that cannot be installed is a dead end, which is why | Slot | State | |:-------|:------------| | **ui** | Nothing registers the slot. `ViewProtocol`'s five methods are declared and unrouted; view CRUD runs through `/api/v1/meta`, and `/api/v1/ui/view/:object` is served by the `protocol` service. | -| **workflow** | Nothing ships. `WorkflowProtocol`'s three methods have no implementation and no consumer. | | **search** | Nothing ships. Contract and engine enum exist in `@objectstack/spec` only. | | **ai** | Nothing in this repo — `service-ai` (chat, completion, models, conversations) is Cloud/EE. | | **realtime transport** | The service exists but no WebSocket/SSE route is mounted, so `routes.realtime` is deliberately never advertised. | +The `workflow` slot used to sit in this table ("nothing ships, no consumer"). +It was retired outright in v17 (#4451, per ADR-0115 Evidence 5): the +capability lives in `state_machine` validation rules, approval flow nodes +(ADR-0019) and `record_change` flows, so there is nothing left for a slot to +promise. + --- ## Plugin Implementation Pattern diff --git a/content/docs/protocol/kernel/http-protocol.mdx b/content/docs/protocol/kernel/http-protocol.mdx index c1be47021a..fbcaf5dcf8 100644 --- a/content/docs/protocol/kernel/http-protocol.mdx +++ b/content/docs/protocol/kernel/http-protocol.mdx @@ -47,14 +47,13 @@ GET /api/v1/discovery HTTP/1.1 "packages": "/api/v1/packages", "auth": "/api/v1/auth", "ui": "/api/v1/ui", - "storage": "/api/v1/storage", - "graphql": "/api/v1/graphql" + "storage": "/api/v1/storage" }, "services": { "data": { "enabled": true, "status": "available", "route": "/api/v1/data", "provider": "objectql" }, "metadata": { "enabled": true, "status": "available", "route": "/api/v1/meta", "provider": "objectql" }, "auth": { "enabled": true, "status": "available", "route": "/api/v1/auth", "provider": "@objectstack/plugin-auth" }, - "workflow": { "enabled": false, "status": "unavailable", "message": "No implementation ships for the 'workflow' slot — register a service under it to enable" }, + "search": { "enabled": false, "status": "unavailable", "message": "No implementation ships for the 'search' slot — register a service under it to enable" }, "ai": { "enabled": false, "status": "unavailable", "message": "Provided by @objectstack/service-ai in ObjectStack Cloud/Enterprise — no implementation ships in the open framework" } }, "locale": { diff --git a/content/docs/references/api/discovery.mdx b/content/docs/references/api/discovery.mdx index 3b1d36b48b..f38ebe4b81 100644 --- a/content/docs/references/api/discovery.mdx +++ b/content/docs/references/api/discovery.mdx @@ -52,7 +52,6 @@ const result = ApiRoutes.parse(data); | **storage** | `string` | optional | e.g. /api/v1/storage | | **analytics** | `string` | optional | e.g. /api/v1/analytics | | **packages** | `string` | optional | e.g. /api/v1/packages | -| **workflow** | `string` | optional | e.g. /api/v1/workflow | | **approvals** | `string` | optional | e.g. /api/v1/approvals | | **realtime** | `string` | optional | e.g. /api/v1/realtime | | **notifications** | `string` | optional | e.g. /api/v1/notifications | diff --git a/content/docs/references/api/dispatcher.mdx b/content/docs/references/api/dispatcher.mdx index 18d82d1b95..8cde2909a2 100644 --- a/content/docs/references/api/dispatcher.mdx +++ b/content/docs/references/api/dispatcher.mdx @@ -51,7 +51,7 @@ const result = DispatcherConfig.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **routes** | `{ prefix: string; service: Enum<'metadata' \| 'data' \| 'auth' \| 'file-storage' \| 'search' \| 'cache' \| 'queue' \| 'automation' \| 'analytics' \| 'realtime' \| 'job' \| 'notification' \| 'ai' \| 'i18n' \| 'ui' \| 'workflow'>; authRequired: boolean; criticality: Enum<'required' \| 'core' \| 'optional'>; … }[]` | ✅ | Route-to-service mappings | +| **routes** | `{ prefix: string; service: Enum<'metadata' \| 'data' \| 'auth' \| 'file-storage' \| 'search' \| 'cache' \| 'queue' \| 'automation' \| 'analytics' \| 'realtime' \| 'job' \| 'notification' \| 'ai' \| 'i18n' \| 'ui'>; authRequired: boolean; criticality: Enum<'required' \| 'core' \| 'optional'>; … }[]` | ✅ | Route-to-service mappings | | **fallback** | `Enum<'404' \| 'proxy' \| 'custom'>` | ✅ | Behavior when no route matches | | **proxyTarget** | `string` | optional | Proxy target URL when fallback is "proxy" | @@ -91,7 +91,7 @@ Route-resolution failure mode emitted in `error.code` | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **prefix** | `string` | ✅ | URL path prefix for routing (e.g. /api/v1/data) | -| **service** | `Enum<'metadata' \| 'data' \| 'auth' \| 'file-storage' \| 'search' \| 'cache' \| 'queue' \| 'automation' \| 'analytics' \| 'realtime' \| 'job' \| 'notification' \| 'ai' \| 'i18n' \| 'ui' \| 'workflow'>` | ✅ | Target core service name | +| **service** | `Enum<'metadata' \| 'data' \| 'auth' \| 'file-storage' \| 'search' \| 'cache' \| 'queue' \| 'automation' \| 'analytics' \| 'realtime' \| 'job' \| 'notification' \| 'ai' \| 'i18n' \| 'ui'>` | ✅ | Target core service name | | **authRequired** | `boolean` | ✅ | Whether authentication is required | | **criticality** | `Enum<'required' \| 'core' \| 'optional'>` | ✅ | Service criticality level for unavailability handling | | **permissions** | `string[]` | optional | Required permissions for this route namespace | diff --git a/content/docs/references/api/plugin-rest-api.mdx b/content/docs/references/api/plugin-rest-api.mdx index f8c0b07a4a..78a8600c9e 100644 --- a/content/docs/references/api/plugin-rest-api.mdx +++ b/content/docs/references/api/plugin-rest-api.mdx @@ -227,7 +227,7 @@ const result = ErrorHandlingConfig.parse(data); | **method** | `Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH' \| 'HEAD' \| 'OPTIONS'>` | ✅ | HTTP method for this endpoint | | **path** | `string` | ✅ | URL path pattern (e.g., /api/v1/data/:object/:id) | | **handler** | `string` | ✅ | Protocol method name or handler identifier | -| **category** | `Enum<'discovery' \| 'metadata' \| 'data' \| 'batch' \| 'permission' \| 'analytics' \| 'automation' \| 'workflow' \| 'ui' \| 'realtime' \| 'notification' \| 'ai' \| 'i18n'>` | ✅ | Route category | +| **category** | `Enum<'discovery' \| 'metadata' \| 'data' \| 'batch' \| 'permission' \| 'analytics' \| 'automation' \| 'ui' \| 'realtime' \| 'notification' \| 'ai' \| 'i18n'>` | ✅ | Route category | | **public** | `boolean` | ✅ | Is publicly accessible without authentication | | **permissions** | `string[]` | optional | Required permissions (e.g., ["data.read", "object.account.read"]) | | **summary** | `string` | optional | Short description for OpenAPI | @@ -253,7 +253,7 @@ const result = ErrorHandlingConfig.parse(data); | **enabled** | `boolean` | ✅ | Enable REST API plugin | | **basePath** | `string` | ✅ | Base path for all API routes | | **version** | `string` | ✅ | API version identifier | -| **routes** | `{ prefix: string; service: string; category: Enum<'discovery' \| 'metadata' \| 'data' \| 'batch' \| 'permission' \| 'analytics' \| 'automation' \| 'workflow' \| 'ui' \| 'realtime' \| 'notification' \| 'ai' \| 'i18n'>; methods?: string[]; … }[]` | ✅ | Route registrations | +| **routes** | `{ prefix: string; service: string; category: Enum<'discovery' \| 'metadata' \| 'data' \| 'batch' \| 'permission' \| 'analytics' \| 'automation' \| 'ui' \| 'realtime' \| 'notification' \| 'ai' \| 'i18n'>; methods?: string[]; … }[]` | ✅ | Route registrations | | **validation** | `{ enabled: boolean; mode: Enum<'strict' \| 'permissive' \| 'strip'>; validateBody: boolean; validateQuery: boolean; … }` | optional | Request validation configuration | | **responseEnvelope** | `{ enabled: boolean; includeMetadata: boolean; includeTimestamp: boolean; includeRequestId: boolean; … }` | optional | Response envelope configuration | | **errorHandling** | `{ enabled: boolean; includeStackTrace: boolean; logErrors: boolean; exposeInternalErrors: boolean; … }` | optional | Error handling configuration | @@ -276,7 +276,6 @@ const result = ErrorHandlingConfig.parse(data); * `permission` * `analytics` * `automation` -* `workflow` * `ui` * `realtime` * `notification` @@ -294,9 +293,9 @@ const result = ErrorHandlingConfig.parse(data); | :--- | :--- | :--- | :--- | | **prefix** | `string` | ✅ | URL path prefix for this route group | | **service** | `string` | ✅ | Core service name (metadata, data, auth, etc.) | -| **category** | `Enum<'discovery' \| 'metadata' \| 'data' \| 'batch' \| 'permission' \| 'analytics' \| 'automation' \| 'workflow' \| 'ui' \| 'realtime' \| 'notification' \| 'ai' \| 'i18n'>` | ✅ | Primary category for this route group | +| **category** | `Enum<'discovery' \| 'metadata' \| 'data' \| 'batch' \| 'permission' \| 'analytics' \| 'automation' \| 'ui' \| 'realtime' \| 'notification' \| 'ai' \| 'i18n'>` | ✅ | Primary category for this route group | | **methods** | `string[]` | optional | Protocol method names implemented | -| **endpoints** | `{ method: Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH' \| 'HEAD' \| 'OPTIONS'>; path: string; handler: string; category: Enum<'discovery' \| 'metadata' \| 'data' \| 'batch' \| 'permission' \| 'analytics' \| 'automation' \| 'workflow' \| 'ui' \| 'realtime' \| 'notification' \| 'ai' \| 'i18n'>; … }[]` | optional | Endpoint definitions | +| **endpoints** | `{ method: Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH' \| 'HEAD' \| 'OPTIONS'>; path: string; handler: string; category: Enum<'discovery' \| 'metadata' \| 'data' \| 'batch' \| 'permission' \| 'analytics' \| 'automation' \| 'ui' \| 'realtime' \| 'notification' \| 'ai' \| 'i18n'>; … }[]` | optional | Endpoint definitions | | **middleware** | `{ name: string; type: Enum<'authentication' \| 'authorization' \| 'logging' \| 'validation' \| 'transformation' \| 'error' \| 'custom'>; enabled: boolean; order: integer; … }[]` | optional | Middleware stack for this route group | | **authRequired** | `boolean` | ✅ | Whether authentication is required by default | | **documentation** | `{ title?: string; description?: string; tags?: string[] }` | optional | Documentation metadata for this route group | @@ -312,7 +311,7 @@ const result = ErrorHandlingConfig.parse(data); | :--- | :--- | :--- | :--- | | **path** | `string` | ✅ | Full URL path (e.g. /api/v1/analytics/query) | | **method** | `Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH' \| 'HEAD' \| 'OPTIONS'>` | ✅ | HTTP method (GET, POST, etc.) | -| **category** | `Enum<'discovery' \| 'metadata' \| 'data' \| 'batch' \| 'permission' \| 'analytics' \| 'automation' \| 'workflow' \| 'ui' \| 'realtime' \| 'notification' \| 'ai' \| 'i18n'>` | ✅ | Route category | +| **category** | `Enum<'discovery' \| 'metadata' \| 'data' \| 'batch' \| 'permission' \| 'analytics' \| 'automation' \| 'ui' \| 'realtime' \| 'notification' \| 'ai' \| 'i18n'>` | ✅ | Route category | | **handlerStatus** | `Enum<'implemented' \| 'stub' \| 'planned'>` | ✅ | Handler status | | **service** | `string` | ✅ | Target service name | | **healthCheckPassed** | `boolean` | optional | Whether the health check probe succeeded | @@ -329,7 +328,7 @@ const result = ErrorHandlingConfig.parse(data); | **timestamp** | `string` | ✅ | ISO 8601 timestamp | | **adapter** | `string` | ✅ | Adapter name (e.g. "hono", "express", "nextjs") | | **summary** | `{ total: integer; implemented: integer; stub: integer; planned: integer }` | ✅ | | -| **entries** | `{ path: string; method: Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH' \| 'HEAD' \| 'OPTIONS'>; category: Enum<'discovery' \| 'metadata' \| 'data' \| 'batch' \| 'permission' \| 'analytics' \| 'automation' \| 'workflow' \| 'ui' \| 'realtime' \| 'notification' \| 'ai' \| 'i18n'>; handlerStatus: Enum<'implemented' \| 'stub' \| 'planned'>; … }[]` | ✅ | Per-endpoint coverage entries | +| **entries** | `{ path: string; method: Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH' \| 'HEAD' \| 'OPTIONS'>; category: Enum<'discovery' \| 'metadata' \| 'data' \| 'batch' \| 'permission' \| 'analytics' \| 'automation' \| 'ui' \| 'realtime' \| 'notification' \| 'ai' \| 'i18n'>; handlerStatus: Enum<'implemented' \| 'stub' \| 'planned'>; … }[]` | ✅ | Per-endpoint coverage entries | --- diff --git a/content/docs/references/api/protocol.mdx b/content/docs/references/api/protocol.mdx index fd59ab995f..bd77bbecb9 100644 --- a/content/docs/references/api/protocol.mdx +++ b/content/docs/references/api/protocol.mdx @@ -20,8 +20,8 @@ validation. Each entry is a canonical `ActionDescriptorSchema`. ## TypeScript Usage ```typescript -import { AiAgentCapabilities, AiAgentChatRequest, AiAgentSummary, AiAgentsResponse, AiChatRequest, AiChatResponse, AiCompleteRequest, AiConversation, AiMessage, AiModelsResponse, AiPendingAction, AiPendingActionStatus, AiStreamChunk, ApproveAiPendingActionResponse, AutomationActionsResponse, AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CheckPermissionRequest, CheckPermissionResponse, CreateAiConversationRequest, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, CreateViewRequest, CreateViewResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, DeleteMetaItemRequest, DeleteMetaItemResponse, DeleteViewRequest, DeleteViewResponse, FindDataRequest, FindDataResponse, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetEffectivePermissionsRequest, GetEffectivePermissionsResponse, GetFieldLabelsRequest, GetFieldLabelsResponse, GetLocalesRequest, GetLocalesResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetObjectPermissionsRequest, GetObjectPermissionsResponse, GetPresenceRequest, GetPresenceResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetUiViewResponse, GetViewRequest, GetViewResponse, GetWorkflowConfigRequest, GetWorkflowConfigResponse, GetWorkflowStateRequest, GetWorkflowStateResponse, HttpFindQueryParams, ListAiConversationsRequest, ListAiConversationsResponse, ListAiPendingActionsRequest, ListAiPendingActionsResponse, ListNotificationsRequest, ListNotificationsResponse, ListViewsRequest, ListViewsResponse, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, NotificationPreferences, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, RejectAiPendingActionResponse, SaveMetaItemRequest, SaveMetaItemResponse, SetPresenceRequest, SetPresenceResponse, UnregisterDeviceRequest, UnregisterDeviceResponse, UpdateAiConversationRequest, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse, UpdateNotificationPreferencesRequest, UpdateNotificationPreferencesResponse, UpdateViewRequest, UpdateViewResponse, WorkflowState, WorkflowTransitionRequest, WorkflowTransitionResponse } from '@objectstack/spec/api'; -import type { AiAgentCapabilities, AiAgentChatRequest, AiAgentSummary, AiAgentsResponse, AiChatRequest, AiChatResponse, AiCompleteRequest, AiConversation, AiMessage, AiModelsResponse, AiPendingAction, AiPendingActionStatus, AiStreamChunk, ApproveAiPendingActionResponse, AutomationActionsResponse, AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CheckPermissionRequest, CheckPermissionResponse, CreateAiConversationRequest, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, CreateViewRequest, CreateViewResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, DeleteMetaItemRequest, DeleteMetaItemResponse, DeleteViewRequest, DeleteViewResponse, FindDataRequest, FindDataResponse, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetEffectivePermissionsRequest, GetEffectivePermissionsResponse, GetFieldLabelsRequest, GetFieldLabelsResponse, GetLocalesRequest, GetLocalesResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetObjectPermissionsRequest, GetObjectPermissionsResponse, GetPresenceRequest, GetPresenceResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetUiViewResponse, GetViewRequest, GetViewResponse, GetWorkflowConfigRequest, GetWorkflowConfigResponse, GetWorkflowStateRequest, GetWorkflowStateResponse, HttpFindQueryParams, ListAiConversationsRequest, ListAiConversationsResponse, ListAiPendingActionsRequest, ListAiPendingActionsResponse, ListNotificationsRequest, ListNotificationsResponse, ListViewsRequest, ListViewsResponse, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, NotificationPreferences, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, RejectAiPendingActionResponse, SaveMetaItemRequest, SaveMetaItemResponse, SetPresenceRequest, SetPresenceResponse, UnregisterDeviceRequest, UnregisterDeviceResponse, UpdateAiConversationRequest, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse, UpdateNotificationPreferencesRequest, UpdateNotificationPreferencesResponse, UpdateViewRequest, UpdateViewResponse, WorkflowState, WorkflowTransitionRequest, WorkflowTransitionResponse } from '@objectstack/spec/api'; +import { AiAgentCapabilities, AiAgentChatRequest, AiAgentSummary, AiAgentsResponse, AiChatRequest, AiChatResponse, AiCompleteRequest, AiConversation, AiMessage, AiModelsResponse, AiPendingAction, AiPendingActionStatus, AiStreamChunk, ApproveAiPendingActionResponse, AutomationActionsResponse, AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CheckPermissionRequest, CheckPermissionResponse, CreateAiConversationRequest, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, CreateViewRequest, CreateViewResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, DeleteMetaItemRequest, DeleteMetaItemResponse, DeleteViewRequest, DeleteViewResponse, FindDataRequest, FindDataResponse, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetEffectivePermissionsRequest, GetEffectivePermissionsResponse, GetFieldLabelsRequest, GetFieldLabelsResponse, GetLocalesRequest, GetLocalesResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetObjectPermissionsRequest, GetObjectPermissionsResponse, GetPresenceRequest, GetPresenceResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetUiViewResponse, GetViewRequest, GetViewResponse, HttpFindQueryParams, ListAiConversationsRequest, ListAiConversationsResponse, ListAiPendingActionsRequest, ListAiPendingActionsResponse, ListNotificationsRequest, ListNotificationsResponse, ListViewsRequest, ListViewsResponse, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, NotificationPreferences, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, RejectAiPendingActionResponse, SaveMetaItemRequest, SaveMetaItemResponse, SetPresenceRequest, SetPresenceResponse, UnregisterDeviceRequest, UnregisterDeviceResponse, UpdateAiConversationRequest, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse, UpdateNotificationPreferencesRequest, UpdateNotificationPreferencesResponse, UpdateViewRequest, UpdateViewResponse } from '@objectstack/spec/api'; +import type { AiAgentCapabilities, AiAgentChatRequest, AiAgentSummary, AiAgentsResponse, AiChatRequest, AiChatResponse, AiCompleteRequest, AiConversation, AiMessage, AiModelsResponse, AiPendingAction, AiPendingActionStatus, AiStreamChunk, ApproveAiPendingActionResponse, AutomationActionsResponse, AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CheckPermissionRequest, CheckPermissionResponse, CreateAiConversationRequest, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, CreateViewRequest, CreateViewResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, DeleteMetaItemRequest, DeleteMetaItemResponse, DeleteViewRequest, DeleteViewResponse, FindDataRequest, FindDataResponse, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetEffectivePermissionsRequest, GetEffectivePermissionsResponse, GetFieldLabelsRequest, GetFieldLabelsResponse, GetLocalesRequest, GetLocalesResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetObjectPermissionsRequest, GetObjectPermissionsResponse, GetPresenceRequest, GetPresenceResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetUiViewResponse, GetViewRequest, GetViewResponse, HttpFindQueryParams, ListAiConversationsRequest, ListAiConversationsResponse, ListAiPendingActionsRequest, ListAiPendingActionsResponse, ListNotificationsRequest, ListNotificationsResponse, ListViewsRequest, ListViewsResponse, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, NotificationPreferences, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, RejectAiPendingActionResponse, SaveMetaItemRequest, SaveMetaItemResponse, SetPresenceRequest, SetPresenceResponse, UnregisterDeviceRequest, UnregisterDeviceResponse, UpdateAiConversationRequest, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse, UpdateNotificationPreferencesRequest, UpdateNotificationPreferencesResponse, UpdateViewRequest, UpdateViewResponse } from '@objectstack/spec/api'; // Validate data const result = AiAgentCapabilities.parse(data); @@ -923,54 +923,6 @@ const result = AiAgentCapabilities.parse(data); | **view** | `{ list?: object; form?: object; listViews?: Record; data?: { provider: 'object'; object: string } \| { provider: 'api'; read?: object; write?: object } \| { provider: 'value'; items: any[] } \| { provider: 'schema'; schemaId: string; schema?: Record }; … }>; formViews?: Record; layout?: Enum<'vertical' \| 'horizontal' \| 'inline' \| 'grid'>; columns?: integer; title?: string; … }>; … }` | ✅ | View definition | ---- - -## GetWorkflowConfigRequest - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **object** | `string` | ✅ | Object name to get workflow config for | - - ---- - -## GetWorkflowConfigResponse - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **object** | `string` | ✅ | Object name | -| **workflows** | `{ id: string; description?: string; contextSchema?: Record; initial: string; … }[]` | ✅ | Active state-machine workflows for this object | - - ---- - -## GetWorkflowStateRequest - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **object** | `string` | ✅ | Object name | -| **recordId** | `string` | ✅ | Record ID to get workflow state for | - - ---- - -## GetWorkflowStateResponse - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **object** | `string` | ✅ | Object name | -| **recordId** | `string` | ✅ | Record ID | -| **state** | `{ currentState: string; availableTransitions: { name: string; targetState: string; label?: string; requiresApproval: boolean }[]; history?: { fromState: string; toState: string; action: string; userId: string; … }[] }` | ✅ | Current workflow state and available transitions | - - --- ## HttpFindQueryParams @@ -1476,45 +1428,3 @@ const result = AiAgentCapabilities.parse(data); --- -## WorkflowState - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **currentState** | `string` | ✅ | Current workflow state name | -| **availableTransitions** | `{ name: string; targetState: string; label?: string; requiresApproval: boolean }[]` | ✅ | Available transitions from current state | -| **history** | `{ fromState: string; toState: string; action: string; userId: string; … }[]` | optional | State transition history | - - ---- - -## WorkflowTransitionRequest - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **object** | `string` | ✅ | Object name | -| **recordId** | `string` | ✅ | Record ID | -| **transition** | `string` | ✅ | Transition name to execute | -| **comment** | `string` | optional | Optional comment for the transition | -| **data** | `Record` | optional | Additional data for the transition | - - ---- - -## WorkflowTransitionResponse - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **object** | `string` | ✅ | Object name | -| **recordId** | `string` | ✅ | Record ID | -| **success** | `boolean` | ✅ | Whether the transition succeeded | -| **state** | `{ currentState: string; availableTransitions: { name: string; targetState: string; label?: string; requiresApproval: boolean }[]; history?: { fromState: string; toState: string; action: string; userId: string; … }[] }` | ✅ | New workflow state after transition | - - ---- - diff --git a/content/docs/references/system/core-services.mdx b/content/docs/references/system/core-services.mdx index 5a28e0fba2..fd17fb5884 100644 --- a/content/docs/references/system/core-services.mdx +++ b/content/docs/references/system/core-services.mdx @@ -52,7 +52,6 @@ const result = CoreServiceName.parse(data); * `ai` * `i18n` * `ui` -* `workflow` --- @@ -67,7 +66,7 @@ const result = CoreServiceName.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **id** | `string` | ✅ | | -| **name** | `Enum<'metadata' \| 'data' \| 'auth' \| 'file-storage' \| 'search' \| 'cache' \| 'queue' \| 'automation' \| 'analytics' \| 'realtime' \| 'job' \| 'notification' \| 'ai' \| 'i18n' \| 'ui' \| 'workflow'>` | ✅ | | +| **name** | `Enum<'metadata' \| 'data' \| 'auth' \| 'file-storage' \| 'search' \| 'cache' \| 'queue' \| 'automation' \| 'analytics' \| 'realtime' \| 'job' \| 'notification' \| 'ai' \| 'i18n' \| 'ui'>` | ✅ | | | **options** | `Record` | optional | | @@ -90,7 +89,7 @@ const result = CoreServiceName.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **name** | `Enum<'metadata' \| 'data' \| 'auth' \| 'file-storage' \| 'search' \| 'cache' \| 'queue' \| 'automation' \| 'analytics' \| 'realtime' \| 'job' \| 'notification' \| 'ai' \| 'i18n' \| 'ui' \| 'workflow'>` | ✅ | | +| **name** | `Enum<'metadata' \| 'data' \| 'auth' \| 'file-storage' \| 'search' \| 'cache' \| 'queue' \| 'automation' \| 'analytics' \| 'realtime' \| 'job' \| 'notification' \| 'ai' \| 'i18n' \| 'ui'>` | ✅ | | | **enabled** | `boolean` | ✅ | | | **status** | `Enum<'running' \| 'stopped' \| 'degraded' \| 'initializing'>` | ✅ | | | **version** | `string` | optional | | diff --git a/content/docs/releases/v17.mdx b/content/docs/releases/v17.mdx index f71a3c9550..8cac880ebe 100644 --- a/content/docs/releases/v17.mdx +++ b/content/docs/releases/v17.mdx @@ -1015,6 +1015,7 @@ import or the authored key. | `DEFAULT_DISPATCHER_ROUTES` | dead route table | | Aspirational config on Theme / Translation / Webhook | still-dead after #3494 | | `ChartInteraction.zoom` / `.clickAction` | never implemented (#3752) | +| The `workflow` service slot — `CoreServiceName 'workflow'`, `IWorkflowService`, `WorkflowProtocol`, the `Get/WorkflowState/Config/Transition` schema cluster, discovery `routes.workflow` / `services.workflow` / `features.workflow`, the `RestApiRouteCategory 'workflow'` member and the stray `graphql` provider entry | declared end to end and implemented nowhere: nothing ever registered or resolved the slot (ADR-0115 Evidence 5), no method of `WorkflowProtocol` was ever implemented, no host ever mounted `/api/v1/workflow`. State machines are `state_machine` validation rules; approvals are flow nodes (ADR-0019); record-triggered automation is hooks + `record_change` flows (#4451) | | `datasource.readReplicas` | replica connections nothing ever opened — no driver reads the key and no query path splits reads from writes, so every statement went to the primary. #4410 had just taught the schema to validate each entry against the declared driver's contract, which made a dead slot look rigorously alive (#4468) | The Console side follows: `@object-ui/types` drops its @@ -2088,6 +2089,13 @@ covers are folded into the list below rather than left to the changelog.) device/preference helpers, `client.ai.{nlq,suggest,insights}` and `projects.listTemplates()`; repoint marketplace publish to `POST /api/v1/packages/publish`; drop `os environments create --template`. + Imports of `IWorkflowService`, `WorkflowProtocol` or the + `Get/WorkflowState/Config/Transition` types no longer resolve — the + `workflow` slot retired with them (#4451); use `state_machine` validation + rules, approval flow nodes and `record_change` flows instead. Discovery + responses no longer carry `services.workflow` / `routes.workflow` / + `features.workflow` — a reader keying on them saw only `unavailable`/`false` + before, so delete the read. - **Console hosts:** the same removals land in objectui — drop the `useClientNotifications` device/preference delegates (`@object-ui/react`) and replace the retired `@object-ui/types` Capabilities re-exports with imports diff --git a/docs/protocol-upgrade-guide.md b/docs/protocol-upgrade-guide.md index 49ec3e58a3..5510962613 100644 --- a/docs/protocol-upgrade-guide.md +++ b/docs/protocol-upgrade-guide.md @@ -219,6 +219,9 @@ Closing the same audit on the data side, `datasource.readReplicas` is removed (# - **`query-distinct-retired`** — `data.query.distinct` → `groupBy` for unique combinations; the `count_distinct` aggregation for deduplicated counts; the SQL/memory drivers' `distinct(object, field)` door for one column's values - Why not automatic: The `distinct` flag promised SELECT DISTINCT and no driver ever rendered it — but it was MIS-WIRED rather than merely dead (the harsher ADR-0078 class): the REST list path treated a distinct query as not countable and silently degraded `total`/`hasMore` to a page-local estimate, so the caller got duplicate rows AND worse pagination metadata, and a side effect that "confirmed" the flag was doing something. It had a shipped public producer (`QueryBuilder.distinct()`, removed with the key). The count suppression is deleted in the same change — `total` is truthful for those queries again. A REQUEST surface, never stored; nothing to rewrite. ADR-0049 / ADR-0078, #4286. - Done when: No caller sends `distinct` and no SDK call site uses `QueryBuilder.distinct()`; deduplication goes through `groupBy` / `count_distinct` / the drivers' `distinct()` door. A query still carrying the key fails to parse with the removal prescription, and the REST list response reports a real `total` for queries that used to send it. +- **`workflow-service-slot-retired`** — `CoreServiceName 'workflow' / IWorkflowService / WorkflowProtocol / discovery routes.workflow / RestApiRouteCategory workflow` → the live mechanisms the slot only ever pointed at: `state_machine` validation rules for record state machines, approval flow nodes on the approvals runtime (ADR-0019) for approvals, lifecycle hooks + `record_change` flows (service-automation) for record-triggered automation + - Why not automatic: The workflow slot was declared end to end and implemented nowhere: no code in either repository ever registered or resolved it (ADR-0115 Evidence 5 — the only touches were plugin-dev's retired stub probe and the generic discovery walk), no implementation of any WorkflowProtocol method ever existed, and no host ever mounted `/api/v1/workflow` (the pre-#3586 DEFAULT_DISPATCHER_ROUTES listed it among routes that never existed). Every part of it was ADR-0078's silently-inert declaration: a CoreServiceName nothing filled, a contract nothing implemented, a protocol nothing served, a discovery route field no builder could truthfully populate. These are TS/API surfaces and a discovery RESPONSE field — never stored in stack metadata, so there is no source for the chain to rewrite; consumers of the deleted types move their imports themselves. ADR-0049 / ADR-0078, #4451. + - Done when: No import of IWorkflowService, WorkflowProtocol or the Get/WorkflowState/Config/Transition types resolves; no code calls getService('workflow') or reads discovery `routes.workflow` / `services.workflow`; record state machines, approvals and record-triggered automation go through the replacement mechanisms. Discovery output on a default boot is unchanged (the slot was always reported unavailable; now it is simply absent). --- diff --git a/packages/cli/src/commands/explain.ts b/packages/cli/src/commands/explain.ts index 03ba3f6042..271905f8b9 100644 --- a/packages/cli/src/commands/explain.ts +++ b/packages/cli/src/commands/explain.ts @@ -258,32 +258,23 @@ export const SCHEMAS: Record = { docsPath: 'ui/action', }, + // Kept as a redirect topic (mirroring content/docs/automation/workflows.mdx): + // there is NO standalone Workflow authoring type. The shape this entry used + // to teach (states[]/transitions[]/approvers) never existed in the spec — + // ADR-0019 folded approval processes into Flow, and the workflow service + // slot itself retired in #4451 (v17). workflow: { - name: 'Workflow', - description: 'State machine and approval process that governs record lifecycle transitions.', - required: [ - { name: 'name', type: 'string (snake_case)', description: 'Machine name identifier' }, - { name: 'object', type: 'string', description: 'Target object' }, - ], - optional: [ - { name: 'label', type: 'string', description: 'Display name' }, - { name: 'states', type: 'State[]', description: 'Defined workflow states' }, - { name: 'transitions', type: 'Transition[]', description: 'Allowed state transitions' }, - { name: 'approvers', type: 'ApproverConfig', description: 'Approval chain configuration' }, - ], - example: `{ - name: 'task_approval', - object: 'project_task', - label: 'Task Approval', - states: ['draft', 'pending', 'approved', 'rejected'], - transitions: [ - { from: 'draft', to: 'pending', action: 'submit' }, - { from: 'pending', to: 'approved', action: 'approve' }, - { from: 'pending', to: 'rejected', action: 'reject' }, - ], -}`, + name: 'Workflow (no standalone type)', + description: 'ObjectStack has no standalone Workflow authoring type. Use Flow for event-triggered or scheduled automation, an object validation rule of type "state_machine" for strict lifecycle transitions, and Approval nodes inside a flow for human approval pauses (ADR-0019).', + required: [], + optional: [], + example: `// No workflow metadata exists. Compose the live mechanisms instead: +// - Flow (type: 'record_change' | 'schedule' | 'screen') for automation +// - object validation rule { type: 'state_machine', ... } for transitions +// - a flow node of type 'approval' for human approval steps +// See: os explain flow`, related: ['object', 'flow', 'action'], - docsPath: 'automation/workflow', + docsPath: 'automation/workflows', }, trigger: { diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 29b139e26a..dcb3a9a28f 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -4542,7 +4542,8 @@ export class ObjectStackClient { automation: '/api/v1/automation', packages: '/api/v1/packages', realtime: '/api/v1/realtime', - workflow: '/api/v1/workflow', + // `workflow` removed (#4451, v17): the slot retired with the ApiRoutes + // field — there was never a surface behind the convention. approvals: '/api/v1/approvals', notifications: '/api/v1/notifications', ai: '/api/v1/ai', @@ -4963,10 +4964,8 @@ export type { RealtimeSubscribeRequest, RealtimeSubscribeResponse, GetPresenceResponse, - GetWorkflowConfigResponse, - GetWorkflowStateResponse, - WorkflowTransitionRequest, - WorkflowTransitionResponse, + // Workflow re-exports removed (#4451, v17): the types were deleted from + // @objectstack/spec/api with the retired workflow slot. ListViewsResponse, GetViewResponse, CreateViewResponse, diff --git a/packages/metadata-protocol/src/protocol.ts b/packages/metadata-protocol/src/protocol.ts index c43dd79b8a..8ec5dede16 100644 --- a/packages/metadata-protocol/src/protocol.ts +++ b/packages/metadata-protocol/src/protocol.ts @@ -1247,7 +1247,10 @@ const SERVICE_CONFIG: Record expect(discovery.services.auth.enabled).toBe(false); expect(discovery.services.auth.status).toBe('unavailable'); expect(discovery.services.auth.message).toContain('plugin-auth'); - // capabilities removed — derive from services - expect(discovery.services.workflow).toBeDefined(); - expect(discovery.services.workflow.enabled).toBe(false); + // capabilities removed — derive from services. (`workflow` was the slot + // pinned here until it retired in #4451; it must now be absent entirely.) + expect(discovery.services).not.toHaveProperty('workflow'); }); it('should return available auth service when auth is registered', async () => { @@ -109,18 +109,20 @@ describe('ObjectStackProtocolImplementation - Dynamic Service Discovery', () => }); it('should report a __serviceInfo-marked service with its declared status', async () => { + // Was pinned on `workflow` until that slot retired (#4451, v17); `auth` + // exercises the same marked-service path. const mockServices = new Map(); - mockServices.set('workflow', { + mockServices.set('auth', { __serviceInfo: { status: 'degraded', message: 'partial impl' }, }); protocol = new ObjectStackProtocolImplementation(engine, () => mockServices); const discovery = await protocol.getDiscovery(); - expect(discovery.services.workflow.enabled).toBe(true); - expect(discovery.services.workflow.status).toBe('degraded'); - expect(discovery.services.workflow.handlerReady).toBe(true); - expect(discovery.services.workflow.message).toBe('partial impl'); + expect(discovery.services.auth.enabled).toBe(true); + expect(discovery.services.auth.status).toBe('degraded'); + expect(discovery.services.auth.handlerReady).toBe(true); + expect(discovery.services.auth.message).toBe('partial impl'); }); it('should report a registered analytics service with its self-declared status', async () => { @@ -332,13 +334,14 @@ describe('ObjectStackProtocolImplementation - Dynamic Service Discovery', () => expect(discovery.routes.i18n).toBe('/api/v1/i18n'); }); - // Not every SERVICE_CONFIG entry is dispatcher-owned: `search` (REST layer), - // `workflow` and `graphql` have their routes mounted by the plugin that - // registers the service, so `handlerReady` there says nothing about whether - // THAT route is mounted and the advertisement stays presence-gated. - // Suppressing it would be a guess, not honesty. (cache/queue/job used to be - // named here on the same theory, but nothing mounts routes for them at all — - // they are route-less kernel-internal slots since #4318.) + // Not every SERVICE_CONFIG entry is dispatcher-owned: `search` (REST + // layer) has its route mounted by the plugin that registers the service, + // so `handlerReady` there says nothing about whether THAT route is mounted + // and the advertisement stays presence-gated. Suppressing it would be a + // guess, not honesty. (cache/queue/job used to be named here on the same + // theory, but nothing mounts routes for them at all — route-less + // kernel-internal slots since #4318; `workflow` and `graphql` retired + // outright in #4451.) it('should leave non-dispatcher-owned routes presence-gated', async () => { const mockServices = new Map(); mockServices.set('search', { __serviceInfo: { status: 'stub', message: 'dev fake' } }); @@ -433,17 +436,19 @@ describe('ObjectStackProtocolImplementation - Dynamic Service Discovery', () => }); it('should return capabilities field populated from registered services', async () => { + // Was pinned on `workflow` until that slot retired (#4451, v17); `ui` + // is likewise registered without mapping to a well-known capability. const mockServices = new Map(); - mockServices.set('workflow', {}); - + mockServices.set('ui', {}); + protocol = new ObjectStackProtocolImplementation(engine, () => mockServices); const discovery = await protocol.getDiscovery(); - + // capabilities field should now exist in the response expect(discovery.capabilities).toBeDefined(); - // workflow is registered but doesn't map to a well-known capability directly - expect(discovery.services.workflow.enabled).toBe(true); - // All well-known capabilities should be disabled since workflow doesn't map to any + // ui is registered but doesn't map to a well-known capability directly + expect(discovery.services.ui.enabled).toBe(true); + // All well-known capabilities should be disabled since ui doesn't map to any // (comments derives from the sys_comment object, which is not registered here). expect(discovery.capabilities!.comments).toEqual({ enabled: false }); expect(discovery.capabilities!.automation).toEqual({ enabled: false }); diff --git a/packages/runtime/src/http-dispatcher.test.ts b/packages/runtime/src/http-dispatcher.test.ts index f3ac0d8d5a..41507b0568 100644 --- a/packages/runtime/src/http-dispatcher.test.ts +++ b/packages/runtime/src/http-dispatcher.test.ts @@ -2326,12 +2326,20 @@ describe('HttpDispatcher', () => { it('says nothing ships rather than naming a package that does not exist', async () => { const info = await dispatcher.getDiscoveryInfo('/api/v1'); - for (const slot of ['ai', 'search', 'workflow'] as const) { + // `workflow` left this list with its slot (#4451, v17). + for (const slot of ['ai', 'search'] as const) { expect(info.services[slot].message, `services.${slot}.message`).not.toMatch(/Install/); expect(info.services[slot].message, `services.${slot}.message`).toContain(slot); } }); + it('reports no entry at all for the retired workflow slot (#4451)', async () => { + const info = await dispatcher.getDiscoveryInfo('/api/v1'); + expect(info.services).not.toHaveProperty('workflow'); + expect(info.routes).not.toHaveProperty('workflow'); + expect(info.features).not.toHaveProperty('workflow'); + }); + it('never emits the old slot-name-derived template', async () => { const info = await dispatcher.getDiscoveryInfo('/api/v1'); for (const [slot, entry] of Object.entries(info.services as Record)) { @@ -2520,15 +2528,17 @@ describe('HttpDispatcher', () => { }); it('keeps reporting unmarked services as available', async () => { + // Was pinned on `workflow` until that slot retired (#4451, v17); + // `auth` exercises the same unmarked-service path. (kernel as any).getService = vi.fn().mockImplementation((name: string) => { - if (name === 'workflow') return { getConfig: vi.fn() }; + if (name === 'auth') return { validateToken: vi.fn() }; return null; }); const info = await dispatcher.getDiscoveryInfo('/api/v1'); - expect(info.services.workflow.enabled).toBe(true); - expect(info.services.workflow.status).toBe('available'); - expect(info.services.workflow.handlerReady).toBe(true); + expect(info.services.auth.enabled).toBe(true); + expect(info.services.auth.status).toBe('available'); + expect(info.services.auth.handlerReady).toBe(true); }); // ── The `metadata` slot: computed, not hardcoded (#4089) ────────────── diff --git a/packages/runtime/src/http-dispatcher.ts b/packages/runtime/src/http-dispatcher.ts index 88a90aefb9..23887a75d9 100644 --- a/packages/runtime/src/http-dispatcher.ts +++ b/packages/runtime/src/http-dispatcher.ts @@ -876,7 +876,7 @@ export class HttpDispatcher { // that request handlers (handleI18n, handleAuth, …) use. const [ authSvc, searchSvc, realtimeSvc, filesSvc, - analyticsSvc, workflowSvc, aiSvc, notificationSvc, i18nSvc, + analyticsSvc, aiSvc, notificationSvc, i18nSvc, protocolSvc, automationSvc, cacheSvc, queueSvc, jobSvc, mcpSvc, metadataSvc, dataSvc, ] = await Promise.all([ @@ -885,7 +885,6 @@ export class HttpDispatcher { this.resolveService(CoreServiceName.enum.realtime), this.resolveService(CoreServiceName.enum['file-storage']), this.resolveService(CoreServiceName.enum.analytics), - this.resolveService(CoreServiceName.enum.workflow), this.resolveService(CoreServiceName.enum.ai), this.resolveService(CoreServiceName.enum.notification), this.resolveService(CoreServiceName.enum.i18n), @@ -943,7 +942,6 @@ export class HttpDispatcher { const automationRegistered = !!automationSvc; const hasFiles = isServiceServeable(filesSvc); const hasAnalytics = isServiceServeable(analyticsSvc); - const hasWorkflow = !!workflowSvc; const hasAi = isServiceServeable(aiSvc); const hasNotification = isServiceServeable(notificationSvc); const hasI18n = isServiceServeable(i18nSvc); @@ -977,7 +975,9 @@ export class HttpDispatcher { storage: hasFiles ? `${prefix}/storage` : undefined, analytics: hasAnalytics ? `${prefix}/analytics` : undefined, automation: hasAutomation ? `${prefix}/automation` : undefined, - workflow: hasWorkflow ? `${prefix}/workflow` : undefined, + // `workflow` removed (#4451, v17): the slot retired — nothing + // ever registered it and this dispatcher never had a /workflow + // branch, so the advertisement could never come true. // Never advertised (ADR-0076 D12, #2462): service-realtime is an // in-process pub/sub bus — the dispatcher has no /realtime branch // and no plugin mounts one, so an advertised route would 404. @@ -1090,7 +1090,6 @@ export class HttpDispatcher { files: hasFiles, analytics: hasAnalytics, ai: hasAi, - workflow: hasWorkflow, notifications: hasNotification, i18n: hasI18n, }, @@ -1181,7 +1180,8 @@ export class HttpDispatcher { enabled: false, status: 'unavailable' as const, handlerReady: false, message: serviceUnavailableMessage('ui'), }, - workflow: hasWorkflow ? svcAvailable(routes.workflow, undefined, workflowSvc) : svcUnavailable('workflow'), + // `workflow` entry removed (#4451, v17) with the slot itself — + // it could only ever report `unavailable`. // Honest entry (ADR-0076 D12, #2462): the registered realtime // service is an in-process event bus with NO mounted HTTP/WS // surface — report it degraded with handlerReady:false (or as diff --git a/packages/spec/api-surface.json b/packages/spec/api-surface.json index e45d9a700c..b8a2cdca4a 100644 --- a/packages/spec/api-surface.json +++ b/packages/spec/api-surface.json @@ -2785,14 +2785,6 @@ "GetViewRequestSchema (const)", "GetViewResponse (type)", "GetViewResponseSchema (const)", - "GetWorkflowConfigRequest (type)", - "GetWorkflowConfigRequestSchema (const)", - "GetWorkflowConfigResponse (type)", - "GetWorkflowConfigResponseSchema (const)", - "GetWorkflowStateRequest (type)", - "GetWorkflowStateRequestSchema (const)", - "GetWorkflowStateResponse (type)", - "GetWorkflowStateResponseSchema (const)", "HandlerStatus (type)", "HandlerStatusSchema (const)", "HttpFindQueryParamsSchema (const)", @@ -3243,13 +3235,6 @@ "WebhookEventSchema (const)", "WellKnownCapabilities (type)", "WellKnownCapabilitiesSchema (const)", - "WorkflowProtocol (interface)", - "WorkflowState (type)", - "WorkflowStateSchema (const)", - "WorkflowTransitionRequest (type)", - "WorkflowTransitionRequestSchema (const)", - "WorkflowTransitionResponse (type)", - "WorkflowTransitionResponseSchema (const)", "envelopeViolations (function)", "getAuthEndpointUrl (function)", "getDefaultRouteRegistrations (function)", @@ -3808,7 +3793,6 @@ "IStorageService (interface)", "ITeamGraphService (interface)", "ITenantRouter (interface)", - "IWorkflowService (interface)", "ImportObjectOpts (interface)", "ImportObjectResult (interface)", "InboxListResult (interface)", @@ -3944,9 +3928,6 @@ "UploadArtifactResult (interface)", "UserModelMessage (type)", "ValidationResult (interface)", - "WorkflowStatus (interface)", - "WorkflowTransition (interface)", - "WorkflowTransitionResult (interface)", "WriteObservabilityOptions (interface)" ], "./integration": [ diff --git a/packages/spec/authorable-surface.json b/packages/spec/authorable-surface.json index 1309c98404..a5778751aa 100644 --- a/packages/spec/authorable-surface.json +++ b/packages/spec/authorable-surface.json @@ -589,7 +589,6 @@ "api/ApiRoutes:realtime", "api/ApiRoutes:storage", "api/ApiRoutes:ui", - "api/ApiRoutes:workflow", "api/ApiTestCollection:description", "api/ApiTestCollection:folders", "api/ApiTestCollection:name", @@ -1205,14 +1204,6 @@ "api/GetViewRequest:viewId", "api/GetViewResponse:object", "api/GetViewResponse:view", - "api/GetWorkflowConfigRequest:object", - "api/GetWorkflowConfigResponse:object", - "api/GetWorkflowConfigResponse:workflows", - "api/GetWorkflowStateRequest:object", - "api/GetWorkflowStateRequest:recordId", - "api/GetWorkflowStateResponse:object", - "api/GetWorkflowStateResponse:recordId", - "api/GetWorkflowStateResponse:state", "api/HttpFindQueryParams:count", "api/HttpFindQueryParams:distinct [RETIRED]", "api/HttpFindQueryParams:expand", @@ -2121,18 +2112,6 @@ "api/WellKnownCapabilities:export", "api/WellKnownCapabilities:search", "api/WellKnownCapabilities:transactionalBatch", - "api/WorkflowState:availableTransitions", - "api/WorkflowState:currentState", - "api/WorkflowState:history", - "api/WorkflowTransitionRequest:comment", - "api/WorkflowTransitionRequest:data", - "api/WorkflowTransitionRequest:object", - "api/WorkflowTransitionRequest:recordId", - "api/WorkflowTransitionRequest:transition", - "api/WorkflowTransitionResponse:object", - "api/WorkflowTransitionResponse:recordId", - "api/WorkflowTransitionResponse:state", - "api/WorkflowTransitionResponse:success", "automation/ActionDescriptor:aliasOf", "automation/ActionDescriptor:category", "automation/ActionDescriptor:configSchema", diff --git a/packages/spec/json-schema.manifest.json b/packages/spec/json-schema.manifest.json index 991c7e8330..06de9b650f 100644 --- a/packages/spec/json-schema.manifest.json +++ b/packages/spec/json-schema.manifest.json @@ -1,5 +1,5 @@ { - "description": "Ratchet manifest of every JSON Schema emitted by scripts/build-schemas.ts. Auto-appended when new schemas are added (commit the change). A listed schema that a build no longer emits fails gen:schema — remove a key ONLY for a deliberate retirement. See #2978.", + "description": "Ratchet manifest of every JSON Schema emitted by scripts/build-schemas.ts. Auto-appended when new schemas are added (commit the change). A listed schema that a build no longer emits fails gen:schema \u2014 remove a key ONLY for a deliberate retirement. See #2978.", "schemas": [ "ai/AIModelConfig", "ai/AIUsageRecord", @@ -272,10 +272,6 @@ "api/GetUiViewResponse", "api/GetViewRequest", "api/GetViewResponse", - "api/GetWorkflowConfigRequest", - "api/GetWorkflowConfigResponse", - "api/GetWorkflowStateRequest", - "api/GetWorkflowStateResponse", "api/HandlerStatus", "api/HttpFindQueryParams", "api/HttpMethod", @@ -499,9 +495,6 @@ "api/WebhookConfig", "api/WebhookEvent", "api/WellKnownCapabilities", - "api/WorkflowState", - "api/WorkflowTransitionRequest", - "api/WorkflowTransitionResponse", "automation/ActionCategory", "automation/ActionDescriptor", "automation/ActionParadigm", diff --git a/packages/spec/spec-changes.json b/packages/spec/spec-changes.json index af4642b007..fb5df1b4ac 100644 --- a/packages/spec/spec-changes.json +++ b/packages/spec/spec-changes.json @@ -353,6 +353,13 @@ "migrationId": "query-distinct-retired", "toMajor": 17, "rationale": "The `distinct` flag promised SELECT DISTINCT and no driver ever rendered it — but it was MIS-WIRED rather than merely dead (the harsher ADR-0078 class): the REST list path treated a distinct query as not countable and silently degraded `total`/`hasMore` to a page-local estimate, so the caller got duplicate rows AND worse pagination metadata, and a side effect that \"confirmed\" the flag was doing something. It had a shipped public producer (`QueryBuilder.distinct()`, removed with the key). The count suppression is deleted in the same change — `total` is truthful for those queries again. A REQUEST surface, never stored; nothing to rewrite. ADR-0049 / ADR-0078, #4286." + }, + { + "surface": "CoreServiceName 'workflow' / IWorkflowService / WorkflowProtocol / discovery routes.workflow / RestApiRouteCategory workflow", + "replacement": "the live mechanisms the slot only ever pointed at: `state_machine` validation rules for record state machines, approval flow nodes on the approvals runtime (ADR-0019) for approvals, lifecycle hooks + `record_change` flows (service-automation) for record-triggered automation", + "migrationId": "workflow-service-slot-retired", + "toMajor": 17, + "rationale": "The workflow slot was declared end to end and implemented nowhere: no code in either repository ever registered or resolved it (ADR-0115 Evidence 5 — the only touches were plugin-dev's retired stub probe and the generic discovery walk), no implementation of any WorkflowProtocol method ever existed, and no host ever mounted `/api/v1/workflow` (the pre-#3586 DEFAULT_DISPATCHER_ROUTES listed it among routes that never existed). Every part of it was ADR-0078's silently-inert declaration: a CoreServiceName nothing filled, a contract nothing implemented, a protocol nothing served, a discovery route field no builder could truthfully populate. These are TS/API surfaces and a discovery RESPONSE field — never stored in stack metadata, so there is no source for the chain to rewrite; consumers of the deleted types move their imports themselves. ADR-0049 / ADR-0078, #4451." } ], "removed": [] @@ -765,6 +772,13 @@ "migrationId": "query-distinct-retired", "toMajor": 17, "rationale": "The `distinct` flag promised SELECT DISTINCT and no driver ever rendered it — but it was MIS-WIRED rather than merely dead (the harsher ADR-0078 class): the REST list path treated a distinct query as not countable and silently degraded `total`/`hasMore` to a page-local estimate, so the caller got duplicate rows AND worse pagination metadata, and a side effect that \"confirmed\" the flag was doing something. It had a shipped public producer (`QueryBuilder.distinct()`, removed with the key). The count suppression is deleted in the same change — `total` is truthful for those queries again. A REQUEST surface, never stored; nothing to rewrite. ADR-0049 / ADR-0078, #4286." + }, + { + "surface": "CoreServiceName 'workflow' / IWorkflowService / WorkflowProtocol / discovery routes.workflow / RestApiRouteCategory workflow", + "replacement": "the live mechanisms the slot only ever pointed at: `state_machine` validation rules for record state machines, approval flow nodes on the approvals runtime (ADR-0019) for approvals, lifecycle hooks + `record_change` flows (service-automation) for record-triggered automation", + "migrationId": "workflow-service-slot-retired", + "toMajor": 17, + "rationale": "The workflow slot was declared end to end and implemented nowhere: no code in either repository ever registered or resolved it (ADR-0115 Evidence 5 — the only touches were plugin-dev's retired stub probe and the generic discovery walk), no implementation of any WorkflowProtocol method ever existed, and no host ever mounted `/api/v1/workflow` (the pre-#3586 DEFAULT_DISPATCHER_ROUTES listed it among routes that never existed). Every part of it was ADR-0078's silently-inert declaration: a CoreServiceName nothing filled, a contract nothing implemented, a protocol nothing served, a discovery route field no builder could truthfully populate. These are TS/API surfaces and a discovery RESPONSE field — never stored in stack metadata, so there is no source for the chain to rewrite; consumers of the deleted types move their imports themselves. ADR-0049 / ADR-0078, #4451." } ], "removed": [] diff --git a/packages/spec/src/api/discovery.zod.ts b/packages/spec/src/api/discovery.zod.ts index a926fef9e2..70511090a3 100644 --- a/packages/spec/src/api/discovery.zod.ts +++ b/packages/spec/src/api/discovery.zod.ts @@ -178,8 +178,10 @@ export const ApiRoutesSchema = lazySchema(() => z.object({ /** Base URL for Package Management */ packages: z.string().optional().describe('e.g. /api/v1/packages'), - /** Base URL for Workflow Engine */ - workflow: z.string().optional().describe('e.g. /api/v1/workflow'), + // `workflow` was removed here (#4451, v17): no host ever mounted a workflow + // surface and nothing ever registered the slot (ADR-0115 Evidence 5), so no + // builder could truthfully populate the field. State machines are enforced + // by the `state_machine` validation rule; approvals live below. /** Base URL for Approvals (ADR-0019: approval as a flow node) */ approvals: z.string().optional().describe('e.g. /api/v1/approvals'), diff --git a/packages/spec/src/api/plugin-rest-api.zod.ts b/packages/spec/src/api/plugin-rest-api.zod.ts index fecd761a81..d0e0073421 100644 --- a/packages/spec/src/api/plugin-rest-api.zod.ts +++ b/packages/spec/src/api/plugin-rest-api.zod.ts @@ -81,7 +81,9 @@ export const RestApiRouteCategory = z.enum([ 'permission', // Permission/authorization checks 'analytics', // Analytics and reporting 'automation', // Automation triggers and flows - 'workflow', // Workflow state management + // 'workflow' removed (#4451, v17): no workflow surface ever existed for a + // route to belong to (ADR-0115 Evidence 5); state machines are a validation + // rule, approvals are flow nodes. Routes in that space are 'automation'. 'ui', // UI metadata (views, layouts) 'realtime', // Realtime/WebSocket 'notification', // Notification management diff --git a/packages/spec/src/api/protocol.test.ts b/packages/spec/src/api/protocol.test.ts index 6990d26137..5533727535 100644 --- a/packages/spec/src/api/protocol.test.ts +++ b/packages/spec/src/api/protocol.test.ts @@ -28,12 +28,7 @@ import { GetObjectPermissionsRequestSchema, GetObjectPermissionsResponseSchema, GetEffectivePermissionsResponseSchema, - // Workflows - GetWorkflowConfigRequestSchema, - WorkflowStateSchema, - GetWorkflowStateRequestSchema, - WorkflowTransitionRequestSchema, - WorkflowTransitionResponseSchema, + // Workflow schemas removed with the retired slot (#4451, v17) // Realtime RealtimeConnectRequestSchema, RealtimeConnectResponseSchema, @@ -219,27 +214,9 @@ describe('ObjectStack Protocol', () => { } }); - it('validates Workflow operations', () => { - expect(GetWorkflowConfigRequestSchema.safeParse({ object: 'lead' }).success).toBe(true); - const state = { - currentState: 'open', - availableTransitions: [ - { name: 'approve', targetState: 'approved', label: 'Approve', requiresApproval: true }, - ], - history: [{ - fromState: 'draft', toState: 'open', action: 'submit', - userId: 'u1', timestamp: '2024-01-15T10:00:00Z', - }], - }; - expect(WorkflowStateSchema.safeParse(state).success).toBe(true); - expect(GetWorkflowStateRequestSchema.safeParse({ object: 'lead', recordId: 'l1' }).success).toBe(true); - expect(WorkflowTransitionRequestSchema.safeParse({ - object: 'lead', recordId: 'l1', transition: 'approve', comment: 'Looks good', - }).success).toBe(true); - expect(WorkflowTransitionResponseSchema.safeParse({ - object: 'lead', recordId: 'l1', success: true, state, - }).success).toBe(true); - }); + // 'validates Workflow operations' removed (#4451, v17): the workflow + // schemas were deleted with the retired slot — nothing ever implemented + // the protocol they described. it('validates Realtime operations', () => { expect(RealtimeConnectRequestSchema.safeParse({ diff --git a/packages/spec/src/api/protocol.zod.ts b/packages/spec/src/api/protocol.zod.ts index 33d3f5c62c..e8c46afaa1 100644 --- a/packages/spec/src/api/protocol.zod.ts +++ b/packages/spec/src/api/protocol.zod.ts @@ -21,7 +21,6 @@ import { } from './analytics.zod'; import { RealtimePresenceSchema, TransportProtocol } from './realtime.zod'; import { ObjectPermissionSchema, EffectiveObjectPermissionSchema, FieldPermissionSchema } from '../security/permission.zod'; -import { StateMachineSchema } from '../automation/state-machine.zod'; import { ActionDescriptorSchema } from '../automation/node-executor.zod'; import { TranslationDataSchema } from '../system/translation.zod'; import { @@ -684,67 +683,20 @@ export const GetEffectivePermissionsResponseSchema = lazySchema(() => z.object({ })); // ========================================== -// Workflow Operations +// Workflow Operations — REMOVED (#4451, v17) // ========================================== - -export const GetWorkflowConfigRequestSchema = lazySchema(() => z.object({ - object: z.string().describe('Object name to get workflow config for'), -})); - -export const GetWorkflowConfigResponseSchema = lazySchema(() => z.object({ - object: z.string().describe('Object name'), - workflows: z.array(StateMachineSchema).describe('Active state-machine workflows for this object'), -})); - -export const WorkflowStateSchema = lazySchema(() => z.object({ - currentState: z.string().describe('Current workflow state name'), - availableTransitions: z.array(z.object({ - name: z.string().describe('Transition name'), - targetState: z.string().describe('Target state after transition'), - label: z.string().optional().describe('Display label'), - requiresApproval: z.boolean().default(false).describe('Whether transition requires approval'), - })).describe('Available transitions from current state'), - history: z.array(z.object({ - fromState: z.string().describe('Previous state'), - toState: z.string().describe('New state'), - action: z.string().describe('Action that triggered the transition'), - userId: z.string().describe('User who performed the action'), - timestamp: z.string().datetime().describe('When the transition occurred'), - comment: z.string().optional().describe('Optional comment'), - })).optional().describe('State transition history'), -})); - -export const GetWorkflowStateRequestSchema = lazySchema(() => z.object({ - object: z.string().describe('Object name'), - recordId: z.string().describe('Record ID to get workflow state for'), -})); - -export const GetWorkflowStateResponseSchema = lazySchema(() => z.object({ - object: z.string().describe('Object name'), - recordId: z.string().describe('Record ID'), - state: WorkflowStateSchema.describe('Current workflow state and available transitions'), -})); - -export const WorkflowTransitionRequestSchema = lazySchema(() => z.object({ - object: z.string().describe('Object name'), - recordId: z.string().describe('Record ID'), - transition: z.string().describe('Transition name to execute'), - comment: z.string().optional().describe('Optional comment for the transition'), - data: z.record(z.string(), z.unknown()).optional().describe('Additional data for the transition'), -})); - -export const WorkflowTransitionResponseSchema = lazySchema(() => z.object({ - object: z.string().describe('Object name'), - recordId: z.string().describe('Record ID'), - success: z.boolean().describe('Whether the transition succeeded'), - state: WorkflowStateSchema.describe('New workflow state after transition'), -})); - -// ADR-0019: approval is no longer a workflow step. The approve/reject surface -// moved off `workflow` onto the dedicated approvals runtime (a flow's Approval -// node opens a request and suspends; a decision resumes it). Decisions are -// recorded via `POST /approvals/requests/:id/{approve,reject}`, not on a -// workflow record. `workflow` is reclaimed for state machines (transitions). +// The Get/WorkflowState/Config/Transition schemas and the `WorkflowProtocol` +// interface were deleted: no code ever implemented any of the three methods, +// nothing ever registered the `workflow` service slot they notionally fronted +// (ADR-0115 Evidence 5 — "no code in this repository resolves either slot", +// verified across both repositories), and no HTTP surface ever mounted +// `/api/v1/workflow` (the pre-#3586 DEFAULT_DISPATCHER_ROUTES listed it among +// routes that never existed). The capability the wrappers promised is live +// elsewhere: record state machines are enforced by the `state_machine` +// validation rule (`StateMachineSchema` stays authorable on the object), +// approvals are first-class flow nodes on the approvals runtime (ADR-0019 — +// decisions via `POST /approvals/requests/:id/{approve,reject}`), and +// record-triggered automation is lifecycle hooks + `record_change` flows. // ========================================== // Realtime Operations @@ -1312,14 +1264,8 @@ export type GetObjectPermissionsResponse = z.infer; export type GetEffectivePermissionsResponse = z.infer; -// Workflow Types -export type GetWorkflowConfigRequest = z.input; -export type GetWorkflowConfigResponse = z.infer; -export type WorkflowState = z.infer; -export type GetWorkflowStateRequest = z.input; -export type GetWorkflowStateResponse = z.infer; -export type WorkflowTransitionRequest = z.input; -export type WorkflowTransitionResponse = z.infer; +// Workflow Types — removed with the schemas (#4451, v17); see the block comment +// at "Workflow Operations — REMOVED" above. // Realtime Types export type RealtimeConnectRequest = z.input; @@ -1498,12 +1444,9 @@ export interface PermissionProtocol { getEffectivePermissions?(request: GetEffectivePermissionsRequest): Promise; } -/** Workflows (optional). */ -export interface WorkflowProtocol { - getWorkflowConfig?(request: GetWorkflowConfigRequest): Promise; - getWorkflowState?(request: GetWorkflowStateRequest): Promise; - workflowTransition?(request: WorkflowTransitionRequest): Promise; -} +// `WorkflowProtocol` (optional sub-protocol) was removed here (#4451, v17): +// no implementation of any of its three methods ever existed. See the +// "Workflow Operations — REMOVED" block comment above. /** Realtime / presence (optional). */ export interface RealtimeProtocol { diff --git a/packages/spec/src/api/router.zod.ts b/packages/spec/src/api/router.zod.ts index ffadc9cb56..37dcff7db0 100644 --- a/packages/spec/src/api/router.zod.ts +++ b/packages/spec/src/api/router.zod.ts @@ -90,7 +90,8 @@ export const RouterConfigSchema = lazySchema(() => z.object({ storage: z.string().default('/storage').describe('Storage Protocol'), analytics: z.string().default('/analytics').describe('Analytics Protocol'), ui: z.string().default('/ui').describe('UI Metadata Protocol (Views, Layouts)'), - workflow: z.string().default('/workflow').describe('Workflow Engine Protocol'), + // `workflow` mount removed (#4451, v17): no workflow surface ever existed + // to mount (ADR-0115 Evidence 5). realtime: z.string().default('/realtime').describe('Realtime/WebSocket Protocol'), notifications: z.string().default('/notifications').describe('Notification Protocol'), ai: z.string().default('/ai').describe('AI Engine Protocol (NLQ, Chat, Suggest)'), @@ -104,7 +105,6 @@ export const RouterConfigSchema = lazySchema(() => z.object({ storage: '/storage', analytics: '/analytics', ui: '/ui', - workflow: '/workflow', realtime: '/realtime', notifications: '/notifications', ai: '/ai', diff --git a/packages/spec/src/contracts/core-service-contracts.test.ts b/packages/spec/src/contracts/core-service-contracts.test.ts index 0c4049f0c2..340358a255 100644 --- a/packages/spec/src/contracts/core-service-contracts.test.ts +++ b/packages/spec/src/contracts/core-service-contracts.test.ts @@ -29,10 +29,11 @@ describe('CoreServiceName → contract map (#4127)', () => { // The map's keys are checked against the enum at compile time below; // this asserts the same thing at runtime so a rename shows up as a // failing test and not only as a type error in an unrelated package. + // ('workflow' left both lists with its slot, #4451 v17.) const mapped: Array = [ 'metadata', 'data', 'auth', 'file-storage', 'search', 'cache', 'queue', 'automation', 'analytics', 'realtime', 'job', 'notification', 'ai', - 'i18n', 'workflow', + 'i18n', ]; const slots = new Set(CoreServiceName.options); for (const key of mapped) { @@ -44,7 +45,7 @@ describe('CoreServiceName → contract map (#4127)', () => { const mapped = new Set([ 'metadata', 'data', 'auth', 'file-storage', 'search', 'cache', 'queue', 'automation', 'analytics', 'realtime', 'job', 'notification', 'ai', - 'i18n', 'workflow', + 'i18n', ]); const unmapped = CoreServiceName.options.filter((s) => !mapped.has(s)); // `ui` has a slot and a serving domain but no `IUiService` — mapping it diff --git a/packages/spec/src/contracts/core-service-contracts.ts b/packages/spec/src/contracts/core-service-contracts.ts index f0d9473028..6afaf338bf 100644 --- a/packages/spec/src/contracts/core-service-contracts.ts +++ b/packages/spec/src/contracts/core-service-contracts.ts @@ -37,7 +37,6 @@ import type { IJobService } from './job-service'; import type { INotificationService } from './notification-service'; import type { IAIService } from './ai-service'; import type { II18nService } from './i18n-service'; -import type { IWorkflowService } from './workflow-service'; import type { ISecurityService } from './security-service'; import type { IShareLinkService } from './share-link-service'; import type { IHttpServer } from './http-server'; @@ -79,7 +78,8 @@ export interface CoreServiceContracts { ai: IAIService; /** `service-i18n`, or the `app-plugin` in-memory fallback (#4143). */ i18n: II18nService; - workflow: IWorkflowService; + // `workflow: IWorkflowService` removed with the slot (#4451, v17) — no + // implementation ever existed, so there was no evidenced binding here. } /** diff --git a/packages/spec/src/contracts/index.ts b/packages/spec/src/contracts/index.ts index 12d6d355a6..be0b3d0187 100644 --- a/packages/spec/src/contracts/index.ts +++ b/packages/spec/src/contracts/index.ts @@ -31,7 +31,9 @@ export * from './job-service.js'; export * from './ai-service.js'; export * from './llm-adapter.js'; export * from './i18n-service.js'; -export * from './workflow-service.js'; +// './workflow-service.js' removed (#4451, v17): IWorkflowService had no +// implementation and no `getService('workflow')` call site anywhere +// (ADR-0115 Evidence 5); the slot retired with it. // CoreServiceName → contract map (#4127). Lets a slot lookup return the slot's // contract instead of `any`, so a call outside it is a compile error. diff --git a/packages/spec/src/contracts/workflow-service.test.ts b/packages/spec/src/contracts/workflow-service.test.ts deleted file mode 100644 index 9050be560b..0000000000 --- a/packages/spec/src/contracts/workflow-service.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import type { IWorkflowService, WorkflowTransition, WorkflowTransitionResult, WorkflowStatus } from './workflow-service'; - -describe('Workflow Service Contract', () => { - it('should allow a minimal IWorkflowService implementation with required methods', () => { - const service: IWorkflowService = { - transition: async (_transition) => ({ success: true, currentState: 'approved' }), - getStatus: async (_object, _recordId) => ({ - recordId: '1', object: 'order', currentState: 'draft', availableTransitions: [], - }), - }; - - expect(typeof service.transition).toBe('function'); - expect(typeof service.getStatus).toBe('function'); - }); - - it('should allow a full implementation with optional methods', () => { - const service: IWorkflowService = { - transition: async () => ({ success: true }), - getStatus: async () => ({ - recordId: '1', object: 'order', currentState: 'draft', availableTransitions: [], - }), - getHistory: async () => [], - }; - - expect(service.getHistory).toBeDefined(); - }); - - it('should transition a record to a new state', async () => { - const states = new Map(); - states.set('order:ord-1', 'draft'); - - const allowedTransitions: Record = { - draft: ['submitted'], - submitted: ['approved', 'rejected'], - approved: ['completed'], - }; - - const service: IWorkflowService = { - transition: async (t): Promise => { - const key = `${t.object}:${t.recordId}`; - const current = states.get(key); - if (!current) return { success: false, error: 'Record not found' }; - - const allowed = allowedTransitions[current] ?? []; - if (!allowed.includes(t.targetState)) { - return { success: false, error: `Cannot transition from ${current} to ${t.targetState}` }; - } - - states.set(key, t.targetState); - return { success: true, currentState: t.targetState }; - }, - getStatus: async (object, recordId) => { - const key = `${object}:${recordId}`; - const currentState = states.get(key) ?? 'unknown'; - return { - recordId, object, currentState, - availableTransitions: allowedTransitions[currentState] ?? [], - }; - }, - }; - - const result = await service.transition({ - recordId: 'ord-1', - object: 'order', - targetState: 'submitted', - comment: 'Ready for review', - }); - - expect(result.success).toBe(true); - expect(result.currentState).toBe('submitted'); - - const status = await service.getStatus('order', 'ord-1'); - expect(status.currentState).toBe('submitted'); - expect(status.availableTransitions).toContain('approved'); - }); - - it('should reject invalid transitions', async () => { - const service: IWorkflowService = { - transition: async (t): Promise => ({ - success: false, - error: `Cannot transition to ${t.targetState}`, - }), - getStatus: async () => ({ - recordId: '1', object: 'order', currentState: 'draft', - availableTransitions: ['submitted'], - }), - }; - - const result = await service.transition({ - recordId: '1', - object: 'order', - targetState: 'completed', - }); - - expect(result.success).toBe(false); - expect(result.error).toContain('completed'); - }); - - it('should return transition history', async () => { - const service: IWorkflowService = { - transition: async () => ({ success: true }), - getStatus: async () => ({ - recordId: '1', object: 'order', currentState: 'approved', - availableTransitions: ['completed'], - }), - getHistory: async () => [ - { fromState: 'draft', toState: 'submitted', userId: 'u1', timestamp: '2025-01-01T00:00:00Z' }, - { fromState: 'submitted', toState: 'approved', userId: 'u2', comment: 'LGTM', timestamp: '2025-01-02T00:00:00Z' }, - ], - }; - - const history = await service.getHistory!('order', 'ord-1'); - expect(history).toHaveLength(2); - expect(history[0].fromState).toBe('draft'); - expect(history[1].comment).toBe('LGTM'); - }); - - it('should get workflow status with available transitions', async () => { - const service: IWorkflowService = { - transition: async () => ({ success: true }), - getStatus: async (_object, _recordId): Promise => ({ - recordId: 'ord-1', - object: 'order', - currentState: 'submitted', - availableTransitions: ['approved', 'rejected'], - }), - }; - - const status = await service.getStatus('order', 'ord-1'); - expect(status.currentState).toBe('submitted'); - expect(status.availableTransitions).toEqual(['approved', 'rejected']); - }); -}); diff --git a/packages/spec/src/contracts/workflow-service.ts b/packages/spec/src/contracts/workflow-service.ts deleted file mode 100644 index d5ef04e5e3..0000000000 --- a/packages/spec/src/contracts/workflow-service.ts +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. - -/** - * IWorkflowService - Workflow State Machine Service Contract - * - * Defines the interface for workflow state management and approval processes - * in ObjectStack. Concrete implementations (state machine engines, BPMN, etc.) - * should implement this interface. - * - * Follows Dependency Inversion Principle - plugins depend on this interface, - * not on concrete workflow engine implementations. - * - * Aligned with CoreServiceName 'workflow' in core-services.zod.ts. - */ - -/** - * A state transition request - */ -export interface WorkflowTransition { - /** Record identifier */ - recordId: string; - /** Object name the record belongs to */ - object: string; - /** Target state to transition to */ - targetState: string; - /** Optional comment for the transition */ - comment?: string; - /** User performing the transition */ - userId?: string; -} - -/** - * Result of a transition attempt - */ -export interface WorkflowTransitionResult { - /** Whether the transition succeeded */ - success: boolean; - /** The new current state (if success) */ - currentState?: string; - /** Error or rejection reason (if failure) */ - error?: string; -} - -/** - * Status of a workflow instance - */ -export interface WorkflowStatus { - /** Record identifier */ - recordId: string; - /** Object name */ - object: string; - /** Current state */ - currentState: string; - /** Available transitions from the current state */ - availableTransitions: string[]; -} - -export interface IWorkflowService { - /** - * Transition a record to a new workflow state - * @param transition - Transition request details - * @returns Transition result - */ - transition(transition: WorkflowTransition): Promise; - - /** - * Get the current workflow status of a record - * @param object - Object name - * @param recordId - Record identifier - * @returns Current workflow status with available transitions - */ - getStatus(object: string, recordId: string): Promise; - - /** - * Get transition history for a record - * @param object - Object name - * @param recordId - Record identifier - * @returns Array of historical transitions - */ - getHistory?(object: string, recordId: string): Promise>; -} diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index 702e0ab806..da0f4f62c1 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -752,6 +752,36 @@ const step17: MigrationStep = { + 'door. A query still carrying the key fails to parse with the removal prescription, ' + 'and the REST list response reports a real `total` for queries that used to send it.', }, + { + id: 'workflow-service-slot-retired', + surface: + "CoreServiceName 'workflow' / IWorkflowService / WorkflowProtocol / " + + 'discovery routes.workflow / RestApiRouteCategory workflow', + replacement: + 'the live mechanisms the slot only ever pointed at: `state_machine` validation rules ' + + 'for record state machines, approval flow nodes on the approvals runtime (ADR-0019) ' + + 'for approvals, lifecycle hooks + `record_change` flows (service-automation) for ' + + 'record-triggered automation', + reason: + 'The workflow slot was declared end to end and implemented nowhere: no code in either ' + + 'repository ever registered or resolved it (ADR-0115 Evidence 5 — the only touches ' + + 'were plugin-dev\'s retired stub probe and the generic discovery walk), no ' + + 'implementation of any WorkflowProtocol method ever existed, and no host ever ' + + 'mounted `/api/v1/workflow` (the pre-#3586 DEFAULT_DISPATCHER_ROUTES listed it among ' + + 'routes that never existed). Every part of it was ADR-0078\'s silently-inert ' + + 'declaration: a CoreServiceName nothing filled, a contract nothing implemented, a ' + + 'protocol nothing served, a discovery route field no builder could truthfully ' + + 'populate. These are TS/API surfaces and a discovery RESPONSE field — never stored ' + + 'in stack metadata, so there is no source for the chain to rewrite; consumers of the ' + + 'deleted types move their imports themselves. ADR-0049 / ADR-0078, #4451.', + acceptanceCriteria: + 'No import of IWorkflowService, WorkflowProtocol or the Get/WorkflowState/Config/' + + 'Transition types resolves; no code calls getService(\'workflow\') or reads ' + + 'discovery `routes.workflow` / `services.workflow`; record state machines, ' + + 'approvals and record-triggered automation go through the replacement mechanisms. ' + + 'Discovery output on a default boot is unchanged (the slot was always reported ' + + 'unavailable; now it is simply absent).', + }, ], }; diff --git a/packages/spec/src/system/core-service-provider.test.ts b/packages/spec/src/system/core-service-provider.test.ts index fc555db15e..d979eb90fb 100644 --- a/packages/spec/src/system/core-service-provider.test.ts +++ b/packages/spec/src/system/core-service-provider.test.ts @@ -22,17 +22,29 @@ describe('CORE_SERVICE_PROVIDER', () => { expect(CORE_SERVICE_PROVIDER['notification']).toBe('@objectstack/service-messaging'); }); + // ['workflow', 'graphql'] left this list in #4451 (v17): the workflow slot + // was retired outright, and graphql was never a CoreServiceName — its + // stray entry here named a surface the dispatcher had already removed. it('uses null — not a plausible name — where no package can be installed', () => { - for (const slot of ['ai', 'search', 'workflow', 'graphql']) { + for (const slot of ['ai', 'search']) { expect(CORE_SERVICE_PROVIDER[slot], `${slot} must name no installable package`).toBeNull(); } }); + it('carries no entry for retired or never-real slots (#4451)', () => { + for (const slot of ['workflow', 'graphql']) { + expect( + Object.prototype.hasOwnProperty.call(CORE_SERVICE_PROVIDER, slot), + `${slot} must have no entry`, + ).toBe(false); + } + }); + // `null` covers two different situations, and only one of them means // "nothing exists". Verified against objectstack-ai/cloud: nothing there - // registers search/workflow/graphql, but `@objectstack/service-ai` does - // register `ai` — it is simply `private: true`, so there is no package to - // install and no name that belongs in an "Install X" sentence. + // registers search, but `@objectstack/service-ai` does register `ai` — it + // is simply `private: true`, so there is no package to install and no + // name that belongs in an "Install X" sentence. it('still says something ships for a slot whose provider is real but uninstallable', () => { const ai = serviceUnavailableMessage('ai'); expect(ai).not.toMatch(/No implementation ships/); @@ -41,10 +53,8 @@ describe('CORE_SERVICE_PROVIDER', () => { // Still not an instruction a reader could act on and fail at. expect(ai).not.toMatch(/^Install /); - // The genuinely-empty slots keep the plain sentence. - for (const slot of ['search', 'workflow', 'graphql']) { - expect(serviceUnavailableMessage(slot), slot).toMatch(/No implementation ships/); - } + // The genuinely-empty slot keeps the plain sentence. + expect(serviceUnavailableMessage('search')).toMatch(/No implementation ships/); }); it('scopes every named package, so the remedy is copy-pasteable', () => { diff --git a/packages/spec/src/system/core-services.test.ts b/packages/spec/src/system/core-services.test.ts index a25ae1a6af..a83efa52a2 100644 --- a/packages/spec/src/system/core-services.test.ts +++ b/packages/spec/src/system/core-services.test.ts @@ -10,11 +10,12 @@ import { describe('CoreServiceName', () => { it('should accept all valid service names', () => { + // ('workflow' retired with its slot, #4451 v17.) const services = [ 'metadata', 'data', 'auth', 'file-storage', 'search', 'cache', 'queue', 'automation', 'analytics', 'realtime', - 'job', 'notification', 'ai', 'i18n', 'ui', 'workflow', + 'job', 'notification', 'ai', 'i18n', 'ui', ]; services.forEach((service) => { @@ -67,7 +68,8 @@ describe('ServiceRequirementDef', () => { expect(ServiceRequirementDef.notification).toBe('optional'); expect(ServiceRequirementDef.ai).toBe('optional'); expect(ServiceRequirementDef.ui).toBe('optional'); - expect(ServiceRequirementDef.workflow).toBe('optional'); + // `workflow` retired with its slot (#4451, v17). + expect(ServiceRequirementDef).not.toHaveProperty('workflow'); }); }); diff --git a/packages/spec/src/system/core-services.zod.ts b/packages/spec/src/system/core-services.zod.ts index ae7004c160..3ff7aa571a 100644 --- a/packages/spec/src/system/core-services.zod.ts +++ b/packages/spec/src/system/core-services.zod.ts @@ -38,7 +38,13 @@ export const CoreServiceName = z.enum([ 'ai', // AI Engine (NLQ, Chat, Suggest, Insights) 'i18n', // Internationalization Service 'ui', // UI Metadata Service (View CRUD) - 'workflow', // Workflow State Machine Engine + // `workflow` (Workflow State Machine Engine) retired in #4451 (v17): + // nothing ever registered or resolved the slot (ADR-0115 Evidence 5), no + // provider ships in either repository, and the capability is live elsewhere + // — `state_machine` validation rules, approval flow nodes (ADR-0019), + // lifecycle hooks + `record_change` flows (service-automation). + // (Backticks, not quotes: scripts/check-service-providers.mjs reads every + // quoted token in this block as an enum member, comments included.) ]); export type CoreServiceName = z.infer; @@ -94,10 +100,9 @@ export const CORE_SERVICE_PROVIDER: Readonly> = { // different situations — see REMEDY_DETAIL, which is how the second one still // gets an accurate message. // - // Nothing provides the slot at all: `search`, `workflow` (no consumer - // either — ADR-0115 Evidence 5) and `graphql` (a surface with no provider). - // Verified across BOTH repositories: nothing in `objectstack-ai/cloud` - // registers them. + // Nothing provides the slot at all: `search` (no consumer either — + // ADR-0115 Evidence 5). Verified across BOTH repositories: nothing in + // `objectstack-ai/cloud` registers it. // // A provider exists but cannot be installed: `ai`. `@objectstack/service-ai` // registers this slot in `objectstack-ai/cloud` and is `private: true`, so @@ -105,10 +110,16 @@ export const CORE_SERVICE_PROVIDER: Readonly> = { // exact failure this table was written to end. It carries a REMEDY_DETAIL // sentence instead; a bare `null` here would tell a Cloud/Enterprise // deployment that nothing ships, which is false. + // + // Two entries left in #4451 (v17). `graphql` was never a `CoreServiceName` + // — it existed only here and in metadata-protocol's discovery table, naming + // a `/graphql` surface the dispatcher had already removed as out of the + // product plan (#2462 follow-on); this table's guard + // (`scripts/check-service-providers.mjs`) only checks that every SLOT has an + // entry, never that every entry is a slot, so the stray sat unchallenged. + // `workflow` retired with its slot — see the CoreServiceName note above. 'ai': null, 'search': null, - 'workflow': null, - 'graphql': null, } as const; /** @@ -205,7 +216,6 @@ export const ServiceRequirementDef = { notification: 'optional', ai: 'optional', ui: 'optional', - workflow: 'optional', } as const; // ==========================================