From 679c6de5f938909a9df483f5d5696bb38f67b9ee Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 06:28:51 +0000 Subject: [PATCH 01/10] fix(metadata-protocol): drop the bracketed opener that restates the throw's own code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every refusal `ObjectStackProtocolImplementation` and `SysMetadataRepository` raise opened with a lowercase `[tag]` that is the restatement of the `code` the same throw declares. `withoutDeclaredCodePrefix` strips only `CODE: ` casing and separator, so the bracketed spelling was never stripped and reached the caller in `error.message` — the duplication the 2026-08-29 ruling removes, because the same fact already rides the `code` axis. 37 openers removed across the two files; in-file prose that named a tag now names the code, which is the axis that still carries it. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude --- packages/metadata-protocol/src/protocol.ts | 113 +++++++++--------- .../src/sys-metadata-repository.ts | 12 +- 2 files changed, 63 insertions(+), 62 deletions(-) diff --git a/packages/metadata-protocol/src/protocol.ts b/packages/metadata-protocol/src/protocol.ts index be9dd23ad9c..ce0ae6aafeb 100644 --- a/packages/metadata-protocol/src/protocol.ts +++ b/packages/metadata-protocol/src/protocol.ts @@ -289,7 +289,7 @@ function canonicalizeMetaRequestType(request: T): T const refusal = metaUrlSpellingRefusal(request.type); if (refusal) { const err = new Error( - `[invalid_request] '${request.type}' is not a recognised spelling of metadata type ` + `'${request.type}' is not a recognised spelling of metadata type ` + `'${refusal.declared}'. Address it as '${refusal.declared}' or '${refusal.hint}'. ` + `Refused rather than treated as a plugin-registered type, because forwarding an unrecognised ` + `spelling of a declared type would create a second namespace under type='${request.type}'.`, @@ -2304,8 +2304,8 @@ function carryCatalogedErrorCode(target: Error, source: unknown): void { * met. It is the inverse and bounded question: **did we author this sentence * for a caller?** A producer that declared 4xx has said the failure is the * caller's to fix and has written the remedy into the message — the - * self-correcting refusals `SysMetadataRepository` raises (`[item_locked]`, - * `[writable_package_required]`, `[no_draft]`, …) are exactly that, and they + * self-correcting refusals `SysMetadataRepository` raises (`ITEM_LOCKED`, + * `WRITABLE_PACKAGE_REQUIRED`, `NO_DRAFT`, …) are exactly that, and they * must survive intact. Everything else is withheld by DEFAULT, so a dialect * this repo has never run is handled correctly without anyone having enumerated * it. @@ -2639,7 +2639,7 @@ export function seedRequestValidationError(zodIssues: unknown): Error { // refusal thread it onto `seedApplied.issues` beside the headline, so // the author's curated per-key prose still arrives exactly once. const err = new Error( - `[invalid_metadata] the published seed bodies failed spec validation: ` + `The published seed bodies failed spec validation: ` + metadataIssueHeadline(issues), ); (err as any).code = 'INVALID_METADATA'; @@ -8201,7 +8201,7 @@ export class ObjectStackProtocolImplementation implements if (readState === 'draft') { if (item === undefined) { const err: any = new Error( - `[no_draft] No pending draft exists for ${request.type}/${request.name}.`, + `No pending draft exists for ${request.type}/${request.name}.`, ); err.code = 'NO_DRAFT'; err.status = 404; @@ -13521,7 +13521,7 @@ export class ObjectStackProtocolImplementation implements */ private static codeOnlyCreateError(type: string): Error { const err = new Error( - `[not_creatable] Metadata type '${type}' is code-only: the metadata-type registry declares ` + `Metadata type '${type}' is code-only: the metadata-type registry declares ` + `allowRuntimeCreate=false and allowOrgOverride=false, so it cannot be created through the ` + `runtime metadata API (PUT /api/v1/meta/${type}/:name) on any kernel.` + ObjectStackProtocolImplementation.codeOnlySourceHint(type) @@ -13540,7 +13540,7 @@ export class ObjectStackProtocolImplementation implements */ private static codeOnlyOverrideError(type: string, name: string): Error { const err = new Error( - `[not_overridable] Metadata item '${type}/${name}' is provided by a code package and its type is ` + `Metadata item '${type}/${name}' is provided by a code package and its type is ` + `code-only (allowRuntimeCreate=false, allowOrgOverride=false), so it cannot be overlaid through ` + `the runtime metadata API on any kernel.` + ObjectStackProtocolImplementation.codeOnlySourceHint(type) @@ -13636,7 +13636,7 @@ export class ObjectStackProtocolImplementation implements if (this.isOverlayAllowed(type)) return null; if (!this.STATIC_REGISTRY_TYPES.has(singular) && !this.STATIC_REGISTRY_TYPES.has(type)) return null; const err: any = new Error( - `[not_overridable] Metadata item '${type}/${name}' cannot be written org-scoped ` + `Metadata item '${type}/${name}' cannot be written org-scoped ` + `(organization '${organizationId}'). ` + `The metadata-type registry declares allowOrgOverride=false for '${singular}', so the platform has ` + `no per-org channel for it: boot hydration loads env-wide rows only, so this row would be absent ` @@ -14023,7 +14023,7 @@ export class ObjectStackProtocolImplementation implements const canonical = canonicalMetaType(entry.type); if (canonical !== entry.type) { const err: any = new Error( - `[audit_type_not_canonical] Refusing to write a sys_metadata_audit row under the ` + `Refusing to write a sys_metadata_audit row under the ` + `non-canonical metadata type '${entry.type}' (canonical: '${canonical}') for ` + `'${entry.name}'. ADR-0010's trail is keyed on (type, name) and read back through ` + `the '/meta' boundary, which folds — a row filed under '${entry.type}' is a row no ` @@ -14090,7 +14090,7 @@ export class ObjectStackProtocolImplementation implements if (!refusal) return null; const reason = state.lockReason ?? refusal.reason; const err = new Error( - `[item_locked] ${args.type}/${args.name} is locked (_lock=${state.lock}${state.lockSource ? `, source=${state.lockSource}` : ''}). ` + `${args.type}/${args.name} is locked (_lock=${state.lock}${state.lockSource ? `, source=${state.lockSource}` : ''}). ` + `${reason} — See ADR-0010 §3.3.`, ); (err as any).code = 'ITEM_LOCKED'; @@ -14158,7 +14158,7 @@ export class ObjectStackProtocolImplementation implements if (!refusal) return null; const reason = state.lockReason ?? refusal.reason; const err = new Error( - `[item_locked] ${args.type}/${args.name} is locked (_lock=${state.lock}${state.lockSource ? `, source=${state.lockSource}` : ''}). ` + `${args.type}/${args.name} is locked (_lock=${state.lock}${state.lockSource ? `, source=${state.lockSource}` : ''}). ` + `${reason} — See ADR-0010 §3.3.`, ); (err as any).code = 'ITEM_LOCKED'; @@ -14451,7 +14451,7 @@ export class ObjectStackProtocolImplementation implements name: string, rowPackageId: string, ownerPackageId: string, ): Error { const err: any = new Error( - `[object_overlay_package_mismatch] Cannot layer object '${name}': the overlay is bound to package ` + `Cannot layer object '${name}': the overlay is bound to package ` + `'${rowPackageId}', but the object is owned by package '${ownerPackageId}'. ` + `An object has exactly one registry entry, so it can carry exactly one overlay layer — bind the ` + `customization to '${ownerPackageId}', or have '${rowPackageId}' extend the object instead. ` @@ -14609,7 +14609,7 @@ export class ObjectStackProtocolImplementation implements const canonicalType = canonicalMetaType(type); if (canonicalType !== type) { const err: any = new Error( - `[registry_type_not_canonical] Refusing to register a SchemaRegistry overlay entry under ` + `Refusing to register a SchemaRegistry overlay entry under ` + `the non-canonical metadata type '${type}' (canonical: '${canonicalType}'). The registry ` + `holds exactly one plain key per (type, name) and every reader addresses it through the ` + `'/meta' boundary, which folds — an entry minted under '${type}' is a second namespace no ` @@ -15298,7 +15298,7 @@ export class ObjectStackProtocolImplementation implements private refuseUngrammaticalMetaItemName(request: { type: string, name: string }): void { if (METADATA_ITEM_NAME_PATTERN.test(request.name)) return; const err = new Error( - `[invalid_request] ${JSON.stringify(request.name)} is not a legal metadata item name. ` + `${JSON.stringify(request.name)} is not a legal metadata item name. ` + `Item names are lowercase snake_case segments, optionally dot-qualified — ` + `/^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$/ — e.g. 'crm_lead' or 'crm_lead.pipeline'. ` + `No slashes, spaces, uppercase, empty segments, or leading/trailing dots. ` @@ -15387,7 +15387,7 @@ export class ObjectStackProtocolImplementation implements // Exemption 2 — the namespace predates this write. if (await this.metaTypeNamespaceExists(unrecognised.type)) return; const err = new Error( - `[invalid_request] '${unrecognised.type}' is not a metadata type. The platform declares ` + `'${unrecognised.type}' is not a metadata type. The platform declares ` + `no such type, and since #8586 retired 'additionalTypes' a plugin cannot declare one ` + `either — so this write would mint a sys_metadata namespace under ` + `type='${unrecognised.type}' that nothing reads and nothing serves. Address a real ` @@ -15460,10 +15460,10 @@ export class ObjectStackProtocolImplementation implements // failure and break that convention. The structural twin is // {@link rollbackMetaItem}'s own opening guard — same class, same // position, a malformed REQUEST ENVELOPE rather than an off-spec - // document — which is `[invalid_request]`/400. + // document — which is `INVALID_REQUEST`/400. if (!request.item) { const err: any = new Error( - `[invalid_request] saveMetaItem requires an 'item' body for '${request.type}/${request.name}'. ` + `saveMetaItem requires an 'item' body for '${request.type}/${request.name}'. ` + `Send the metadata document as the request body, or wrap it as {"item": {...}} / {"metadata": {...}}. ` + `An explicitly null item is refused rather than persisted as an empty document.`, ); @@ -15702,7 +15702,7 @@ export class ObjectStackProtocolImplementation implements ); } const err = new Error( - `[not_overridable] Metadata item '${request.type}/${request.name}' is provided by a code package ` + `Metadata item '${request.type}/${request.name}' is provided by a code package ` + `and the type has not opted into per-org overlay writes (allowOrgOverride=false). ` + `Edit the source artifact and redeploy, or set OS_METADATA_WRITABLE to grant a runtime escape hatch. ` + `See docs/adr/0005-metadata-customization-overlay.md.` @@ -15857,7 +15857,7 @@ export class ObjectStackProtocolImplementation implements // circle. See {@link destructiveChangeRemedy}. const summary = issues.slice(0, 3).map((i) => i.message).join('; '); const err = new Error( - `[destructive_change] ${request.type}/${request.name} would drop or transform existing data: ${summary}` + `${request.type}/${request.name} would drop or transform existing data: ${summary}` + (issues.length > 3 ? ` (+${issues.length - 3} more)` : '') + ` — ${destructiveChangeRemedy(request.writeFace, request.name)}` ); @@ -15895,7 +15895,7 @@ export class ObjectStackProtocolImplementation implements && 'code' in it && 'overlay' in it && 'overlayScope' in it && 'effective' in it; if (looksLikeLayeredEnvelope) { const err = new Error( - `[invalid_metadata] ${request.type}/${request.name}: the request body is a layered read ` + `${request.type}/${request.name}: the request body is a layered read ` + `envelope ({ code, overlay, overlayScope, effective }), not a metadata body. ` + `Unwrap and send the effective/overlay document instead — the layered shape is read-only ` + `(GET ?layers=true) and must never be persisted.` @@ -15992,7 +15992,7 @@ export class ObjectStackProtocolImplementation implements // resubmitting the same body cannot help. const first = result.conflicts[0]!; const err = new Error( - `[flow_conversion_conflict] ${request.type}/${request.name}: conversion refused — ` + `${request.type}/${request.name}: conversion refused — ` + `'${first.token}' at ${first.path} is a live name in this environment ` + `(${result.conflicts.length} conflict(s)). ${first.message}` ); @@ -16045,7 +16045,7 @@ export class ObjectStackProtocolImplementation implements // with it. `err.issues` below is unconditional — the split // decides only what the SENTENCE repeats. const err = new Error( - `[invalid_metadata] ${request.type}/${request.name} failed spec validation: ` + `${request.type}/${request.name} failed spec validation: ` + specValidationFindings(request.writeFace, issues) ); (err as any).code = 'INVALID_METADATA'; @@ -16456,7 +16456,7 @@ export class ObjectStackProtocolImplementation implements } catch (err: any) { if (err instanceof ConflictError) { const conflict = new Error( - `[metadata_conflict] ${request.type}/${request.name} has been modified since you loaded it. ` + `${request.type}/${request.name} has been modified since you loaded it. ` + `Expected parent ${err.expectedParent ?? 'null'} but current is ${err.actualHead ?? 'null'}.`, ); (conflict as any).code = 'METADATA_CONFLICT'; @@ -16855,7 +16855,7 @@ export class ObjectStackProtocolImplementation implements // this line changed, not assumed: every refusal this `try` can // receive declares 4xx — `saveMetaItem`'s spec rejection // (422 `INVALID_METADATA`), `SysMetadataRepository`'s - // `[item_locked]` / `[writable_package_required]` (403 / 422) — + // `ITEM_LOCKED` / `WRITABLE_PACKAGE_REQUIRED` (403 / 422) — // so each is still quoted verbatim and still names the fix. // // ⚠️ The old fallback was `String(e)`, which is itself a @@ -16956,7 +16956,7 @@ export class ObjectStackProtocolImplementation implements /** * Promote the pending draft overlay to the live (`active`) row. - * Records a history event with `op='publish'`. 404 (`[no_draft]`) + * Records a history event with `op='publish'`. 404 (`NO_DRAFT`) * when there is nothing to publish. */ async publishMetaItem(request: { @@ -17344,7 +17344,7 @@ export class ObjectStackProtocolImplementation implements if (!ObjectStackProtocolImplementation.isOverlayAllowed(singularType) && !ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularType)) { const err: any = new Error( - `[not_overridable] Metadata type '${request.type}' is not draftable — no overlay/runtime-create permission.`, + `Metadata type '${request.type}' is not draftable — no overlay/runtime-create permission.`, ); err.code = 'NOT_OVERRIDABLE'; err.status = 403; @@ -17467,7 +17467,7 @@ export class ObjectStackProtocolImplementation implements } catch (err: any) { if (err instanceof ConflictError) { const conflict: any = new Error( - `[metadata_conflict] ${request.type}/${request.name} published row advanced while you held the draft. ` + `${request.type}/${request.name} published row advanced while you held the draft. ` + `Expected parent ${err.expectedParent ?? 'null'} but current is ${err.actualHead ?? 'null'}.`, ); conflict.code = 'METADATA_CONFLICT'; @@ -18998,7 +18998,7 @@ export class ObjectStackProtocolImplementation implements // composition `resolveActiveOrganizationId` makes real. if (request.organizationId && request.allTenants === true) { const err = new Error( - `[tenant_scope_required] Refusing to uninstall '${request.packageId}':` + `Refusing to uninstall '${request.packageId}':` + ` organizationId ('${request.organizationId}') and allTenants: true are mutually exclusive —` + ` one scopes the uninstall to a single tenant, the other clears every tenant's rows.` + ` — pass organizationId alone to scope it, or allTenants: true alone to confirm the cross-tenant uninstall.` @@ -19013,7 +19013,7 @@ export class ObjectStackProtocolImplementation implements // affirmative `true` does. if (!request.organizationId && request.allTenants !== true) { const err = new Error( - `[tenant_scope_required] Refusing to uninstall '${request.packageId}' with no organization scope:` + `Refusing to uninstall '${request.packageId}' with no organization scope:` + ` an uninstall that names neither an organization nor an explicit cross-tenant intent would delete` + ` EVERY organization's rows for this package.` + ` — pass organizationId to scope it, or allTenants: true to confirm the cross-tenant uninstall.` @@ -20185,7 +20185,7 @@ export class ObjectStackProtocolImplementation implements } const row = (await this.engine.findOne('sys_metadata_commit', { where })) as any; if (!row) { - const err: any = new Error(`[commit_not_found] No commit '${request.commitId}'.`); + const err: any = new Error(`No commit '${request.commitId}'.`); err.code = 'COMMIT_NOT_FOUND'; err.status = 404; throw err; @@ -20593,10 +20593,10 @@ export class ObjectStackProtocolImplementation implements // field carried `SQLITE_ERROR: no such table: sys_metadata`. // // Every authored refusal this `try` can receive declares 4xx — - // `SysMetadataRepository`'s `[version_not_found]` 404 (measured - // end to end and still quoted whole), `[no_draft]` 404, - // `[version_not_restorable]` 409, `[item_locked]` 403, - // `[writable_package_required]` 422 — so the self-correcting + // `SysMetadataRepository`'s `VERSION_NOT_FOUND` 404 (measured + // end to end and still quoted whole), `NO_DRAFT` 404, + // `VERSION_NOT_RESTORABLE` 409, `ITEM_LOCKED` 403, + // `WRITABLE_PACKAGE_REQUIRED` 422 — so the self-correcting // sentences survive and only the driver line is withheld. // [#8441] …and the `code` limb beside it, which #8333 left // alone as a different field with a different rule. Measured @@ -20709,7 +20709,7 @@ export class ObjectStackProtocolImplementation implements } const target = (await this.engine.findOne('sys_metadata_commit', { where })) as any; if (!target) { - const err: any = new Error(`[commit_not_found] No commit '${request.commitId}'.`); + const err: any = new Error(`No commit '${request.commitId}'.`); err.code = 'COMMIT_NOT_FOUND'; err.status = 404; throw err; @@ -20753,7 +20753,7 @@ export class ObjectStackProtocolImplementation implements // sys_metadata` reached the field. // // This `try` wraps exactly one call, and `revertCommit` throws - // only refusals it declared — `[commit_not_found]` 404 + // only refusals it declared — `COMMIT_NOT_FOUND` 404 // (measured, still quoted whole) and the lock/authorization // 4xx above it; its PER-ITEM failures never throw at all, they // are collected into its own `failed[]` (P11). @@ -20772,8 +20772,8 @@ export class ObjectStackProtocolImplementation implements /** * Restore the body recorded at history `toVersion` as the new * live row. Writes a history event with `op='revert'`. 404 - * (`[version_not_found]`) when the target version doesn't exist; - * 409 (`[version_not_restorable]`) when the target is a delete + * (`VERSION_NOT_FOUND`) when the target version doesn't exist; + * 409 (`VERSION_NOT_RESTORABLE`) when the target is a delete * tombstone (no body to bring back). */ async rollbackMetaItem(request: { @@ -20792,7 +20792,7 @@ export class ObjectStackProtocolImplementation implements }> { if (!Number.isFinite(request.toVersion) || request.toVersion < 1) { const err: any = new Error( - `[invalid_request] rollbackMetaItem requires a positive integer 'toVersion' (got ${request.toVersion}).`, + `rollbackMetaItem requires a positive integer 'toVersion' (got ${request.toVersion}).`, ); err.code = 'INVALID_REQUEST'; err.status = 400; @@ -20807,7 +20807,7 @@ export class ObjectStackProtocolImplementation implements // the very top: that is the position {@link saveMetaItem} documents for // this exact pair, calling this method's opening guard its structural // twin — a malformed request envelope is refused before its type key is - // canonicalised, and both refusals are `[invalid_request]`/400 anyway. + // canonicalised, and both refusals are `INVALID_REQUEST`/400 anyway. // // What the fold reaches here, measured rather than assumed: // @@ -20825,7 +20825,7 @@ export class ObjectStackProtocolImplementation implements // `external_catalog`, `translation`), which stayed plural through // the manifest map and so took the PERMISSIVE PLUGIN branch — the // #7894 shape, one verb over. - // • the `[not_overridable]` refusal, the two ADR-0010 audit rows and + // • the `NOT_OVERRIDABLE` refusal, the two ADR-0010 audit rows and // both receipt sentences, which read `request.type` and so reported // the CALLER's spelling for a row written under the canonical one. // `recordMetadataAudit` re-folds through `PLURAL_TO_SINGULAR` @@ -20854,7 +20854,7 @@ export class ObjectStackProtocolImplementation implements if (!ObjectStackProtocolImplementation.isOverlayAllowed(singularType) && !ObjectStackProtocolImplementation.isRuntimeCreateAllowed(singularType)) { const err: any = new Error( - `[not_overridable] Metadata type '${request.type}' is not revertable — no overlay/runtime-create permission.`, + `Metadata type '${request.type}' is not revertable — no overlay/runtime-create permission.`, ); err.code = 'NOT_OVERRIDABLE'; err.status = 403; @@ -20991,7 +20991,7 @@ export class ObjectStackProtocolImplementation implements } catch (err: any) { if (err instanceof ConflictError) { const conflict: any = new Error( - `[metadata_conflict] ${request.type}/${request.name} advanced during rollback. ` + `${request.type}/${request.name} advanced during rollback. ` + `Expected parent ${err.expectedParent ?? 'null'} but current is ${err.actualHead ?? 'null'}.`, ); conflict.code = 'METADATA_CONFLICT'; @@ -21350,7 +21350,7 @@ export class ObjectStackProtocolImplementation implements .mergesOverlayAtRead(request.type); if (artifactBacked && !overlayAllowed && !legacyOverlayRemoval) { const err = new Error( - `[not_overridable] Metadata item '${request.type}/${request.name}' is provided by a code package ` + `Metadata item '${request.type}/${request.name}' is provided by a code package ` + `and the type has not opted into per-org overlay writes. ` + `See docs/adr/0005-metadata-customization-overlay.md.` ); @@ -21360,7 +21360,7 @@ export class ObjectStackProtocolImplementation implements } if (!artifactBacked && !overlayAllowed && !runtimeCreateAllowed) { const err = new Error( - `[not_creatable] Metadata type '${request.type}' does not allow runtime creation or deletion.` + `Metadata type '${request.type}' does not allow runtime creation or deletion.` ); (err as any).code = 'NOT_CREATABLE'; (err as any).status = 403; @@ -21560,7 +21560,7 @@ export class ObjectStackProtocolImplementation implements } catch (err: any) { if (err instanceof ConflictError) { const conflict = new Error( - `[metadata_conflict] ${request.type}/${request.name} has been modified since you loaded it. ` + `${request.type}/${request.name} has been modified since you loaded it. ` + `Expected parent ${err.expectedParent ?? 'null'} but current is ${err.actualHead ?? 'null'}.`, ); (conflict as any).code = 'METADATA_CONFLICT'; @@ -22395,16 +22395,17 @@ export class ObjectStackProtocolImplementation implements // `rest-server-meta-references-refusal-envelope.test.ts` — and the // producer-side ORDER by `protocol.reference-target-unanswerable.test.ts`. // - // ⛔ And it opens with NO bracketed tag. The `[item_locked]`-style tags - // this file writes elsewhere are lowercase restatements of the throw's OWN - // declared `code`, so the wire carries the same token on the `code` axis; - // this refusal's code is `NOT_IMPLEMENTED`, so an `[unanswerable_target]` - // opener restated nothing the envelope carries and nothing ever read it. - // #12975 (2026-08-29) rules that `error` is HUMAN LANGUAGE while `code` is - // the MACHINE TOKEN, and since #15685 this prose reaches the operator - // VERBATIM — so the tag was the first thing they read. What separates this - // refusal from the route's other 501 is the sentence itself, not a tag. - // Its absence is pinned by `protocol.reference-target-unanswerable.test.ts`. + // ⛔ And it opens with NO bracketed tag — as no refusal this file raises + // does any more. The lowercase `[item_locked]`-style openers this producer + // once wrote were restatements of the throw's OWN declared `code`, so the + // wire already carried the same token on the `code` axis; they are gone, + // and `protocol.bracketed-refusal-opener-absence.test.ts` pins their + // absence across the whole family. #12975 (2026-08-29) rules that `error` is HUMAN + // LANGUAGE while `code` is the MACHINE TOKEN, and since #15685 this prose + // reaches the operator VERBATIM — so an opener is the first thing they + // read. What separates this refusal from the route's other 501 is the + // sentence itself, not a tag. Its absence is pinned here too, by + // `protocol.reference-target-unanswerable.test.ts`. if (REFERENCE_SITES.unanswerableTargetTypes.includes(singularTarget)) { const owner = targetName.includes('.') ? targetName.slice(0, targetName.indexOf('.')) : ''; const err = new Error( diff --git a/packages/metadata-protocol/src/sys-metadata-repository.ts b/packages/metadata-protocol/src/sys-metadata-repository.ts index ce0567fc46f..eaa0104d3af 100644 --- a/packages/metadata-protocol/src/sys-metadata-repository.ts +++ b/packages/metadata-protocol/src/sys-metadata-repository.ts @@ -941,7 +941,7 @@ export class SysMetadataRepository implements MetadataRepository { }); if (!draftRow) { const err: any = new Error( - `[no_draft] No pending draft exists for ${ref.type}/${ref.name} — nothing to publish.`, + `No pending draft exists for ${ref.type}/${ref.name} — nothing to publish.`, ); err.code = 'NO_DRAFT'; err.status = 404; @@ -1001,7 +1001,7 @@ export class SysMetadataRepository implements MetadataRepository { * a Studio package is UPDATED in place rather than missed by a lookup * narrowed to `package_id IS NULL` (#6215). * - * Throws `[version_not_found]` (404) if the target version row is + * Throws `VERSION_NOT_FOUND` (404) if the target version row is * missing or is a delete tombstone (no body to restore). */ async restoreVersion( @@ -1021,7 +1021,7 @@ export class SysMetadataRepository implements MetadataRepository { }); if (!row) { const err: any = new Error( - `[version_not_found] No history row at version ${targetVersion} for ${ref.type}/${ref.name}.`, + `No history row at version ${targetVersion} for ${ref.type}/${ref.name}.`, ); err.code = 'VERSION_NOT_FOUND'; err.status = 404; @@ -1030,7 +1030,7 @@ export class SysMetadataRepository implements MetadataRepository { const raw = (row as any).metadata; if (raw === null || raw === undefined) { const err: any = new Error( - `[version_not_restorable] Version ${targetVersion} for ${ref.type}/${ref.name} is a delete tombstone — nothing to restore.`, + `Version ${targetVersion} for ${ref.type}/${ref.name} is a delete tombstone — nothing to restore.`, ); err.code = 'VERSION_NOT_RESTORABLE'; err.status = 409; @@ -1691,7 +1691,7 @@ export class SysMetadataRepository implements MetadataRepository { name?: string, ): Error { const err: any = new Error( - `[writable_package_required] Cannot create ${name ? `${type}/${name}` : type} in package '${packageId}': ` + `Cannot create ${name ? `${type}/${name}` : type} in package '${packageId}': ` + `that package is read-only (provided by code or an installed app), so it is not a writable base. ` + `Switch to a writable package in the package selector, or create a new one, and retry.` // [#8146] Said only when the hatch IS set, because otherwise it is noise. @@ -1741,7 +1741,7 @@ export class SysMetadataRepository implements MetadataRepository { static readOnlyBaseOverrideError(type: string, packageId: string, hatchOpen = false): Error { const singular = PLURAL_TO_SINGULAR[type] ?? type; const err: any = new Error( - `[item_locked] Cannot overlay '${type}' in package '${packageId}': that package is read-only ` + `Cannot overlay '${type}' in package '${packageId}': that package is read-only ` + `(provided by code or an installed app) and the type has no per-org overlay channel ` + `(allowOrgOverride=false), so this item is locked against runtime edits. ` // [#8146] The prescription is chosen by whether the hatch is ALREADY From ab24709fe0f4191e951b59df1d5760aace854c19 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 06:35:57 +0000 Subject: [PATCH 02/10] test(metadata-protocol): repoint the byte pins and pin the family absence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The per-door pins asserted the bracketed opener as part of the refusal text; they now assert the prose that opens instead, so each still proves the declared 4xx sentence is quoted verbatim. Mock refusals that fabricated a producer spelling were repointed too — a fixture that no longer resembles its subject is how the idiom spreads. Adds `protocol.bracketed-refusal-opener-absence.test.ts`: nothing else notices a tag coming back, since a newly-written refusal reds no existing pin. Scans both producers for the shape and carries a `new Error(` floor, so a scan that matches nothing reds instead of passing for free. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude --- .../protocol.batch-verb-driver-code.test.ts | 8 +- .../protocol.batch-verb-driver-text.test.ts | 24 +-- ...l.bracketed-refusal-opener-absence.test.ts | 148 ++++++++++++++++++ ...col.destructive-409-face-inventory.test.ts | 6 +- .../protocol.driver-text-disclosure.test.ts | 8 +- ...nvalid-metadata-422-face-inventory.test.ts | 4 +- .../protocol.org-scoped-write-refused.test.ts | 2 +- .../src/seed-loader-driver-text.test.ts | 4 +- 8 files changed, 176 insertions(+), 28 deletions(-) create mode 100644 packages/metadata-protocol/src/protocol.bracketed-refusal-opener-absence.test.ts diff --git a/packages/metadata-protocol/src/protocol.batch-verb-driver-code.test.ts b/packages/metadata-protocol/src/protocol.batch-verb-driver-code.test.ts index 0fb2299dea2..5020cbefdf4 100644 --- a/packages/metadata-protocol/src/protocol.batch-verb-driver-code.test.ts +++ b/packages/metadata-protocol/src/protocol.batch-verb-driver-code.test.ts @@ -333,7 +333,7 @@ describe('[#8441] [GUARD] a catalogued code reaches the caller unchanged — red const r = await protocol.publishPackageDrafts({ packageId: PKG }); expect(byName(r.failed, 'acct_api').code).toBe('NOT_OVERRIDABLE'); - expect(byName(r.failed, 'acct_api').error).toContain('[not_overridable]'); + expect(byName(r.failed, 'acct_api').error).toContain("Metadata type 'api' is not draftable"); // The sibling declared code on the SAME array — the reason the issue // says this limb must be filtered rather than deleted. expect(byName(r.failed, 'acct_view').code).toBe('BATCH_ABORTED'); @@ -348,7 +348,7 @@ describe('[#8441] [GUARD] a catalogued code reaches the caller unchanged — red const r = await protocol.revertCommit({ commitId: 'c1' }); expect(r.failed[0].code).toBe('VERSION_NOT_FOUND'); - expect(r.failed[0].error).toContain('[version_not_found]'); + expect(r.failed[0].error).toContain('No history row at version 99'); }); /** @@ -523,7 +523,7 @@ describe('[#8441] what replaces an uncatalogued code, and what stays absent', () // disagree only about the vocabulary. That is the honest hard case: the // substitution has to preserve the meaning, not merely erase the code. const refusalWithDialect = () => Object.assign( - new Error('[item_locked] Cannot overlay this item: the package is read-only.'), + new Error('Cannot overlay this item: the package is read-only.'), { code: '42501', status: 403 }, ); const { protocol } = makeKernel({ @@ -536,7 +536,7 @@ describe('[#8441] what replaces an uncatalogued code, and what stays absent', () const r = await protocol.publishPackageDrafts({ packageId: PKG }); // #8333's rule: DECLARED 4xx, so the authored sentence survives whole. - expect(r.failed[0].error).toContain('[item_locked]'); + expect(r.failed[0].error).toContain('the package is read-only'); expect(r.failed[0].error).toContain('the package is read-only.'); // #8441's rule: not a catalog member, so the status's standard code. expect(r.failed[0].code).toBe('PERMISSION_DENIED'); diff --git a/packages/metadata-protocol/src/protocol.batch-verb-driver-text.test.ts b/packages/metadata-protocol/src/protocol.batch-verb-driver-text.test.ts index fea7800e1f9..d23ec0b7b38 100644 --- a/packages/metadata-protocol/src/protocol.batch-verb-driver-text.test.ts +++ b/packages/metadata-protocol/src/protocol.batch-verb-driver-text.test.ts @@ -36,8 +36,8 @@ * below is that reproduction, kept as the pin). * - **Every authored refusal reaching these catches already declares 4xx** — * `NOT_OVERRIDABLE` 403, `INVALID_METADATA` 422, `METADATA_CONFLICT` 409, - * and the repository's `[version_not_found]` 404 / `[item_locked]` 403 / - * `[writable_package_required]` 422 — with ONE exception, P9's, handled at + * and the repository's `VERSION_NOT_FOUND` 404 / `ITEM_LOCKED` 403 / + * `WRITABLE_PACKAGE_REQUIRED` 422 — with ONE exception, P9's, handled at * its producer (section 4). * - **P8's authored population never enters its catch at all.** The real * materializer (plugin-security) reports a refusal by RETURNING @@ -430,7 +430,7 @@ describe('[#8333] [GUARD] a declared 4xx refusal is quoted verbatim — green in const r = await protocol.publishPackageDrafts({ packageId: PKG }); - expect(r.failed[0].error).toContain('[not_overridable]'); + expect(r.failed[0].error).toContain("Metadata type 'api' is not draftable"); expect(r.failed[0].error).toContain('is not draftable'); expect(r.failed[0].code).toBe('NOT_OVERRIDABLE'); }); @@ -489,12 +489,12 @@ describe('[#8333] [GUARD] a declared 4xx refusal is quoted verbatim — green in // The whole #4277 self-correcting sentence, not just the code: it names // the offending key AND how to spell it correctly. - expect(r.failed[0].error).toContain('[invalid_metadata]'); + expect(r.failed[0].error).toContain('failed spec validation'); expect(r.failed[0].error).toContain('Unrecognized key(s) on this view container'); expect(r.failed[0].error).toContain('defineView('); }); - it('P11 keeps the repository’s `[version_not_found]`, with its `code`', async () => { + it('P11 keeps the repository’s `VERSION_NOT_FOUND` sentence, with its `code`', async () => { const { protocol, engine } = makeKernel({ seed: [row({ type: 'view', name: 'acct_view' })], }); @@ -504,12 +504,12 @@ describe('[#8333] [GUARD] a declared 4xx refusal is quoted verbatim — green in const r = await protocol.revertCommit({ commitId: 'c1' }); - expect(r.failed[0].error).toContain('[version_not_found]'); + expect(r.failed[0].error).toContain('No history row at version 99'); expect(r.failed[0].error).toContain('version 99'); expect(r.failed[0].code).toBe('VERSION_NOT_FOUND'); }); - it('P12 keeps `[commit_not_found]`', async () => { + it('P12 keeps `COMMIT_NOT_FOUND`’s sentence', async () => { const { protocol, engine } = makeKernel(); let seen = 0; engine.findOne = async (t: string) => { @@ -523,11 +523,11 @@ describe('[#8333] [GUARD] a declared 4xx refusal is quoted verbatim — green in const r = await protocol.rollbackToPackageCommit({ commitId: 'c1' }); - expect(r.failed[0].error).toContain('[commit_not_found]'); + expect(r.failed[0].error).toContain("No commit 'c1'."); expect(r.failed[0].error).toContain("No commit 'c1'"); }); - it('P13 keeps `[item_locked]`’s remedy', async () => { + it('P13 keeps `ITEM_LOCKED`’s remedy', async () => { const { protocol } = makeKernel({ seed: [row({ type: 'page', name: 'crm_landing', @@ -536,7 +536,7 @@ describe('[#8333] [GUARD] a declared 4xx refusal is quoted verbatim — green in }); protocol.saveMetaItem = async () => { throw declaredRefusal( - "[item_locked] Cannot overlay 'page' in package 'showcase': that package is read-only. " + "Cannot overlay 'page' in package 'showcase': that package is read-only. " + 'Edit the source artifact and redeploy.', 'ITEM_LOCKED', 403, ); @@ -544,7 +544,7 @@ describe('[#8333] [GUARD] a declared 4xx refusal is quoted verbatim — green in const r = await protocol.migrateStoredMetadata({ apply: true }); - expect(r.rows[0].reason).toContain('[item_locked]'); + expect(r.rows[0].reason).toContain('that package is read-only'); expect(r.rows[0].reason).toContain('Edit the source artifact and redeploy.'); }); }); @@ -633,7 +633,7 @@ describe('[#8333] the seed request’s schema rejection DECLARES itself, so the ); expect(r.success).toBe(false); - expect(r.error).toContain('[invalid_metadata]'); + expect(r.error).toContain('failed spec validation'); expect(r.error).toContain('failed spec validation'); // The dotted path an author can act on. The pre-#8333 dump spelled it // as a raw JSON array (`"path": [ "seeds", 0, "mode" ]`) inside a diff --git a/packages/metadata-protocol/src/protocol.bracketed-refusal-opener-absence.test.ts b/packages/metadata-protocol/src/protocol.bracketed-refusal-opener-absence.test.ts new file mode 100644 index 00000000000..e4b068722e8 --- /dev/null +++ b/packages/metadata-protocol/src/protocol.bracketed-refusal-opener-absence.test.ts @@ -0,0 +1,148 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * THE PIN: no refusal this package raises opens with a bracketed lowercase tag. + * + * Every refusal `ObjectStackProtocolImplementation` and `SysMetadataRepository` + * raised used to open with a `[lower_snake]` tag that was the restatement of the + * `code` the very same throw declared — `[no_draft]` in front of `NO_DRAFT`, + * `[item_locked]` in front of `ITEM_LOCKED`, and so on for the whole family. + * + * They were not invisible. `withoutDeclaredCodePrefix` + * (`packages/rest/src/error-response.ts`) strips a leading restatement only when + * the message opens with the producer's declared code followed by a colon + * (`INVALID_REQUEST: …`). The bracketed lowercase spelling matches neither the + * casing nor the separator, so it was never stripped and reached the caller in + * `error.message` — the repo's own de-duplication mechanism existed and did not + * fire here. + * + * The maintainer ruling of 2026-08-29 on the `/data` door shipping `FORBIDDEN:` + * in front of a localized refusal is ONE envelope semantics: `error` is HUMAN + * LANGUAGE, `code` is the MACHINE TOKEN, and a prefix is removed *because* the + * same fact already rides the `code` axis. Each of these tags met that condition + * by construction, so all of them are gone. + * + * ## Why this pin is written as an ABSENCE, over SOURCE + * + * Nothing else notices the tag coming back. The per-refusal pins elsewhere in + * this package assert the prose a given door answers, so one re-introduced tag + * reds exactly one of them and a newly-written refusal reds none — and a new + * refusal copied from a neighbouring producer is precisely how the idiom spread + * in the first place. Reading the source covers every throw site in both files, + * including ones no test can provoke. + * + * ⚠️ A scan that matches nothing passes for free, so the family floor below is + * part of the pin: the scanner must still be finding refusals to have an opinion + * about. Without it, moving every `throw` out of these files would green this + * file rather than red it. + * + * ⛔ Two bracketed vocabularies in this package are NOT this family and are + * deliberately untouched, because neither restates a declared `code`: + * `path [zod code]` locators inside `metadataIssueHeadline`'s issue list, and + * the `[rule]` locators the author-time gate composes. They name WHICH finding, + * which is a fact the envelope carries nowhere else. + */ + +import { describe, it, expect, vi } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { ObjectStackProtocolImplementation } from './protocol.js'; + +const HERE = dirname(fileURLToPath(import.meta.url)); + +/** The two producers the ruling was applied to. */ +const PRODUCERS = ['protocol.ts', 'sys-metadata-repository.ts'] as const; + +/** + * A string literal whose FIRST characters are a bracketed lowercase tag — + * the shape `withoutDeclaredCodePrefix` cannot strip. + */ +const TAGGED_OPENER = /(`|')\[[a-z][a-z0-9_]*\]/; + +function scan(file: string): { openers: string[]; refusals: number } { + const lines = readFileSync(join(HERE, file), 'utf8').split('\n'); + const openers: string[] = []; + let refusals = 0; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]!; + const trimmed = line.trim(); + refusals += line.split('new Error(').length - 1; + // Prose ABOUT a refusal is not a refusal: comments may name a code freely. + if (trimmed.startsWith('*') || trimmed.startsWith('//') || trimmed.startsWith('/*')) continue; + const m = TAGGED_OPENER.exec(line); + // The bracket must open the literal, not merely appear inside it. + if (!m || line[m.index + 1] !== '[') continue; + openers.push(`${file}:${i + 1} ${trimmed.slice(0, 100)}`); + } + return { openers, refusals }; +} + +describe('refusal messages open with prose, never with a bracketed restatement of their own code', () => { + it.each(PRODUCERS)('%s raises no message opening with a bracketed lowercase tag', (file) => { + const { openers, refusals } = scan(file); + + // THE FLOOR — the scan has to still be looking at refusals for its silence + // to mean anything. A `new Error(` count, not a total of matched openers: + // the quantity this pin is about is zero, so it can never be its own floor. + expect( + refusals, + `${file} no longer constructs errors here — this pin is scanning the wrong file`, + ).toBeGreaterThanOrEqual(5); + + expect( + openers, + `these refusals open with a tag restating their own declared code:\n${openers.join('\n')}`, + ).toEqual([]); + }); + + it('the whole family is covered — both producers together still raise the refusals this pin is about', () => { + const total = PRODUCERS.reduce((n, f) => n + scan(f).refusals, 0); + expect(total).toBeGreaterThanOrEqual(30); + }); +}); + +describe('the refusal a caller actually receives', () => { + /** The envelope guards below refuse before any engine call, so none is needed. */ + function protocol() { + return new ObjectStackProtocolImplementation({ + registry: { getObject: () => undefined }, + findOne: vi.fn(async () => null), + } as any); + } + + async function refusalFrom(run: () => Promise): Promise { + try { + await run(); + } catch (e) { + return e; + } + throw new Error('expected a refusal, got a resolved call'); + } + + it('carries the token on `code` and opens with the sentence — `INVALID_REQUEST`', async () => { + const err = await refusalFrom(() => (protocol() as any).saveMetaItem({ type: 'view', name: 'task_list' })); + + // The machine axis is unchanged: this change moved nothing off it. + expect(err.code).toBe('INVALID_REQUEST'); + expect(err.status).toBe(400); + + // …and the human axis opens with the human sentence. Asserted as an absence + // AND as the prose that opens instead, so the pin cannot go green by the + // message becoming empty or generic. + expect(err.message.startsWith('['), `message opens with a tag: ${err.message.slice(0, 48)}`).toBe(false); + expect(err.message).not.toContain('[invalid_request]'); + expect(err.message).toContain("saveMetaItem requires an 'item' body for 'view/task_list'"); + }); + + it('carries the token on `code` and opens with the sentence — the rollback envelope guard', async () => { + const err = await refusalFrom( + () => (protocol() as any).rollbackMetaItem({ type: 'view', name: 'task_list', toVersion: 0 }), + ); + + expect(err.code).toBe('INVALID_REQUEST'); + expect(err.status).toBe(400); + expect(err.message.startsWith('['), `message opens with a tag: ${err.message.slice(0, 48)}`).toBe(false); + expect(err.message).toContain("rollbackMetaItem requires a positive integer 'toVersion'"); + }); +}); diff --git a/packages/metadata-protocol/src/protocol.destructive-409-face-inventory.test.ts b/packages/metadata-protocol/src/protocol.destructive-409-face-inventory.test.ts index 0deb85470ef..0eae0342dd1 100644 --- a/packages/metadata-protocol/src/protocol.destructive-409-face-inventory.test.ts +++ b/packages/metadata-protocol/src/protocol.destructive-409-face-inventory.test.ts @@ -429,7 +429,7 @@ describe('[#10886] [GUARD] `duplicatePackage`’s `failed[].error` is the SOLE c expect(r.failedCount).toBe(1); expect(r.failed[0]).toMatchObject({ type: 'object', name: 'crm_task' }); - expect(r.failed[0].error).toContain('[destructive_change]'); + expect(r.failed[0].error).toContain('would drop or transform existing data'); }); it('⛔ carries the per-field prose with NO structured channel beside it', async () => { @@ -525,7 +525,7 @@ describe('[#11015] [GUARD] the destructive remedy clause is face-aware', () => { // findings prose stays, because `failed[].error` is its sole carrier on // this face — is untouched, and this is the assertion that says so. expect(r.failed[0].error).toContain(FINDING_PROSE); - expect(r.failed[0].error).toContain('[destructive_change]'); + expect(r.failed[0].error).toContain('would drop or transform existing data'); }); it('the refusal still REFUSES — this is a message repair, not a behaviour one', async () => { @@ -616,7 +616,7 @@ describe('[#11095] [GUARD] the `meta-dispatch` face prescribes a remedy that doo // it was untouched by #11015: only the remedy clause is face-aware, and // the findings the refusal renders stay whole on every face. expect(err.message).toContain(FINDING_PROSE); - expect(err.message).toContain('[destructive_change]'); + expect(err.message).toContain('would drop or transform existing data'); }); it('⛔ the three faces are a SWITCH — repairing one did not move the others', async () => { diff --git a/packages/metadata-protocol/src/protocol.driver-text-disclosure.test.ts b/packages/metadata-protocol/src/protocol.driver-text-disclosure.test.ts index c9611ded543..31f0a6e2943 100644 --- a/packages/metadata-protocol/src/protocol.driver-text-disclosure.test.ts +++ b/packages/metadata-protocol/src/protocol.driver-text-disclosure.test.ts @@ -430,15 +430,15 @@ describe('[#8136] the overlay-delete re-wraps name the operation without quoting describe('[#8136] [GUARD] a declared 4xx refusal is quoted verbatim — green in BOTH directions, red under the over-broad variant', () => { /** * The bound that stops this fix being satisfied by "withhold everything". - * `SysMetadataRepository`'s refusals (`[item_locked]`, - * `[writable_package_required]`, `[no_draft]`, …) name the exact remedy, + * `SysMetadataRepository`'s refusals (`ITEM_LOCKED`, + * `WRITABLE_PACKAGE_REQUIRED`, `NO_DRAFT`, …) name the exact remedy, * and #4277 is the card that exists so they do. Blanking them would trade a * usability regression for no disclosure gain — measured red under the * over-broad variant, see the PR body. */ it('keeps a repository refusal intact through the re-wrap', async () => { const refusal: any = new Error( - "[item_locked] Cannot overlay 'view' in package 'showcase': that package is read-only. " + "Cannot overlay 'view' in package 'showcase': that package is read-only. " + 'Edit the source artifact and redeploy.', ); refusal.code = 'ITEM_LOCKED'; @@ -455,7 +455,7 @@ describe('[#8136] [GUARD] a declared 4xx refusal is quoted verbatim — green in // The prescription survives, whole — this is the half a blanket // sanitizer would destroy. - expect(String(err.message)).toContain('[item_locked]'); + expect(String(err.message)).toContain('that package is read-only'); expect(String(err.message)).toContain('Edit the source artifact and redeploy.'); // …and the envelope #7426 installed is unchanged. expectDeclaredEnvelope(err, 'ITEM_LOCKED', 403); diff --git a/packages/metadata-protocol/src/protocol.invalid-metadata-422-face-inventory.test.ts b/packages/metadata-protocol/src/protocol.invalid-metadata-422-face-inventory.test.ts index e61cb4b9a70..d6111c1fba7 100644 --- a/packages/metadata-protocol/src/protocol.invalid-metadata-422-face-inventory.test.ts +++ b/packages/metadata-protocol/src/protocol.invalid-metadata-422-face-inventory.test.ts @@ -217,7 +217,7 @@ describe('[#10888] `meta-envelope` renders the headline, not the prose', () => { const { protocol } = makeProtocol(); const err = await refusal(protocol, 'meta-envelope'); - expect(err.message).toContain('[invalid_metadata] view/task_list failed spec validation: '); + expect(err.message).toContain('view/task_list failed spec validation: '); expect(err.message).toContain(`${err.issues.length} issue`); // The same grammar `seedRequestValidationError` composes — count plus // `path [zod code]` locators — so one mistake reads the same whichever @@ -279,7 +279,7 @@ describe('[#10888 · GUARD] a face that carries no `issues[]` keeps the whole se + (err.issues.length > 3 ? ` (+${err.issues.length - 3} more)` : ''); expect(err.message).toBe( - `[invalid_metadata] view/task_list failed spec validation: ${expected}`, + `view/task_list failed spec validation: ${expected}`, ); // The #4001 self-correcting prescription, whole. expect(err.message).toContain(PRESCRIPTION); diff --git a/packages/metadata-protocol/src/protocol.org-scoped-write-refused.test.ts b/packages/metadata-protocol/src/protocol.org-scoped-write-refused.test.ts index 1c9f1d16e0b..015c05c00dd 100644 --- a/packages/metadata-protocol/src/protocol.org-scoped-write-refused.test.ts +++ b/packages/metadata-protocol/src/protocol.org-scoped-write-refused.test.ts @@ -410,7 +410,7 @@ describe('#6190 — org-scoped writes of non-org-overridable types are refused', .catch((e: any) => e); expect(err.message).toContain( - "[not_overridable] Metadata item 'object/org_widget' cannot be written org-scoped (organization 'org_a').", + "Metadata item 'object/org_widget' cannot be written org-scoped (organization 'org_a').", ); expect(err.message).toContain('allowOrgOverride=false'); expect(err.message).toContain('Save it env-wide instead'); diff --git a/packages/metadata-protocol/src/seed-loader-driver-text.test.ts b/packages/metadata-protocol/src/seed-loader-driver-text.test.ts index a2dae2ad398..3a53f957a0d 100644 --- a/packages/metadata-protocol/src/seed-loader-driver-text.test.ts +++ b/packages/metadata-protocol/src/seed-loader-driver-text.test.ts @@ -313,11 +313,11 @@ describe('[#8442] [GUARD] a DECLARED refusal is quoted whole — the per-record it('a declared 4xx refusal keeps its sentence (#8333’s rule, still in force)', async () => { const { result } = await loadFailing(() => Object.assign( - new Error('[item_locked] Cannot overlay this item: the package is read-only.'), + new Error('Cannot overlay this item: the package is read-only.'), { code: 'ITEM_LOCKED', status: 403 }, )); - expect(result.errors[0].message).toContain('[item_locked]'); + expect(result.errors[0].message).toContain('the package is read-only'); expect(result.errors[0].message).toContain('the package is read-only.'); }); }); From 6e31e8302bf59e2566469d4882fd34dd22a786a9 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 06:54:09 +0000 Subject: [PATCH 03/10] fix(metadata-protocol): drop the INTERPOLATED bracketed opener too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `assertOverlayAllowed`'s shared emitter opened its message with `[${code}]`, built from the same variable it assigns to `err.code` three lines down. Same family, most redundant member, and invisible to every grep for a literal tag — which is why the card's inventory did not carry it. The absence pin now detects the interpolated spelling as well, and declares the two bracketed vocabularies that are NOT this family (the `[Protocol]` and `[SysMetadataRepository]` logger prefixes) by name rather than by heuristic. Pins that read the code off the PROSE now read it off `err.code`: three of them were only ever green because the tag happened to spell the token. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude --- ...l.bracketed-refusal-opener-absence.test.ts | 29 +++++++++++++++++-- .../protocol.delete-rewrap-envelope.test.ts | 10 +++++-- ...col.hydrate-overlay-canonical-type.test.ts | 6 ++-- .../protocol.legacy-overlay-delete.test.ts | 26 ++++++++++------- .../src/protocol.stored-migration.test.ts | 4 +-- .../src/sys-metadata-repository.ts | 6 +++- 6 files changed, 58 insertions(+), 23 deletions(-) diff --git a/packages/metadata-protocol/src/protocol.bracketed-refusal-opener-absence.test.ts b/packages/metadata-protocol/src/protocol.bracketed-refusal-opener-absence.test.ts index e4b068722e8..20522271f56 100644 --- a/packages/metadata-protocol/src/protocol.bracketed-refusal-opener-absence.test.ts +++ b/packages/metadata-protocol/src/protocol.bracketed-refusal-opener-absence.test.ts @@ -55,10 +55,32 @@ const HERE = dirname(fileURLToPath(import.meta.url)); const PRODUCERS = ['protocol.ts', 'sys-metadata-repository.ts'] as const; /** - * A string literal whose FIRST characters are a bracketed lowercase tag — - * the shape `withoutDeclaredCodePrefix` cannot strip. + * A string literal whose FIRST characters are a bracketed tag — the shape + * `withoutDeclaredCodePrefix` cannot strip, in all three spellings it was + * found in. + * + * ⚠️ The INTERPOLATED arm is not thoroughness for its own sake. One site wrote + * its opener as `[${code}]` from the same variable it assigned to `err.code` a + * few lines down — the most redundant member of the whole family, and the one + * every grep for a literal tag walked straight past. A detector that reads only + * literals would let exactly that shape back in. + */ +const TAGGED_OPENER = /(`|')\[(?:([A-Za-z][A-Za-z0-9_]*)\]|\$\{)/; + +/** + * The bracketed openers that are NOT this family, by name. + * + * Both are LOGGER subsystem prefixes on log lines — they say which component is + * speaking into a shared stream, a fact no envelope carries because a log line + * has no envelope. Neither restates a declared `code`, and neither is addressed + * to a caller. + * + * ⚠️ Declared by name rather than by a "looks like a log" heuristic: a new + * vocabulary should red this pin and be argued, which is the ratchet. Adding a + * refusal tag here instead of removing it is ⛔ the one edit this list must + * never absorb. */ -const TAGGED_OPENER = /(`|')\[[a-z][a-z0-9_]*\]/; +const NON_REFUSAL_PREFIXES = new Set(['Protocol', 'SysMetadataRepository']); function scan(file: string): { openers: string[]; refusals: number } { const lines = readFileSync(join(HERE, file), 'utf8').split('\n'); @@ -73,6 +95,7 @@ function scan(file: string): { openers: string[]; refusals: number } { const m = TAGGED_OPENER.exec(line); // The bracket must open the literal, not merely appear inside it. if (!m || line[m.index + 1] !== '[') continue; + if (m[2] !== undefined && NON_REFUSAL_PREFIXES.has(m[2])) continue; openers.push(`${file}:${i + 1} ${trimmed.slice(0, 100)}`); } return { openers, refusals }; diff --git a/packages/metadata-protocol/src/protocol.delete-rewrap-envelope.test.ts b/packages/metadata-protocol/src/protocol.delete-rewrap-envelope.test.ts index b514cd9b170..38dfbcee3d3 100644 --- a/packages/metadata-protocol/src/protocol.delete-rewrap-envelope.test.ts +++ b/packages/metadata-protocol/src/protocol.delete-rewrap-envelope.test.ts @@ -304,9 +304,13 @@ describe('#7426 — the repository refusal reaches the caller with its code', () expect(err, `${type}: the delete was accepted`).toBeInstanceOf(Error); expect(err.code, type).toBe('NOT_OVERRIDABLE'); expect(err.status, type).toBe(403); - // The prose the operator reads is unchanged — the code is ADDED to - // the envelope, it does not replace the message it was trapped in. - expect(String(err.message), type).toContain('NOT_OVERRIDABLE'); + // The prose the operator reads still names the reason and survives + // the rewrap whole. It no longer RESTATES the code: #7426 promoted + // the token to the envelope, and the bracketed `[NOT_OVERRIDABLE]` + // opener it had been trapped in was the duplicate left behind — + // `error` is human language, `code` is the machine token. + expect(String(err.message), type).toContain('is not allowOrgOverride in the registry'); + expect(String(err.message), type).not.toContain('[NOT_OVERRIDABLE]'); // A refusal that already deleted the row is a log line. expect(rows.size, type).toBe(1); }); diff --git a/packages/metadata-protocol/src/protocol.hydrate-overlay-canonical-type.test.ts b/packages/metadata-protocol/src/protocol.hydrate-overlay-canonical-type.test.ts index 3b5cb2b1d84..ef17866398c 100644 --- a/packages/metadata-protocol/src/protocol.hydrate-overlay-canonical-type.test.ts +++ b/packages/metadata-protocol/src/protocol.hydrate-overlay-canonical-type.test.ts @@ -257,7 +257,7 @@ describe('[#9111] `hydrateOverlayIntoRegistry` asserts its type is canonical', ( expect(() => protocol.hydrateOverlayIntoRegistry('objects', { name: 'ticket' }, { packageId: null, organizationId: null, - })).toThrowError(/registry_type_not_canonical/); + })).toThrowError(/Refusing to register a SchemaRegistry overlay entry/); expect(registeredItems).toHaveLength(0); }); @@ -269,12 +269,12 @@ describe('[#9111] `hydrateOverlayIntoRegistry` asserts its type is canonical', ( expect(() => protocol.hydrateOverlayIntoRegistry('fields', { name: 'x' }, { packageId: null, organizationId: 'org_alpha', - })).toThrowError(/registry_type_not_canonical/); + })).toThrowError(/Refusing to register a SchemaRegistry overlay entry/); // …and a body with no `name` at all. expect(() => protocol.hydrateOverlayIntoRegistry('fields', { label: 'no name' }, { packageId: null, organizationId: null, - })).toThrowError(/registry_type_not_canonical/); + })).toThrowError(/Refusing to register a SchemaRegistry overlay entry/); }); it('cannot refuse a canonical type or a plugin-registered kind', () => { diff --git a/packages/metadata-protocol/src/protocol.legacy-overlay-delete.test.ts b/packages/metadata-protocol/src/protocol.legacy-overlay-delete.test.ts index 8a0447579b2..77953af831b 100644 --- a/packages/metadata-protocol/src/protocol.legacy-overlay-delete.test.ts +++ b/packages/metadata-protocol/src/protocol.legacy-overlay-delete.test.ts @@ -397,23 +397,27 @@ describe('#6960 — the boundary holds: the `object` tier does NOT move', () => * substitute one: the prose is what an operator reads, and #7426 adds a * field to the envelope without restating the sentence. * - * ⚠️ …and it is deliberately CASE-INSENSITIVE, which is a measurement, not - * a convenience. Promoting the old control-plane-only substring check to - * every leg turned the project-kernel legs red: the two producers spell the - * marker inside their *prose* differently — `deleteMetaItem`'s own block - * writes `[not_overridable]`, `SysMetadataRepository` writes - * `[NOT_OVERRIDABLE]`. Only the `code` FIELD is uniform, which is exactly - * ADR-0112's point (the catalog governs `error.code`; message prose is a - * different surface) and exactly why the field is the assertion that - * belongs here. Recorded rather than papered over — the prose divergence is - * pre-existing and outside #7426's scope. + * ⚠️ …and the marker is read off the `code` FIELD, never off the prose. + * That was a measurement before it was a rule: promoting the old + * control-plane-only substring check to every leg turned the project-kernel + * legs red, because the two producers spelled the marker inside their prose + * differently — one lowercase, one uppercased out of the declared code by + * interpolation. Only the `code` field was ever uniform, which is exactly + * ADR-0112's point: the catalog governs `error.code`, and message prose is + * a different surface. Both prose markers are now gone — `error` is human + * language and `code` is the machine token — so the field is not merely the + * assertion that belongs here, it is the only one there could be. */ const expectRefused = (err: any, environmentId: string | undefined, ctx: string) => { void environmentId; expect(err, `${ctx}: the delete was accepted`).toBeInstanceOf(Error); expect(err.status, ctx).toBe(403); expect(err.code, ctx).toBe('NOT_OVERRIDABLE'); - expect(String(err.message).toUpperCase(), ctx).toContain('NOT_OVERRIDABLE'); + // The prose still has to SAY something — the envelope assertion above + // is about the machine axis, and a refusal that lost its sentence would + // pass it while telling the operator nothing. + expect(String(err.message), ctx).toMatch(/code package|allowOrgOverride/); + expect(String(err.message).startsWith('['), ctx).toBe(false); }; for (const { label, environmentId } of KERNELS) { diff --git a/packages/metadata-protocol/src/protocol.stored-migration.test.ts b/packages/metadata-protocol/src/protocol.stored-migration.test.ts index 8919ac3f1d6..440da2ed1e6 100644 --- a/packages/metadata-protocol/src/protocol.stored-migration.test.ts +++ b/packages/metadata-protocol/src/protocol.stored-migration.test.ts @@ -568,7 +568,7 @@ describe('migrateStoredMetadata — what it declines to touch, loudly (#4327)', expect(report.failed).toBe(1); expect(report.rewritten).toBe(0); - expect(report.rows[0]!.reason).toMatch(/invalid_metadata/); + expect(report.rows[0]!.reason).toMatch(/failed spec validation/); expect(historyRows(tables)).toHaveLength(0); expect(JSON.parse(metaRows(tables)[0]!.metadata).fields.amount.conditionalRequired).toBe('x'); }); @@ -594,7 +594,7 @@ describe('migrateStoredMetadata — what it declines to touch, loudly (#4327)', const report = await protocol.migrateStoredMetadata({ apply: true }); expect(report.failed).toBe(1); - expect(report.rows[0]!.reason).toMatch(/metadata_conflict/); + expect(report.rows[0]!.reason).toMatch(/has been modified since you loaded it/); // The other writer's row is untouched. expect(metaRows(tables)[0]!.checksum).toBe('sha256:moved_by_someone_else'); }); diff --git a/packages/metadata-protocol/src/sys-metadata-repository.ts b/packages/metadata-protocol/src/sys-metadata-repository.ts index eaa0104d3af..4e73bfac7a7 100644 --- a/packages/metadata-protocol/src/sys-metadata-repository.ts +++ b/packages/metadata-protocol/src/sys-metadata-repository.ts @@ -1640,8 +1640,12 @@ export class SysMetadataRepository implements MetadataRepository { const detail = intent === 'runtime-only' ? `'${type}' has neither allowOrgOverride nor allowRuntimeCreate in the registry. ` : `'${type}' is not allowOrgOverride in the registry. `; + // ⛔ No `[${code}]` opener: the token below IS the `code` this throw + // declares three lines down, so a bracketed restatement duplicates onto the + // prose axis a fact the envelope already carries — and, spelled by + // interpolation, it is invisible to every grep for a literal tag. const err: any = new Error( - `[${code}] ${detail}` + + `${detail}` + `Overlay-allowed: ${Array.from(new Set(allowed)).join(', ') || '(none)'}. ` + `Set OS_METADATA_WRITABLE to enable additional types at runtime.`, ); From 18dc3ee5e2eb0d3af75ece922721d45bfd7e772f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 07:01:45 +0000 Subject: [PATCH 04/10] fix(runtime,metadata-protocol): repoint the downstream pins and the last stale prose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `packages/runtime`'s two publish-drafts pins asserted the bracketed opener on the seed-apply refusal, which `metadata-protocol` produces — the only two cross-package assertions the change moves. They now assert the sentence; the class still rides `code`. Also corrects three in-file comments that still spelled a tag the producer no longer writes. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude --- packages/metadata-protocol/src/protocol.ts | 7 ++++--- .../packages-publish-drafts-response-conformance.test.ts | 5 +++-- .../src/domains/packages-seed-apply-disclosure.test.ts | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/metadata-protocol/src/protocol.ts b/packages/metadata-protocol/src/protocol.ts index ce0ae6aafeb..e8cad42a166 100644 --- a/packages/metadata-protocol/src/protocol.ts +++ b/packages/metadata-protocol/src/protocol.ts @@ -5873,7 +5873,7 @@ export class ObjectStackProtocolImplementation implements * `translation`, `email_template`): a draft authored env-wide — which is * what package/AI authoring writes, and what `PUT ?mode=draft` writes when no * active org is threaded — is looked up under `organization_id = `, - * matches nothing, and answers `404 [no_draft] … nothing to publish` over a + * matches nothing, and answers `404 NO_DRAFT` — `… nothing to publish` — over a * row the console's own pending-changes list is showing. Measured on a cloud * rig: four AI-authored `view` drafts, visible in `sys_metadata` at * `state='draft'`, all four refused by the per-item door while the batch @@ -5903,7 +5903,7 @@ export class ObjectStackProtocolImplementation implements * a package-stating publish resolves the scope of the draft it NAMED. * Without the dimension, probe 1 could match ANOTHER package's row in the * caller's org, name a scope the package-exact promote then finds empty, - * and answer `404 [no_draft]` over a publishable draft sitting env-wide. + * and answer `404 NO_DRAFT` over a publishable draft sitting env-wide. * Accepted cost, on the record: a caller stating a package no longer * discovers a no-package draft of the same `(type, name)` — it 404s and the * caller retries without `?package=`; that narrowing is the ruling, not a @@ -20236,7 +20236,8 @@ export class ObjectStackProtocolImplementation implements // `reverted[0].action === 'restored'`, `registerItem` called ZERO // times, and the only trace anywhere is // `[Protocol] registry write-through failed for fields/… : - // [registry_type_not_canonical] …` on the server's stderr. The + // Refusing to register a SchemaRegistry overlay entry …` + // (`REGISTRY_TYPE_NOT_CANONICAL`) on the server's stderr. The // receipt claims the pre-commit body is what the platform now // serves. It is not, and for this class it cannot be: #9111's mint // door refuses the entry and boot refuses it too, so the restored diff --git a/packages/runtime/src/domains/packages-publish-drafts-response-conformance.test.ts b/packages/runtime/src/domains/packages-publish-drafts-response-conformance.test.ts index 39aca7c79c7..a799930490c 100644 --- a/packages/runtime/src/domains/packages-publish-drafts-response-conformance.test.ts +++ b/packages/runtime/src/domains/packages-publish-drafts-response-conformance.test.ts @@ -234,8 +234,9 @@ describe('publish-drafts wire payload conforms to PublishPackageDraftsResponseSc expect(strippedKeys(data)).toEqual([]); const parsed = PublishPackageDraftsResponseSchema.parse(data); expect(parsed.seedApplied?.success).toBe(false); - // The headline names the refusal class and the offending key… - expect(parsed.seedApplied?.error).toContain('[invalid_metadata]'); + // The headline names the refusal class and the offending key. The + // class rides `code`, not a bracketed restatement inside the prose — + // `error` is human language, `code` is the machine token. expect(parsed.seedApplied?.error).toContain('failed spec validation'); expect(parsed.seedApplied?.error).toContain('seeds.0.mode'); // …and the prose lives ONCE, on the declared structured channel. diff --git a/packages/runtime/src/domains/packages-seed-apply-disclosure.test.ts b/packages/runtime/src/domains/packages-seed-apply-disclosure.test.ts index 7fce671b99d..2d263da554c 100644 --- a/packages/runtime/src/domains/packages-seed-apply-disclosure.test.ts +++ b/packages/runtime/src/domains/packages-seed-apply-disclosure.test.ts @@ -270,7 +270,7 @@ describe('#8443 · 2 · a malformed seed body still reaches its author', () => { const { seedApplied, body } = await publishDrafts({ malformedSeedBody: true }); expect(seedApplied?.success).toBe(false); - expect(seedApplied?.error).toContain('[invalid_metadata]'); + expect(seedApplied?.error).toContain('failed spec validation'); // The author learns WHICH key — the whole reason this population may // not be blanked. `seeds.0.mode` is the path through the request the // loader parses. From 81a7ec306fa5c311b7abace761991190a58da74b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 07:03:11 +0000 Subject: [PATCH 05/10] chore: changeset for the refusal-opener change Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude --- .../16245-bracketed-tag-refusal-openers.md | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .changeset/16245-bracketed-tag-refusal-openers.md diff --git a/.changeset/16245-bracketed-tag-refusal-openers.md b/.changeset/16245-bracketed-tag-refusal-openers.md new file mode 100644 index 00000000000..34b2d48e7f6 --- /dev/null +++ b/.changeset/16245-bracketed-tag-refusal-openers.md @@ -0,0 +1,26 @@ +--- +'@objectstack/metadata-protocol': minor +--- + +Refusal messages no longer open with a bracketed tag restating the `code` the same throw declares — `error` carries the human sentence, `code` carries the machine token, and the token is no longer duplicated onto the prose axis. + +Clause-②: yes + +Every refusal `ObjectStackProtocolImplementation` and `SysMetadataRepository` raised opened with a lowercase `[tag]` that was the restatement of the `code` that very throw declared: `[no_draft]` in front of `NO_DRAFT`, `[item_locked]` in front of `ITEM_LOCKED`, and so on for 38 throw sites across the two producers. They were not invisible. `withoutDeclaredCodePrefix` strips a leading restatement only when the message opens with the declared code followed by a colon (`INVALID_REQUEST: …`); the bracketed lowercase spelling matches neither the casing nor the separator, so it was never stripped and reached the caller in `error.message`. The repo's own de-duplication mechanism existed and did not fire here. + +The maintainer ruling of 2026-08-29 on the `/data` door shipping `FORBIDDEN:` in front of a localized refusal is ONE envelope semantics — `error` is HUMAN LANGUAGE, `code` is the MACHINE TOKEN — and a prefix is removed *because* the same fact already rides the `code` axis. All 38 met that condition by construction. + +## FROM → TO + +| before | now | +| --- | --- | +| `error: "[no_draft] No pending draft exists for view/task_list."` | `error: "No pending draft exists for view/task_list."` | +| `error: "[item_locked] view/task_list is locked (_lock=…)."` | `error: "view/task_list is locked (_lock=…)."` | +| `error: "[NOT_OVERRIDABLE] 'action' is not allowOrgOverride…"` | `error: "'action' is not allowOrgOverride…"` | + +**`code` is unchanged on every one of them**, and it is where the token always also was — `NO_DRAFT`, `ITEM_LOCKED`, `NOT_OVERRIDABLE`, and the 14 others. A reader matching `error.message` for `[]` reads `error.code` for `` instead; a reader already using `code` needs no change. The HTTP `status` is untouched. + +- **Measured, not assumed, before it was removed**: 37 literal openers plus one written as `` `[${code}]` `` from the same variable the throw assigns to `err.code` three lines down — that one spelled by interpolation, so it was invisible to every grep for a literal tag and is absent from the card's own inventory. +- **Nothing consumed the tag.** The only consumers found anywhere are strippers: `@object-ui/react`'s `extractWriteErrorMessage` and two `plugin-detail` call sites each remove a leading bracketed prefix before showing the sentence to a user, next to the `SCREAMING_SNAKE:` strip. They confirm the tag was arriving and they cannot break on its absence — the regex simply matches nothing. +- **Two bracketed vocabularies are deliberately kept**: the `path [zod code]` locators inside a validation headline and the `[rule]` locators the author-time gate composes. Neither restates a declared `code` — they name WHICH finding, a fact the envelope carries nowhere else. +- **Pinned as an absence**, because nothing else would notice one coming back: a re-introduced tag reds exactly one per-door pin and a newly-written refusal reds none. From ea92663819b62af61af87579020c971c32d9ae26 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 07:45:47 +0000 Subject: [PATCH 06/10] docs(spec,api): repair the published docs this change falsifies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removing the bracketed openers made four published carriers wrong. The three that are editable are repaired here, because a change that makes a published claim false owns the repair. - `ProtocolSchema`'s promotion `describe()` said the lookup answers 404 `[no_draft]`; it now names `NO_DRAFT`, the axis that still carries it. - `content/docs/references/api/protocol.mdx` regenerated from that source via `check:generated --fix` — AUTO-GEN, never hand-edited. One line moved. - The error catalog's two documented `INVALID_REQUEST` payloads showed a `message` opening with the tag beside a `code` field already carrying the token; they now show what the platform emits. ⛔ The three carriers in `content/docs/releases/v17/` are deliberately left untouched: release pages record what shipped, and a code PR does not edit them. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude --- .changeset/16245-bracketed-tag-refusal-openers.md | 5 ++++- content/docs/api/error-catalog.mdx | 4 ++-- content/docs/references/api/protocol.mdx | 2 +- packages/spec/src/api/protocol.zod.ts | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.changeset/16245-bracketed-tag-refusal-openers.md b/.changeset/16245-bracketed-tag-refusal-openers.md index 34b2d48e7f6..2bb62af9acf 100644 --- a/.changeset/16245-bracketed-tag-refusal-openers.md +++ b/.changeset/16245-bracketed-tag-refusal-openers.md @@ -1,5 +1,6 @@ --- '@objectstack/metadata-protocol': minor +'@objectstack/spec': minor --- Refusal messages no longer open with a bracketed tag restating the `code` the same throw declares — `error` carries the human sentence, `code` carries the machine token, and the token is no longer duplicated onto the prose axis. @@ -18,9 +19,11 @@ The maintainer ruling of 2026-08-29 on the `/data` door shipping `FORBIDDEN:` in | `error: "[item_locked] view/task_list is locked (_lock=…)."` | `error: "view/task_list is locked (_lock=…)."` | | `error: "[NOT_OVERRIDABLE] 'action' is not allowOrgOverride…"` | `error: "'action' is not allowOrgOverride…"` | -**`code` is unchanged on every one of them**, and it is where the token always also was — `NO_DRAFT`, `ITEM_LOCKED`, `NOT_OVERRIDABLE`, and the 14 others. A reader matching `error.message` for `[]` reads `error.code` for `` instead; a reader already using `code` needs no change. The HTTP `status` is untouched. +**`code` is unchanged on every one of them**, and it is where the token always also was — `NO_DRAFT`, `ITEM_LOCKED`, `NOT_OVERRIDABLE`, and the 14 others. A reader matching `error.message` for a bracketed tag reads `error.code` for that tag, upper-cased, instead; a reader already using `code` needs no change. The HTTP `status` is untouched. - **Measured, not assumed, before it was removed**: 37 literal openers plus one written as `` `[${code}]` `` from the same variable the throw assigns to `err.code` three lines down — that one spelled by interpolation, so it was invisible to every grep for a literal tag and is absent from the card's own inventory. - **Nothing consumed the tag.** The only consumers found anywhere are strippers: `@object-ui/react`'s `extractWriteErrorMessage` and two `plugin-detail` call sites each remove a leading bracketed prefix before showing the sentence to a user, next to the `SCREAMING_SNAKE:` strip. They confirm the tag was arriving and they cannot break on its absence — the regex simply matches nothing. - **Two bracketed vocabularies are deliberately kept**: the `path [zod code]` locators inside a validation headline and the `[rule]` locators the author-time gate composes. Neither restates a declared `code` — they name WHICH finding, a fact the envelope carries nowhere else. +- **The published docs that quoted the openers are corrected in the same change.** `ProtocolSchema`'s promotion `describe()` said the lookup 「answers 404 `[no_draft]`」 and now names `NO_DRAFT`, the axis that still carries it; `content/docs/references/api/protocol.mdx` is regenerated from it, never hand-edited. The error catalog's two documented `INVALID_REQUEST` payloads showed a `message` opening with the tag beside a `code` field already carrying the token, and now show what the platform emits. +- ⛔ **Three carriers in `content/docs/releases/v17/` are deliberately left**: release pages record what shipped and a code change does not rewrite them. - **Pinned as an absence**, because nothing else would notice one coming back: a re-introduced tag reds exactly one per-door pin and a newly-written refusal reds none. diff --git a/content/docs/api/error-catalog.mdx b/content/docs/api/error-catalog.mdx index 479c2b4116c..e7fdf496d6a 100644 --- a/content/docs/api/error-catalog.mdx +++ b/content/docs/api/error-catalog.mdx @@ -606,7 +606,7 @@ unchanged returns the same `400`. "success": false, "error": { "code": "INVALID_REQUEST", - "message": "[invalid_request] 'viewes' is not a recognised spelling of metadata type 'view'. Address it as 'view' or 'views'. Refused rather than treated as a plugin-registered type, because forwarding an unrecognised spelling of a declared type would create a second namespace under type='viewes'.", + "message": "'viewes' is not a recognised spelling of metadata type 'view'. Address it as 'view' or 'views'. Refused rather than treated as a plugin-registered type, because forwarding an unrecognised spelling of a declared type would create a second namespace under type='viewes'.", "httpStatus": 400 } } @@ -657,7 +657,7 @@ the type segment, and the exemption below decides whether it fires at all. "success": false, "error": { "code": "INVALID_REQUEST", - "message": "[invalid_request] 'fieldz' is not a metadata type. The platform declares no such type, and since #8586 retired 'additionalTypes' a plugin cannot declare one either — so this write would mint a sys_metadata namespace under type='fieldz' that nothing reads and nothing serves. Address a real metadata type; GET /api/v1/meta/types lists the ones this deployment carries.", + "message": "'fieldz' is not a metadata type. The platform declares no such type, and since #8586 retired 'additionalTypes' a plugin cannot declare one either — so this write would mint a sys_metadata namespace under type='fieldz' that nothing reads and nothing serves. Address a real metadata type; GET /api/v1/meta/types lists the ones this deployment carries.", "httpStatus": 400 } } diff --git a/content/docs/references/api/protocol.mdx b/content/docs/references/api/protocol.mdx index 88d647fc075..c7dfeae3fb8 100644 --- a/content/docs/references/api/protocol.mdx +++ b/content/docs/references/api/protocol.mdx @@ -2274,7 +2274,7 @@ Installed package with runtime lifecycle state | :--- | :--- | :--- | :--- | | **type** | `string` | ✅ | Metadata type name | | **name** | `string` | ✅ | Item name — lowercase snake_case segments, optionally dot-qualified (`crm_lead`, `crm_lead.pipeline`). The promotion door enforces the same grammar as `saveMetaItem`. | -| **organizationId** | `string` | optional | Organization (tenant) scope for the promotion. The implementation resolves the draft through the org partition (ADR-0005), so a draft authored org-scoped must be published under the same scope or the lookup answers 404 `[no_draft]`. Absent = environment-wide. | +| **organizationId** | `string` | optional | Organization (tenant) scope for the promotion. The implementation resolves the draft through the org partition (ADR-0005), so a draft authored org-scoped must be published under the same scope or the lookup answers 404 `NO_DRAFT`. Absent = environment-wide. | | **actor** | `string` | optional | Identity recorded on the `op='publish'` history event. On the REST door this is the request's authenticated identity (one producer) — never a caller-supplied header. | | **message** | `string` | optional | Optional human-readable note recorded with the publish history event. | | **packageId** | `string \| null` | optional | ADR-0048 — the software package the draft being promoted was listed under, when the caller has one to state (`?package=` on the REST door). ⚠️ `null` is NOT the same as absent, and the difference is load-bearing: the implementation branches on the KEY BEING PRESENT, so an ABSENT key keeps the historical "match any package" resolution while `null` pins the lookup to the package-UNBOUND row. Spread it in conditionally; a present-and-`undefined` key coerces to `null` downstream and makes a package-bound draft unfindable — a silent `no_draft` on the untouched path. | diff --git a/packages/spec/src/api/protocol.zod.ts b/packages/spec/src/api/protocol.zod.ts index 5db152247ae..b74dc909522 100644 --- a/packages/spec/src/api/protocol.zod.ts +++ b/packages/spec/src/api/protocol.zod.ts @@ -812,7 +812,7 @@ export const PublishMetaItemRequestSchema = lazySchema(() => z.object({ 'Organization (tenant) scope for the promotion. The implementation resolves ' + 'the draft through the org partition (ADR-0005), so a draft ' + 'authored org-scoped must be published under the same scope or the lookup ' - + 'answers 404 `[no_draft]`. Absent = environment-wide.', + + 'answers 404 `NO_DRAFT`. Absent = environment-wide.', ), actor: z.string().optional().describe( 'Identity recorded on the `op=\'publish\'` history event. On the REST door ' From 828219c85317d89b97c557611658a8188c5f6305 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 08:16:53 +0000 Subject: [PATCH 07/10] =?UTF-8?q?fix(objectql,metadata-protocol):=20finish?= =?UTF-8?q?=20the=20blast=20radius=20=E2=80=94=20and=20give=20the=20boot?= =?UTF-8?q?=20log=20its=20own=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six objectql assertions read the retired opener. Five are the same family as the producer change and move to the envelope, which is strictly stronger than a regex over prose: `code` plus `status` instead of a lowercase token that only ever appeared inside the bracket. The sixth is NOT that. `protocol-object-overlay-layer.test.ts` asserts captured `console.warn` output, and a log line has no envelope beside it — the `code` axis a caller reads does not exist there, so removing the opener took the only machine-readable token with it and left nothing to fall back on. Fixed at the PRODUCER's log site rather than in the assertion: the boot warning now prints the declared code itself, as the sibling branch twelve lines above already does. The caller-facing message stays prose. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude --- packages/metadata-protocol/src/protocol.ts | 12 +++++++++++- .../objectql/src/protocol-commit-history.test.ts | 8 ++++++-- .../objectql/src/protocol-meta-types-rich.test.ts | 5 ++++- packages/objectql/src/protocol-meta.test.ts | 4 +++- .../src/protocol-object-overlay-layer.test.ts | 4 +++- .../src/protocol-writepath-object-ownership.test.ts | 2 +- 6 files changed, 28 insertions(+), 7 deletions(-) diff --git a/packages/metadata-protocol/src/protocol.ts b/packages/metadata-protocol/src/protocol.ts index e8cad42a166..51689459321 100644 --- a/packages/metadata-protocol/src/protocol.ts +++ b/packages/metadata-protocol/src/protocol.ts @@ -21979,7 +21979,17 @@ export class ObjectStackProtocolImplementation implements `DELETE /api/v1/metadata/object/${record.name}.`, ); } else { - console.warn(`[Protocol] Failed to hydrate ${record.type}/${record.name}: ${e instanceof Error ? e.message : String(e)}`); + // The declared `code` is printed, not left to the prose. + // A LOG LINE has no envelope beside it — the `code` + // axis a caller reads does not exist here — so the one + // machine-readable token an operator can grep for has + // to be IN the line. Until the refusal messages stopped + // restating their own code, this line inherited the + // token by accident from the message it interpolates; + // now it names it deliberately, which is also what the + // sibling branch above already does. + const hydrateCode = (e as any)?.code; + console.warn(`[Protocol] Failed to hydrate ${record.type}/${record.name}: ${e instanceof Error ? e.message : String(e)}${typeof hydrateCode === 'string' && hydrateCode.length > 0 ? ` (code=${hydrateCode})` : ''}`); } } } diff --git a/packages/objectql/src/protocol-commit-history.test.ts b/packages/objectql/src/protocol-commit-history.test.ts index 3e67cba878f..8b3cf8a9b0e 100644 --- a/packages/objectql/src/protocol-commit-history.test.ts +++ b/packages/objectql/src/protocol-commit-history.test.ts @@ -656,8 +656,10 @@ describe('#6563 — revertCommit restores a runtime-created `object`', () => { name: 'myapp_invoice', code: 'NOT_OVERRIDABLE', }); + // The token is asserted on `code` just above; the message carries the + // human sentence and no longer restates it. expect(res.failed[0].error).toContain( - `[NOT_OVERRIDABLE] 'object' is not allowOrgOverride in the registry.`, + `'object' is not allowOrgOverride in the registry.`, ); // Refused means refused: the edit the commit made is still the live body. expect(storedFields(rows, 'myapp_invoice').fields).toContain('due_date'); @@ -852,8 +854,10 @@ describe('#6620 — revertCommit soft-removes a runtime-CREATED `object`', () => name: 'myapp_invoice', code: 'NOT_OVERRIDABLE', }); + // The token is asserted on `code` just above; the message carries the + // human sentence and no longer restates it. expect(res.failed[0].error).toContain( - `[NOT_OVERRIDABLE] 'object' is not allowOrgOverride in the registry.`, + `'object' is not allowOrgOverride in the registry.`, ); // Refused means refused: the artifact-backed row is still there. expect(storedRows(rows, 'myapp_invoice')).toHaveLength(1); diff --git a/packages/objectql/src/protocol-meta-types-rich.test.ts b/packages/objectql/src/protocol-meta-types-rich.test.ts index b7215e443c7..2abb843eb79 100644 --- a/packages/objectql/src/protocol-meta-types-rich.test.ts +++ b/packages/objectql/src/protocol-meta-types-rich.test.ts @@ -128,9 +128,12 @@ describe('ObjectStackProtocolImplementation - getMetaTypes rich response', () => delete process.env.OS_METADATA_WRITABLE; ObjectStackProtocolImplementation.resetEnvWritableCache(); resetEnvWritableMetadataTypes(); + // Asserted on the ENVELOPE, not on a token inside the prose: the + // message is human language and the code is the machine axis, so a + // regex over the sentence is not what pins this gate. await expect( scoped.saveMetaItem({ type: 'agent', name: 'my_agent', item: { name: 'my_agent' } }) - ).rejects.toThrow(/not_(overridable|creatable)/); + ).rejects.toMatchObject({ code: expect.stringMatching(/^NOT_(OVERRIDABLE|CREATABLE)$/), status: 403 }); // With env var: `agent` writes allowed. process.env.OS_METADATA_WRITABLE = 'agent'; diff --git a/packages/objectql/src/protocol-meta.test.ts b/packages/objectql/src/protocol-meta.test.ts index 7795188ffa5..87a773ed417 100644 --- a/packages/objectql/src/protocol-meta.test.ts +++ b/packages/objectql/src/protocol-meta.test.ts @@ -631,7 +631,9 @@ describe('ObjectStackProtocolImplementation - Metadata Persistence', () => { expect(caught).toBeDefined(); expect(caught.code).toBe('INVALID_METADATA'); expect(caught.status).toBe(422); - expect(caught.message).toMatch(/invalid_metadata/); + // `code` and `status` above ARE the machine assertion; what the + // message owes is the human sentence, which is asserted as prose. + expect(caught.message).toMatch(/failed spec validation/); expect(Array.isArray(caught.issues)).toBe(true); expect(mockEngine.insert).not.toHaveBeenCalled(); }); diff --git a/packages/objectql/src/protocol-object-overlay-layer.test.ts b/packages/objectql/src/protocol-object-overlay-layer.test.ts index 736fa931600..715048b53d4 100644 --- a/packages/objectql/src/protocol-object-overlay-layer.test.ts +++ b/packages/objectql/src/protocol-object-overlay-layer.test.ts @@ -570,7 +570,9 @@ describe('ADR-0029 D9.9 / #6995 — the row\'s package_id is provenance, never a } expect(booted.res).toMatchObject({ loaded: 0, errors: 1 }); - expect(warned.join('\n')).toContain('object_overlay_package_mismatch'); + // A log line has no envelope, so the boot warning names the declared + // code itself rather than inheriting it from the refusal prose. + expect(warned.join('\n')).toContain('code=OBJECT_OVERLAY_PACKAGE_MISMATCH'); // The packaged definition is served, untouched — no half-applied layer. expect(kinds(booted.registry, 'myapp_invoice')).toEqual(['own']); diff --git a/packages/objectql/src/protocol-writepath-object-ownership.test.ts b/packages/objectql/src/protocol-writepath-object-ownership.test.ts index 4ea7b6efd6b..4ea52435d5d 100644 --- a/packages/objectql/src/protocol-writepath-object-ownership.test.ts +++ b/packages/objectql/src/protocol-writepath-object-ownership.test.ts @@ -318,7 +318,7 @@ describe('#4636 — cloud#970 counter-example: a freshly created app stays edita name: 'myapp_invoice', packageId: APP_PKG, item: objectBody('myapp_invoice'), - })).rejects.toThrow(/not_overridable/); + })).rejects.toMatchObject({ code: 'NOT_OVERRIDABLE', status: 403 }); // The mechanism, stated so a future reader does not have to re-derive // it: with no `_provenance`, `applyProtection` defaults the row to From c70c5afe659da2e039891ac3758d82ed877052cf Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 08:51:17 +0000 Subject: [PATCH 08/10] test(cli): repoint the two reset-door pins onto the axis each surface owns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `os meta delete` carries the refusal on two surfaces and they are not the same surface, so the two assertions do not move the same way. stdout is the HUMAN one: `printError` prints the message and nothing else, so the token never belonged there. That assertion now reads the sentence, and the structural controls already in the test — exit code, the reset request's parentVersion, the `if-match` header — are what keep it from degrading into "something went wrong". `--format json` is the MACHINE one, and it already carries the token on its own axis. Measured rather than assumed, by dumping the real envelope: { success: false, error: '…has been modified…', code: 'METADATA_CONFLICT', httpStatus: 409 } So the assertion reads `code` and `httpStatus`, which a prose match over `error` only ever approximated. ⛔ No producer change was needed and the opener was not re-added: `errorCodeFields` has carried this since #13347. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude --- .../commands/meta/delete-reset-carriers.test.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/commands/meta/delete-reset-carriers.test.ts b/packages/cli/src/commands/meta/delete-reset-carriers.test.ts index 8d1ca417520..42b68370b00 100644 --- a/packages/cli/src/commands/meta/delete-reset-carriers.test.ts +++ b/packages/cli/src/commands/meta/delete-reset-carriers.test.ts @@ -468,7 +468,13 @@ describe('[#13024] `os meta delete --if-match` against the real reset door', () // envelope, never merely "something went wrong" — a bare failure assertion // stays green against a command that never sent the header at all. expect(run.exitCode).toBe(1); - expect(run.out).toContain('metadata_conflict'); + // stdout is the HUMAN surface and carries prose: `printError` prints the + // message and nothing else, and the machine token rides `--format json` + // via `errorCodeFields` (the case below). So the operator-facing assertion + // is the sentence that names the conflict — not a token that never + // belonged on this surface. The structural controls below are what keep + // this from degrading into "something went wrong". + expect(run.out).toContain('has been modified since you loaded it'); expect(run.out).toContain('view/race_probe'); // THE point of the pin: the other author's row is still there. @@ -508,7 +514,14 @@ describe('[#13024] `os meta delete --if-match` against the real reset door', () expect(run.exitCode).toBe(1); const payload = JSON.parse(run.out); expect(payload.success).toBe(false); - expect(String(payload.error)).toContain('metadata_conflict'); + // `--format json` IS the machine-readable path, and the envelope carries the + // token on its own axis — measured here, not assumed: + // { success: false, error: '…has been modified…', code: 'METADATA_CONFLICT', httpStatus: 409 } + // so the assertion reads `code`, which a prose match over `error` only ever + // approximated. `error` keeps the human sentence. + expect(payload.code).toBe('METADATA_CONFLICT'); + expect(payload.httpStatus).toBe(409); + expect(String(payload.error)).toContain('has been modified since you loaded it'); expect(await overlayRows(engine, 'json_probe')).toHaveLength(1); }, 60_000); }); From 570536ecb553105f22517d1b5a641f68c1ea111f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 10:49:53 +0000 Subject: [PATCH 09/10] =?UTF-8?q?fix(spec):=20one=20door,=20one=20descript?= =?UTF-8?q?ion=20=E2=80=94=20and=20scope=20the=20changeset=20to=20what=20i?= =?UTF-8?q?t=20changed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two corrections from the at-tier review, one line each. `plugin-rest-api.zod.ts` still described `publishMetaItem` as answering 404 `[no_draft]` while `protocol.zod.ts` now says `NO_DRAFT`. Both ship in the SAME `@objectstack/spec` minor, twice over — as published `.zod.ts` source and in `dist/api/index.{js,mjs}` — so one release would have carried two contradictory descriptions of one door. That is not a follow-up; it is a defect in this change. ⛔ Nothing regenerates from it: all 15 spec artifacts re-checked clean, `content/docs`, `json-schema` and `api-surface` included. The changeset headline claimed refusal messages no longer open with a bracketed tag restating their own code. Measured in the built package, that is false: the third producer, `runtime-authoring-gate.ts`, still throws `[invalid_metadata]` and the opener is present 1/1 in both `dist/index.js` and `dist/index.cjs`. The headline is now scoped to the two producers this change actually touched. ⛔ The removal is NOT extended into that third producer — beyond triage's boundary, and the review does not ask for it. `metadata_conflict` on the same line is deliberately left lowercase: it is the shared spelling in `protocol.zod.ts` too, so changing it here would create the inconsistency this commit is removing. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude --- .changeset/16245-bracketed-tag-refusal-openers.md | 2 +- packages/spec/src/api/plugin-rest-api.zod.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/16245-bracketed-tag-refusal-openers.md b/.changeset/16245-bracketed-tag-refusal-openers.md index 2bb62af9acf..f8075f0ef7c 100644 --- a/.changeset/16245-bracketed-tag-refusal-openers.md +++ b/.changeset/16245-bracketed-tag-refusal-openers.md @@ -3,7 +3,7 @@ '@objectstack/spec': minor --- -Refusal messages no longer open with a bracketed tag restating the `code` the same throw declares — `error` carries the human sentence, `code` carries the machine token, and the token is no longer duplicated onto the prose axis. +`ObjectStackProtocolImplementation` and `SysMetadataRepository` no longer open their refusal messages with a bracketed tag restating the `code` the same throw declares — `error` carries the human sentence, `code` carries the machine token, and the token is no longer duplicated onto the prose axis. Clause-②: yes diff --git a/packages/spec/src/api/plugin-rest-api.zod.ts b/packages/spec/src/api/plugin-rest-api.zod.ts index a6f98505b36..f7e5236c4fc 100644 --- a/packages/spec/src/api/plugin-rest-api.zod.ts +++ b/packages/spec/src/api/plugin-rest-api.zod.ts @@ -895,7 +895,7 @@ export const DEFAULT_METADATA_ROUTES: RestApiRouteRegistration = { 'Promotes the item\'s pending DRAFT overlay to the live `active` row and records an ' + '`op=\'publish\'` history event. The sibling write door of `PUT /:type/:name` — the ' + 'ADR-0033 two-step spelling, where `?mode=draft` stages a body and this makes it live. ' - + '404 `[no_draft]` when there is nothing to publish; 409 `metadata_conflict` when the ' + + '404 `NO_DRAFT` when there is nothing to publish; 409 `metadata_conflict` when the ' + 'published row advanced while the draft was held. Served since before #7294 with no ' + 'declaration behind it — this entry is what makes its response contract nameable.', tags: ['Metadata'], From 5e294644b26155abef6c6595ec0e80c68bb40537 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 11:02:25 +0000 Subject: [PATCH 10/10] chore(spec): regenerate protocol.mdx after the merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Discharges the `os-regen-pending` deferral the merge driver recorded. The merge brought an `enableOnInstall` description that git's textual merge dropped from the generated page; only a regeneration on the committed merge restores it, which is why the driver defers instead of trusting the text merge. Not this card's content — regenerated, never hand-edited. Claude-Session: https://claude.ai/code/session_013RDBh5DqXd2xnLwvHLgLFr Co-authored-by: Claude --- content/docs/references/api/protocol.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/docs/references/api/protocol.mdx b/content/docs/references/api/protocol.mdx index c7dfeae3fb8..0acb9c6cd17 100644 --- a/content/docs/references/api/protocol.mdx +++ b/content/docs/references/api/protocol.mdx @@ -1910,7 +1910,7 @@ Install package request | :--- | :--- | :--- | :--- | | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest to install | | **settings** | `Record` | optional | User-provided settings at install time | -| **enableOnInstall** | `boolean` | optional (default: `true`) | Whether to enable immediately after install — restates the install-door request key, whose one authority is api/PackageInstallRequest; this protocol primitive does not read it | +| **enableOnInstall** | `boolean` | optional (default: `true`) | Whether to enable immediately after install — restates the install-door request key, whose one authority is api/PackageInstallRequest; this protocol primitive honours it on the registry row: true enables, false disables, absent makes no lifecycle call | | **platformVersion** | `string` | optional | Current platform version for compatibility verification | ### Nested Shape: `InstallPackageRequest.manifest`