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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down
6 changes: 3 additions & 3 deletions packages/metadata-protocol/src/migrations/overlay-index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' };
Expand Down Expand Up @@ -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;
Expand All @@ -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,
);
}
30 changes: 15 additions & 15 deletions packages/metadata-protocol/src/migrations/seed-tenancy-backfill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1147,15 +1147,15 @@ export async function recordSeedTenancyReceipt(
): Promise<void> {
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
// (a raw seam built by hand, a test double), so there is no ledger to miss.
// `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;
}
Expand All @@ -1164,15 +1164,15 @@ 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;
}
const flag = buildSeedTenancyReceipt(result, new Date().toISOString());
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) {
Expand All @@ -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 });
}
Expand Down Expand Up @@ -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}'), <field>) does not ` +
Expand Down Expand Up @@ -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 ` +
Expand All @@ -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 ` +
Expand All @@ -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.
Expand Down Expand Up @@ -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) },
);
Expand All @@ -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) },
Expand Down Expand Up @@ -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) },
);
Expand All @@ -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 ` +
Expand All @@ -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, ` +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down Expand Up @@ -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()}`,
Expand All @@ -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;
Expand All @@ -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,
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -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,
);
}
Loading
Loading