From e18310f6d1f508ce784b4efcedaa6b1b01bb222f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 6 Sep 2026 04:19:57 +0000 Subject: [PATCH 1/2] fix(platform-objects): attestFreshDatastore looks the remedy up, never defaults it (#16067) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `attestFreshDatastore`'s contradiction warning built its `os migrate` sentence from a two-way branch: the file-references id got `files-to-references` and EVERY other id got `value-shapes` by default. `CREATION_ATTESTED_MIGRATION_IDS` has had three members since the ADR-0030 cut-over id joined it, and for that third id the default is a wrong prescription — `os migrate value-shapes --apply` neither attests nor clears it, and there is no `os migrate notification-event` to send an operator to at all (measured: no such sub-command exists under `packages/cli/src/commands/migrate/`). Replaced with an explicit id -> remedy register that is TOTAL over the ids a value-shape tally can contradict, and the loop now asks it instead of falling into an arm: an id with no value-shape contract is never-contradictable by this evidence and is attested on the birth observation. A new member therefore inherits NO remedy — adding a third arm that happened to be right today would only have moved the same defect onto the fourth member. A `Map` rather than an object literal: `id` arrives from a caller-supplied array and an object literal would answer `'toString'` with a function. Pins the default, which is where the defect lived: a contradiction fed for a non-ADR-0104 id asserts the warning does not name `value-shapes`, and a case total over `CREATION_ATTESTED_MIGRATION_IDS` asserts no id is ever handed another migration's command. A pin over only the two known ids passed on the broken code. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ --- .../src/system/migration-flag.test.ts | 86 +++++++++++++++++++ .../src/system/migration-flag.ts | 46 +++++++++- 2 files changed, 130 insertions(+), 2 deletions(-) diff --git a/packages/platform-objects/src/system/migration-flag.test.ts b/packages/platform-objects/src/system/migration-flag.test.ts index 7cd223c8a4..036625e059 100644 --- a/packages/platform-objects/src/system/migration-flag.test.ts +++ b/packages/platform-objects/src/system/migration-flag.test.ts @@ -11,7 +11,9 @@ import { describe, it, expect, vi } from 'vitest'; import { assertEngineUpdateDispatch } from '@objectstack/metadata-core'; import { CREATION_ATTESTED_MIGRATION_IDS, + FILE_REFERENCES_MIGRATION_ID, NOTIFICATION_EVENT_MIGRATION_ID, + VALUE_SHAPES_MIGRATION_ID, } from '@objectstack/spec/system'; import { readDataMigrationFlag, @@ -301,5 +303,89 @@ describe('fresh-datastore attestation (ADR-0104, 2026-07-30 addendum)', () => { expect(await attestFreshDatastore(engine)).toEqual([...CREATION_ATTESTED_MIGRATION_IDS]); }); + + /** + * #16067. The remedy sentence used to be a two-way branch whose `else` + * gave `os migrate value-shapes` to every id that was not the file one. + * These pins test that DEFAULT, which is where the defect lived — a pin + * that only exercised the two ADR-0104 ids passed on the broken code, and + * still would. + * + * ⚠️ Reachability, measured rather than assumed: the SHIPPED engine cannot + * key this tally with a third id (`ObjectQL.noteAdmittedValueShapeViolation` + * derives the key from a closed `'media' | 'value-shape'` union, and it is + * the only writer of the map). But `valueShapeViolationsAdmitted` is an + * OPTIONAL, duck-typed member of {@link MigrationFlagEngine} returning an + * open `Record` — any other engine satisfies it, as the doubles + * in this very file do. So the default was one non-ObjectQL producer away + * from being read by an operator, and "unreachable" was never a property + * of the seam. + */ + describe('the remedy is looked up, never defaulted (#16067)', () => { + /** The mapping the operator-facing sentence must obey, restated here so + * a change to the production map has to be made twice, on purpose. */ + const REMEDY_BY_ID: Record = { + [FILE_REFERENCES_MIGRATION_ID]: 'files-to-references', + [VALUE_SHAPES_MIGRATION_ID]: 'value-shapes', + }; + + it('an id with NO value-shape contract is never-contradictable, and is told to run nothing', async () => { + const engine = fakeEngine(); + engine.valueShapeViolationsAdmitted = () => ({ + [NOTIFICATION_EVENT_MIGRATION_ID]: VIOLATED, + }); + const logger = { info: vi.fn(), warn: vi.fn() }; + + const attested = await attestFreshDatastore(engine, { logger }); + + // A value-shape tally is evidence about value shapes. The ADR-0030 + // cut-over's fact — no legacy per-user inbox row here — is not one, so + // this counterexample disproves nothing about it and the birth + // observation still settles it. + expect(attested).toContain(NOTIFICATION_EVENT_MIGRATION_ID); + expect(await isDataMigrationVerified(engine, NOTIFICATION_EVENT_MIGRATION_ID)).toBe(true); + + const warnings = logger.warn.mock.calls.map((c) => String(c[0] ?? '')).join('\n'); + // ⭐ The card's pin: the operator is NOT sent to `os migrate + // value-shapes`, which neither attests nor clears this id. There is no + // `os migrate notification-event` to send them to either — that + // cut-over is an operator call with no self-check — so the correct + // sentence here is no sentence. + expect(warnings).not.toContain('value-shapes'); + expect(warnings).toBe(''); + }); + + /** + * Total over the array, so a FOURTH member is judged the moment it is + * added instead of inheriting whatever the last branch happened to say. + */ + it.each([...CREATION_ATTESTED_MIGRATION_IDS])( + 'a contradiction for %s is never handed another migration\'s command', + async (id) => { + const engine = fakeEngine(); + engine.valueShapeViolationsAdmitted = () => ({ [id]: VIOLATED }); + const logger = { info: vi.fn(), warn: vi.fn() }; + + const attested = await attestFreshDatastore(engine, { logger }); + const warnings = logger.warn.mock.calls.map((c) => String(c[0] ?? '')).join('\n'); + const own = REMEDY_BY_ID[id]; + + if (own === undefined) { + expect(attested).toContain(id); + expect(warnings).toBe(''); + } else { + expect(attested).not.toContain(id); + expect(warnings).toContain(`os migrate ${own} --apply`); + } + + // The half a bigger ternary would still get wrong: no id may ever be + // prescribed a command that belongs to a different id. + for (const [other, command] of Object.entries(REMEDY_BY_ID)) { + if (other === id) continue; + expect(warnings).not.toContain(`os migrate ${command}`); + } + }, + ); + }); }); }); diff --git a/packages/platform-objects/src/system/migration-flag.ts b/packages/platform-objects/src/system/migration-flag.ts index a93da9b803..ea21430213 100644 --- a/packages/platform-objects/src/system/migration-flag.ts +++ b/packages/platform-objects/src/system/migration-flag.ts @@ -6,6 +6,7 @@ import { DATA_MIGRATION_FLAG_OBJECT, FILE_REFERENCES_MIGRATION_ID, isDataMigrationFlagVerified, + VALUE_SHAPES_MIGRATION_ID, type DataMigrationFlag, } from '@objectstack/spec/system'; @@ -205,6 +206,42 @@ export async function recordDataMigrationRun( return flag; } +/** + * The `os migrate` sub-command that re-earns each id a boot's own admitted + * value can CONTRADICT — and, by having no row for anything else, the register + * of which ids that is. + * + * ## Why a map and not a branch + * + * The counterexample this reads comes from + * {@link MigrationFlagEngine.valueShapeViolationsAdmitted}, whose whole subject + * is ADR-0104 value shapes. So membership here is one question — *does this id + * stand for a value-shape contract a stored value can disprove?* — and the + * answer for every id that has one is a DIFFERENT command. A two-way branch + * answered both at once: it read the file id and gave every other id + * `value-shapes` by default. That default was silently wrong the moment a third + * id joined {@link CREATION_ATTESTED_MIGRATION_IDS} — `adr-0030-notification-event` + * would have been told to run `os migrate value-shapes --apply`, a command that + * does not attest it, does not clear it, and has nothing to do with it (there is + * no `os migrate notification-event` at all; that cut-over is an operator call + * with no self-check, ruled on `NOTIFICATION_EVENT_MIGRATION_ID`'s docblock). + * + * ⛔ So a new member must NOT inherit a remedy. An id absent from this map is + * never-contradictable *by this evidence* — a value-shape tally says nothing + * about a fact that is not about value shapes — and it is attested on the birth + * observation like any other. Adding a third arm that happens to be right today + * would only move the same defect onto the fourth member; adding a ROW is a + * deliberate act, and its absence prescribes nothing rather than prescribing + * the wrong thing. + * + * A `Map`, not an object literal: `id` reaches this from a caller-supplied + * array, and an object would answer `'toString'` with a function. + */ +const VALUE_SHAPE_CONTRACT_REMEDY: ReadonlyMap = new Map([ + [FILE_REFERENCES_MIGRATION_ID, 'files-to-references'], + [VALUE_SHAPES_MIGRATION_ID, 'value-shapes'], +]); + /** Marker written into a creation-attested row's `details`, so an operator * reading a verified flag can tell evidence-by-scan from evidence-by-birth. */ export const CREATION_ATTESTATION_DETAIL = { attested: 'datastore-created-empty' } as const; @@ -291,7 +328,12 @@ export async function attestFreshDatastore( try { if (await readDataMigrationFlag(engine, id)) continue; // not ours to write // #4769 — a boot may not prove a contract it has already broken. - const contradiction = admitted[id]; + // Asked of the register, not of a default: an id with no value-shape + // contract cannot be contradicted by a value-shape tally, so it is + // attested on the birth observation instead of being handed another + // migration's remedy. See VALUE_SHAPE_CONTRACT_REMEDY. + const remedy = VALUE_SHAPE_CONTRACT_REMEDY.get(id); + const contradiction = remedy === undefined ? undefined : admitted[id]; if (contradiction && contradiction.count > 0) { const at = contradiction.first; const where = at?.object && at?.field ? `${at.object}.${at.field}` : 'a record'; @@ -301,7 +343,7 @@ export async function attestFreshDatastore( `(${where}${at?.detail ? `: ${at.detail}` : ''}). The store was created empty, but it ` + 'is no longer empty and what it now holds contradicts the claim — the gate stays ' + 'open (warn-first). Fix the data, then run `os migrate ' + - (id === FILE_REFERENCES_MIGRATION_ID ? 'files-to-references' : 'value-shapes') + + remedy + ' --apply` to close it on real evidence (ADR-0104).', ); continue; From 5eb248ff2b6edee89e47ec27e35ade10fa15d93b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 6 Sep 2026 04:39:56 +0000 Subject: [PATCH 2/2] chore(changeset): patch for the attestFreshDatastore remedy register (#16067) Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ --- .../attest-fresh-datastore-remedy-register.md | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .changeset/attest-fresh-datastore-remedy-register.md diff --git a/.changeset/attest-fresh-datastore-remedy-register.md b/.changeset/attest-fresh-datastore-remedy-register.md new file mode 100644 index 0000000000..f76fc11ee5 --- /dev/null +++ b/.changeset/attest-fresh-datastore-remedy-register.md @@ -0,0 +1,28 @@ +--- +'@objectstack/platform-objects': patch +--- + +`attestFreshDatastore` looks its `os migrate` remedy up instead of defaulting it + +When a fresh datastore's own boot has already admitted a value that contradicts a +migration's contract, that id is not attested and the operator is told what closes +the gate on real evidence. The sentence used to be built from a two-way branch: the +file-references id got `files-to-references`, and **every other id** got +`value-shapes` by default. + +`CREATION_ATTESTED_MIGRATION_IDS` has three members. For the third — +`adr-0030-notification-event` — that default is a wrong prescription: `os migrate +value-shapes --apply` neither attests nor clears it, and there is no `os migrate +notification-event` sub-command to send an operator to at all (that cut-over is an +operator call with no self-check). + +The branch is now an explicit id-to-remedy register, total over the ids a +value-shape tally can contradict. The loop asks it rather than falling into an arm, +so an id with no value-shape contract is never-contradictable by that evidence and +is attested on the birth observation as before. A new member therefore inherits no +remedy: adding a third arm that happened to be right today would only have moved the +same defect onto the fourth member. + +No behaviour changes for the two ADR-0104 ids, which is where every reachable path +runs today: the shipped engine keys its admitted-violation tally from a closed +`'media' | 'value-shape'` union, so it cannot name a third id.