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
17 changes: 17 additions & 0 deletions .changeset/20749-spec-strings-stage7-registry-rationales.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@objectstack/spec': patch
---

The error-code waiver reasons and the public auth-feature registry's notes no longer cite tracker numbers; each one states the decision behind it in words

Clause-②: no

Two registries in `@objectstack/spec` carry a written reason beside each entry. In `@objectstack/spec/api`, every `STANDARD_SYNONYM_WAIVERS` and `PROVENANCE_WAIVERS` entry records why a registered error code is kept or placed where it is. In `@objectstack/spec/kernel`, `PUBLIC_AUTH_FEATURES` records how each public auth flag is consumed. Seventeen of those reasons pointed at an issue-tracker number for the decision behind them. The number goes; where the sentence did not already say what was decided, it now does. For example:

- The five grandfathered synonyms (`CONFLICT`, `FORBIDDEN`, `INTERNAL`, `NOT_FOUND`, `UNAUTHORIZED`) say their consolidation onto the standard member is deferred until a code has a measured victim.
- The `FLOW_DISABLED` waiver says every door that dispatches a flow answers from one status table. The `TENANT_SCOPE_REQUIRED` waiver says an uninstall across every organization must be declared, never inferred from a missing one.
- The `phoneNumber` note names the fix the registry generalizes: create-user's phone field follows the opt-in phoneNumber plugin.

One reason also corrects a stale fact. The `deviceAuthorization` exemption described a known gap in objectui's `DeviceAuthPage`. That gap was closed in objectui on 2026-07-15: the page reads the flag and says device authorization is not enabled, rather than calling the device-auth endpoints. The text now says so.

Text only: no waiver's code, package, shadowed member or registration, no flag's surface, semantics or gated inputs, and no export, type, schema, order or count moves. Each reason still parses under its schema's non-empty rule. A tool that matches one of these reasons by its old text (for example by a tracker-number substring) needs the new spelling.
107 changes: 59 additions & 48 deletions packages/spec/src/api/error-code-ledger.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1615,36 +1615,39 @@ export const STANDARD_SYNONYM_WAIVERS: readonly StandardSynonymWaiver[] = [
{
code: 'CONFLICT',
shadows: 'RESOURCE_CONFLICT',
reason: 'Pre-gate synonym on the wire (respondSharingError 409 arm; registered by #8111). ' +
'Wire value kept; consolidation deferred per #8211.',
reason: 'Pre-gate synonym on the wire (respondSharingError 409 arm), registered as it stood ' +
'when the record-sharing errors moved onto the ADR-0112 envelope, so the wire stayed ' +
'byte-identical. Wire value kept; consolidation deferred until it has a measured victim.',
},
{
code: 'FORBIDDEN',
shadows: 'PERMISSION_DENIED',
reason: 'Pre-gate synonym on the wire from @objectstack/rest, plugin-sharing and ' +
'plugin-approvals; #13353 added the cloud-connection provenance row for the same ' +
'pre-existing wire value (its marketplace-install plugin-route 403). ' +
'Wire value kept; consolidation deferred per #8211.',
'plugin-approvals; cloud-connection lists the same pre-existing wire value under its own ' +
'provenance row (its marketplace-install plugin-route 403). ' +
'Wire value kept; consolidation deferred until it has a measured victim.',
},
{
code: 'INTERNAL',
shadows: 'INTERNAL_ERROR',
reason: 'Pre-gate synonym on the wire from five packages. Wire value kept; ' +
'consolidation deferred per #8211.',
'consolidation deferred until it has a measured victim.',
},
{
code: 'NOT_FOUND',
shadows: 'RESOURCE_NOT_FOUND',
reason: 'Pre-gate synonym on the wire from @objectstack/rest and plugin-sharing; ' +
'#19441 added the plugin-security provenance row for the same pre-existing wire value ' +
'(its permission-set overlay-discard 404). Wire value kept; consolidation deferred per #8211.',
'plugin-security lists the same pre-existing wire value under its own provenance row ' +
'(its permission-set overlay-discard 404). Wire value kept; consolidation deferred until ' +
'it has a measured victim.',
},
{
code: 'UNAUTHORIZED',
shadows: 'UNAUTHENTICATED',
reason: 'Pre-gate synonym (401 reason phrase) on the wire from @objectstack/rest — ' +
'surfaced by the detector when the #8211 gate landed, beyond the four the card named; ' +
'same class, same grandfather rationale. Wire value kept; consolidation deferred per #8211.',
'surfaced by the synonym detector when it landed, beyond the four first reported; ' +
'same class, same grandfather rationale. Wire value kept; consolidation deferred until ' +
'it has a measured victim.',
},
];

Expand Down Expand Up @@ -1749,52 +1752,57 @@ export const PROVENANCE_WAIVERS: readonly ProvenanceWaiver[] = [
reason: 'Shared constructor one package over: metadata-core\'s ' +
'`engineUpdateDispatchRejectError` spells the string, but the throw ships in ' +
'production from `ObjectQL.update` (engine.ts) — the objectql row\'s own comment ' +
'records "hence registered here" (#11142/#11230).',
'records "hence registered here".',
},
{
package: '@objectstack/service-automation',
code: 'FLOW_DISABLED',
registeredUnder: '@objectstack/runtime',
reason: 'The trigger door, not the producer, names the wire vocabulary: the engine ' +
'returns `AutomationResult.code` and runtime\'s doors read it and answer 409 ' +
'(#9415/#9446; the runtime row\'s comment records the decision).',
'returns `AutomationResult.code` and runtime\'s doors read it and answer 409 — every ' +
'door that dispatches a flow answers from one status table, by ruling (the runtime ' +
'row\'s comment records the decision).',
},
{
package: '@objectstack/service-automation',
code: 'FLOW_NO_START_NODE',
registeredUnder: '@objectstack/runtime',
reason: 'Same decision as FLOW_DISABLED, 422 arm (#9415/#9446): the trigger door ' +
reason: 'Same decision as FLOW_DISABLED, its 422 arm: the trigger door ' +
'names the wire vocabulary; the engine result carries the classification.',
},
{
package: '@objectstack/service-automation',
code: 'FLOW_INPUT_SCHEMA_INVALID',
registeredUnder: '@objectstack/runtime',
reason: 'Registered ahead of its producer by design (#10025 → #11504, the #10413 → ' +
'#10576 split shape): the engine\'s `execute()` catch classifies the refusal, the ' +
'trigger door serves it — the runtime row\'s comment records "registered HERE and ' +
'not under the engine\'s package" with its three FLOW_* siblings.',
reason: 'Registered ahead of its producer by design: the ruling that a definition-level ' +
'input-schema refusal is non-retryable and never dispatched split into a contract ' +
'half, which minted this code, and a services half that emits it, the contract half ' +
'landing first. The engine\'s `execute()` catch classifies the refusal, the trigger ' +
'door serves it — the runtime row\'s comment records "registered HERE and not under ' +
'the engine\'s package" with its three FLOW_* siblings.',
},
{
package: '@objectstack/service-datasource',
code: 'EXTERNAL_IMPORT_ERROR',
registeredUnder: '@objectstack/rest',
reason: 'Adjudicated on #13353: the only door for `importObject` is rest\'s ' +
'`POST …/tables/:remote/import` (external-datasource-routes.ts), whose catch stamps ' +
'this code itself for EVERY importObject throw and never reads the producer\'s ' +
'declaration — the door names the wire vocabulary. The producer\'s `err.code` ' +
'(`importNameRefusedError`) is the #8016 declaration shape, agreeing with the door ' +
'by construction, not a second wire emitter.',
reason: 'Adjudicated when the provenance gate landed: the only door for `importObject` ' +
'is rest\'s `POST …/tables/:remote/import` (external-datasource-routes.ts), whose ' +
'catch stamps this code itself for EVERY importObject throw and never reads the ' +
'producer\'s declaration — the door names the wire vocabulary. The producer\'s ' +
'`err.code` (`importNameRefusedError`) declares its own `status` and `code`, the ' +
'shape the shared thrown-error resolver honours, agreeing with the door by ' +
'construction, not a second wire emitter.',
},
{
package: '@objectstack/client',
code: 'UPLOAD_SESSION_EXPIRED',
registeredUnder: '@objectstack/service-storage',
reason: 'Client-side synthesis (#7870): `resumeUpload` mirrors the server\'s 410 pair ' +
'when the progress poll reports `expired`, so caller branches fire identically. The ' +
'ledger\'s scope prose covers the SERVING side; whether a client-synthesised code ' +
'belongs in the ledger at all is the open scope question #13353 recorded — ' +
'deliberately a waiver, not a row, until that question is ruled.',
reason: 'Client-side synthesis: `resumeUpload` mirrors the server\'s 410 pair when the ' +
'progress poll reports `expired`, so caller branches fire identically. The ledger\'s ' +
'scope prose covers the SERVING side; whether a client-synthesised code belongs in ' +
'the ledger at all is an open scope question, recorded when the provenance gate ' +
'landed and left unruled for want of pull — deliberately a waiver, not a row, until ' +
'that question is ruled.',
},
{
package: '@objectstack/spec',
Expand All @@ -1809,35 +1817,38 @@ export const PROVENANCE_WAIVERS: readonly ProvenanceWaiver[] = [
package: '@objectstack/types',
code: 'VALIDATION_FAILED',
registeredUnder: '@objectstack/runtime',
reason: 'Shared constructor by design (#8016/#3918): `validationFailure()` lives in ' +
'the dependency-light package so BOTH doors recognise one shape; the throws are ' +
'served under the emitting doors\' own registrations (runtime\'s dispatcher exits, ' +
'rest\'s `mapDataError` — both packages list the code).',
reason: 'Shared constructor by design: `validationFailure()` lives in the ' +
'dependency-light package beside the one thrown-error mapping both doors share, so ' +
'BOTH doors recognise one shape and answer it 400 with its `fields[]`, never 500; the ' +
'throws are served under the emitting doors\' own registrations (runtime\'s ' +
'dispatcher exits, rest\'s `mapDataError` — both packages list the code).',
},
{
package: '@objectstack/core',
code: 'ANALYTICS_DATE_RANGE_UNRECOGNIZED',
registeredUnder: '@objectstack/runtime',
reason: 'Shared constructor one package over, the #8016 shape (#16322): ' +
'`analyticsDateRangeUnrecognizedError` (utils/analytics-date-range.ts) spells the ' +
'string ONCE so driver-memory\'s cube face and BOTH service-analytics strategies ' +
'refuse identically — which is the property the card\'s shared conformance fixture ' +
'exists to hold, and which two independent refusals could not give. Core ships no ' +
'HTTP door; the wire emission stays runtime\'s, whose row names this exact second ' +
'moment. ⛔ Deliberately ONE waiver rather than a row per driver: with one ' +
'constructor there is one stamp site, and rows for packages that stamp nothing ' +
'would be the dead weight this file\'s gate refuses.',
reason: 'Shared constructor one package over: `analyticsDateRangeUnrecognizedError` ' +
'(utils/analytics-date-range.ts) spells the string ONCE so driver-memory\'s cube face ' +
'and BOTH service-analytics strategies refuse an unrecognised `dateRange` identically ' +
'— which is the property the shared date-range conformance fixture exists to hold, ' +
'and which two independent refusals could not give. Core ships no HTTP door; the wire ' +
'emission stays runtime\'s, whose row names this exact second moment. ⛔ Deliberately ' +
'ONE waiver rather than a row per driver: with one constructor there is one stamp ' +
'site, and rows for packages that stamp nothing would be the dead weight this file\'s ' +
'gate refuses.',
},
{
package: '@objectstack/runtime',
code: 'TENANT_SCOPE_REQUIRED',
registeredUnder: '@objectstack/metadata-protocol',
reason: 'The door mirrors the producer\'s refusal; it is not a second emitter. ' +
'`DELETE /packages/:id` (domains/packages.ts, `requireUninstallOrganizationScope`) asks ' +
'`deletePackage`\'s organization-scope question BEFORE `registry.uninstallPackage`, and ' +
'answers with the code `deletePackage` refuses with (#7780), so a refused uninstall ' +
'changes nothing (#20492). The door never sends `allTenants`, so its condition is exactly ' +
'the producer\'s "no organization"; the protocol keeps its own refusal as the second line ' +
'and stays the registered emitter.',
'`DELETE /packages/:id` (domains/packages.ts, `requireUninstallOrganizationScope`) ' +
'asks `deletePackage`\'s organization-scope question BEFORE ' +
'`registry.uninstallPackage`, and answers with the code `deletePackage` refuses a ' +
'scope-less uninstall with (an uninstall across every organization must be declared, ' +
'never inferred from a missing one), so a refused uninstall changes nothing. The door ' +
'never sends `allTenants`, so its condition is exactly the producer\'s "no ' +
'organization"; the protocol keeps its own refusal as the second line and stays the ' +
'registered emitter.',
},
];
15 changes: 9 additions & 6 deletions packages/spec/src/kernel/public-auth-features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,10 +191,10 @@ export const PUBLIC_AUTH_FEATURES = {
semantics: 'opt-in',
exempt: {
reason:
'No spec input (sys_device_code declares no actions). Known gap: ' +
'objectui DeviceAuthPage hits the device-auth endpoints without ' +
'checking this flag (absent from its client type) — tracked in ' +
'objectui#2513 (#2874 P2②).',
'No spec input (sys_device_code declares no actions). Login ' +
'consumption verified: objectui DeviceAuthPage reads this flag and, ' +
'when it is off, says device authorization is not enabled instead ' +
'of calling the device-auth endpoints.',
},
},
admin: {
Expand All @@ -217,7 +217,9 @@ export const PUBLIC_AUTH_FEATURES = {
semantics: 'opt-in',
gatedInputs: ['sys_user.actions.create_user.params.phoneNumber'],
notes:
'The original #2871 fix. Also read by objectui LoginForm for the ' +
'The fix this registry generalizes: create-user\'s phone field ' +
'follows the opt-in phoneNumber plugin instead of offering a field ' +
'the backend refuses. Also read by objectui LoginForm for the ' +
'phone+password sign-in mode.',
},
phoneNumberOtp: {
Expand All @@ -227,7 +229,8 @@ export const PUBLIC_AUTH_FEATURES = {
reason:
'Login-surface only: gates the "sign in with verification code" link ' +
'(LoginForm) and the phone branch of forgot-password. Only advertised ' +
'when SMS is actually deliverable (#2780).',
'when an SMS service can actually deliver the code; a log-only ' +
'transport in production keeps it off.',
},
},
} as const satisfies Record<string, PublicAuthFeatureEntry>;
Expand Down
Loading