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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .changeset/20752-runtime-strings-state-the-decision.md
Original file line number Diff line number Diff line change
@@ -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 /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.
7 changes: 4 additions & 3 deletions packages/runtime/src/action-execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2472,9 +2472,10 @@ export function doubledPostSuccessNavigationWarning(
const where = objectName ? `${objectName}/${actionDef?.name ?? '<unnamed>'}` : String(actionDef?.name ?? '<unnamed>');
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.'
);
Expand Down
6 changes: 5 additions & 1 deletion packages/runtime/src/app-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
});
}
Expand Down
32 changes: 19 additions & 13 deletions packages/runtime/src/dispatcher-error-vocabulary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/<action>` 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.',
},

Expand Down Expand Up @@ -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.',
Expand All @@ -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 ───────
Expand Down Expand Up @@ -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.",
Expand Down Expand Up @@ -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 " +
Expand Down Expand Up @@ -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 ' +
Expand All @@ -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.',
},

Expand Down
4 changes: 2 additions & 2 deletions packages/runtime/src/domains/activation-gate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
),
Expand Down
3 changes: 2 additions & 1 deletion packages/runtime/src/domains/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) };
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime/src/endpoint-executor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});

Expand Down
4 changes: 2 additions & 2 deletions packages/runtime/src/endpoint-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand All @@ -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.',
};
}

Expand Down
2 changes: 1 addition & 1 deletion packages/runtime/src/endpoint-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
3 changes: 2 additions & 1 deletion packages/runtime/src/resolve-project-database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
};
Expand Down
Loading
Loading