diff --git a/.changeset/27842-disabled-actions-served.md b/.changeset/27842-disabled-actions-served.md index 0d215c19ffc..06e160f4d25 100644 --- a/.changeset/27842-disabled-actions-served.md +++ b/.changeset/27842-disabled-actions-served.md @@ -12,4 +12,4 @@ Metadata-protocol: none - **Served beside the object** (`@objectstack/metadata-protocol`, `@objectstack/rest`). `GET /api/v1/meta/object/:name` serves `disabledActions` beside `item`: the names in `item.actions` that the deployment's activation ledger switches off. `GET /api/v1/meta/object` serves the same answer keyed by object name. The default cached door carries it too, read live on every call and kept out of the ETag, so a read that carries it is answered in full rather than `304 Not Modified`. The key is absent where no ledger answered (no engine, or an engine with no ledger attached), on a draft read, and on every type other than `object`. `item.actions` is unchanged. The protocol reads the engine's `getDisabledActions` through the kernel's service table by name and imports no engine. - **The fact it serves** (`@objectstack/objectql`). `ObjectQL.getDisabledActions()` returns the names the engine's activation projection switches off, the same projection both dispatch doors consult before they refuse a call, or `undefined` when no ledger is attached. `listDisabledActions()` is unchanged. -- **The refusal sentence** (`@objectstack/objectql`, `@objectstack/runtime`). A switched-off action is still refused 409 `ACTION_DISABLED` at the REST and MCP doors, and the message now reads: "This action is turned off for this deployment. An administrator can turn it on again under Setup › Packaged automation, or you can use another action." It no longer names the ledger table or the ADR. The engine writes those, with the code and the action's name, to an `info` log line each time it refuses one. +- **The refusal sentence** (`@objectstack/objectql`, `@objectstack/runtime`). A switched-off action is still refused 409 `ACTION_DISABLED` at the REST and MCP doors, and the message now reads: "This action is turned off for this deployment. An administrator can turn it on again under Setup › Packaged Actions, or you can use another action." It no longer names the ledger table or the ADR. The engine writes those, with the code and the action's name, to an `info` log line each time it refuses one. diff --git a/.changeset/27966-flow-disabled-plain-sentence.md b/.changeset/27966-flow-disabled-plain-sentence.md index c5f8c54c440..45945ea1093 100644 --- a/.changeset/27966-flow-disabled-plain-sentence.md +++ b/.changeset/27966-flow-disabled-plain-sentence.md @@ -9,5 +9,5 @@ fix: a flow an administrator switched off is refused with a plain sentence, the Clause-②: no Metadata-protocol: none -- **The flow refusal** (`@objectstack/service-automation`). A packaged flow switched off under Setup › Packaged automation is still refused `FLOW_DISABLED`, at every door that runs it: its trigger, a direct run, and a parent flow that calls it as a subflow. The message now reads: "This flow is turned off for this deployment. An administrator can turn it on again under Setup › Packaged automation, or copy it there under a new name." It no longer names the ledger table or the ADR. The engine writes those, with the code and the flow's name, to an `info` log line each time it refuses one. A flow disabled by its own status (`obsolete` / `invalid`) still reads "Flow '…' is disabled", so the two reasons stay distinguishable. -- **One sentence for both** (`@objectstack/types`, `@objectstack/objectql`). `describeTurnedOffPackagedItem('action' | 'flow')` builds the sentence. The action refusal (`ACTION_DISABLED`) reads from it too, and its wording is unchanged. Only the last clause differs: a flow can be copied under a new name, and an action cannot, so the action sentence still offers another action instead. +- **The flow refusal** (`@objectstack/service-automation`). A packaged flow an administrator switched off is still refused `FLOW_DISABLED`, at every door that runs it: its trigger, a direct run, and a parent flow that calls it as a subflow. The message now reads: "This flow is turned off for this deployment. An administrator can turn it on again in its package's Automations in Studio, or copy it there under a new name." It no longer names the ledger table or the ADR. The engine writes those, with the code and the flow's name, to an `info` log line each time it refuses one. A flow disabled by its own status (`obsolete` / `invalid`) still reads "Flow '…' is disabled", so the two reasons stay distinguishable. +- **One sentence for both** (`@objectstack/types`, `@objectstack/objectql`). `describeTurnedOffPackagedItem('action' | 'flow')` builds the sentence. The action refusal (`ACTION_DISABLED`) reads from it too, and its wording is unchanged. The two sentences differ in two clauses: where an administrator turns the item back on (an action under Setup › Packaged Actions, a flow in its package's Automations in Studio), and what can be done meanwhile (a flow can be copied under a new name, and an action cannot, so the action sentence offers another action instead). diff --git a/.changeset/29374-turned-off-sentence-names-live-pages.md b/.changeset/29374-turned-off-sentence-names-live-pages.md new file mode 100644 index 00000000000..1cae6030605 --- /dev/null +++ b/.changeset/29374-turned-off-sentence-names-live-pages.md @@ -0,0 +1,15 @@ +--- +'@objectstack/types': patch +--- + +fix: the switched-off packaged item sentence names where the switch is now, not the retired Setup › Packaged automation page + +Clause-②: no +Metadata-protocol: none + +`describeTurnedOffPackagedItem`, the sentence both refusal engines send when an administrator has switched a packaged action or flow off (`ACTION_DISABLED`, `FLOW_DISABLED`), named Setup › Packaged automation as the place to turn it on again. That page is retired: an action's switch is on Setup's Packaged Actions page, and a flow's switch, with New similar beside it, is on the flow in its package's Automations in Studio. The sentence now names each: + +- Action: "This action is turned off for this deployment. An administrator can turn it on again under Setup › Packaged Actions, or you can use another action." +- Flow: "This flow is turned off for this deployment. An administrator can turn it on again in its package's Automations in Studio, or copy it there under a new name." + +Words only. The envelope codes are unchanged, and a machine reads those, not the sentence. diff --git a/content/docs/data-modeling/analytics.mdx b/content/docs/data-modeling/analytics.mdx index 85d129298a0..40a4542822c 100644 --- a/content/docs/data-modeling/analytics.mdx +++ b/content/docs/data-modeling/analytics.mdx @@ -145,8 +145,8 @@ It saves an ordinary `dataset` draft in the package, published with the rest of the package, and the picker selects it. Every dataset written this way counts its records (a `count` measure), so a report or a chart can bind it at once. Nothing is derived without the author: there are no implicit datasets. Joins, -filters, date dimensions and derived measures are edited afterwards in -**Setup › Datasets**. +filters, date dimensions and derived measures are edited afterwards on the +dataset's page in Studio's **All metadata** area. While no dataset reads one of the package's objects, the Studio pickers say so beside the button, and they list the package's own datasets first. diff --git a/content/docs/permissions/access-recipes.mdx b/content/docs/permissions/access-recipes.mdx index 057b5544c86..10cddb322c9 100644 --- a/content/docs/permissions/access-recipes.mdx +++ b/content/docs/permissions/access-recipes.mdx @@ -47,7 +47,7 @@ definePermissionSet({ Every business app meets this shape: a contract's legal terms after approval, an order's amount after shipping, an invoice after posting, a purchase order's supplier after approval. The answer is a field permission with a **row condition**, `when`, on the permission set that should lose the edit. -**In the permission-set editor** (Setup → Access Control → Permission Sets, then open the set): +**In the permission-set editor** (Studio → **All metadata** → Permission Set, then open the set): 1. Expand the object (`clm_contract`) to show its field rows. 2. On each field row to lock (`governing_law`, `liability_cap`), set **Read** on and **Edit** off. diff --git a/content/docs/permissions/administrator-guide.mdx b/content/docs/permissions/administrator-guide.mdx index 090f5b2802a..702ebe4b64c 100644 --- a/content/docs/permissions/administrator-guide.mdx +++ b/content/docs/permissions/administrator-guide.mdx @@ -104,8 +104,11 @@ This is the daily 90%. An assignment is one row: **User Position** they hold it in. The anchor decides *where* the position's depth grants apply — "sales manager **of East**" sees East's records, not the whole company's. -From **Setup → Access Control → Positions**, open a position and add -assignments from its related list (or create User Position rows directly). +From **Setup → People & Organization → Position Holders**, pick a position +and add its holders. Holders added there carry no business-unit anchor; to +anchor an assignment to a unit, open the user's record page, go to its +**Positions** tab and choose **New**: the assignment form there takes the +position and the business unit. These writes are governed: tenant-level admins pass; a [delegated admin](/docs/permissions/delegated-administration) can only assign allowlisted sets inside their own subtree — self-escalation is structurally @@ -113,10 +116,10 @@ refused. Two adjacent surfaces complete the picture: -- **Direct grants** — on a permission set's record page - (**Setup → Access Control → Permission Sets**), the **Assigned Users** panel - adds per-user grants without a position. Rows held *via a position* are - shown but removed on the position, not here. +- **Direct grants** — **Setup → People & Organization → Permission Set + Assignment**: pick a permission set, and its **Assigned Users** panel adds + per-user grants without a position. Rows held *via a position* are shown but removed + on the position (Position Holders), not here. - **Business-unit membership** — from Step 2; independent of the assignment anchor. @@ -148,8 +151,8 @@ One rule of thumb: **capability ships with the apps you install — you rarely author a permission set by hand.** If you are in this section, you've confirmed no shipped position covers the need. -The permission-set **matrix editor** (Setup → Access Control → Permission Sets) -is a structured spreadsheet — you never hand-write JSON: +The permission-set **matrix editor** (Studio → **All metadata** → Permission +Set, then open the set) is a structured spreadsheet — you never hand-write JSON: - **Object grid** — per object: Create / Read / Update / Delete, lifecycle bits (Transfer / Restore / Purge), and View All / Modify All, with per-row @@ -209,7 +212,8 @@ sets can't be edited in place either, but they can be cloned and adjusted | Users, invitations | Setup → People & Organization → Users / Invitations | | Business-unit tree | Setup → People & Organization → Business Units | | Teams | Setup → People & Organization → Teams | -| Positions, permission sets, capabilities | Setup → Access Control | +| Positions, permission sets, capabilities: what they grant, and a position's or set's on/off switch | Studio → All metadata | +| Who holds a position, who holds a permission set | Setup → People & Organization → Position Holders / Permission Set Assignment | | Sharing rules, record shares | Setup → Access Control | | Password policy, MFA, lockout, SSO | Setup → Configuration → Authentication | | Company info, localization, branding | Setup → Configuration | diff --git a/content/docs/permissions/authorization.mdx b/content/docs/permissions/authorization.mdx index e3de008add8..eee8940bbe5 100644 --- a/content/docs/permissions/authorization.mdx +++ b/content/docs/permissions/authorization.mdx @@ -30,7 +30,7 @@ Authorization splits into three concerns that stay decoupled: 1. **Capability** — *what can be done* (`manage_users`, `export_data`). Defined by the platform (curated `PLATFORM_CAPABILITIES`) or by a package via [`defineCapability`](#package-capability-declaration-adr-0066-d1); extended - by admins in Setup. A capability is **not** a contract and has no `inputs` — a + by admins in Studio. A capability is **not** a contract and has no `inputs` — a resource merely *references* one by name (see Requirement, below). 2. **Assignment** — *who holds it* — permission sets and positions, which are catalog definitions served by name from the environment registry (a @@ -248,10 +248,11 @@ only what it owns: **live** (config); the sets a position distributes are its definition's `permissionSets`. It owns environment-authored sets and assignments — not package sets. The set **definition** has one authoritative store, the - metadata layer (ADR-0094 D1, ADR-0131 D3): Setup → Access Control → - Permission Sets opens the catalog list - (`/apps/setup/metadata/permission?scope=environment`), and a create or edit - there is a metadata save (`PUT /api/v1/meta/permission/:name`) into the + metadata layer (ADR-0094 D1, ADR-0131 D3): Studio's **All metadata** area + lists the catalog (`/studio/~all/metadata/permission?scope=environment`; + Setup carries no metadata catalog, and its old + `/apps/setup/metadata/permission` address redirects there), and a create or + edit there is a metadata save (`PUT /api/v1/meta/permission/:name`) into the environment's definition ledger (`sys_metadata`), served from the registry beside the packages' sets. No row mirrors it: the `sys_permission_set` object is no longer registered, so no data route serves one. The save @@ -416,8 +417,9 @@ discipline: **resolution-time filtering, fail-closed**, in No definition carries the switch, and no row does: the `active` column of the retired catalog row objects (`sys_permission_set.active`, `sys_position.active`) decides nothing, and neither object is registered any -more (ADR-0131 D3). Setup's active switch on the Positions and Permission Sets -catalog lists writes the ledger row, through the door below. +more (ADR-0131 D3). The on/off switch on a position's or a permission set's +page in Studio's **All metadata** area writes the ledger row, through the door +below. The ledger row is written through `POST /api/v1/security/_activation/:type/:name`, with `:type` either diff --git a/content/docs/permissions/capabilities.mdx b/content/docs/permissions/capabilities.mdx index 2e9220c786f..49add913a8b 100644 --- a/content/docs/permissions/capabilities.mdx +++ b/content/docs/permissions/capabilities.mdx @@ -121,7 +121,7 @@ export default defineStack({ | Field | Type | Required | Notes | |:--|:--|:--|:--| | `name` | `string` | ✅ | The contract. `^[a-z][a-z0-9_.]*$` — lowercase, digits, `_` and `.` | -| `label` | `string` | optional | Shown in Setup. Defaults to a humanized `name` | +| `label` | `string` | optional | Shown to the administrator who grants it. Defaults to a humanized `name` | | `description` | `string` | optional | What holding the capability permits | | `scope` | `'platform' \| 'org'` | optional | Defaults to `'platform'`. `org` = scoped to the caller's organization | | `packageId` | `string` | optional | Author-declared fallback provenance (ADR-0086 D3). Normally the registry stamps this for you | @@ -269,7 +269,7 @@ to store arbitrary JSON on the authorization surface. ## Checklist - [ ] Name is namespaced and lowercase, and does not shadow a platform capability -- [ ] `label` and `description` are written for the administrator who will grant it in Setup +- [ ] `label` and `description` are written for the administrator who will grant it - [ ] `scope` is `org` unless the privilege really is platform-wide - [ ] The declaration is reachable from the stack (`capabilities: [...]`) or a `*.capability.ts` file - [ ] No other installed package declares the same name — the registry refuses a second holder at boot diff --git a/content/docs/permissions/delegated-administration.mdx b/content/docs/permissions/delegated-administration.mdx index b24a57dbe82..4c5ad992fe5 100644 --- a/content/docs/permissions/delegated-administration.mdx +++ b/content/docs/permissions/delegated-administration.mdx @@ -52,7 +52,7 @@ Setup app ("You don't have access to this app"); read on `sys_user`, `sys_business_unit` and `sys_business_unit_member`, so the lookups can pick people and units; and CRUD on `sys_user_position`, the assignment rows themselves. They may sit on the scope's own set or on any other set the -delegate holds. In Setup's permission set editor, the Delegated Admin Scope +delegate holds. In the permission set editor (Studio's **All metadata** area), the Delegated Admin Scope block lists the five, marks which ones the set already grants, and adds the missing ones to the set in one click. diff --git a/docs/qa/platform-checklist/areas/api-backend.json b/docs/qa/platform-checklist/areas/api-backend.json index b1c14e233e2..6b8380c2ac8 100644 --- a/docs/qa/platform-checklist/areas/api-backend.json +++ b/docs/qa/platform-checklist/areas/api-backend.json @@ -1678,7 +1678,7 @@ "title": "A disabled packaged action is refused 409 ACTION_DISABLED at BOTH dispatch doors — after the capability gate (a 403 is never an oracle for the switch), before the param contract and the record load, durable across restart", "since": "v17", "status": "active", - "revision": 12, + "revision": 13, "priority": "P0", "surface": "api", "personas": [ @@ -1695,8 +1695,7 @@ ], "knownGaps": [ "The MCP door cannot be driven live with showcase_mark_done: the bridge's AI-exposure gate (actionAiExposureError, fail-closed on ai.exposed !== true) sits AHEAD of the activation consult, so a non-exposed action answers the exposure refusal regardless of its switch. showcase_portfolio_snapshot is the only stock ai.exposed action. The MCP transport IS drivable live on a stock boot: the boot banner prints `MCP: http://localhost:/api/v1/mcp`, and QA run #21056 drove POST /api/v1/mcp tools/call run_action against showcase_portfolio_snapshot (disabled → isError carrying code ACTION_DISABLED, status 409; re-enabled → ok). Score the MCP clause live. Where the endpoint is not mounted, record acceptance[4] blocked(environment), with the unit pin cited beside it: action-activation-dispatch.test.ts 'door 2' calls invokeBusinessAction in-process over a stubbed engine and metadata service, with no MCP transport and no HTTP, so it is evidence of that layer only, never the clause's pass (RUNNER rule 6, a pin scores only the layer it drives).", - "The record-load half of the ordering proof is scored from the DISABLED traces: code order plus the absence of any read/side effect. An enabled control on the same nonexistent recordId answers 404 RECORD_NOT_FOUND, because every door refuses a subject the caller-scope load did not deliver (refuseDeniedSubjectLoad, packages/runtime/src/action-execution.ts — the script/body door, the flow door and the declarative branch Mark Done now takes all call it; measured on a dogfood boot of #25468: enabled 404, disabled 409, same id). That 404 corroborates the order but is not the leg's evidence. The load itself is still caught (loadActionSubjectRecord), so a new-record or object-less action, which attempts no load, never 404s.", - "TRANSITIONAL — the remedy half of acceptance[0] and acceptance[6] (ADR-0146 D4/D11): the ACTION_DISABLED sentence still says an administrator can turn the action on again 'under Setup › Packaged automation' (describeTurnedOffPackagedItem('action'), packages/types/src/packaged-item-turned-off.ts). That page is retired: an action's switch is now Setup › Packaged actions (nav_packaged_actions, componentRef 'automation:packaged-actions'), and the old page's bookmark opens Studio (#28785, #28818). Both doors, the 409, the code and every ordering leg are unchanged, and the clauses keep the sentence as served at dfbe34d887, so a run still scores them on that text. The remedy naming a page that no longer exists is a product defect of the sentence's producer, carried in the #28579 slice-1 report's out_of_scope_findings; when the producer names Setup › Packaged actions, the 'Setup › Packaged automation remedy' spelling in acceptance[0] and [6] is re-pointed in the same change" + "The record-load half of the ordering proof is scored from the DISABLED traces: code order plus the absence of any read/side effect. An enabled control on the same nonexistent recordId answers 404 RECORD_NOT_FOUND, because every door refuses a subject the caller-scope load did not deliver (refuseDeniedSubjectLoad, packages/runtime/src/action-execution.ts — the script/body door, the flow door and the declarative branch Mark Done now takes all call it; measured on a dogfood boot of #25468: enabled 404, disabled 409, same id). That 404 corroborates the order but is not the leg's evidence. The load itself is still caught (loadActionSubjectRecord), so a new-record or object-less action, which attempts no load, never 404s." ] }, "steps": [ @@ -1711,9 +1710,9 @@ ], "acceptance": [ { - "clause": "the REST door refuses a disabled packaged action 409 ACTION_DISABLED and dispatches NOTHING: status 409, code ACTION_DISABLED (registered in the ADR-0112 ledger, error-code-ledger.zod.ts), message = the engine's own describeDisabledAction sentence, written for the end user: it says the action is turned off for this deployment and that an administrator can turn it on again under Setup › Packaged automation, and names no system table and no ADR (the engine's log line carries those) — and no side effect lands (mark_done's declared done/progress write never ran)", + "clause": "the REST door refuses a disabled packaged action 409 ACTION_DISABLED and dispatches NOTHING: status 409, code ACTION_DISABLED (registered in the ADR-0112 ledger, error-code-ledger.zod.ts), message = the engine's own describeDisabledAction sentence, written for the end user: it says the action is turned off for this deployment and that an administrator can turn it on again under Setup › Packaged Actions, and names no system table and no ADR (the engine's log line carries those) — and no side effect lands (mark_done's declared done/progress write never ran)", "oracle": "api", - "verify": "step-3 trace: 409 + code ACTION_DISABLED + the disabled-action sentence; follow-up reads show no task mutated. packaged-action-disabled-dispatch-live.dogfood.test.ts scores it over HTTP: the exact code, the sentence (first sentence \"This action is turned off for this deployment.\", the Setup › Packaged automation remedy, and no ledger table or ADR in it), and the real task re-read with done, progress and updated_at unchanged. packaged-activation-ledger-reach.dogfood.test.ts still asserts 409 and the code ACTION_DISABLED only", + "verify": "step-3 trace: 409 + code ACTION_DISABLED + the disabled-action sentence; follow-up reads show no task mutated. packaged-action-disabled-dispatch-live.dogfood.test.ts scores it over HTTP: the exact code, the sentence (first sentence \"This action is turned off for this deployment.\", the Setup › Packaged Actions remedy, and no ledger table or ADR in it), and the real task re-read with done, progress and updated_at unchanged. packaged-activation-ledger-reach.dogfood.test.ts still asserts 409 and the code ACTION_DISABLED only", "evidence": "the trace + the control re-reads" }, { @@ -1749,7 +1748,7 @@ { "clause": "a disabled `type: flow` action is refused by its OWN switch, ahead of the type branch — the refusal is 409 ACTION_DISABLED on the action's own route (POST /api/v1/actions/showcase_task/showcase_bulk_reassign) with the turned-off sentence of acceptance[0], which names no action (describeDisabled ignores the name, packages/objectql/src/action-activation.ts), independent of anything the target flow's own ledger row says, and the target flow is left untouched", "oracle": "api", - "verify": "step-8 on /actions/showcase_task/showcase_bulk_reassign: status 409, body.error.code ACTION_DISABLED and body.error.message the turned-off sentence (first sentence \"This action is turned off for this deployment.\", the Setup › Packaged automation remedy). Do not look for the action's name in it: the sentence names none, and the live pin's expectDisabledSentence ignores the name too. The automation service is never consulted for the run, so showcase_reassign_wizard records no run from it", + "verify": "step-8 on /actions/showcase_task/showcase_bulk_reassign: status 409, body.error.code ACTION_DISABLED and body.error.message the turned-off sentence (first sentence \"This action is turned off for this deployment.\", the Setup › Packaged Actions remedy). Do not look for the action's name in it: the sentence names none, and the live pin's expectDisabledSentence ignores the name too. The automation service is never consulted for the run, so showcase_reassign_wizard records no run from it", "evidence": "the trace" } ], @@ -1856,6 +1855,12 @@ "date": "2026-11-02", "change": "Re-measured against main after N-28/N-30 (#28818, #29264), at base dfbe34d887: the REST and MCP doors, the 409 ACTION_DISABLED, the D4 → activation → params → record order and the restart leg are all unchanged, so steps and clauses stand. fixtures.knownGaps[2] (new) marks the remedy half of acceptance[0] and [6] TRANSITIONAL: the served sentence (describeTurnedOffPackagedItem('action'), @objectstack/types) still names 'Setup › Packaged automation', which ADR-0146 D4/D11 retired; the action switch is Setup › Packaged actions. No clause is weakened: the sentence is still pinned as served. The wrong place is reported as a product defect for its own card, not fixed here", "ref": "#28579" + }, + { + "revision": 13, + "date": "2026-11-02", + "change": "The producer now names the live page: describeTurnedOffPackagedItem('action') (@objectstack/types) tells the reader an administrator can turn the action on again under Setup › Packaged Actions (nav_packaged_actions), not the retired Setup › Packaged automation page. acceptance[0] (clause and verify) and acceptance[6] (verify) re-point their remedy phrase to Setup › Packaged Actions, as fixtures.knownGaps[2] said they would, and that TRANSITIONAL entry is retired. The doors, the 409, the code and every ordering leg are unchanged; only the quoted remedy moved, and the live pin (packaged-action-disabled-dispatch-live.dogfood.test.ts expectDisabledSentence) asserts the same phrase", + "ref": "#29374" } ] }, diff --git a/docs/qa/platform-checklist/areas/automation.json b/docs/qa/platform-checklist/areas/automation.json index 1df7cd358dc..aa6b8bc9121 100644 --- a/docs/qa/platform-checklist/areas/automation.json +++ b/docs/qa/platform-checklist/areas/automation.json @@ -1877,7 +1877,7 @@ "title": "Disabling a packaged flow writes a durable sys_metadata_activation row that survives a cold restart with the trigger left unbound; re-enable UPDATES the row and restores firing", "since": "v17", "status": "active", - "revision": 9, + "revision": 10, "priority": "P0", "surface": "api", "personas": [ @@ -1889,9 +1889,6 @@ "a FILE-backed database — restart survival is structurally unreachable on the in-memory store; record the db path in the run env (the same requirement automation.durable-suspend-restart carries, and its cold-restart recipe — stop the process entirely, boot a second server over the SAME database file — is the one to reuse here)", "showcase_urgent_task_alert (examples/app-showcase/src/automation/flows/index.ts UrgentTaskAlertFlow) — a PACKAGED record_change flow (package com.example.showcase, examples/app-showcase/objectstack.config.ts) with NO packaged subflow callers, so its disable is never refused by the §7.3 guard this item does not test", "⚠️ export OS_PORT= as well as passing -p (RUNNER.md's connector self-URL standing fact) — the showcase's self-pinging connectors otherwise fail every unrelated flow run in the boot" - ], - "knownGaps": [ - "TRANSITIONAL — acceptance[1] and negative[3] (ADR-0146 D4/D11): the FLOW_DISABLED sentence this item pins verbatim still tells the reader to turn the flow on 'under Setup › Packaged automation, or copy it there under a new name' (describeTurnedOffPackagedItem('flow'), packages/types/src/packaged-item-turned-off.ts), and that page is retired: the flow's ledger switch and its New similar are on the flow in Studio's Automations pillar, and the old page's bookmark redirects there (#28785, #28818). The item drives only the API doors (toggle, trigger, _status, the data API), so nothing else in it moved. The clause keeps the sentence exactly as served at dfbe34d887, so a run still scores it PASS on that text; the remedy's wrong place is a product defect of the sentence's producer, carried in the #28579 slice-1 report's out_of_scope_findings and not re-pointed here. When the producer names the live door, acceptance[1].verify's quoted sentence and negative[3]'s remedy phrase are re-pointed to it in the same change" ] }, "steps": [ @@ -1914,9 +1911,9 @@ "evidence": "the row read (the four ledger columns, and the key set showing no tenant column) + the toggle response" }, { - "clause": "the FLOW_DISABLED refusal message says the flow was switched off by the deployment's packaged-automation switch, in plain words: triggering the disabled flow answers 409 FLOW_DISABLED whose message is the switched-off packaged flow sentence — the flow is turned off for this deployment, an administrator can turn it on again under Setup › Packaged automation, or copy it there under a new name — and names no system table and no ADR (the engine's info log line carries those)", + "clause": "the FLOW_DISABLED refusal message says the flow was switched off by the deployment's packaged-automation switch, in plain words: triggering the disabled flow answers 409 FLOW_DISABLED whose message is the switched-off packaged flow sentence — the flow is turned off for this deployment, an administrator can turn it on again in its package's Automations in Studio, or copy it there under a new name — and names no system table and no ADR (the engine's info log line carries those)", "oracle": "api", - "verify": "the step-5 error message is exactly \"This flow is turned off for this deployment. An administrator can turn it on again under Setup › Packaged automation, or copy it there under a new name.\" (engine.ts describeDisabledFlow, built by describeTurnedOffPackagedItem in @objectstack/types — the one sentence the switched-off action refusal also reads as) and contains neither 'sys_metadata_activation' nor an ADR number. ⛔ The CODE alone is NOT the oracle: ADR-0126 §7.2 deliberately REUSES FLOW_DISABLED for the ledger dimension (no new ADR-0112 entry), and a status-disabled flow answers the same 409 FLOW_DISABLED with the old bare \"Flow '' is disabled\" — the ledger-vs-status distinction rides the MESSAGE, so a verdict keyed on code alone cannot tell this item's dimension from the authoring one", + "verify": "the step-5 error message is exactly \"This flow is turned off for this deployment. An administrator can turn it on again in its package's Automations in Studio, or copy it there under a new name.\" (engine.ts describeDisabledFlow, built by describeTurnedOffPackagedItem in @objectstack/types — the one sentence the switched-off action refusal also reads as) and contains neither 'sys_metadata_activation' nor an ADR number. ⛔ The CODE alone is NOT the oracle: ADR-0126 §7.2 deliberately REUSES FLOW_DISABLED for the ledger dimension (no new ADR-0112 entry), and a status-disabled flow answers the same 409 FLOW_DISABLED with the old bare \"Flow '' is disabled\" — the ledger-vs-status distinction rides the MESSAGE, so a verdict keyed on code alone cannot tell this item's dimension from the authoring one", "evidence": "the full 409 body with the message quoted" }, { @@ -1942,7 +1939,7 @@ "a toggle answering 2xx with NO sys_metadata_activation row written is a FAIL on this boot — the engine has a legitimate ledger-less degraded mode, but it WARNS 'IN PROCESS ONLY … will NOT survive a restart' (engine.ts) and the showcase composition attaches the ledger, so silence plus no row means the durable write was skipped where it was promised", "a restart that re-arms the flow (enabled or bound true, or a run row from the post-restart create) is a FAIL against the whole point of ADR-0126 §7.2 — it is the retired flowEnabled map's behaviour reappearing", "a re-enable that DELETES the row is a FAIL of the row contract even though firing resumes — absence-means-active makes deletion look equivalent right up until something needs the recorded choice", - "a ledger-disabled flow's trigger answered 409 FLOW_DISABLED with the status dimension's bare sentence (\"Flow 'showcase_urgent_task_alert' is disabled\", with no 'turned off for this deployment' sentence and no Setup › Packaged automation remedy is a FAIL: the refusal then reports the administrator's ledger choice as an authoring status, and the code alone cannot tell the two apart, because ADR-0126 §7.2 shares it between them by design" + "a ledger-disabled flow's trigger answered 409 FLOW_DISABLED with the status dimension's bare sentence (\"Flow 'showcase_urgent_task_alert' is disabled\", with no 'turned off for this deployment' sentence and no Studio Automations remedy is a FAIL: the refusal then reports the administrator's ledger choice as an authoring status, and the code alone cannot tell the two apart, because ADR-0126 §7.2 shares it between them by design" ], "traps": [ "first-boot-cold-start", @@ -2015,6 +2012,12 @@ "date": "2026-11-02", "change": "Re-measured against main after N-28/N-30 (#28818, #29264), at base dfbe34d887: the item drives the API doors only (POST /automation/:name/toggle, /trigger, _status, the data API), none of which moved, so steps and clauses stand. fixtures.knownGaps[0] (new) marks acceptance[1]'s and negative[3]'s remedy phrase TRANSITIONAL: the served FLOW_DISABLED sentence (describeTurnedOffPackagedItem('flow'), @objectstack/types) still names 'Setup › Packaged automation' and a copy made 'there', a page ADR-0146 D4/D11 retired into Studio's Automations pillar. The clause keeps the sentence exactly as served, so no clause is weakened; the remedy's wrong place is reported as a product defect for its own card, not fixed here", "ref": "#28579" + }, + { + "revision": 10, + "date": "2026-11-02", + "change": "The producer now names the live door: describeTurnedOffPackagedItem('flow') (@objectstack/types) tells the reader an administrator can turn the flow on again in its package's Automations in Studio, or copy it there under a new name (New similar), not the retired Setup › Packaged automation page. acceptance[1] (clause and the verbatim sentence in its verify) and negative[3]'s remedy phrase are re-pointed to it, as fixtures.knownGaps[0] said they would be, and that TRANSITIONAL entry, the item's only known gap, is retired. The item still drives the API doors only; the code FLOW_DISABLED and the ledger-vs-status distinction by message are unchanged", + "ref": "#29374" } ] }, diff --git a/packages/objectql/src/action-activation.test.ts b/packages/objectql/src/action-activation.test.ts index 20dbba62ed6..0ed4b87a974 100644 --- a/packages/objectql/src/action-activation.test.ts +++ b/packages/objectql/src/action-activation.test.ts @@ -318,7 +318,7 @@ describe('ActionActivationProjection — absence means ACTIVE', () => { // What happened, who can undo it and where, and what to do meanwhile. expect(message).toBe( 'This action is turned off for this deployment. An administrator can turn it on again under ' - + 'Setup › Packaged automation, or you can use another action.', + + 'Setup › Packaged Actions, or you can use another action.', ); // The machine half (the ledger table, the ADR) belongs to the log line // and the envelope's code, never to what an end user reads. diff --git a/packages/qa/dogfood/test/api-action-door-forward.dogfood.test.ts b/packages/qa/dogfood/test/api-action-door-forward.dogfood.test.ts index ec7ef8bfb85..56eba5c8d0e 100644 --- a/packages/qa/dogfood/test/api-action-door-forward.dogfood.test.ts +++ b/packages/qa/dogfood/test/api-action-door-forward.dogfood.test.ts @@ -195,7 +195,7 @@ describe('[#27506] a same-origin api action is dispatched through the action doo // turns the action back on, and no system table (the code is the machine half). const message = String(refused.json?.error?.message); expect(message).toMatch(/^This action is turned off for this deployment\. /); - expect(message).toContain('An administrator can turn it on again under Setup › Packaged automation'); + expect(message).toContain('An administrator can turn it on again under Setup › Packaged Actions'); expect(message).not.toContain('sys_metadata_activation'); expect(await isBanned(), 'a switched-off Ban User still banned — the #27506 defect').toBe(false); }, 120_000); diff --git a/packages/qa/dogfood/test/packaged-action-disabled-dispatch-live.dogfood.test.ts b/packages/qa/dogfood/test/packaged-action-disabled-dispatch-live.dogfood.test.ts index 1406821c47d..29dedd38cd9 100644 --- a/packages/qa/dogfood/test/packaged-action-disabled-dispatch-live.dogfood.test.ts +++ b/packages/qa/dogfood/test/packaged-action-disabled-dispatch-live.dogfood.test.ts @@ -67,7 +67,7 @@ const activation = (object: string, action: string) => `/actions/_activation/${o */ function expectDisabledSentence(message: unknown, _action: string) { expect(String(message)).toMatch(/^This action is turned off for this deployment\. /); - expect(String(message)).toContain('An administrator can turn it on again under Setup › Packaged automation'); + expect(String(message)).toContain('An administrator can turn it on again under Setup › Packaged Actions'); expect(String(message)).not.toContain(LEDGER); expect(String(message)).not.toMatch(/ADR-\d+/); } diff --git a/packages/runtime/src/domains/action-activation-dispatch.test.ts b/packages/runtime/src/domains/action-activation-dispatch.test.ts index 7d6327bc05f..5a79991293d 100644 --- a/packages/runtime/src/domains/action-activation-dispatch.test.ts +++ b/packages/runtime/src/domains/action-activation-dispatch.test.ts @@ -142,7 +142,7 @@ describe('door 1 — REST `POST /actions/:object/:action`', () => { // The words are the contract here (they are what the toast reads), so // the first sentence is pinned, and the remedy beside it. expect(message.startsWith('This action is turned off for this deployment.')).toBe(true); - expect(message).toContain('An administrator can turn it on again under Setup › Packaged automation'); + expect(message).toContain('An administrator can turn it on again under Setup › Packaged Actions'); // No system table, no ADR citation, no tracker number in what an end user reads. expect(message).not.toContain('sys_metadata_activation'); expect(message).not.toMatch(/ADR-\d+/); diff --git a/packages/services/service-automation/src/flow-activation-ledger.test.ts b/packages/services/service-automation/src/flow-activation-ledger.test.ts index d556edb1b0c..7e490382772 100644 --- a/packages/services/service-automation/src/flow-activation-ledger.test.ts +++ b/packages/services/service-automation/src/flow-activation-ledger.test.ts @@ -67,8 +67,8 @@ function packagedFlow(name: string, startConfig: Record = {}, e /** [#27966] What a caller reads for a flow switched off in the activation ledger. */ const LEDGER_SENTENCE = - 'This flow is turned off for this deployment. An administrator can turn it on again under ' - + 'Setup › Packaged automation, or copy it there under a new name.'; + 'This flow is turned off for this deployment. An administrator can turn it on again ' + + "in its package's Automations in Studio, or copy it there under a new name."; /** A recording trigger, so binding state is asserted for real rather than inferred. */ function recordingTrigger(type: string) { @@ -241,8 +241,8 @@ describe('ADR-0126 §7.2 — a ledger-disabled flow refuses at the execute() sea // meanwhile: flow clone is chartered (ADR-0126 §7.2), so the copy is // offered, unlike the action sentence. expect(refused.error).toBe( - 'This flow is turned off for this deployment. An administrator can turn it on again under ' - + 'Setup › Packaged automation, or copy it there under a new name.', + 'This flow is turned off for this deployment. An administrator can turn it on again ' + + "in its package's Automations in Studio, or copy it there under a new name.", ); expect(refused.error).not.toContain('sys_metadata_activation'); expect(refused.error).not.toMatch(/ADR-\d+/); @@ -296,8 +296,8 @@ describe('ADR-0126 §7.2 — a ledger-disabled flow refuses at the execute() sea expect(refused.error).toBe("Flow 'obsolete_flow' is disabled"); // The point of the distinction: an operator reading this must not be // sent to the activation switch for an authoring-state problem. - // [#27966] The switch is now named by its Setup page, not its table. - expect(refused.error).not.toContain('Packaged automation'); + // [#27966] The switch is named by where it is, not by its table. + expect(refused.error).not.toContain('turn it on again'); }); it('a ledger-disabled flow stays disabled across re-registration (publish / hot reload / boot pull)', async () => { diff --git a/packages/types/src/packaged-item-turned-off.test.ts b/packages/types/src/packaged-item-turned-off.test.ts index 3e04a4e1de8..4909def07c3 100644 --- a/packages/types/src/packaged-item-turned-off.test.ts +++ b/packages/types/src/packaged-item-turned-off.test.ts @@ -7,14 +7,14 @@ describe('describeTurnedOffPackagedItem — the one switched-off packaged item s it('the action sentence is the one #27842 settled, byte for byte', () => { expect(describeTurnedOffPackagedItem('action')).toBe( 'This action is turned off for this deployment. An administrator can turn it on again under ' - + 'Setup › Packaged automation, or you can use another action.', + + 'Setup › Packaged Actions, or you can use another action.', ); }); it('the flow sentence has the same shape, and offers the copy flow clone charters', () => { expect(describeTurnedOffPackagedItem('flow')).toBe( - 'This flow is turned off for this deployment. An administrator can turn it on again under ' - + 'Setup › Packaged automation, or copy it there under a new name.', + 'This flow is turned off for this deployment. An administrator can turn it on again ' + + "in its package's Automations in Studio, or copy it there under a new name.", ); }); @@ -24,4 +24,8 @@ describe('describeTurnedOffPackagedItem — the one switched-off packaged item s expect(message).not.toMatch(/ADR-\d+/); expect(message).not.toMatch(/#\d+/); }); + + it.each(['action', 'flow'] as const)('the %s sentence does not send anyone to the retired Packaged automation page', (kind) => { + expect(describeTurnedOffPackagedItem(kind)).not.toContain('Packaged automation'); + }); }); diff --git a/packages/types/src/packaged-item-turned-off.ts b/packages/types/src/packaged-item-turned-off.ts index 5d3a3746ee9..eb5a5d2e3ce 100644 --- a/packages/types/src/packaged-item-turned-off.ts +++ b/packages/types/src/packaged-item-turned-off.ts @@ -2,8 +2,8 @@ /** * [#27842 / #27966] The one sentence a caller reads when a packaged item an - * administrator switched off under Setup › Packaged automation is refused - * (ADR-0126 §7.2 for flows, §8 for actions). + * administrator switched off is refused (ADR-0126 §7.2 for flows, §8 for + * actions). * * It is written for the person who asked for the item, and it is what their * error toast reads: what happened, who can undo it and where, and what can @@ -18,9 +18,13 @@ * `@objectstack/service-automation` the flow refusal, and both already depend * on this package, so the one sentence adds no edge. * - * The "meanwhile" clause is the one place the kinds differ, and it follows - * what is chartered. Flow clone is (ADR-0126 §7.2: `POST /automation/:name/clone`, - * offered on the same Setup page), so the flow sentence offers a copy. + * The kinds differ in two places, and both follow where the switch is + * (ADR-0146 D4, D11). WHERE: an action's switch is on Setup's Packaged Actions + * page (the `nav_packaged_actions` entry); a flow's switch is on the flow, in + * its package's Automations in Studio. Setup's Packaged automation page, which + * held both, is retired, so neither sentence names it. MEANWHILE: flow clone is + * chartered (ADR-0126 §7.2: `POST /automation/:name/clone`), offered as New + * similar on the same Studio flow, so the flow sentence offers a copy there. * Action-clone is not (§8 item 2), so the action sentence ⛔ never does: * recommending one would advertise machinery that does not exist. * @@ -29,6 +33,11 @@ */ export type PackagedItemKind = 'action' | 'flow'; +const WHERE: Record = { + action: 'under Setup › Packaged Actions', + flow: "in its package's Automations in Studio", +}; + const MEANWHILE: Record = { action: 'or you can use another action', flow: 'or copy it there under a new name', @@ -36,7 +45,7 @@ const MEANWHILE: Record = { export function describeTurnedOffPackagedItem(kind: PackagedItemKind): string { return ( - `This ${kind} is turned off for this deployment. An administrator can turn it on again under ` + - `Setup › Packaged automation, ${MEANWHILE[kind]}.` + `This ${kind} is turned off for this deployment. An administrator can turn it on again ` + + `${WHERE[kind]}, ${MEANWHILE[kind]}.` ); }