From dc8a1a1123f249472b166f013316f8f0fbd46ce4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 09:05:24 +0000 Subject: [PATCH 1/3] fix(metadata-protocol): refusals and hints state each decision in words instead of a tracker number (stage 2, author-visible) The thrown refusals, the stored-type preflight and revert refusals, and the schedule-flow organization hint no longer send the reader to a tracker number: each says what was decided, or loses only the citation where the sentence already said it. Text only: no code, field, status or export moves. The prose-id ledger is recomputed with --census-ledger; only metadata-protocol rows move. Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude --- packages/metadata-protocol/src/protocol.ts | 22 +++++++++---------- .../src/runtime-authoring-gate.ts | 3 ++- scripts/doc-authoring-prose-id.baseline.json | 13 ++--------- 3 files changed, 15 insertions(+), 23 deletions(-) diff --git a/packages/metadata-protocol/src/protocol.ts b/packages/metadata-protocol/src/protocol.ts index 77cb8eeb221..29aa4651b6d 100644 --- a/packages/metadata-protocol/src/protocol.ts +++ b/packages/metadata-protocol/src/protocol.ts @@ -10308,7 +10308,7 @@ export class ObjectStackProtocolImplementation implements `'${param}' entry #${badShape + 1} on object '${object}' is not a field name.` + (retiredForm ? ' The nested-select object form `{ field, fields, alias }` was removed in ' - + '@objectstack/spec 17 (#4196) — no engine or driver ever read it.' + + '@objectstack/spec 17 — no engine or driver ever read it.' : '') // [#7532] The dotted-path half of this prescription is GONE. // It pointed at a spelling this same gate now refuses — and @@ -13320,7 +13320,7 @@ export class ObjectStackProtocolImplementation implements this.assertObjectRegistered(request.object); // [#3770] const engineInsertMany = (this.engine as any)?.insertMany; if (typeof engineInsertMany !== 'function') { - throw new Error('insertManyData requires an engine with insertMany (framework#3172)'); + throw new Error('insertManyData requires an engine with insertMany: the partial-success batch insert, which reports an outcome per row so a bad row neither fails the whole batch nor makes the good rows run their beforeInsert hooks twice'); } // [#5503/#14147] The engine's events are collected WHOLE and merged — // the same handling `createManyData` gives them, and for the reason @@ -14077,7 +14077,7 @@ export class ObjectStackProtocolImplementation implements + `overlay"). An operator may set OS_METADATA_WRITABLE=${singular} to grant a runtime escape hatch, ` + `but note the row still will not survive a restart — the hatch unlocks the write, not the read, ` + `and boot logs every such row it walks past. ` - + `See docs/adr/0005-metadata-customization-overlay.md and #6190.` + + `See docs/adr/0005-metadata-customization-overlay.md.` ); err.code = 'NOT_OVERRIDABLE'; err.status = 403; @@ -15043,7 +15043,7 @@ export class ObjectStackProtocolImplementation implements + `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 ` - + `canonical read, listing or declaration lookup can reach (#4432). Fold the type at the ` + + `canonical read, listing or declaration lookup can reach. Fold the type at the ` + `producer (canonicalMetaType), not here; see this method's header for why the mint door ` + `refuses instead of folding.`, ); @@ -15818,8 +15818,8 @@ export class ObjectStackProtocolImplementation implements if (await this.metaTypeNamespaceExists(unrecognised.type)) return; const err = new Error( `'${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 ` + + `no such type, and a plugin cannot declare one either: 'additionalTypes' was retired because ` + + `nothing ever read it — so this write would mint a sys_metadata namespace under ` + `type='${unrecognised.type}' that nothing reads and nothing serves. Address a real ` + `metadata type; GET /api/v1/meta/types lists the ones this deployment carries.`, ); @@ -18667,11 +18667,11 @@ export class ObjectStackProtocolImplementation implements error: `Draft '${d.type}/${d.name}' is stored under the non-canonical metadata type ` + `'${d.type}'; the canonical type for this item is '${canonical}'. Publishing it would ` + `mint an ACTIVE row in a second namespace that no registry read and no compliance ` - + `query on '${canonical}' can see (#7894 closed this namespace at the '/meta' URL door; ` + + `query on '${canonical}' can see (the '/meta' URL door now folds a type to its canonical spelling before it writes; ` + `this row predates that). Re-author the item under '${canonical}' ` + `(PUT /meta/${canonical}/${d.name}) and drop the '${d.type}' row. Note that ` + `POST /meta/_migrate-stored does NOT rewrite a stored type spelling — it canonicalizes ` - + `bodies, and reports rows of this class as 'skipped' with that same reason (#8957).`, + + `bodies, and reports rows of this class as 'skipped' with that same reason.`, code: 'STORED_TYPE_NOT_CANONICAL', organizationId: d.organizationId ?? null, }); @@ -20873,13 +20873,13 @@ export class ObjectStackProtocolImplementation implements + `metadata type '${it.type}'; the canonical type for this item is ` + `'${canonical}'. Restoring it would write the pre-commit body back into a ` + `second namespace that no registry read and no compliance query on ` - + `'${canonical}' can see (#7894 closed this namespace at the '/meta' URL ` - + `door; this row predates that), and the registry refuses to serve it ` + + `'${canonical}' can see (the '/meta' URL door now folds a type to its canonical ` + + `spelling before it writes; this row predates that), and the registry refuses to serve it ` + `(REGISTRY_TYPE_NOT_CANONICAL), so the restored body would reach no reader. ` + `Re-author the item under '${canonical}' (PUT /meta/${canonical}/${it.name}) ` + `and drop the '${it.type}' row. Note that POST /meta/_migrate-stored does NOT ` + `rewrite a stored type spelling — it canonicalizes bodies, and reports rows ` - + `of this class as 'skipped' with that same reason (#8957).`, + + `of this class as 'skipped' with that same reason.`, code: 'STORED_TYPE_NOT_CANONICAL', }); continue; diff --git a/packages/metadata-protocol/src/runtime-authoring-gate.ts b/packages/metadata-protocol/src/runtime-authoring-gate.ts index 6d449a783b3..65ca4141fac 100644 --- a/packages/metadata-protocol/src/runtime-authoring-gate.ts +++ b/packages/metadata-protocol/src/runtime-authoring-gate.ts @@ -329,7 +329,8 @@ export function findPlatformScheduleOrgGaps(args: { hint: `Declare the owning organization on this node: ` + `config.fields.${ORGANIZATION_FIELD}. An author-supplied value always wins over the ` - + `engine's fill (#6153), so this is the one place the answer can come from for a ` + + `engine's fill, and the engine fills only an organization the run resolved — so this is ` + + `the one place the answer can come from for a ` + `scheduled run. A NULL ${ORGANIZATION_FIELD} is not merely untidy: an ` + `(${ORGANIZATION_FIELD}, …) unique index does not constrain across NULL and org-scoped ` + `queries never see the row. Alternatively, publish this flow into an organization, or ` diff --git a/scripts/doc-authoring-prose-id.baseline.json b/scripts/doc-authoring-prose-id.baseline.json index e5bc03465eb..5dec3e62411 100644 --- a/scripts/doc-authoring-prose-id.baseline.json +++ b/scripts/doc-authoring-prose-id.baseline.json @@ -324,19 +324,10 @@ }, "packages/metadata-protocol/src/protocol.ts": { "#10377": 1, - "#3172": 1, "#3770": 1, - "#4196": 1, - "#4432": 1, - "#6190": 3, + "#6190": 2, "#6992": 1, - "#7894": 2, - "#8502": 1, - "#8586": 1, - "#8957": 2 - }, - "packages/metadata-protocol/src/runtime-authoring-gate.ts": { - "#6153": 1 + "#8502": 1 }, "packages/metadata-protocol/src/sys-metadata-repository.ts": { "#4867": 1 From 730cbcaf6375886f9a61f1e3beab184b89eb82a5 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 09:06:00 +0000 Subject: [PATCH 2/3] fix(metadata-protocol): migration, boot and protocol log lines state each decision in words instead of a tracker number (stage 2, log lines) The kernel:ready index migrations, the seed/API tenancy repair and its receipt, the three migration-skipped warnings, and the protocol's warn and error lines no longer cite a tracker number. Where the sentence already said what was decided, only the citation goes; the three skipped warnings now say what the migration that did not run would have ensured. Two tests that pinned a number now pin the sentence. Text only. The ledger is recomputed; only metadata-protocol rows move. Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude --- .../src/migrations/overlay-index.ts | 6 ++-- .../src/migrations/seed-tenancy-backfill.ts | 30 ++++++++--------- .../migrations/sys-setting-identity-index.ts | 8 ++--- .../view-definition-active-index.test.ts | 4 +-- .../view-definition-active-index.ts | 13 ++++---- packages/metadata-protocol/src/plugin.ts | 6 ++-- .../protocol.batch-row-driver-text.test.ts | 2 +- packages/metadata-protocol/src/protocol.ts | 10 +++--- .../src/sys-metadata-repository.ts | 2 +- scripts/doc-authoring-prose-id.baseline.json | 32 ------------------- 10 files changed, 40 insertions(+), 73 deletions(-) diff --git a/packages/metadata-protocol/src/migrations/overlay-index.ts b/packages/metadata-protocol/src/migrations/overlay-index.ts index 3bfdfe75a90..d1e238be948 100644 --- a/packages/metadata-protocol/src/migrations/overlay-index.ts +++ b/packages/metadata-protocol/src/migrations/overlay-index.ts @@ -271,7 +271,7 @@ export async function ensureOverlayStateIndex( logger, `[metadata-protocol] could not create '${indexName}' on "${OVERLAY_TABLE}" after the probe ` + `succeeded — the table may currently have NO unique index over ` + - `(${overlayIndexKeyParts().join(', ')}) among state='${state}' rows. Restart to retry (#6418).`, + `(${overlayIndexKeyParts().join(', ')}) among state='${state}' rows. Restart to retry.`, detail, ); return { status: 'failed', detail, fallback: 'not-attempted' }; @@ -374,7 +374,7 @@ function reportDegradation( `(${columns}) is enforced only as far as it was before; ADR-0005 overlay uniqueness is NOT ` + `enforced until the duplicates are resolved, and getMetaItem has no defined answer for ` + `which of the colliding rows wins. List them with: ${duplicateQuery} — or run ` + - `"os migrate duplicates" — then restart (ADR-0120 D4, #6418, #8725).`, + `"os migrate duplicates" — then restart (ADR-0120 D4).`, detail, ); return; @@ -390,7 +390,7 @@ function reportDegradation( `[metadata-protocol] could not rebuild '${indexName}' on "${OVERLAY_TABLE}" as the ` + `state='${state}' partial UNIQUE index; the existing index is unchanged, so two ${state} ` + `overlay rows for one (${columns}) can still coexist while everything else looks healthy. ` + - `Fix the cause below and restart (#6418).`, + `Fix the cause below and restart.`, detail, ); } diff --git a/packages/metadata-protocol/src/migrations/seed-tenancy-backfill.ts b/packages/metadata-protocol/src/migrations/seed-tenancy-backfill.ts index e04ab5d4a1a..95b2c142371 100644 --- a/packages/metadata-protocol/src/migrations/seed-tenancy-backfill.ts +++ b/packages/metadata-protocol/src/migrations/seed-tenancy-backfill.ts @@ -1147,7 +1147,7 @@ export async function recordSeedTenancyReceipt( ): Promise { const ledger = seam?.ledger; const notRecorded = - `[metadata-protocol] the seed/API tenancy repair (#8686) ran and rewrote stored rows, but this ` + + `[metadata-protocol] the seed/API tenancy repair ran and rewrote stored rows, but this ` + `deployment has NO durable record that it did`; if (!ledger) { // Functional absence, not a durability failure: this host is not an engine @@ -1155,7 +1155,7 @@ export async function recordSeedTenancyReceipt( // `warn` per AGENTS.md — the system is visibly smaller, not silently lying. logger?.warn?.( `${notRecorded} — no engine was resolved beside the raw-SQL seam, so ${DATA_MIGRATION_FLAG_OBJECT} ` + - `could not be written. Capture this boot's log before restarting (#9451).`, + `could not be written. Capture this boot's log before restarting.`, ); return; } @@ -1164,7 +1164,7 @@ export async function recordSeedTenancyReceipt( logger?.warn?.( `${notRecorded} — ${DATA_MIGRATION_FLAG_OBJECT} is not registered on this kernel, so the ` + `deployment ledger does not exist here. Compose PlatformObjectsPlugin (it carries the ledger ` + - `every served kernel gets) or capture this boot's log before restarting (#9451).`, + `every served kernel gets) or capture this boot's log before restarting.`, ); return; } @@ -1172,7 +1172,7 @@ export async function recordSeedTenancyReceipt( const outcome = await persistSeedTenancyReceiptRow(ledger, flag); logger?.info?.( `[metadata-protocol] seed/API tenancy repair recorded in ${DATA_MIGRATION_FLAG_OBJECT} ` + - `(id '${SEED_TENANCY_MIGRATION_ID}', ${outcome}) — the run survives this process (#9451).`, + `(id '${SEED_TENANCY_MIGRATION_ID}', ${outcome}) — the run survives this process.`, { id: SEED_TENANCY_MIGRATION_ID, outcome, details: flag.details }, ); } catch (e: unknown) { @@ -1184,7 +1184,7 @@ export async function recordSeedTenancyReceipt( `Nothing else reports it: capture this boot's log NOW, before the container is replaced. Fix: make ` + `${DATA_MIGRATION_FLAG_OBJECT} writable on this deployment (it is provisioned by ` + `PlatformObjectsPlugin) and verify with ` + - `SELECT * FROM ${DATA_MIGRATION_FLAG_OBJECT} WHERE id = '${SEED_TENANCY_MIGRATION_ID}' (#9451).`; + `SELECT * FROM ${DATA_MIGRATION_FLAG_OBJECT} WHERE id = '${SEED_TENANCY_MIGRATION_ID}'.`; if (logger?.error) logger.error(message, e instanceof Error ? e : new Error(detail)); else logger?.warn?.(message, { error: detail }); } @@ -1291,7 +1291,7 @@ export async function backfillSeedTenancy( if (postureEnforcesWall(resolveTenancyPosture())) { logger?.warn?.( `[metadata-protocol] seed/API tenancy split detected on a MULTI-ORGANIZATION install — ` + - `backfill skipped (#8686). Affected: ${affected}. ` + + `backfill skipped. Affected: ${affected}. ` + `Seed rows carry ${ORGANIZATION_FIELD} = NULL while API rows carry a real organization, so each ` + `object runs two autonumber counters and can mint the same "unique" identifier twice — the ` + `partitioned unique index (COALESCE(${ORGANIZATION_FIELD}, '${GLOBAL_TENANT}'), ) does not ` + @@ -1369,7 +1369,7 @@ export async function backfillSeedTenancy( if (organizationIds.length === 0 && organizationProbeError === undefined) { logger?.info?.( `[metadata-protocol] seed/API tenancy split detected on an install with no organization yet — ` + - `nothing to adopt, and nothing at risk (#8686). Affected: ${affected}. ` + + `nothing to adopt, and nothing at risk. Affected: ${affected}. ` + `${ORGANIZATION_TABLE} is empty, so each of these objects runs exactly ONE counter (its ` + `'${GLOBAL_TENANT}' row) and no "unique" identifier can be minted twice while there is only ` + `one partition. No operator action: this self-heals at the first sign-up, when the ` + @@ -1384,7 +1384,7 @@ export async function backfillSeedTenancy( if (organizationIds.length !== 1) { logger?.warn?.( `[metadata-protocol] seed/API tenancy split detected but the target organization is not ` + - `derivable — backfill skipped (#8686). Affected: ${affected}. ` + + `derivable — backfill skipped. Affected: ${affected}. ` + `The install reports tenancy posture 'single' but holds ${organizationIds.length} rows in ` + `${ORGANIZATION_TABLE} (exactly 1 is required to adopt one without guessing). Until this is ` + `resolved these objects run two autonumber counters and can mint the same "unique" identifier ` + @@ -1399,7 +1399,7 @@ export async function backfillSeedTenancy( `NOTE: the ${ORGANIZATION_TABLE} probe FAILED` + (organizationProbeError === '' ? '' : ` (${organizationProbeError})`) + `, so the count above is "unknown", not a measured zero — an unreadable probe is ` + - `reported here rather than through the benign no-organization-yet path (#9261). `) + + `reported here rather than through the benign no-organization-yet path. `) + SNAPSHOT_CAVEAT, // `organizationProbeError` is `undefined` — and so serializes AWAY — when // the probe answered; a probe that failed carries its text, `''` and all. @@ -1431,7 +1431,7 @@ export async function backfillSeedTenancy( } catch (e) { logger?.warn?.( `[metadata-protocol] could not list already-minted duplicates for ${split.object}.${split.field} ` + - `(#8686) — the backfill continues; verify manually with: ` + + `— the backfill continues; verify manually with: ` + `${buildCollisionProbeSql(split.object, split.field, client)}`, { error: operatorFacingErrorText(e) }, ); @@ -1456,7 +1456,7 @@ export async function backfillSeedTenancy( } catch (e) { stampFailures.push(object); logger?.warn?.( - `[metadata-protocol] seed tenancy backfill could not stamp ${object} (#8686) — its rows keep ` + + `[metadata-protocol] seed tenancy backfill could not stamp ${object} — its rows keep ` + `${ORGANIZATION_FIELD} = NULL and the counter merge below is SKIPPED for it, so the split ` + `survives and the next boot retries. Nothing was lost; nothing was repaired for this object.`, { error: operatorFacingErrorText(e) }, @@ -1490,7 +1490,7 @@ export async function backfillSeedTenancy( } catch (e) { logger?.warn?.( `[metadata-protocol] seed tenancy backfill could not merge the counter for ` + - `${split.object}.${split.field} (#8686) — the '${GLOBAL_TENANT}' counter is left in ` + + `${split.object}.${split.field} — the '${GLOBAL_TENANT}' counter is left in ` + `place, so the high-water mark is intact and the next boot retries the repair`, { error: operatorFacingErrorText(e) }, ); @@ -1499,8 +1499,8 @@ export async function backfillSeedTenancy( if (objectsStamped > 0) { logger?.info?.( - `[metadata-protocol] seed/API tenancy split repaired for ${objectsStamped} object(s) ` + - `(#8686): untenanted seed rows adopted organization ${organizationId} and the ` + + `[metadata-protocol] seed/API tenancy split repaired for ${objectsStamped} object(s): ` + + `untenanted seed rows adopted organization ${organizationId} and the ` + `'${GLOBAL_TENANT}' counter was merged into the organization-scoped one` + (collisions.length > 0 ? `. ${collisions.length} row(s) could NOT be adopted because their identifier is already ` + @@ -1517,7 +1517,7 @@ export async function backfillSeedTenancy( // will NOT rewrite, so an operator has to decide what happens to them. logger?.warn?.( `[metadata-protocol] ${collisions.length} business identifier(s) were already minted TWICE before ` + - `this repair (#8686) — reported, NOT renumbered. These values each exist on both a seeded row and ` + + `this repair — reported, NOT renumbered. These values each exist on both a seeded row and ` + `an API-created row: ` + collisions.map((c) => `${c.object}.${c.field}=${c.value} (${c.rows} rows)`).join(', ') + `. The platform does not renumber them: a record number that has already appeared on a document, ` + diff --git a/packages/metadata-protocol/src/migrations/sys-setting-identity-index.ts b/packages/metadata-protocol/src/migrations/sys-setting-identity-index.ts index 5b06d0d08fb..63452c84a23 100644 --- a/packages/metadata-protocol/src/migrations/sys-setting-identity-index.ts +++ b/packages/metadata-protocol/src/migrations/sys-setting-identity-index.ts @@ -534,7 +534,7 @@ export async function ensureSysSettingIdentityIndex( logger, `[metadata-protocol] could not create '${SYS_SETTING_IDENTITY_INDEX_NAME}' on ` + `"${SYS_SETTING_TABLE}" after the probe succeeded — the table may currently have NO unique ` + - `index on (${sysSettingIdentityKeyParts().join(', ')}). Restart to retry (#8629).`, + `index on (${sysSettingIdentityKeyParts().join(', ')}). Restart to retry.`, detail, ); return { status: 'failed', detail }; @@ -592,7 +592,7 @@ function reportDegradation( `over (${columns}). The system keeps looking healthy while the declared row identity is void on ` + `every row that is not scope='user' — user_id is NULL there — so two tenant-scope rows for one ` + `(namespace, key) in ONE organization, or two platform defaults on the global layer, can coexist ` + - `and SettingsService has no defined answer for which one wins (#8629). MySQL/MariaDB before ` + + `and SettingsService has no defined answer for which one wins. MySQL/MariaDB before ` + `8.0.13 has no functional key parts, so there is no in-dialect fix: run this platform on ` + `SQLite/PostgreSQL for the guarantee, and meanwhile watch for duplicates with this MySQL ` + `statement: ${buildSysSettingDuplicateProbeSqlMysql()}`, @@ -615,7 +615,7 @@ function reportDegradation( `enforced until the duplicates are resolved: settings rows are admin-authored configuration, so ` + `no row is discarded automatically and this migration will keep refusing until an operator ` + `decides which row survives. List them with: ${duplicateQuery} — or run "os migrate duplicates" — ` + - `then restart (ADR-0120 D4, #8629).`, + `then restart (ADR-0120 D4).`, detail, ); return; @@ -631,7 +631,7 @@ function reportDegradation( `[metadata-protocol] could not rebuild '${SYS_SETTING_IDENTITY_INDEX_NAME}' on ` + `"${SYS_SETTING_TABLE}" as the NULL-safe row-identity index; the existing index is unchanged, so ` + `duplicate tenant-scope and global-scope settings rows can still be created while everything else ` + - `looks healthy. Fix the cause below and restart (#8629).`, + `looks healthy. Fix the cause below and restart.`, detail, ); } diff --git a/packages/metadata-protocol/src/migrations/view-definition-active-index.test.ts b/packages/metadata-protocol/src/migrations/view-definition-active-index.test.ts index a4c35ba2f9a..147df12415d 100644 --- a/packages/metadata-protocol/src/migrations/view-definition-active-index.test.ts +++ b/packages/metadata-protocol/src/migrations/view-definition-active-index.test.ts @@ -339,8 +339,8 @@ describe('sys_view_definition active-row uniqueness (#5839) on a NULL-safe key ( const note = String(logger.error.mock.calls[0]![0]); expect(note).toContain('UNRESTRICTED and NULL-distinct'); expect(note).toContain('keeps looking healthy'); - expect(note).toContain('#5839'); - expect(note).toContain('#6417'); + expect(note).toContain('an archived view keeps occupying its name slot'); + expect(note).toContain('two same-name ACTIVE shared views (owner NULL)'); // The fix, and the query that surfaces the duplicates meanwhile. expect(note).toContain('SQLite/PostgreSQL'); expect(note).toContain(buildDuplicateProbeSql()); diff --git a/packages/metadata-protocol/src/migrations/view-definition-active-index.ts b/packages/metadata-protocol/src/migrations/view-definition-active-index.ts index 51002613bd0..7ba2b9dc75b 100644 --- a/packages/metadata-protocol/src/migrations/view-definition-active-index.ts +++ b/packages/metadata-protocol/src/migrations/view-definition-active-index.ts @@ -327,7 +327,7 @@ export async function ensureViewDefinitionActiveIndex( logger, `[metadata-protocol] could not create '${VIEW_ACTIVE_INDEX_NAME}' on ` + `"${VIEW_DEFINITION_TABLE}" after the probe succeeded — the table may currently have NO ` + - `unique index on (${viewActiveIndexKeyParts().join(', ')}). Restart to retry (#5839).`, + `unique index on (${viewActiveIndexKeyParts().join(', ')}). Restart to retry.`, detail, ); return { status: 'failed', detail }; @@ -378,9 +378,9 @@ function reportDegradation( `'${VIEW_ACTIVE_INDEX_NAME}' on "${VIEW_DEFINITION_TABLE}" stays UNRESTRICTED and NULL-distinct ` + `over (${columns}), the bare-composite degradation of ADR-0120 D3. The system keeps looking ` + `healthy while two consequences hold on this dialect: an archived view keeps occupying its ` + - `name slot (#5839), and two same-name ACTIVE shared views (owner NULL) or environment-level ` + - `views (organization_id NULL) can still coexist even though the platform states they cannot ` + - `(#6417). MySQL/MariaDB has no partial indexes, so there is no in-dialect fix: run this ` + + `name slot, and two same-name ACTIVE shared views (owner NULL) or environment-level ` + + `views (organization_id NULL) can still coexist even though the platform states they cannot. ` + + `MySQL/MariaDB has no partial indexes, so there is no in-dialect fix: run this ` + `platform on SQLite/PostgreSQL for the guarantee, and meanwhile watch for duplicates with: ` + `${buildDuplicateProbeSql()}`, detail, @@ -402,7 +402,7 @@ function reportDegradation( `existing rows violate (${keyParts}) among state='active'. The previous index is left in ` + `place, so (${columns}) is enforced only as far as it was before; the NULL-safe key is NOT ` + `enforced until the duplicates are resolved. List them with: ${buildDuplicateProbeSql()} — or ` + - `run "os migrate duplicates" — then restart (ADR-0120 D4, #6417, #8725).`, + `run "os migrate duplicates" — then restart (ADR-0120 D4).`, detail, ); return; @@ -416,8 +416,7 @@ function reportDegradation( logger, `[metadata-protocol] could not rebuild '${VIEW_ACTIVE_INDEX_NAME}' on "${VIEW_DEFINITION_TABLE}" as ` + `the active-row NULL-safe index; the existing index is unchanged, so two same-name ACTIVE shared ` + - `views can still coexist while everything else looks healthy. Fix the cause below and restart ` + - `(#5839 / #6417).`, + `views can still coexist while everything else looks healthy. Fix the cause below and restart.`, detail, ); } diff --git a/packages/metadata-protocol/src/plugin.ts b/packages/metadata-protocol/src/plugin.ts index e43ae638a1a..13b9eb9c7e1 100644 --- a/packages/metadata-protocol/src/plugin.ts +++ b/packages/metadata-protocol/src/plugin.ts @@ -313,7 +313,7 @@ export function assembleMetadataProtocol( await ensureViewDefinitionActiveIndex(resolveIndexExec(ql), ctx.logger); } catch (e: unknown) { ctx.logger.warn( - '[metadata-protocol] sys_view_definition active-row index migration skipped (#5839)', + '[metadata-protocol] sys_view_definition active-row index migration skipped — the index that keeps a view name unique among ACTIVE rows only (an archived view frees its name) was not ensured this boot', { error: e instanceof Error ? e.message : String(e) }, ); } @@ -330,7 +330,7 @@ export function assembleMetadataProtocol( }); } catch (e: unknown) { ctx.logger.warn( - '[metadata-protocol] sys_setting row-identity index migration skipped (#8629)', + '[metadata-protocol] sys_setting row-identity index migration skipped — the NULL-safe index that enforces the declared row identity on tenant and global rows (user_id NULL there) was not ensured this boot', { error: e instanceof Error ? e.message : String(e) }, ); } @@ -351,7 +351,7 @@ export function assembleMetadataProtocol( await backfillSeedTenancy(resolveSeedTenancySeam(ql), ctx.logger); } catch (e: unknown) { ctx.logger.warn( - '[metadata-protocol] seed/API tenancy backfill skipped (#8686)', + '[metadata-protocol] seed/API tenancy backfill skipped — the repair that adopts untenanted seed rows into the install\'s one organization and merges their autonumber counter did not run this boot', { error: e instanceof Error ? e.message : String(e) }, ); } diff --git a/packages/metadata-protocol/src/protocol.batch-row-driver-text.test.ts b/packages/metadata-protocol/src/protocol.batch-row-driver-text.test.ts index 7de21344d35..9a16abb0bc2 100644 --- a/packages/metadata-protocol/src/protocol.batch-row-driver-text.test.ts +++ b/packages/metadata-protocol/src/protocol.batch-row-driver-text.test.ts @@ -393,7 +393,7 @@ describe('[#8502] section 4 — the operator half: withheld, not discarded', () expect(warn).toHaveBeenCalledTimes(1); const [line, cause] = warn.mock.calls[0]; - expect(line).toContain('#8502'); + expect(line).toContain('must not be quoted back on response data'); expect(line).toContain('withheld from the response'); // The ORIGINAL error object, not a re-spelling of it, so a log reader // gets the stack too. diff --git a/packages/metadata-protocol/src/protocol.ts b/packages/metadata-protocol/src/protocol.ts index 29aa4651b6d..853dead5d97 100644 --- a/packages/metadata-protocol/src/protocol.ts +++ b/packages/metadata-protocol/src/protocol.ts @@ -2565,7 +2565,7 @@ function clientFacingRowFailureText(err: unknown, fallback: string): string { if (typeof declared === 'string' && declared.length > 0) return declared; } console.warn( - '[Protocol] Withheld a caught error\'s text from a batch row (#8502): the producer declared no ' + '[Protocol] Withheld a caught error\'s text from a batch row: the producer declared no ' + 'client-facing refusal (no 4xx status or statusCode, and not the VALIDATION_FAILED shape), so its ' + 'sentence must not be quoted back on response data. The row says: ' + `"${fallback}" — cause (withheld from the response):`, @@ -9604,7 +9604,7 @@ export class ObjectStackProtocolImplementation implements warnedNoRegistryForDataGate = true; console.warn( '[Protocol] engine exposes no schema registry — the data-plane object-existence ' - + 'gate (#3770) is INACTIVE for this process; unregistered object names reach the ' + + 'gate is INACTIVE for this process; unregistered object names reach the ' + 'driver as raw table names.', ); } @@ -18448,7 +18448,7 @@ export class ObjectStackProtocolImplementation implements console.warn( `[Protocol] publishPackageDrafts: this overlay repository declares no 'get', so the batch's own ` + `pending drafts cannot be read (first reached at ${type}/${name}). Author-time validation falls ` - + `back to the LIVE declarations only — the pre-#10377 closure — so a draft that references a ` + + `back to the LIVE declarations only, without this batch's own drafts in the closure, so a draft that references a ` + `sibling drafted in the SAME batch may be refused as unresolved. Nothing is published unchecked: ` + `the gate still runs, with strictly less resolution context.`, ); @@ -22858,11 +22858,11 @@ export class ObjectStackProtocolImplementation implements `A 'flow' listed here will NOT bind its triggers in this process (the kernel:ready binder ` + `reads flows env-wide) — it fired until the last restart and stops now. ` + (reportedUndeclared - ? `Types marked [plugin-registered] have no metadata-type registry entry, so the #6190 ` + ? `Types marked [plugin-registered] have no metadata-type registry entry, so the declared-types-only ` + `org-scope write refusal does NOT cover them: rows of those types can still be written ` + `org-scoped, and will be listed here again after every restart until the author stops. ` : '') + - `Re-save the item env-wide (no active organization), or delete the row. See #6190 / #6992 / ADR-0005.`, + `Re-save the item env-wide (no active organization), or delete the row. See ADR-0005.`, ); } catch { // Diagnostics never break boot — see the TSDoc. Deliberately not diff --git a/packages/metadata-protocol/src/sys-metadata-repository.ts b/packages/metadata-protocol/src/sys-metadata-repository.ts index 4e73bfac7a7..143b63ab82d 100644 --- a/packages/metadata-protocol/src/sys-metadata-repository.ts +++ b/packages/metadata-protocol/src/sys-metadata-repository.ts @@ -2027,7 +2027,7 @@ export class SysMetadataRepository implements MetadataRepository { `[SysMetadataRepository] Could not read \`${this.historyTable}\` to determine the next ` + `\`${counter}\` (${subject}) — the metadata write is being ABORTED and the enclosing ` + `transaction rolled back, so nothing is committed and the caller sees the failure. ` + - `Before #4867 this path answered \`${counter} = 1\` instead: against a table that ` + + `This path used to answer \`${counter} = 1\` instead, taking a failed read for an empty table: against a table that ` + `already has rows that number COLLIDES with an existing row, while the insert SUCCEEDS ` + `and not one line is logged — leaving version ordering untrustworthy and rollback ` + `targets ambiguous (a rollback can then resolve to a different record's same-numbered ` + diff --git a/scripts/doc-authoring-prose-id.baseline.json b/scripts/doc-authoring-prose-id.baseline.json index 5dec3e62411..ca03e638412 100644 --- a/scripts/doc-authoring-prose-id.baseline.json +++ b/scripts/doc-authoring-prose-id.baseline.json @@ -300,38 +300,6 @@ "packages/metadata-protocol/src/migrations/live-mysql-database.testkit.ts": { "#10382": 1 }, - "packages/metadata-protocol/src/migrations/overlay-index.ts": { - "#6418": 3, - "#8725": 1 - }, - "packages/metadata-protocol/src/migrations/seed-tenancy-backfill.ts": { - "#8686": 9, - "#9261": 1, - "#9451": 4 - }, - "packages/metadata-protocol/src/migrations/sys-setting-identity-index.ts": { - "#8629": 4 - }, - "packages/metadata-protocol/src/migrations/view-definition-active-index.ts": { - "#5839": 3, - "#6417": 3, - "#8725": 1 - }, - "packages/metadata-protocol/src/plugin.ts": { - "#5839": 1, - "#8629": 1, - "#8686": 1 - }, - "packages/metadata-protocol/src/protocol.ts": { - "#10377": 1, - "#3770": 1, - "#6190": 2, - "#6992": 1, - "#8502": 1 - }, - "packages/metadata-protocol/src/sys-metadata-repository.ts": { - "#4867": 1 - }, "packages/metadata/src/endpoint-matcher.ts": { "#5040": 1 }, From 442473234551d7cb909e2a927d1adc446a9b4e35 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 09:06:50 +0000 Subject: [PATCH 3/3] fix(metadata-protocol): the src-shipped live-MySQL testkit error drops its tracker number; changeset (stage 2) The last metadata-protocol row leaves the prose-id ledger: the testkit's isolation error already says what it guards, so only the citation goes. The ledger is recomputed with --census-ledger and holds no metadata-protocol row. Changeset: @objectstack/metadata-protocol patch. Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude --- ...ocol-runtime-strings-state-the-decision.md | 30 +++++++++++++++++++ .../migrations/live-mysql-database.testkit.ts | 2 +- scripts/doc-authoring-prose-id.baseline.json | 3 -- 3 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 .changeset/20513-metadata-protocol-runtime-strings-state-the-decision.md diff --git a/.changeset/20513-metadata-protocol-runtime-strings-state-the-decision.md b/.changeset/20513-metadata-protocol-runtime-strings-state-the-decision.md new file mode 100644 index 00000000000..663166eafc4 --- /dev/null +++ b/.changeset/20513-metadata-protocol-runtime-strings-state-the-decision.md @@ -0,0 +1,30 @@ +--- +'@objectstack/metadata-protocol': patch +--- + +metadata-protocol refusals, hints and log lines no longer cite tracker numbers; each states the reason in words + +Clause-②: no + +Many messages the metadata protocol shows to authors, administrators and operators ended with an +issue-tracker number where the reason belonged. The number goes, and where the sentence did not +already say what was decided, it now does: + +- Refusals: `insertManyData` without an engine `insertMany` now names what that method is (the + partial-success batch insert, so a bad row neither fails the whole batch nor runs the good rows' + `beforeInsert` hooks twice); the unknown-metadata-type refusal says a plugin cannot declare a type + because `additionalTypes` was retired, having never been read; the stored non-canonical type + refusals on publish and revert say the `/meta` URL door now folds a type to its canonical spelling + before it writes, so such a row predates that. +- The schedule-flow `organization_id` hint says why the author's value is the only source: the engine + fills only an organization the run resolved, and a schedule resolves none. +- Log lines: the three `kernel:ready` "migration skipped" warnings now say what the migration that did + not run would have ensured; the history-counter abort says the old path took a failed read for an + empty table; the publish-closure degrade says the batch's own drafts are left out of the closure; + the cold-boot org-scoped audit calls the write refusal it points at declared-types-only. The + overlay, `sys_view_definition` and `sys_setting` index messages, the seed/API tenancy repair and its + receipt, the batch-row withhold and the object-existence gate's no-registry warning lose only the + citation, because their sentences already said it. +- The live-MySQL testkit's isolation error loses its citation. + +Text only: no error code, field name, status or behaviour changes. diff --git a/packages/metadata-protocol/src/migrations/live-mysql-database.testkit.ts b/packages/metadata-protocol/src/migrations/live-mysql-database.testkit.ts index fc0c887389f..177767e9635 100644 --- a/packages/metadata-protocol/src/migrations/live-mysql-database.testkit.ts +++ b/packages/metadata-protocol/src/migrations/live-mysql-database.testkit.ts @@ -216,7 +216,7 @@ export function currentLiveMysqlDatabase(): string { const testPath = expect.getState().testPath; if (!testPath) { throw new Error( - 'live-mysql isolation (#10382): vitest reported no testPath, so this live connection ' + + 'live-mysql isolation: vitest reported no testPath, so this live connection ' + 'cannot be given a per-file database and would fall back to sharing one with every ' + 'other live file in this package — including its `drop database` in afterAll. Call ' + 'currentLiveMysqlDatabase() from a test file.', diff --git a/scripts/doc-authoring-prose-id.baseline.json b/scripts/doc-authoring-prose-id.baseline.json index ca03e638412..12311965e9d 100644 --- a/scripts/doc-authoring-prose-id.baseline.json +++ b/scripts/doc-authoring-prose-id.baseline.json @@ -297,9 +297,6 @@ "#6603": 1, "#7020": 1 }, - "packages/metadata-protocol/src/migrations/live-mysql-database.testkit.ts": { - "#10382": 1 - }, "packages/metadata/src/endpoint-matcher.ts": { "#5040": 1 },