Repository navigation
Commit 4b114ed
fix(metadata-core, metadata-protocol): two refusals say what 18.0 supports, not the ownership command 18.0 refuses (#24726)
Fixes #24710
Clause-②: no (text only: no refusal code, outcome, accept set or served
shape moves)
## What changed
In 18.0, `os migrate organization-ownership` refuses plan and apply on
every database. Two refusals still told the operator to run it. Each now
closes on PR #24635's release 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." The text
is only made true for the readers who can actually meet each refusal on
18.0 (below).
| refusal | old closing text | new closing text |
|:--|:--|:--|
| `LEGACY_ORGANIZATION_ROWS_UNREADABLE`
(`packages/metadata-core/src/legacy-organization-row.ts`) | "Nothing was
listed or written. Run `os migrate organization-ownership` on this
database first." | "Nothing was listed or written. A fresh 18.0 install
holds no legacy organization rows: there, the cause is the one named
above, and the fix is in the SQL driver or the caller that met it. A
database that does hold them is a v17 one. 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." |
| seed refusal for a natural key held by a row with no organization
(`packages/metadata-protocol/src/seed-loader.ts`) | "Attribute the
unowned rows first (`os migrate organization-ownership`, then the same
command with --apply), then re-run this seed: the row is this
organization's own by then, and the upsert matches it." | "A row with no
organization predates organization stamping, so it is a v17 database's.
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." |
Unchanged: both codes, "Nothing was listed or written.", the seed
refusal's opening sentences (what was refused, and why), its `errored`
count, and the dropped record. There is no new export, error code or
constructor parameter.
## Who meets each refusal on 18.0 (read before the edit)
**`readLegacyOrganizationRows`.** It throws in seven places. Each is
listed with whether a fresh 18.0 install reaches it.
| throw (`detail`) | shape | fresh 18.0 install? |
|:--|:--|:--|
| not a sys_metadata-family table | caller defect | yes, if a caller
passes a wrong table (every in-tree caller passes a family constant) |
| the SQL driver names no dialect this read can compile a column list
for | driver | **yes**: checked before the column question |
| the SQL driver has no raw-SQL seam | driver | **yes** |
| the driver's column list is not a list | driver | **yes** |
| the raw-SQL seam returned no result set (column read) | driver |
**yes** |
| its column list came back empty for a table that exists |
driver/catalog | **yes** |
| the raw-SQL seam returned no result set (legacy row read) | database:
the column is on disk | no: only a database that still carries
`organization_id` |
The callers reach it on a fresh install. `protocol.ts`'s
`findEnvironmentRows` / `findEnvironmentRow` run it on every family
read. Hydration in `ObjectQLPlugin.start()` asks it even when no row
came back ("Asked even when no row came back"), so `os serve` meets it
on every SQL boot. `sys-metadata-repository.ts`
`legacyOrganizationRowIds` runs it on every addressed write. `cli`'s
`security-catalog-overlays.ts` runs it on the `os migrate` step. The old
remedy was already wrong for the five driver-shaped throws: the ceremony
itself refuses an unresolved dialect (`dialectOfDriver`'s doc). So the
new text does not tell a fresh-install operator to hold a v17 database
on 17.x. It says a fresh install has no legacy rows, so there the named
cause is the fix, and it gives the release sentence to the database that
does hold them. This is one message for both readers, and the
constructor is unchanged. Following the detail per throw would need a
new parameter: the shared `SEAM_NO_ANSWER_DETAIL` is passed by both a
driver-shaped throw and the database-shaped one.
**The seed refusal.** It fires only on an organization-scoped
`upsert`/`ignore` load (`config.organizationId`) whose natural key is
held by an `organization_id IS NULL` row. Under ADR-0131 D9, as
`Engine.resolveSystemInsertOrganization` implements it on 18.0, a system
insert on a tenant-column object is stamped with the Default
Organization under `single`, or refused. 18.0 refuses a walled posture
on the operator's boot path. The single-posture inline seed is a system
insert, so it is stamped too. The one 18.0 shape that leaves
`organization_id` NULL is the `no-organization-object` composition,
which registers no `sys_organization`. That composition has no
organization insert, so no per-organization replay runs. The marketplace
scoped install and the reseed pass an organization only when walled. So
no in-tree producer runs a scoped load there. The reader who meets this
refusal holds rows that predate stamping: a v17 database.
`seed-loader-null-organization-holder-real-driver.integration.test.ts`
measures the same population: "a single-posture 17.x install, every
business row `organization_id` NULL". The release sentence is true for
that reader. The old remedy's bare `--apply` was refused even before
18.0.
Comments beside the two texts only describe the ceremony (the module
header, `dialectOfDriver`'s doc, the `#24301` note in `seed-loader.ts`).
None of them prescribes it as an 18.0 remedy, so none moved.
## Pins
- `packages/metadata-core/src/legacy-organization-row.test.ts`: a new
`it.each` in "fails closed" covers a driver-shaped throw (no raw seam)
and the database-shaped one (column on disk, legacy select answered
nothing). It asserts `code === 'LEGACY_ORGANIZATION_ROWS_UNREADABLE'`,
"Nothing was listed or written.", the fresh-install sentence, the
message ending on the release sentence, and `os migrate
organization-ownership` absent.
-
`packages/metadata-protocol/src/seed-loader-null-organization-holder.test.ts`:
the existing upsert/ignore refusal test, which already asserts
`totalErrored === 1` and the dropped record. It now also asserts that
the opening sentences stay, that the message ends on the new remedy, and
that `os migrate organization-ownership` and `--apply` are absent.
- **A pin moved outside the claim's file surface:**
`packages/runtime/src/seed-loader-null-organization-holder-real-driver.integration.test.ts`
asserted `toContain('os migrate organization-ownership')`. It now
asserts the release sentence and the command's absence. Without that
change, `main` would go red.
Pin sweep (`LEGACY_ORGANIZATION_ROWS_UNREADABLE`, "on this database
first", "Attribute the unowned rows first", "then the same command with
--apply", plus the messages' other sentences) over `packages`, `qa`,
`docs/qa/platform-checklist` and `content/docs`. It found the runtime
pin above and no other pin of either text.
`protocol.stored-migration.test.ts`'s `/legacy organization rows/` reads
the unchanged first sentence. The hits in the generated
`content/docs/references/api/*.mdx` ledgers and in released
`CHANGELOG.md` entries name only the code and do not move.
## Verification
- Build: the `@objectstack/runtime^...` closure plus
`@objectstack/metadata-protocol`, VERDICT command-exit 0. After merging
`origin/main` (`54159fd74`, which brought PR #24696's `seed-loader.ts`
change; the merge was clean), full `pnpm build --concurrency=2` ran:
76/76 tasks, VERDICT command-exit 0.
- Typecheck on the merged head: `@objectstack/metadata-core` exit 0,
`@objectstack/metadata-protocol` exit 0.
- Full suites on the merged head: `@objectstack/metadata-core` 18 files
/ 776 tests passed. `@objectstack/metadata-protocol` 250 files passed, 3
skipped / 20802 tests passed, 19 skipped.
- `seed-loader-null-organization-holder-real-driver.integration.test.ts`
(`@objectstack/runtime`): 6/6 passed.
- `pnpm --filter @objectstack/core test:repo`: 6 files / 82 tests
passed.
- `@objectstack/cli`: no existing pin reads either text, and none was
added, so its suite is not owed.
- **Ablation:** with `scripts/ablation-replace.mjs --hold`, both texts
were reverted to the `main` spelling. The on-disk counts moved (old1=1
new1=0, old2=1 new2=0). Both packages were rebuilt, and
`ablation-dist-preflight` found the old markers in `dist/`. The
legacy-row pin went red (2 failed), the seed pin went red (2 failed),
and the runtime pin went red (1 failed). Restored with `git checkout
HEAD --`: `git diff HEAD` was empty and `git status --porcelain` was
clean. Blobs equal `HEAD` (`e19b19399a05` / `67a3560d3c2c`). Rebuilt,
`--absent` preflight passed for both packages, and the runtime pin was
green again.
- Dispatch gates (`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`, no paths, run on the final head
`396fcb9c0`): every one of the 65 derived lines exited 0.
`check:dual-build-cjs-loads` first exited 3 (PREREQUISITE NOT MET, no
`dist/`) on the pre-merge head. It was built for and re-run, and exited
0. `--ran` with `COMMAND :: exit CODE` records reported: "65 derived, 65
run, 0 NOT-MEASURED, 0 UNRUN" (a derived zero). `check-nul-bytes` exited
0.
## Acceptance notes
- The seed text says such a row "is a v17 database's". On 18.0 the only
NULL-organization producer is the `no-organization-object` composition,
and no in-tree caller runs a scoped load there. An embedder that calls
`SeedLoaderService.load` with an `organizationId` against such a
composition would read the v17 sentence for a row its own composition
wrote. That reader is speculative (no producer), so it is not filed.
- Per the card's order note, PR #24681 (18.x steps 5–6) reopens the
ceremony and restates these two texts when it lands.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01KQiKEtTxcLqE3TeLATbWFb)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 7e7e605 commit 4b114ed
6 files changed
Lines changed: 57 additions & 6 deletions
File tree
- .changeset
- packages
- metadata-core/src
- metadata-protocol/src
- runtime/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
115 | 115 | | |
116 | 116 | | |
117 | 117 | | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
118 | 140 | | |
119 | 141 | | |
120 | 142 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
75 | 75 | | |
76 | 76 | | |
77 | 77 | | |
78 | | - | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
79 | 82 | | |
80 | 83 | | |
81 | 84 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
135 | 135 | | |
136 | 136 | | |
137 | 137 | | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
138 | 147 | | |
139 | 148 | | |
140 | 149 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1848 | 1848 | | |
1849 | 1849 | | |
1850 | 1850 | | |
1851 | | - | |
1852 | | - | |
1853 | | - | |
| 1851 | + | |
| 1852 | + | |
| 1853 | + | |
| 1854 | + | |
1854 | 1855 | | |
1855 | 1856 | | |
1856 | 1857 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
209 | 209 | | |
210 | 210 | | |
211 | 211 | | |
212 | | - | |
| 212 | + | |
| 213 | + | |
213 | 214 | | |
214 | 215 | | |
215 | | - | |
| 216 | + | |
| 217 | + | |
216 | 218 | | |
217 | 219 | | |
218 | 220 | | |
| |||
0 commit comments