From 7eca8ebff6de6804b8877381f144144fd6ee5dc5 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 16:54:05 +0000 Subject: [PATCH 1/3] fix(runtime): runtime refusals, warnings and route-ledger notes state each decision in words instead of a tracker number (stage 3) The 67 tracker-number occurrences in packages/runtime strings (55 sites in 11 files) are gone. Each cited card was read; where the sentence already stated the decision only the citation leaves, otherwise the decision is written in its place. The dispatcher twins of the notes stage 2 rewrote reuse stage 2's wording. Text only: no error code, status, route, field, export or control flow moves. Three pins that asserted an id now assert the sentence that carries the decision. The prose-id ledger is recomputed with --census-ledger: the runtime rows leave, no other row moves. Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude --- packages/runtime/src/action-execution.ts | 7 ++- packages/runtime/src/app-plugin.ts | 6 +- .../src/dispatcher-error-vocabulary.ts | 32 ++++++---- .../runtime/src/domains/activation-gate.ts | 4 +- packages/runtime/src/domains/auth.ts | 3 +- .../runtime/src/endpoint-executor.test.ts | 2 +- packages/runtime/src/endpoint-executor.ts | 4 +- packages/runtime/src/endpoint-policy.ts | 2 +- ...ispatcher.actions-doubled-redirect.test.ts | 4 +- .../runtime/src/resolve-project-database.ts | 3 +- packages/runtime/src/route-ledger.ts | 62 +++++++++--------- .../runtime/src/sandbox/body-runner.test.ts | 2 +- packages/runtime/src/sandbox/body-runner.ts | 7 ++- packages/runtime/src/standalone-stack.ts | 6 +- scripts/doc-authoring-prose-id.baseline.json | 63 ------------------- 15 files changed, 79 insertions(+), 128 deletions(-) diff --git a/packages/runtime/src/action-execution.ts b/packages/runtime/src/action-execution.ts index 7c81014a2e6..f8958cdbc34 100644 --- a/packages/runtime/src/action-execution.ts +++ b/packages/runtime/src/action-execution.ts @@ -2472,9 +2472,10 @@ export function doubledPostSuccessNavigationWarning( const where = objectName ? `${objectName}/${actionDef?.name ?? ''}` : String(actionDef?.name ?? ''); return ( `[action-contract] Action '${where}': the handler returned \`redirectUrl\` while the action ` - + 'also declares `onSuccess.navigate` — two post-success destinations for one success ' - + '(#11519). The DECLARED `onSuccess` wins and the handler\'s `redirectUrl` is ignored ' - + '(interim renderer precedence, objectui#5933). Fix the action, not the renderer: keep ' + + 'also declares `onSuccess.navigate` — two post-success destinations for one success, ' + + 'a pair the contract refuses rather than ranks. The DECLARED `onSuccess` wins and the ' + + 'handler\'s `redirectUrl` is ignored (the interim precedence the console renderer ' + + 'applies, which no contract promises). Fix the action, not the renderer: keep ' + '`onSuccess` and stop returning `redirectUrl` from the handler, or drop `onSuccess` and ' + 'let the handler return drive the navigation. There is no `precedence` field, by ruling.' ); diff --git a/packages/runtime/src/app-plugin.ts b/packages/runtime/src/app-plugin.ts index b5996d1278b..b114e045dfe 100644 --- a/packages/runtime/src/app-plugin.ts +++ b/packages/runtime/src/app-plugin.ts @@ -1777,7 +1777,11 @@ export class AppPlugin implements Plugin { // organization was just created and that must stand whatever // happens here. `warn` and not `error` — nothing was lost, and the // `kernel:ready` migration retries the same repair on next boot. - ctx.logger.warn('[AppPlugin] seed tenancy handoff failed (#8686)', { + ctx.logger.warn( + '[AppPlugin] seed tenancy handoff failed: the seed rows were not stamped with the new ' + + 'organization, so seed and API writes stay on separate autonumber counters until the ' + + 'next boot\'s migration repairs it', + { error: e?.message ?? String(e), }); } diff --git a/packages/runtime/src/dispatcher-error-vocabulary.ts b/packages/runtime/src/dispatcher-error-vocabulary.ts index b8f62ab46fe..7ecfe16c4f6 100644 --- a/packages/runtime/src/dispatcher-error-vocabulary.ts +++ b/packages/runtime/src/dispatcher-error-vocabulary.ts @@ -346,14 +346,15 @@ export const UNREGISTERED_CODE_SITES: readonly UnregisteredCodeSite[] = [ why: "Three approvals route factories (`decisionRoute`, `flowMoveRoute`, `threadRoute`) spell the " + 'terminal 500 catch\'s code as a template — `` `APPROVAL_${action.toUpperCase()}_FAILED` `` and ' + - 'two siblings — so the family, not a literal, is what exists in source. #8885 registered all ' + - 'nine codes the family produces, and its pin is what keeps that true: it enumerates the ' + + 'two siblings — so the family, not a literal, is what exists in source. All nine codes the ' + + 'family produces are registered in the ledger, and this row\'s pin is what keeps that true: it enumerates the ' + 'registered `POST /approvals/requests/:id/` routes and asserts the code each catch arm ' + "would generate parses against ApiErrorSchema's closed union, mirroring the production " + "template exactly (single-occurrence `.replace('-', '_')` included). So a tenth action route " + 'whose generated code nobody registers fails THERE, mechanically. This row records that ' + - 'division of labour instead of letting the scan imply it checked something it cannot: #9223 ' + - 'widened the scan enough to SEE the template, and seeing it is what makes the pin an ' + + 'division of labour instead of letting the scan imply it checked something it cannot: the scan ' + + 'reports a template-spelled code under its family identity rather than dropping it, so it ' + + 'SEES the template, and seeing it is what makes the pin an ' + 'accounted-for half rather than a local habit in one package.', }, @@ -433,8 +434,8 @@ export const UNREGISTERED_CODE_SITES: readonly UnregisteredCodeSite[] = [ why: "better-auth's own `APIError` vocabulary, and it cannot reach this door: `domains/auth.ts` " + 'catches everything the auth service throws and answers `deps.error(INTERNAL_ERROR_MESSAGE, 500)` ' + - '— the message withheld UNCONDITIONALLY and the code status-derived, never `errorFromThrown` ' + - '(#5085). better-auth answers its own failures with a `Response` rather than by throwing, and ' + + '— the message withheld UNCONDITIONALLY and the code status-derived, never `errorFromThrown`. ' + + 'better-auth answers its own failures with a `Response` rather than by throwing, and ' + 'that body is returned untouched as `result`. So the string never lands in an ADR-0112 ' + '`error.code`. This is the row that shows why verdicts are DECLARED: it is written exactly ' + 'like FLOW_FAILED and a documented catch one layer up makes it unreachable.', @@ -452,7 +453,7 @@ export const UNREGISTERED_CODE_SITES: readonly UnregisteredCodeSite[] = [ '`remove-member-permission-guard.ts`; only the envelope differs). It cannot reach this door, ' + 'by the same route the IMPERSONATION_ROTATION_FAILED row documents and re-verified here: ' + '`domains/auth.ts` catches everything the auth service throws and answers ' + - '`deps.error(INTERNAL_ERROR_MESSAGE, 500)` — unconditionally, never `errorFromThrown` (#5085). ' + + '`deps.error(INTERNAL_ERROR_MESSAGE, 500)` — unconditionally, never `errorFromThrown`. ' + 'So the string never lands in an ADR-0112 `error.code`.', }, // ── [#10352] better-auth's OWN vocabulary, now restamped in-repo ─────── @@ -516,7 +517,8 @@ export const UNREGISTERED_CODE_SITES: readonly UnregisteredCodeSite[] = [ why: 'The target-side twin of the row above, from the same better-auth 1.7.1 file ' + "(`dist/plugins/admin/error-codes`), likewise read off `plugin.$ERROR_CODES` and raised " + - "`APIError.from('FORBIDDEN', cannotImpersonateAdmins)`. #9968 makes it reachable for the " + + "`APIError.from('FORBIDDEN', cannotImpersonateAdmins)`. The in-repo re-implementation of the " + + "vendor's impersonation handler, which admits an ADR-0068 platform admin, makes it reachable for the " + "first time — the vendor gated it on the legacy `user.role` scalar nothing writes post " + "ADR-0068 D2, so the vendor's own promise was inert — but reachable in the vendor's wire " + "shape under the vendor's spelling, which changes nothing about whose vocabulary it is.", @@ -592,7 +594,8 @@ export const UNREGISTERED_CODE_SITES: readonly UnregisteredCodeSite[] = [ why: 'The same ADR-0087 conversion-notice vocabulary as the apply.ts row above, met at a TYPE ' + 'position: `ArtifactConversionNotice.code` is the literal in a structural mirror of ' + - "ConversionNotice, declared so the artifact-ingestion forward-conversion policy (#12772) " + + "ConversionNotice, declared so the artifact-ingestion forward-conversion policy — which runs the " + + 'ADR-0087 conversions over an artifact built by older tooling before its strict parse — ' + 'keeps the spec ROOT import out of its public declaration surface (the root reference made ' + "every downstream type program load the 2MB root twice and pushed a TEST_DEBT re-measure " + "over CI's tsc heap ceiling). A literal type stamps nothing at runtime — notices flow to an " + @@ -674,7 +677,7 @@ export const UNREGISTERED_CODE_SITES: readonly UnregisteredCodeSite[] = [ why: 'The ADR-0090 D7 / ADR-0086 D1 refusal: an environment overlay may only TIGHTEN a packaged ' + "object's OWD. The token reaches the wire verbatim but NOT in `code` — it rides the wire in " + - 'TWO fields since #9232 narrowed the flat REST door like every other: the 403 body carries the ' + + 'TWO fields because the flat REST door narrows like every other door: the 403 body carries the ' + 'closed member the status derives in `code` (`PERMISSION_DENIED`) and this string, unchanged, ' + 'in the open `declaredCode` sibling beside it. `packages/rest/src/meta-object-owd-gate.test.ts` ' + 'drives `PUT /api/v1/meta/object/:name` and asserts BOTH fields on the refusal body. So the ' + @@ -684,13 +687,16 @@ export const UNREGISTERED_CODE_SITES: readonly UnregisteredCodeSite[] = [ 'already names: the body parses, and what an unswept producer loses instead is its semantic ' + 'code, silently demoted off `error.code` until registered. Which is exactly what a ' + '`pending-registration` row records, and registering the code is still what ratchets it out. ' + - '[#9460] Invisible to BOTH vocabulary gates until now, and not for its casing: the file throws ' + + 'Invisible to BOTH vocabulary gates until the scan learned the code-carrying helper shape, and not for ' + + 'its casing: the file throws ' + 'through a code-carrying helper (`postureError(code, message)`), so the stamp `(err as any).code ' + '= code` knows the token `code` but not the value, while the call site knows the value and never ' + 'writes the token. Every pattern in this gate and in `check:error-code-casing` anchors on that ' + 'token, so both read the file and both reported nothing. ⚠️ The spelling is LOWERCASE, so ' + - 'ADR-0112 D1 forbids registering it as spelled — the rename-or-keep-the-#9106-demote call is ' + - "the `packages/spec` lane's, tracked as #9460 half (2) and NOT decided here. The row records " + + 'ADR-0112 D1 forbids registering it as spelled — the call between renaming it and keeping the ' + + 'demote (the closed member in `code`, this spelling in `declaredCode`) is ' + + "the `packages/spec` lane's; until that lane registers a code the standing demote answers " + + 'this spelling, and the call is NOT decided here. The row records ' + 'that a live wire code is outside the vocabulary; it does not prescribe the remedy.', }, diff --git a/packages/runtime/src/domains/activation-gate.ts b/packages/runtime/src/domains/activation-gate.ts index b31ba9672a4..6c8c6d061d3 100644 --- a/packages/runtime/src/domains/activation-gate.ts +++ b/packages/runtime/src/domains/activation-gate.ts @@ -275,8 +275,8 @@ export function refuseUngrantedActivationAuthoring( handled: true, response: deps.error( `Enabling or disabling ${artifact.subject} requires the \`${ACTIVATION_AUTHORING_CAPABILITY}\` capability — ` + - `switching a shipped artifact off is functionally equivalent to deleting it for as long as it stays off ` + - `(#10243).`, + `switching a shipped artifact off is functionally equivalent to deleting it for as long as it stays off, ` + + `and the switch is not scoped to the caller's organization.`, ACTIVATION_DENY_STATUS, { code: ACTIVATION_DENY_CODE }, ), diff --git a/packages/runtime/src/domains/auth.ts b/packages/runtime/src/domains/auth.ts index b76e01f043e..85809870f1a 100644 --- a/packages/runtime/src/domains/auth.ts +++ b/packages/runtime/src/domains/auth.ts @@ -140,7 +140,8 @@ export async function handleAuthRequest(deps: DomainHandlerDeps, _path: string, const logger = deps.logger ?? console; logger?.error?.( '[auth] the auth service threw while handling the request; the client was answered ' - + 'with a sanitised 500 (#5085)', + + 'with a sanitised 500: the message is withheld unconditionally, and this line is where the ' + + 'original error is read', err instanceof Error ? err : new Error(String(err)), ); return { handled: true, response: deps.error(INTERNAL_ERROR_MESSAGE, 500) }; diff --git a/packages/runtime/src/endpoint-executor.test.ts b/packages/runtime/src/endpoint-executor.test.ts index 349ddb39f2c..2d0a46fdc15 100644 --- a/packages/runtime/src/endpoint-executor.test.ts +++ b/packages/runtime/src/endpoint-executor.test.ts @@ -209,7 +209,7 @@ describe('planEndpointTarget — the unsupported subset is enumerated once', () // The reason names the type so an author is not left guessing which of // their endpoints the runtime declined. expect((plan as any).reason).toContain(`'${type}'`); - expect((plan as any).hint).toContain('#5040 §7-3'); + expect((plan as any).hint).toContain('rejected at publish pending their own rulings'); }); }); diff --git a/packages/runtime/src/endpoint-executor.ts b/packages/runtime/src/endpoint-executor.ts index 937495264b2..1a2f0214008 100644 --- a/packages/runtime/src/endpoint-executor.ts +++ b/packages/runtime/src/endpoint-executor.ts @@ -229,7 +229,7 @@ export function planEndpointTarget(endpoint: ApiEndpoint): EndpointTargetPlan { `Endpoint '${endpoint.name}' declares type 'object_operation' but ` + `objectParams.${!object ? 'object' : 'operation'} is missing.`, hint: 'An object_operation endpoint must declare both `objectParams.object` and ' - + '`objectParams.operation`; publish rejects the incomplete form (#5040 E7).', + + '`objectParams.operation`; publish rejects the incomplete form.', }; } return { kind: 'object_operation', object, operation }; @@ -252,7 +252,7 @@ export function planEndpointTarget(endpoint: ApiEndpoint): EndpointTargetPlan { reason: `Endpoint '${endpoint.name}' declares type '${endpoint.type}', which this runtime does not execute.`, hint: "Only 'object_operation' and 'flow' endpoints execute in 17.x. 'script' and 'proxy' " + 'are rejected at publish pending their own rulings — script reachability through the ' - + 'automation service is unverified, and proxy is an outbound (SSRF) surface (#5040 §7-3).', + + 'automation service is unverified, and proxy is an outbound (SSRF) surface.', }; } diff --git a/packages/runtime/src/endpoint-policy.ts b/packages/runtime/src/endpoint-policy.ts index 3cafa6221d5..7a4b1e03334 100644 --- a/packages/runtime/src/endpoint-policy.ts +++ b/packages/runtime/src/endpoint-policy.ts @@ -260,7 +260,7 @@ export function computeCacheControl( if (method.toUpperCase() !== 'GET') { logger?.warn?.( `[dispatcher] endpoint '${endpoint.name}' declares \`cacheTtlSeconds\` on a ${method.toUpperCase()} endpoint. ` - + '`cacheTtlSeconds` is GET-only (#5040 §3.3) and no Cache-Control header will be sent. Remove the key, or ' + + '`cacheTtlSeconds` is GET-only and no Cache-Control header will be sent. Remove the key, or ' + 'declare the endpoint as GET.', ); return undefined; diff --git a/packages/runtime/src/http-dispatcher.actions-doubled-redirect.test.ts b/packages/runtime/src/http-dispatcher.actions-doubled-redirect.test.ts index 4cf6534c21b..c178010208c 100644 --- a/packages/runtime/src/http-dispatcher.actions-doubled-redirect.test.ts +++ b/packages/runtime/src/http-dispatcher.actions-doubled-redirect.test.ts @@ -98,8 +98,8 @@ describe('REST /actions — doubled post-success navigation diagnostic (#11519)' expect(doubled[0]).toContain("'crm_lead/open_portal'"); expect(doubled[0]).toContain('onSuccess'); expect(doubled[0]).toContain('redirectUrl'); - expect(doubled[0]).toContain('objectui#5933'); - expect(doubled[0]).toContain('#11519'); + expect(doubled[0]).toContain('the interim precedence the console renderer applies'); + expect(doubled[0]).toContain('a pair the contract refuses rather than ranks'); }); it('does NOT alter the wire — the handler return value still reaches the client intact', async () => { diff --git a/packages/runtime/src/resolve-project-database.ts b/packages/runtime/src/resolve-project-database.ts index d96a7a10f62..4bfef378542 100644 --- a/packages/runtime/src/resolve-project-database.ts +++ b/packages/runtime/src/resolve-project-database.ts @@ -311,7 +311,8 @@ export function resolveProjectDatabaseUrl( url: `file:${legacyPath}`, source: 'legacy-file', notice: - `Reading legacy database file ${legacyPath} — the unified default is now ${unifiedPath} (#6469); ` + + `Reading legacy database file ${legacyPath} — dev, start and migrate now share one default, ` + + `${unifiedPath}; ` + `migrate with: mv "${legacyPath}" "${unifiedPath}" (move any -wal/-shm siblings too), ` + `or pin it explicitly via OS_DATABASE_URL=file:${legacyPath}`, }; diff --git a/packages/runtime/src/route-ledger.ts b/packages/runtime/src/route-ledger.ts index 1b7145270bc..b20f94609eb 100644 --- a/packages/runtime/src/route-ledger.ts +++ b/packages/runtime/src/route-ledger.ts @@ -318,7 +318,7 @@ export const ROUTE_LEDGER: readonly RouteLedgerEntry[] = [ // forbidden to be written without. { route: 'GET /discovery', domain: '/discovery', disposition: 'sdk', client: 'connect', responseSchema: 'DiscoverySchema', - note: 'dispatch() returns this one through the { success, data } envelope, so `DiscoverySchema` names the `data` — which is exactly the value getDiscoveryInfo() produces and discovery-schema-conformance.test.ts parses (#5682)' }, + note: 'dispatch() returns this one through the { success, data } envelope, so `DiscoverySchema` names the `data` — which is exactly the value getDiscoveryInfo() produces and discovery-schema-conformance.test.ts holds to the double assertion: the value parses against `DiscoverySchema`, and it carries no key the protocol does not declare' }, // ── analytics ───────────────────────────────────────────────────────────── // Capability-conditional (#3891 follow-through): the plugin mounts these @@ -327,9 +327,9 @@ export const ROUTE_LEDGER: readonly RouteLedgerEntry[] = [ // guarantee — see the header's "A ROW HERE IS NOT A WIRE GUARANTEE". { route: 'POST /analytics/query', domain: '/analytics', disposition: 'sdk', client: 'analytics.query' }, { route: 'GET /analytics/meta', domain: '/analytics', disposition: 'sdk', client: 'analytics.meta', - note: 'optional ?cube= filter honored server-side; was mismatch — the client called /meta/:cube, a shape no server ever mounted (#3584)' }, + note: 'optional ?cube= filter honored server-side; was mismatch — the client called /meta/:cube, a shape no server ever mounted, so the client moved to this route rather than the dispatcher growing an alias' }, { route: 'POST /analytics/sql', domain: '/analytics', disposition: 'sdk', client: 'analytics.explain', - note: 'client keeps the explain name but calls /sql; the /explain route it used to call was served by nothing (#3584)' }, + note: 'client keeps the explain name but calls /sql; the /explain route it used to call was served by nothing, so the client aligned to the dispatcher rather than the dispatcher growing an alias' }, // ── i18n ────────────────────────────────────────────────────────────────── { route: 'GET /i18n/locales', domain: '/i18n', disposition: 'sdk', client: 'i18n.getLocales' }, @@ -399,32 +399,32 @@ export const ROUTE_LEDGER: readonly RouteLedgerEntry[] = [ { route: 'PATCH /packages/:id', domain: '/packages', disposition: 'sdk', client: 'packages.update' }, { route: 'POST /packages/:id/publish', domain: '/packages', disposition: 'sdk', client: 'packages.publish', responseSchema: 'PackagePublishResultSchema', - note: '[#12038] dispatch() answers through the { success, data } envelope, so the named schema is the `data` — `MetadataManager.publishPackage`\'s declared return, whose exact schema already existed in spec `system/metadata-persistence.zod.ts` and is re-exported into `/api` by ruling 5A (never a second copy). Conformance: spec `api/package-lifecycle.test.ts`' }, + note: 'dispatch() answers through the { success, data } envelope, so the named schema is the `data` — `MetadataManager.publishPackage`\'s declared return, whose exact schema already existed in spec `system/metadata-persistence.zod.ts` and is re-exported into `/api`, never declared there a second time. Conformance: spec `api/package-lifecycle.test.ts`' }, { route: 'POST /packages/:id/publish-drafts', domain: '/packages', disposition: 'sdk', client: 'packages.publishDrafts', responseSchema: 'PublishPackageDraftsResponseSchema', - note: '[#9406] dispatch() answers through the { success, data } envelope, so the named schema is the `data` — the protocol result AFTER the door\'s own mutations (seedApplied back-fill, ADR-0045 unhiddenApps/unhideError, rebindError). Fillable because packages-publish-drafts-response-conformance.test.ts drives THIS handler and parses the payload it answers; the producer half is pinned in objectql\'s publish-package-drafts-response-conformance.test.ts. `probes` is deliberately opaque in the declaration (#9406 ruling)' }, + note: 'dispatch() answers through the { success, data } envelope, so the named schema is the `data` — the protocol result AFTER the door\'s own mutations (seedApplied back-fill, ADR-0045 unhiddenApps/unhideError, rebindError). Fillable because packages-publish-drafts-response-conformance.test.ts drives THIS handler and parses the payload it answers; the producer half is pinned in objectql\'s publish-package-drafts-response-conformance.test.ts. `probes` is deliberately opaque in the declaration, upgraded to a modeled schema only when a consumer needs a field of it' }, { route: 'POST /packages/:id/discard-drafts', domain: '/packages', disposition: 'sdk', client: 'packages.discardDrafts', responseSchema: 'DiscardPackageDraftsResponseSchema', - note: '[#12038] enveloped — the named schema is the `data`: `discardPackageDrafts`\'s declared return, transcribed describe-only. Conformance: spec `api/package-lifecycle.test.ts`' }, + note: 'Enveloped — the named schema is the `data`: `discardPackageDrafts`\'s declared return, transcribed describe-only. Conformance: spec `api/package-lifecycle.test.ts`' }, { route: 'GET /packages/:id/commits', domain: '/packages', disposition: 'sdk', client: 'packages.listCommits', responseSchema: 'ListPackageCommitsResponseSchema', - note: '[#12038] enveloped — the named schema is the `data`. The producer (`listCommits`) returns a BARE array; the `{ commits }` wrapper is minted at THIS handler (`domains/packages.ts`) and the schema declares it as the handler\'s own. Conformance: spec `api/package-lifecycle.test.ts`' }, + note: 'Enveloped — the named schema is the `data`. The producer (`listCommits`) returns a BARE array; the `{ commits }` wrapper is minted at THIS handler (`domains/packages.ts`) and the schema declares it as the handler\'s own. Conformance: spec `api/package-lifecycle.test.ts`' }, { route: 'POST /packages/:id/commits/:commitId/revert', domain: '/packages', disposition: 'sdk', client: 'packages.revertCommit', responseSchema: 'RevertPackageCommitResponseSchema', - note: '[#12038] enveloped — the named schema is the `data`: `revertCommit`\'s declared return, transcribed describe-only. Conformance: spec `api/package-lifecycle.test.ts`' }, + note: 'Enveloped — the named schema is the `data`: `revertCommit`\'s declared return, transcribed describe-only. Conformance: spec `api/package-lifecycle.test.ts`' }, { route: 'POST /packages/:id/rollback', domain: '/packages', disposition: 'sdk', client: 'packages.rollback', responseSchema: 'RollbackToPackageCommitResponseSchema', - note: '[#12038 ruling 3A] enveloped — the named schema is the `data`: `rollbackToPackageCommit`\'s declared return, the ADR-0067 COMMIT rollback. Fillable only after the retirement of `PackageRollbackResponseSchema` + `PackageApiContracts.rollbackPackage`, the VERSION-rollback declaration the spec had falsely bound to this exact path. Conformance: spec `api/package-lifecycle.test.ts`' }, + note: 'Enveloped — the named schema is the `data`: `rollbackToPackageCommit`\'s declared return, the ADR-0067 COMMIT rollback. Fillable only after the retirement of `PackageRollbackResponseSchema` + `PackageApiContracts.rollbackPackage`, the VERSION-rollback declaration the spec had falsely bound to this exact path. Conformance: spec `api/package-lifecycle.test.ts`' }, { route: 'POST /packages/:id/revert', domain: '/packages', disposition: 'sdk', client: 'packages.revert' }, { route: 'GET /packages/:id/export', domain: '/packages', disposition: 'sdk', client: 'packages.export', responseSchema: 'PackageExportManifestSchema', - note: '[#12038 ruling 4A] enveloped — the named schema is the `data`: the ADR-0070 portable manifest. Four fixed keys (`id`, `name`, `version`, `label?`) plus an OPEN catch-all — the remaining keys are registry-derived per metadata type present, deliberately not enumerated (freezes nothing). Conformance: spec `api/package-lifecycle.test.ts`' }, + note: 'Enveloped — the named schema is the `data`: the ADR-0070 portable manifest. Four fixed keys (`id`, `name`, `version`, `label?`) plus an OPEN catch-all — the remaining keys are registry-derived per metadata type present, deliberately not enumerated (freezes nothing). Conformance: spec `api/package-lifecycle.test.ts`' }, { route: 'POST /packages/:id/adopt-orphans', domain: '/packages', disposition: 'sdk', client: 'packages.adoptOrphans', responseSchema: 'ReassignOrphanedMetadataResponseSchema', - note: '[#12038] enveloped — the named schema is the `data`: `reassignOrphanedMetadata`\'s declared return, transcribed describe-only. Conformance: spec `api/package-lifecycle.test.ts`' }, + note: 'Enveloped — the named schema is the `data`: `reassignOrphanedMetadata`\'s declared return, transcribed describe-only. Conformance: spec `api/package-lifecycle.test.ts`' }, { route: 'POST /packages/:id/duplicate', domain: '/packages', disposition: 'sdk', client: 'packages.duplicate', responseSchema: 'DuplicatePackageResponseSchema', - note: '[#12038] enveloped — the named schema is the `data`: `duplicatePackage`\'s declared return, transcribed describe-only. ⚠ `data.success` is the operation\'s verdict; the envelope `success` is transport-level and true even for a partial/empty duplicate (the objectui#6593 defect). The SOURCE must be a writable base: a code-loaded or platform/marketplace source is refused 422 `DUPLICATE_SOURCE_NOT_A_BASE` (`requireDuplicableSource`, `domains/packages.ts`), BEFORE the protocol call and therefore before the target package record is minted — `duplicatePackage` installs that record ahead of its copy loop, so refusing any later still left a real, listed, empty shell package behind. ADR-0070 D4 is declared-and-not-built and its object is a BASE, so cloning a code package\'s items would EXTEND D4 rather than implement it; the unbuilt case refuses loudly instead of answering 200 with `copiedCount: 0`, which no caller could tell from a base that really is empty. An empty WRITABLE base still answers 200 / `copiedCount: 0` — that read happened and found nothing. Pinned in `domains/packages-readonly-gate.test.ts`. Conformance: spec `api/package-lifecycle.test.ts`' }, + note: 'Enveloped — the named schema is the `data`: `duplicatePackage`\'s declared return, transcribed describe-only. ⚠ `data.success` is the operation\'s verdict; the envelope `success` is transport-level and true even for a partial/empty duplicate (a console that read the envelope `success` reported a partial or empty duplicate as done). The SOURCE must be a writable base: a code-loaded or platform/marketplace source is refused 422 `DUPLICATE_SOURCE_NOT_A_BASE` (`requireDuplicableSource`, `domains/packages.ts`), BEFORE the protocol call and therefore before the target package record is minted — `duplicatePackage` installs that record ahead of its copy loop, so refusing any later still left a real, listed, empty shell package behind. ADR-0070 D4 is declared-and-not-built and its object is a BASE, so cloning a code package\'s items would EXTEND D4 rather than implement it; the unbuilt case refuses loudly instead of answering 200 with `copiedCount: 0`, which no caller could tell from a base that really is empty. An empty WRITABLE base still answers 200 / `copiedCount: 0` — that read happened and found nothing. Pinned in `domains/packages-readonly-gate.test.ts`. Conformance: spec `api/package-lifecycle.test.ts`' }, // ── automation ──────────────────────────────────────────────────────────── { route: 'POST /automation/trigger/:name', domain: '/automation', disposition: 'sdk', client: 'automation.trigger', @@ -432,19 +432,19 @@ export const ROUTE_LEDGER: readonly RouteLedgerEntry[] = [ // `GET /automation` (flow list, `automation.list`) — RETIRED by #19543 (door ④): // unmounted, and the flow list is `GET /meta/flow`. No row, because nothing serves it. { route: 'POST /automation', domain: '/automation', disposition: 'sdk', client: 'automation.create', - note: "authored metadata, so `manage_metadata` gates it (#10145): a flow definition lives on the metadata plane (ADR-0106), and this door now asks the capability every other door onto that plane already asks. Fail-closed by construction — an absent executionContext, an absent `systemPermissions` or an empty one all fall through to the refusal, 403 with code `PERMISSION_DENIED` (ADR-0112); only engine self-invocation (`isSystem`, never settable from the wire) bypasses. WHICH routes is one predicate, `isFlowAuthoringWrite` in `domains/automation.ts` — this row, PUT/DELETE `/:name` below, and (since the #10243 ruling) `POST /:name/toggle`, with the execution doors (trigger / execute / resume) deliberately outside it. Second layer, not the first: the #5519 anonymous floor answers an unidentified caller 401 here, not 403. Pinned in `domains/automation-write-capability-gate.test.ts`" }, + note: "authored metadata, so `manage_metadata` gates it: a flow definition lives on the metadata plane (ADR-0106), and this door now asks the capability every other door onto that plane already asks. Fail-closed by construction — an absent executionContext, an absent `systemPermissions` or an empty one all fall through to the refusal, 403 with code `PERMISSION_DENIED` (ADR-0112); only engine self-invocation (`isSystem`, never settable from the wire) bypasses. WHICH routes is one predicate, `isFlowAuthoringWrite` in `domains/automation.ts` — this row, PUT/DELETE `/:name` below, and, since the ruling that enablement is an authoring write, `POST /:name/toggle`, with the execution doors (trigger / execute / resume) deliberately outside it. Second layer, not the first: the domain-wide anonymous floor answers an unidentified caller 401 here, not 403. Pinned in `domains/automation-write-capability-gate.test.ts`" }, { route: 'GET /automation/actions', domain: '/automation', disposition: 'sdk', client: 'automation.listActions' }, { route: 'GET /automation/connectors', domain: '/automation', disposition: 'sdk', client: 'automation.listConnectors' }, { route: 'GET /automation/_status', domain: '/automation', disposition: 'sdk', client: 'automation.getRuntimeStatus' }, { route: 'POST /automation/:name/trigger', domain: '/automation', disposition: 'sdk', client: 'automation.execute' }, { route: 'POST /automation/:name/toggle', domain: '/automation', disposition: 'sdk', client: 'automation.toggle', - note: "enablement, and since the #10243 ruling (2026-08-23) `manage_metadata` gates it — the same `isFlowAuthoringWrite` door as `POST /automation` above, NOT a fourth copy of the policy. #10145 deliberately left this one out as engine state and filed the question; the measurement is what settled it. No organization wall scopes the enabled bit: `toggleFlow` writes the ADR-0126 §7.2 activation ledger first — one deployment-wide `sys_metadata_activation` row per flow, keyed by `(metadata_type, name)`, carrying the flow's package id and no organization column — and only then updates the engine's in-process projection, which `getFlowRuntimeStates()` reads with no caller and no organization; the automation service is ONE instance per environment — so an unentitled tenant org owner switched a shipped flow off and an unrelated tenant in a different organization, plus the platform admin, read it off, in both directions. Disabling a shipped flow is equivalent to deleting it for as long as it stays off, and DELETE was already gated. ⚠️ BREAKING: 200 → 403 for callers without the capability. Fail-closed on an absent executionContext, an absent `systemPermissions` or an empty one, refusing 403 `PERMISSION_DENIED`, with only `isSystem` bypassing; the #5519 anonymous floor still answers 401 first. The predicate excludes `POST /automation/trigger/:name` so a flow literally NAMED `toggle` keeps its execution door. Pinned in `domains/automation-write-capability-gate.test.ts` and `qa/dogfood/test/automation-toggle-tenant-scope.dogfood.test.ts`" }, + note: "enablement, and since the 2026-08-23 ruling that enablement is an authoring write, `manage_metadata` gates it — the same `isFlowAuthoringWrite` door as `POST /automation` above, NOT a fourth copy of the policy. The definition-write gate deliberately left this one out as engine state and filed the question; the measurement is what settled it. No organization wall scopes the enabled bit: `toggleFlow` writes the ADR-0126 §7.2 activation ledger first — one deployment-wide `sys_metadata_activation` row per flow, keyed by `(metadata_type, name)`, carrying the flow's package id and no organization column — and only then updates the engine's in-process projection, which `getFlowRuntimeStates()` reads with no caller and no organization; the automation service is ONE instance per environment — so an unentitled tenant org owner switched a shipped flow off and an unrelated tenant in a different organization, plus the platform admin, read it off, in both directions. Disabling a shipped flow is equivalent to deleting it for as long as it stays off, and DELETE was already gated. ⚠️ BREAKING: 200 → 403 for callers without the capability. Fail-closed on an absent executionContext, an absent `systemPermissions` or an empty one, refusing 403 `PERMISSION_DENIED`, with only `isSystem` bypassing; the domain-wide anonymous floor still answers 401 first. The predicate excludes `POST /automation/trigger/:name` so a flow literally NAMED `toggle` keeps its execution door. Pinned in `domains/automation-write-capability-gate.test.ts` and `qa/dogfood/test/automation-toggle-tenant-scope.dogfood.test.ts`" }, // [#20676] The ADR-0126 §7.1 clone door. Its domain arm landed with #12156 and // nothing mounted it until #20676, so the row and the mount arrive together. { route: 'POST /automation/:name/clone', domain: '/automation', disposition: 'server-only', note: "ADR-0126 §7.1 — copy a flow's whole definition under a NEW machine name: the sanctioned way to customize a packaged flow whose base is locked. Body `{ name, label }`, both mandatory, closed. An unknown source answers 404, a taken target name (the source's own included) 409 `RESOURCE_CONFLICT`, and a definition the engine refuses to register 400; success answers `{ flow, notice }`, the notice stating that references are not re-pointed and that the clone is armed (`status: 'draft'` is not an off-switch). ⛔ No ancestry on the definition or the response (ADR-0126 §9). ⚑ `manage_metadata` — the same `isFlowAuthoringWrite` door as `POST /automation` above, because a clone registers flow metadata at environment scope; fail-closed, only `isSystem` bypassing, and the domain-wide anonymous floor answers an unidentified caller 401 first. ⛔ NOT the ADR-0126 §5 activation gate: a clone takes nothing away from any tenant. The domain arm existed long before its mount did — every clone answered the transport's 404 before `dispatch()` ran while the arm's direct-call unit test stayed green — so the door is pinned over HTTP, not only through the handler. NOT JS-SDK surface on this leg, and that is stated rather than left as an open gap: the operational driver ADR-0126 §7.4 charters is the Setup page for packaged metadata, a console surface that calls the platform API directly — the posture the `POST /actions/_activation/:object/:action` row below carries. Adding a client method reclassifies this row to `sdk`. Pinned over HTTP in `qa/dogfood/test/automation-flow-clone-door.dogfood.test.ts`, the environment-scoped mount in `dispatcher-plugin.automation-clone-mount.integration.test.ts`, and the copy itself in `domains/automation-flow-clone.test.ts`" }, { route: 'POST /automation/:name/runs/:runId/resume', domain: '/automation', disposition: 'sdk', client: 'automation.resume', - note: "generic, so the SUSPENDED NODE gates it (#3801): a pause whose descriptor declares resumeAuthority:'service' — today `approval` / `approval_revise` — answers 403 here and continues only through its owning service (ApprovalService.decide), which authorizes and records the decision first. A node type that declares NO resumeAuthority answers 403 too, fail-closed since #5561: this door is an opt-in a descriptor states with 'any'. Screen/wait pauses are unaffected because they declare it; this route is the screen-flow runner's door. The node gate asks WHAT the run is parked on, never WHO is resuming, so the route also gates the CALLER on the screen read's own question: the run's own trigger identity, or the `sys_automation_run` read grant as the operator override — one predicate, `isRunStarterOrRunStateReader` in `domains/automation.ts`. Refused 403 `PERMISSION_DENIED` before the engine is reached, so nothing is consumed. Pinned in `domains/automation-resume-caller-gate.test.ts`" }, + note: "generic, so the SUSPENDED NODE gates it: a pause whose descriptor declares resumeAuthority:'service' — today `approval` / `approval_revise` — answers 403 here and continues only through its owning service (ApprovalService.decide), which authorizes and records the decision first. A node type that declares NO resumeAuthority answers 403 too, fail-closed: this door is an opt-in a descriptor states with 'any'. Screen/wait pauses are unaffected because they declare it; this route is the screen-flow runner's door. The node gate asks WHAT the run is parked on, never WHO is resuming, so the route also gates the CALLER on the screen read's own question: the run's own trigger identity, or the `sys_automation_run` read grant as the operator override — one predicate, `isRunStarterOrRunStateReader` in `domains/automation.ts`. Refused 403 `PERMISSION_DENIED` before the engine is reached, so nothing is consumed. Pinned in `domains/automation-resume-caller-gate.test.ts`" }, { route: 'GET /automation/:name/runs/:runId/screen', domain: '/automation', disposition: 'sdk', client: 'automation.getScreen' }, // [#13953] Cancel a suspended run (ADR-0044) — the maintainer ruling of 2026-09-05 // (option A) on the two operator run-lifecycle verbs. The engine has carried both for @@ -467,17 +467,17 @@ export const ROUTE_LEDGER: readonly RouteLedgerEntry[] = [ { route: 'GET /automation/:name/runs', domain: '/automation', disposition: 'sdk', client: 'automation.listRuns' }, { route: 'GET /automation/:name', domain: '/automation', disposition: 'sdk', client: 'automation.get' }, { route: 'PUT /automation/:name', domain: '/automation', disposition: 'sdk', client: 'automation.update', - note: "authored metadata, so `manage_metadata` gates it (#10145) — the same `isFlowAuthoringWrite` door as `POST /automation` above: fail-closed on an absent executionContext, an absent `systemPermissions` or an empty one, refusing 403 `PERMISSION_DENIED`, with only `isSystem` bypassing" }, + note: "authored metadata, so `manage_metadata` gates it — the same `isFlowAuthoringWrite` door as `POST /automation` above: fail-closed on an absent executionContext, an absent `systemPermissions` or an empty one, refusing 403 `PERMISSION_DENIED`, with only `isSystem` bypassing" }, { route: 'DELETE /automation/:name', domain: '/automation', disposition: 'sdk', client: 'automation.delete', - note: "authored metadata, so `manage_metadata` gates it (#10145) — the same `isFlowAuthoringWrite` door as `POST /automation` above, and the destructive member of the family: before the gate a tenant org owner WITHOUT the capability deregistered a registered flow, 200, and flow metadata is registered at ENVIRONMENT scope so the write crossed the tenant wall. Fail-closed on an absent executionContext, an absent `systemPermissions` or an empty one, refusing 403 `PERMISSION_DENIED`, with only `isSystem` bypassing" }, + note: "authored metadata, so `manage_metadata` gates it — the same `isFlowAuthoringWrite` door as `POST /automation` above, and the destructive member of the family: before the gate a tenant org owner WITHOUT the capability deregistered a registered flow, 200, and flow metadata is registered at ENVIRONMENT scope so the write crossed the tenant wall. Fail-closed on an absent executionContext, an absent `systemPermissions` or an empty one, refusing 403 `PERMISSION_DENIED`, with only `isSystem` bypassing" }, // ── auth (better-auth passthrough) ──────────────────────────────────────── { route: '* /auth/**', domain: '/auth', disposition: 'sdk', client: 'auth.me', - note: 'wholesale delegate to the auth service. Not enumerable route-by-route HERE, but no longer unenumerated: since #3656 plugin-auth/src/auth-route-ledger.ts carries the 55 SDK-reached routes plus the full mounted inventory, read off better-auth\'s live auth.api table' }, + note: 'wholesale delegate to the auth service. Not enumerable route-by-route HERE, but no longer unenumerated: plugin-auth/src/auth-route-ledger.ts carries the 55 SDK-reached routes plus the full mounted inventory, read off better-auth\'s live auth.api table' }, // ── ai (dynamic route table, owned by another repo) ─────────────────────── { route: '* /ai/**', domain: '/ai', disposition: 'dynamic', - note: 'routes come from service-ai buildAIRoutes() at plugin start — service-ai is a Cloud/EE package in the `cloud` repo, so this repo cannot enumerate them and the dispatcher only proxies. With the service absent the mount stays, so the answer is 501 carrying the shared serviceUnavailableMessage (the sentence discovery reports for the slot), NOT 404 — with two narrower arms: an anonymous caller is refused 401 first, and GET /ai/agents answers 200 with an empty list as a console courtesy. Enumerated on the other side of that boundary since #3718: cloud packages/service-ai/src/ai-route-ledger.ts, whose conformance test drives client.ai.* against the table buildAIRoutes() really returns. The client now expresses that table — ai.chat / ai.chatStream / ai.complete / ai.models / ai.conversations.* — but do NOT read a `sdk` disposition into this row: it stays `dynamic` because THIS repo still cannot see the routes. An earlier note here claimed the client "expresses nlq/suggest/insights against the REST AI routes"; that was never verified and was FALSE — nothing has ever mounted those three paths, and both they and the methods calling them are gone (#3718)' }, + note: 'routes come from service-ai buildAIRoutes() at plugin start — service-ai is a Cloud/EE package in the `cloud` repo, so this repo cannot enumerate them and the dispatcher only proxies. With the service absent the mount stays, so the answer is 501 carrying the shared serviceUnavailableMessage (the sentence discovery reports for the slot), NOT 404 — with two narrower arms: an anonymous caller is refused 401 first, and GET /ai/agents answers 200 with an empty list as a console courtesy. Enumerated on the other side of that boundary: cloud packages/service-ai/src/ai-route-ledger.ts, whose conformance test drives client.ai.* against the table buildAIRoutes() really returns. The client now expresses that table — ai.chat / ai.chatStream / ai.complete / ai.models / ai.conversations.* — but do NOT read a `sdk` disposition into this row: it stays `dynamic` because THIS repo still cannot see the routes. An earlier note here claimed the client "expresses nlq/suggest/insights against the REST AI routes"; that was never verified and was FALSE — nothing has ever mounted those three paths, and both they and the methods calling them are gone' }, // ── meta (legacy chain) ─────────────────────────────────────────────────── // [2026-08-31] SEEDED under the field's fill rule: `anonymous-deny-meta` is @@ -494,19 +494,19 @@ export const ROUTE_LEDGER: readonly RouteLedgerEntry[] = [ { route: 'GET /meta/:type', domain: '/meta', disposition: 'sdk', client: 'meta.getItems' }, { route: 'GET /meta/:type/:name', domain: '/meta', disposition: 'sdk', client: 'meta.getItem' }, { route: 'PUT /meta/:type/:name', domain: '/meta', disposition: 'sdk', client: 'meta.saveItem', - note: '[#7019] gated on `manage_metadata` (ADR-0066 D1) — the dispatcher transport of the REST save door. [#12702] the gate is the shared `metaWriteCapabilityVerdict` (`@objectstack/metadata-core`): `manage_org_presentation` is also admitted, ONLY for an `allowOrgOverride: true` type written org-scoped to the caller\'s own active organization; refusals answer 403 `PERMISSION_DENIED`, this transport\'s pinned spelling' }, + note: 'Gated on `manage_metadata` (ADR-0066 D1) — the dispatcher transport of the REST save door. The gate is the shared `metaWriteCapabilityVerdict` (`@objectstack/metadata-core`): `manage_org_presentation` is also admitted, ONLY for an `allowOrgOverride: true` type written org-scoped to the caller\'s own active organization, so a tenant org admin authors their own org\'s overlays without platform-wide `manage_metadata`; refusals answer 403 `PERMISSION_DENIED`, this transport\'s pinned spelling' }, { route: 'GET /meta/:type/:name/published', domain: '/meta', disposition: 'sdk', client: 'meta.getPublished', responseSchema: 'GetPublishedMetaItemResponseSchema', - note: '[#12038 ruling 1C] enveloped on THIS surface — the named schema is the `data`, and it is DELIBERATELY OPAQUE (`z.unknown()`): the route answers an arbitrary metadata item body, never a union frozen against the type registry. The REST twin (`rest-route-ledger.ts`) answers the same payload BARE' }, + note: 'Enveloped on THIS surface — the named schema is the `data`, and it is DELIBERATELY OPAQUE (`z.unknown()`): the route answers an arbitrary metadata item body, never a union frozen against the type registry. The REST twin (`rest-route-ledger.ts`) answers the same payload BARE' }, { route: 'GET /meta/_drafts', domain: '/meta', disposition: 'sdk', client: 'meta.listDrafts', responseSchema: 'ListDraftsResponseSchema', - note: '[#12038] enveloped on THIS surface — the named schema is the `data`; the REST twin answers the same payload BARE. Describe-only transcription of `listDrafts`\'s declared return; conformance: spec `api/protocol.test.ts`' }, + note: 'Enveloped on THIS surface — the named schema is the `data`; the REST twin answers the same payload BARE. Describe-only transcription of `listDrafts`\'s declared return; conformance: spec `api/protocol.test.ts`' }, // [2026-08-31] SEEDED — same rule as `GET /meta` above. { route: 'POST /meta/_migrate-stored', domain: '/meta', disposition: 'sdk', client: 'meta.migrateStored', authz: 'anonymous-deny-meta', - note: 'ADR-0087 stored-row canonicalization (#4327); gated on `manage_metadata`, preview unless { apply: true }. DELIBERATELY UNBOUND (#12038 ruling 2C) — this row would name the schema, but the report\'s only named type, `StoredMigrationReport`, lives in `@objectstack/metadata-protocol` (unreachable from the spec/api namespace this field resolves against); a second declaration in spec would drift against the CLI rendering the same report. Enveloped on this surface, BARE on the REST twin' }, + note: 'ADR-0087 stored-row canonicalization, the route form of `os migrate meta --stored`: it rewrites stored `sys_metadata` rows in place to their canonical form; gated on `manage_metadata`, preview unless { apply: true }. DELIBERATELY UNBOUND — this row would name the schema, but the report\'s only named type, `StoredMigrationReport`, lives in `@objectstack/metadata-protocol` (unreachable from the spec/api namespace this field resolves against); a second declaration in spec would drift against the CLI rendering the same report. Enveloped on this surface, BARE on the REST twin' }, { route: 'GET /meta/object/:name/state/:field', domain: '/meta', disposition: 'sdk', client: 'meta.getLegalNextStates', - note: '#9180 step 2 moved the SDK to the singular spelling and retired the plural REST registration; this row follows the client. DELIBERATE ASYMMETRY, not residue nobody has got to yet: the legacy if-chain branch in `domains/meta.ts` still matches BOTH literals (`objects` and `object`), so `/meta/objects/:name/state/:field` is REFUSED by a REST-fronted deployment (transport 404 — no registration left to match it) and ANSWERED wherever `dispatch()` is the front door (the `createHonoApp` catch-all, the documented embed shape). It stays by the maintainer re-weigh of the #9180 ruling, 2026-08-17 item 3: the tolerance is kept for external callers, no new refusals beyond what step 1 shipped, the external break deferred with no scheduled window — narrowing this arm is a NEW refusal on a SECOND surface and is the maintainer call, not a step of the ruling. ⛔ It is NOT the `META_URL_TO_SINGULAR` fold whose retirement was deferred: that is a map consulted for `/meta/:type`, this is a literal `||` that no request reaches through the fold — separate mechanisms under separate decisions, and conflating them is the specific error to avoid. So this row lists the canonical spelling of a branch that answers two, and `domains/meta-state-plural-tolerance.test.ts` pins BOTH halves so this note cannot quietly stop being true (#10179)' }, + note: 'Step 2 of the singular-segment ruling moved the SDK to the singular spelling and retired the plural REST registration; this row follows the client. DELIBERATE ASYMMETRY, not residue nobody has got to yet: the legacy if-chain branch in `domains/meta.ts` still matches BOTH literals (`objects` and `object`), so `/meta/objects/:name/state/:field` is REFUSED by a REST-fronted deployment (transport 404 — no registration left to match it) and ANSWERED wherever `dispatch()` is the front door (the `createHonoApp` catch-all, the documented embed shape). It stays by the maintainer re-weigh of the singular-segment ruling, 2026-08-17 item 3: the tolerance is kept for external callers, no new refusals beyond what step 1 shipped, the external break deferred with no scheduled window — narrowing this arm is a NEW refusal on a SECOND surface and is the maintainer call, not a step of the ruling. ⛔ It is NOT the `META_URL_TO_SINGULAR` fold whose retirement was deferred: that is a map consulted for `/meta/:type`, this is a literal `||` that no request reaches through the fold — separate mechanisms under separate decisions, and conflating them is the specific error to avoid. So this row lists the canonical spelling of a branch that answers two, and `domains/meta-state-plural-tolerance.test.ts` pins BOTH halves as behaviour so this note cannot quietly stop being true' }, // ── data (legacy chain) ─────────────────────────────────────────────────── { route: 'POST /data/:object/query', domain: '/data', disposition: 'sdk', client: 'data.query' }, @@ -534,7 +534,7 @@ export const ROUTE_LEDGER: readonly RouteLedgerEntry[] = [ note: 'client sends recordId in the body — both server shapes honor it' }, { route: 'POST /actions/global/:action', domain: '/actions', disposition: 'sdk', client: 'actions.invokeGlobal', servedBy: '/api/v1/actions/:object/:action', - note: '[#7526] a CALLING CONVENTION, not a registration: `global` is bound to `:object` on the row above, and handleActionsRequest routes that literal to the global-action table. Written as `servedBy` because the live-mount gate found no `/actions/global/:action` pattern and the honest answer is which pattern answers it — the gate re-derives that from the live router rather than believing this string' }, + note: 'A CALLING CONVENTION, not a registration: `global` is bound to `:object` on the row above, and handleActionsRequest routes that literal to the global-action table. Written as `servedBy` because the live-mount gate found no `/actions/global/:action` pattern and the honest answer is which pattern answers it — the gate re-derives that from the live router rather than believing this string' }, // [#7526] The OBJECT-LESS shape, mounted since #3913 and ledgered by // nothing until the parity gate read it off a booted server. The empty // segment is deliberate and load-bearing: it is the URL an SDK with no @@ -542,11 +542,11 @@ export const ROUTE_LEDGER: readonly RouteLedgerEntry[] = [ // before #3913 it fell to Hono's `notFound`. Ugly on the wire and correct; // what was wrong was that no ledger said it existed. { route: 'POST /actions//:action', domain: '/actions', disposition: 'sdk', client: 'actions.invokeGlobal', - note: 'the object-less spelling of the global-action call (#3913) — same handler and same `global` key as the row above, reached without naming an object' }, + note: 'the object-less spelling of the global-action call, routed rather than refused — same handler and same `global` key as the row above, reached without naming an object' }, { route: 'POST /actions/_activation/:object/:action', domain: '/actions', disposition: 'server-only', servedBy: '/api/v1/actions/:object/:action/:recordId', - note: '[#12160] ADR-0126 §8 item 2 — enable/disable ONE packaged action, the only non-invocation shape this domain serves. ' - + '⚠️ `servedBy`, and MEASURED on a real boot (#7526 caught it): no pattern of this spelling is registered. The ' + note: 'ADR-0126 §8 item 2 — enable/disable ONE packaged action, the only non-invocation shape this domain serves. ' + + '⚠️ `servedBy`, and MEASURED on a real boot (the live-mount parity gate, which asks the running router, caught it): no pattern of this spelling is registered. The ' + 'dispatcher mounts three `/actions` patterns, and a 3-segment activation path is matched by the LAST of them with ' + '`_activation` bound to `:object` — but that mount rebuilds the dispatch path from the matched params ' + '(`/actions/${object}/${action}/${recordId}`), so the path `handleActionsRequest` parses is byte-identical to the ' @@ -575,13 +575,13 @@ export const ROUTE_LEDGER: readonly RouteLedgerEntry[] = [ + 'the domain registry nor the dispatch() if-chain: dispatcher-plugin installs an ' + '`IHttpServer.setFallbackHandler` (Hono `app.notFound`) that runs only after every registered ' + 'route has missed, and for paths under this prefix resolves the request\'s environment + ' - + 'identity, probes `metadata.matchEndpoint`, and on a match runs the full chain (#5040 E5b): ' - + 'the policy keys authRequired / rateLimit / cacheTtlSeconds (E4), then target delegation (E5) — ' + + 'identity, probes `metadata.matchEndpoint`, and on a match runs the full chain: ' + + 'the policy keys authRequired / rateLimit / cacheTtlSeconds, then target delegation — ' + '`object_operation` through the same `callData` as /data, `flow` through the automation ' + 'service. `script` / `proxy` targets and the inputMapping / outputMapping keys are NOT ' + 'executed and answer 501. A miss (or an occupant of the metadata slot with no matchEndpoint, ' + 'or a multi-tenant request that resolves to no environment) writes nothing, leaving the ' - + 'transport\'s 404/405 answer untouched. LIVE since the #5040 E7 publish flip: a non-empty ' + + 'transport\'s 404/405 answer untouched. LIVE since publish flipped from refusing to executing: a non-empty ' + '`apis:` no longer fails wholesale, it is gated shape by shape ' + '(`packages/spec/src/api/endpoint-publish-gate.ts`), so declarations exist and a real boot ' + 'reaches this seam — the showcase serves two of them ' diff --git a/packages/runtime/src/sandbox/body-runner.test.ts b/packages/runtime/src/sandbox/body-runner.test.ts index ec041d7c58e..9eae4f3a0fa 100644 --- a/packages/runtime/src/sandbox/body-runner.test.ts +++ b/packages/runtime/src/sandbox/body-runner.test.ts @@ -366,7 +366,7 @@ describe('actionBodyRunnerFactory', () => { // Refusing silently would only relocate the invisibility the issue is about. expect(warnings).toHaveLength(1); expect(warnings[0]).toContain("type: '" + type + "'"); - expect(warnings[0]).toContain('#4352'); + expect(warnings[0]).toContain("`body` only runs for `type: 'script'`"); }); } diff --git a/packages/runtime/src/sandbox/body-runner.ts b/packages/runtime/src/sandbox/body-runner.ts index 49cfdfb8465..1e79101baf1 100644 --- a/packages/runtime/src/sandbox/body-runner.ts +++ b/packages/runtime/src/sandbox/body-runner.ts @@ -123,7 +123,8 @@ function buildBodyLogSurface( console.warn( `[BodyRunner] ${origin.kind} '${origin.name}' (app '${opts.appId}') declares the 'log' ` + `capability, but this BodyRunner was constructed without a logger — ctx.log output is ` + - `discarded. Pass \`logger\` to ${origin.kind}BodyRunnerFactory({ … }). See #7448.`, + `discarded. Pass \`logger\` to ${origin.kind}BodyRunnerFactory({ … }): the capability writes only ` + + `to that logger, never to \`console\`, so the host's level and sinks apply.`, ); }; // [#7661] `debug` is warned for like the other three. A member missing from @@ -387,7 +388,7 @@ export function actionBodyRunnerFactory( opts.logger?.warn?.( `[BodyRunner] action '${action.name}' declares \`type: '${type}'\` and carries a \`body\` — ` + `no handler was bound. \`body\` only runs for \`type: 'script'\`; a '${type}' action dispatches ` + - `on \`target\`. Set \`type: 'script'\` to run the body, or drop the \`body\`. See #4352.`, + `on \`target\`. Set \`type: 'script'\` to run the body, or drop the \`body\`.`, { appId: opts.appId, action: action.name, object: action.object, type }, ); return undefined; @@ -465,7 +466,7 @@ function warnDiscardedRecordWrites( `[BodyRunner] action '${actionName}' wrote ${fields.length} field(s) to ctx.record, which is a read-only ` + `pre-fetched snapshot — the writes never left the sandbox and the stored record is unchanged. ` + `To persist, call ctx.api.object('${object ?? ''}').update({ id: ctx.recordId, … }) ` + - `(needs the 'api.write' capability). See #4345.`, + `(needs the 'api.write' capability).`, { appId: opts.appId, action: actionName, object, fields }, ); } diff --git a/packages/runtime/src/standalone-stack.ts b/packages/runtime/src/standalone-stack.ts index 97e7597b66c..515c1ea5f15 100644 --- a/packages/runtime/src/standalone-stack.ts +++ b/packages/runtime/src/standalone-stack.ts @@ -149,7 +149,7 @@ function unsupportedDriverMessage(raw: string, source: 'OS_DATABASE_DRIVER' | 'd `[StandaloneStack] Unsupported ${source} value: "${raw}". ` + `Supported drivers: ${DATABASE_DRIVER_SELECTION_ALIASES.join(', ')}. ` + `Booting on the SQLite default instead would silently ignore the driver you asked for ` + - `and write into a local database (#3276). Fix the value, or unset it ` + + `and write into a local database. Fix the value, or unset it ` + `to let the OS_DATABASE_URL scheme select the driver.` ); } @@ -420,7 +420,7 @@ function assertUrlNamedForRemoteDriver( `and ${driver} has no local default to fall back on — its database lives on a server or ` + `endpoint this process cannot guess. Set OS_DATABASE_URL (or --database) to it. ` + `Falling back to the local SQLite file instead would connect you to a database you never ` + - `named, and every write would land in the wrong place (#3276).` + `named, and every write would land in the wrong place.` ); } @@ -672,7 +672,7 @@ export async function createStandaloneStack(config?: StandaloneStackConfig): Pro throw new Error( `[StandaloneStack] No dispatch arm for database driver kind: ${String(unreachable)}. ` + `Every kind in StandaloneDatabaseDriverSchema needs one — falling through to SQLite ` + - `is the #3276 defect.` + `would hand the caller a database engine they never selected.` ); } const defaultDatasourcePlugin = new DefaultDatasourcePlugin( diff --git a/scripts/doc-authoring-prose-id.baseline.json b/scripts/doc-authoring-prose-id.baseline.json index e6f625b16c3..73cbca4ee5b 100644 --- a/scripts/doc-authoring-prose-id.baseline.json +++ b/scripts/doc-authoring-prose-id.baseline.json @@ -437,69 +437,6 @@ "packages/qa/downstream-contract/src/stack.ts": { "#2035": 1 }, - "packages/runtime/src/action-execution.ts": { - "#11519": 1, - "#5933": 1 - }, - "packages/runtime/src/app-plugin.ts": { - "#8686": 1 - }, - "packages/runtime/src/dispatcher-error-vocabulary.ts": { - "#12772": 1, - "#5085": 2, - "#8885": 1, - "#9106": 1, - "#9223": 1, - "#9232": 1, - "#9460": 2, - "#9968": 1 - }, - "packages/runtime/src/domains/activation-gate.ts": { - "#10243": 1 - }, - "packages/runtime/src/domains/auth.ts": { - "#5085": 1 - }, - "packages/runtime/src/endpoint-executor.ts": { - "#5040": 2 - }, - "packages/runtime/src/endpoint-policy.ts": { - "#5040": 1 - }, - "packages/runtime/src/resolve-project-database.ts": { - "#6469": 1 - }, - "packages/runtime/src/route-ledger.ts": { - "#10145": 4, - "#10179": 1, - "#10243": 2, - "#12038": 11, - "#12160": 1, - "#12702": 1, - "#3584": 2, - "#3656": 1, - "#3718": 2, - "#3801": 1, - "#3913": 1, - "#4327": 1, - "#5040": 2, - "#5519": 2, - "#5561": 1, - "#5682": 1, - "#6593": 1, - "#7019": 1, - "#7526": 2, - "#9180": 2, - "#9406": 2 - }, - "packages/runtime/src/sandbox/body-runner.ts": { - "#4345": 1, - "#4352": 1, - "#7448": 1 - }, - "packages/runtime/src/standalone-stack.ts": { - "#3276": 3 - }, "packages/services/service-analytics/src/analytics-service.ts": { "#3867": 1, "#5222": 1, From a67666a600f2ec76ed6367dc83257618b81a90e0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 17:07:25 +0000 Subject: [PATCH 2/3] chore(changeset): @objectstack/runtime patch for the stage-3 runtime strings Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude --- ...0752-runtime-strings-state-the-decision.md | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .changeset/20752-runtime-strings-state-the-decision.md diff --git a/.changeset/20752-runtime-strings-state-the-decision.md b/.changeset/20752-runtime-strings-state-the-decision.md new file mode 100644 index 00000000000..e521832e44b --- /dev/null +++ b/.changeset/20752-runtime-strings-state-the-decision.md @@ -0,0 +1,20 @@ +--- +'@objectstack/runtime': patch +--- + +Runtime refusals, boot errors and warnings no longer cite tracker numbers; each one states the decision behind it in words + +Clause-②: no + +Strings `@objectstack/runtime` shows to callers, authors and operators pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. + +- The enablement refusal (`POST /automation/:name/toggle`, `POST /actions/_activation/:object/:action`) adds that the switch is not scoped to the caller's organization, which is why `manage_metadata` gates it. +- The doubled post-success navigation warning (`[action-contract]`) says the contract refuses a pair of destinations rather than ranking them, and that "declared `onSuccess` wins" is the console renderer's interim precedence, not a contract. +- The legacy database notice says `dev`, `start` and `migrate` now share one default database file. +- The `BodyRunner` warning for a `log` capability with no logger says the capability writes only to the factory's logger, never to `console`. +- The seed tenancy handoff warning says what a failure leaves behind: seed and API writes on separate autonumber counters until the next boot's migration repairs it. +- The auth forwarder's sanitised-500 log line says the client's message was withheld unconditionally and that this line is where the original error is read. +- The `StandaloneStack` guard for a driver kind with no dispatch arm says falling through to SQLite would hand the caller an engine they never selected. +- The `StandaloneStack` refusals for an unsupported or URL-less database driver, the declarative-endpoint hints, the `cacheTtlSeconds` warning and the two other `BodyRunner` warnings drop their citations; each already said what it refuses and why. + +Text only: no status, error code, field, route, export or control flow moves. A log filter or test that matched the old text (for example a `See #NNNN` suffix) needs the new spelling. From 48694cd548c0d98c9e2a3f7484718e7ce53aa6bc Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 17:50:55 +0000 Subject: [PATCH 3/3] chore(changeset): the stage-3 enablement bullet names only the /actions/_activation door The rewritten refusal lives in refuseUngrantedActivationAuthoring, whose one caller is the /actions/_activation door; POST /automation/:name/toggle refuses with its own message, which this change does not touch. Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude --- .changeset/20752-runtime-strings-state-the-decision.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/20752-runtime-strings-state-the-decision.md b/.changeset/20752-runtime-strings-state-the-decision.md index e521832e44b..58aa0ab84d8 100644 --- a/.changeset/20752-runtime-strings-state-the-decision.md +++ b/.changeset/20752-runtime-strings-state-the-decision.md @@ -8,7 +8,7 @@ Clause-②: no Strings `@objectstack/runtime` shows to callers, authors and operators pointed at an issue-tracker number for the reason behind them. The number goes; where the sentence did not already say what was decided, it now does. -- The enablement refusal (`POST /automation/:name/toggle`, `POST /actions/_activation/:object/:action`) adds that the switch is not scoped to the caller's organization, which is why `manage_metadata` gates it. +- The enablement refusal (`POST /actions/_activation/:object/:action`) adds that the switch is not scoped to the caller's organization, which is why `manage_metadata` gates it. - The doubled post-success navigation warning (`[action-contract]`) says the contract refuses a pair of destinations rather than ranking them, and that "declared `onSuccess` wins" is the console renderer's interim precedence, not a contract. - The legacy database notice says `dev`, `start` and `migrate` now share one default database file. - The `BodyRunner` warning for a `log` capability with no logger says the capability writes only to the factory's logger, never to `console`.