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
18 changes: 18 additions & 0 deletions .changeset/24651-boot-refusal-fresh-installs-only.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'@objectstack/runtime': patch
---

fix(runtime): the organization-ownership boot refusals say what 18.0 supports for a v17 database and name no step of `os migrate organization-ownership`, which 18.0 refuses on every database

Clause-②: no (refusal text only: the refusal code `ORGANIZATION_OWNERSHIP_MIGRATION_REQUIRED`, the refused tables and the boot outcome do not move)

In 18.0, `os migrate organization-ownership` refuses plan and apply on every database (`NOT_IMPLEMENTED`): 18.0 supports fresh installs only. Three branches of the v18 boot refusal still told the operator to run that command's plan and its `--apply --plan-file PLAN --backup-taken`, so following them led to a second refusal.

Each branch now closes on the sentence `os migrate apply`'s drop gate and `os dev` already use: "18.0 supports fresh installs only and does not upgrade a v17 database: keep a v17 database on a 17.x release until the 18.x release that carries the upgrade path, and upgrade it there."

- **A table still in the v17 shape** (an object that declares no organization column, over a table that still carries `organization_id`). This branch already said that 18.0 does not upgrade a v17 database. It no longer follows that with the command's plan and apply steps.
- **No table left in the v17 shape, and the ceremony's marker is not verified.** The refusal no longer prescribes a re-plan and a re-apply, which 18.0 cannot run.
- **No table left in the v17 shape, and the marker could not be read.** The refusal now says to fix the read and start again: a database no ceremony ran on boots once the read answers. It no longer prescribes a re-plan and a re-apply.
- **The column read itself failed.** The refusal still says to fix the read. It no longer offers the command as the alternative.

A development boot (`OrganizationOwnershipBootGuardPlugin({ dev: true })`, as `os serve --dev` composes it) says "a v17 development database" in place of "a v17 database". Before, it named the command with `--dev`.
14 changes: 10 additions & 4 deletions docs/qa/platform-checklist/areas/platform-core.json
Original file line number Diff line number Diff line change
Expand Up @@ -3042,10 +3042,10 @@
},
{
"id": "platform-core.v18-boot-refusals",
"title": "A v18 server refuses to boot rather than serve wrongly: against a 17.x-shaped database (a physical organization_id on a non-tenant table, no verified ADR-0131 ceremony marker) it stops before listening with ORGANIZATION_OWNERSHIP_MIGRATION_REQUIRED naming the tables and the command, and with security on but no identity objects it stops with AuthzIdentityObjectsMissingError",
"title": "A v18 server refuses to boot rather than serve wrongly: against a 17.x-shaped database (a physical organization_id on a non-tenant table, no verified ADR-0131 ceremony marker) it stops before listening with ORGANIZATION_OWNERSHIP_MIGRATION_REQUIRED, naming the tables and saying that 18.0 supports fresh installs only, and with security on but no identity objects it stops with AuthzIdentityObjectsMissingError",
"since": "v18",
"status": "active",
"revision": 3,
"revision": 4,
"priority": "P0",
"surface": "cli",
"personas": [
Expand All @@ -3070,9 +3070,9 @@
"evidence": "the vitest summary line per pin file, with the package name and the subject sha"
},
{
"clause": "the v18 serve against the v17 DB exits 1 before kernel:listening with the organization-ownership refusal, which names the tables and `os migrate organization-ownership`. ORGANIZATION_OWNERSHIP_MIGRATION_REQUIRED is that refusal's `code` (packages/runtime/src/organization-ownership-boot-guard.ts#OrganizationOwnershipBootRefusal) and the boot never prints it — `os serve` prints the refusal's message only — so the code is the pins' evidence (acceptance[0]), not this log's",
"clause": "the v18 serve against the v17 DB exits 1 before kernel:listening with the organization-ownership refusal, which names the tables and ends with the 18.0 sentence (\"18.0 supports fresh installs only and does not upgrade a v17 database: keep a v17 database on a 17.x release until the 18.x release that carries the upgrade path, and upgrade it there.\"), and names neither `os migrate organization-ownership` nor any step of it (no `--out PLAN`, no `--apply --plan-file`): 18.0 refuses that command on every database (NOT_IMPLEMENTED). ORGANIZATION_OWNERSHIP_MIGRATION_REQUIRED is that refusal's `code` (packages/runtime/src/organization-ownership-boot-guard.ts#OrganizationOwnershipBootRefusal) and the boot never prints it — `os serve` prints the refusal's message only — so the code is the pins' evidence (acceptance[0]), not this log's",
"oracle": "log",
"verify": "boot output + exit code + no listening line: the printed refusal names each table still carrying organization_id and the `os migrate organization-ownership` command. ⛔ Do not grep the log for the code literal — it is not printed (packages/cli/src/commands/serve.ts prints the refusal's message)",
"verify": "boot output + exit code + no listening line: the printed refusal names each table still carrying organization_id and ends with \"18.0 supports fresh installs only and does not upgrade a v17 database: keep a v17 database on a 17.x release until the 18.x release that carries the upgrade path, and upgrade it there.\"; the refusal excerpt has 0 hits each for `--out PLAN`, `--plan-file` and `os migrate organization-ownership`. ⛔ Do not grep the log for the code literal — it is not printed (packages/cli/src/commands/serve.ts prints the refusal's message)",
"evidence": "the refusal excerpt"
},
{
Expand Down Expand Up @@ -3123,6 +3123,12 @@
"date": "2026-10-19",
"change": "steps[1], acceptance[1] (clause, verify) and acceptance[3].verify corrected — two checklist-accuracy findings of the 18.0 QA sweep (QA run #24557, platform-core.v18-boot-refusals; wave anchor #24579 §3). steps[1] booted `os dev` with `services: {auth: false}`, which is not a stack key (packages/spec/src/stack.zod.ts), so the run recorded acceptance[3] blocked(fixture); a stock boot composes the security plugin only beside AuthPlugin (packages/core/src/stack-auth.ts resolvePlatformAuthComposition), so the step now names a scratch config that mounts SecurityPlugin itself with the auth tier off, marked as not yet replayed live, and acceptance[3] names its pin as the fallback. acceptance[1] said the boot exits with ORGANIZATION_OWNERSHIP_MIGRATION_REQUIRED on a log oracle; that literal is only the refusal error's code (OrganizationOwnershipBootRefusal) and serve.ts prints the message alone, so the log oracle now reads the named tables and command and the code is left to the pins. acceptance[2] and steps[0] (#24592) are unchanged",
"ref": "#24632"
},
{
"revision": 4,
"date": "2026-10-19",
"change": "title and acceptance[1] (clause, verify) re-worded for 18.0's boot refusal text (card #24651). In 18.0 `os migrate organization-ownership` refuses plan and apply on every database (NOT_IMPLEMENTED: 18.0 supports fresh installs only), so the boot refusal no longer names that command or its `--out PLAN` / `--apply --plan-file PLAN --backup-taken` steps. Every branch now ends with \"18.0 supports fresh installs only and does not upgrade a v17 database: keep a v17 database on a 17.x release until the 18.x release that carries the upgrade path, and upgrade it there.\" (packages/runtime/src/organization-ownership-boot-guard.ts, freshInstallsOnly; `os serve --dev` says \"a v17 development database\"). The clause asserted that the refusal names the command, so a run against the new text would have recorded FAIL on a correct product. It now asserts the tables, that sentence, and the absence of the command and its steps. The ⛔ on the code literal is kept. acceptance[0], acceptance[2], acceptance[3] and both steps are unchanged",
"ref": "#24682"
}
]
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
// that declare no organization column) still carrying a physical
// `organization_id`:
//
// 1. the v18 boot is refused, naming the command — and the refused boot has
// 1. the v18 boot is refused, saying what 18.0 supports for a v17 database and
// naming no step of the command 18.0 refuses — and the refused boot has
// ALREADY added the column the ceremony waits for: the guard judges in its
// own `start()`, after the engine's `start()`, which is where the schema
// sync runs (and before every plugin that writes rows at start, so the
Expand Down Expand Up @@ -109,7 +110,7 @@ describe('[ADR-0131 D10 item 5] a 17.x database upgrades through the boot refusa
rmSync(dir, { recursive: true, force: true });
});

it('1. a v17-shape database is refused, naming the command', async () => {
it('1. a v17-shape database is refused, saying what 18.0 supports and naming no step of the refused command', async () => {
const seed = new DatabaseSync(file);
seed.exec(`
CREATE TABLE sys_user_permission_set (id TEXT PRIMARY KEY, user_id TEXT, permission_set_id TEXT, organization_id TEXT);
Expand All @@ -121,7 +122,8 @@ describe('[ADR-0131 D10 item 5] a 17.x database upgrades through the boot refusa

refusal = refusalIn(await boot(file));
expect(refusal, 'the boot was refused for the v17 shape').toBeDefined();
expect(String((refusal as Error).message)).toContain('os migrate organization-ownership');
expect(String((refusal as Error).message)).toContain('18.0 supports fresh installs only and does not upgrade a v17 database');
expect(String((refusal as Error).message)).not.toMatch(/os migrate organization-ownership|--plan-file|--out PLAN|--backup-taken/);
expect((refusal as { tables?: string[] }).tables).toEqual(expect.arrayContaining(['sys_metadata', 'sys_migration_journal']));
}, 240_000);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
* 3. a fresh database boots;
* 4. a database the ceremony ran on to the end (no column left) whose marker
* is NOT verified — its post-check found rows still without their
* organization — is refused, naming the re-run;
* organization — is refused, saying what 18.0 supports (it cannot re-run
* the ceremony);
* 5. ⭐ a refused boot writes nothing: the plugins that write rows in their
* own `start()` — `AppPlugin` (the inline seed) and plugin-auth's
* identity-objects plugin (the Default Organization, `AuthPlugin`'s twin
Expand Down Expand Up @@ -181,7 +182,7 @@ describe('the v18 boot refusal on a real kernel (ADR-0131 D10 item 5)', () => {
await expect(engine.findOne('sys_migration', { where: { id: MARKER }, context: { isSystem: true } })).resolves.toBeNull();
});

it('⭐ a ceremony that ran every step and did not verify (no v17 column left, marker unverified) is refused, naming the re-run', async () => {
it('⭐ a ceremony that ran every step and did not verify (no v17 column left, marker unverified) is refused, saying what 18.0 supports', async () => {
const file = databaseFile();
const db = new DatabaseSync(file);
writeMarker(db, null, 602);
Expand All @@ -191,7 +192,10 @@ describe('the v18 boot refusal on a real kernel (ADR-0131 D10 item 5)', () => {
expect(refusal?.code, String(error)).toBe(ORGANIZATION_OWNERSHIP_BOOT_REFUSAL_CODE);
expect(refusal?.tables).toEqual([]);
expect(refusal?.message).toMatch(/^The organization-ownership ceremony has not finished on this database: its marker/);
expect(refusal?.message).toContain("os migrate organization-ownership --apply --plan-file PLAN --backup-taken");
// 18.0 cannot re-run the ceremony (its `os migrate organization-ownership` refuses on every
// database), so the refusal names no step of it and says what 18.0 supports instead.
expect(refusal?.message).toContain('18.0 supports fresh installs only and does not upgrade a v17 database');
expect(refusal?.message).not.toMatch(/os migrate organization-ownership|--plan-file|--out PLAN|--backup-taken/);
expect(kernel.getState()).not.toBe('running');
});

Expand Down
Loading
Loading