Skip to content

Commit f2ee5ad

Browse files
committed
Merge origin/main into claude/issue-25169-relay-rerun-failed
Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude <noreply@anthropic.com>
2 parents 3a8aecd + 6251f59 commit f2ee5ad

21 files changed

Lines changed: 1060 additions & 85 deletions
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
feat(cli): `os migrate plan` lists the tenant columns still nullable and names the voluntary organization-ownership ceremony; the ceremony reports a rotation shard already NOT NULL as such (#25134)
6+
7+
Clause-②: yes (widening) — `os migrate plan --json` gains the `nullableTenantColumns` field, and the ceremony's `not-null` step gains the `already-not-null` outcome and the `alreadyNotNull` summary count. No exit code, drift entry, op, refusal or accepted input moves.
8+
9+
Metadata-protocol: none
10+
11+
A database the published 18.0.0 created is certified at birth with its tenant columns (`organization_id`) nullable, and `os migrate organization-ownership`, which tightens them to `NOT NULL`, is voluntary on it. Nothing told the operator that.
12+
13+
- **`os migrate plan`: a new advisory section.** It lists every tenant column the database still holds nullable (table and column) and names `os migrate organization-ownership` as the remedy. Each column says whether the ceremony tightens it or leaves it, and why. The ceremony leaves a rotation shard created before shards were created `NOT NULL`, `sys_email_template` (its rows are mirrors the ceremony deletes, and the table retires), a table its inventory exempts, and every table on a database whose transport cannot rebuild a table (the Turso remote face). When the ceremony would tighten none of the listed columns, the section says so and does not offer it. A fresh install lists nothing. A column named `organization_id` on an object with tenancy disabled (`sys_sso_provider`) is not a tenant column and is not listed.
14+
- **Advisory only.** It is not drift: it proposes no change, adds nothing to `total` or `changes`, and the exit code does not change. Under `--json` it is the separate field `nullableTenantColumns`: `{ status: 'read', adr, remedy, columns: [{ table, column, database?, ceremony: 'tightens' | 'leaves', reason? }] }`, or `{ status: 'unreadable', adr, detail }` when the plan could not read it.
15+
- **The ceremony's `not-null` step.** A rotation shard that the driver created `NOT NULL` used to be reported as `unsupported`, with a note saying each new shard is created nullable. That has not been true since the driver began creating shards `NOT NULL`. Such a shard is now `already-not-null` (counted in `alreadyNotNull`) and gets no note. A shard rotated in before then stays `unsupported`, and its note says it is dropped when its period leaves the base table's retention.
16+
17+
**What to do.** Nothing is required: the server starts either way. To tighten the columns the plan lists, run `os migrate organization-ownership --out PLAN`, review it, then run `os migrate organization-ownership --apply --plan-file PLAN --backup-taken`.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/cli': patch
3+
'create-objectstack': patch
4+
---
5+
6+
fix(cli,create-objectstack): new projects declare vitest `~4.1.12`, outside GHSA-qxg6-89mg-rqv8, and still install on npm 10
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
The vitest range every scaffold declares moves from `~4.0.0` to `~4.1.12`: the `create-objectstack` blank template, `os init -t plugin` and `os create plugin` (in `devDependencies`), and the npm `overrides` block that every standalone scaffold emits, `os init -t app` and `-t empty` included. `~4.0.0` resolved to vitest and `@vitest/mocker` 4.0.18, inside GHSA-qxg6-89mg-rqv8 (`@vitest/mocker` from 2.1.0-beta.7 up to the fix in 4.1.12), which has no 4.0.x fix. A new project now resolves vitest and `@vitest/mocker` 4.1.12 or a later 4.1.x patch, and `npm audit` of a fresh scaffold lists no vitest or `@vitest/*` advisory.
12+
13+
npm 10, the npm Node 22 bundles, still installs every scaffold. The `overrides.vitest` entry, which equals `devDependencies.vitest`, is what makes that hold: it settles vitest 4.1.12's contradictory optional peer (`@vitest/browser-webdriverio`), which npm 10 otherwise crashes on with `Cannot read properties of null (reading 'edgesOut')`. Measured on npm 10.9.4, 10.9.9 and 11.21.1. pnpm ignores npm's `overrides` field.
14+
15+
**An existing project made by an earlier `create-objectstack`, `os init` or `os create plugin`** keeps its own `~4.0.0`. To move it, set `overrides.vitest` to `~4.1.12` in `package.json`, and `devDependencies.vitest` too where the project declares it, then run `npm install`. npm refuses an override that differs from the direct dependency (`EOVERRIDE`), so the two move together.

‎content/docs/deployment/cli.mdx‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -986,6 +986,37 @@ without `NODE_ENV=development` describes a production `os serve`, which keeps th
986986
objects in the primary unless `OS_TELEMETRY_DB` names a file. A `plan` against a
987987
sibling that does not exist yet lists its tables to create and leaves no file behind.
988988
989+
#### Tenant columns still nullable (advisory)
990+
991+
`os migrate plan` lists every tenant column the database still holds nullable, in a
992+
section of its own, and names `os migrate organization-ownership` as the remedy. A
993+
database the published 18.0.0 created is certified at birth with its tenant columns
994+
nullable, and the [organization-ownership ceremony](#os-migrate-organization-ownership)
995+
that tightens them is voluntary on it: the server starts either way. A database the
996+
ceremony upgraded keeps only the few the ceremony leaves, and each of those is listed
997+
with the reason it stays nullable:
998+
999+
```text
1000+
ℹ 2 tenant column(s) are still nullable (ADR-0131 D1). This is advisory: it is not drift, and nothing in this plan or in "os migrate apply" changes it.
1001+
• sys_activity__r20261020.organization_id — the ceremony leaves it nullable: a rotation shard of sys_activity, created before shards were created NOT NULL: the ceremony rebuilds no shard, and it is dropped when its period leaves sys_activity's retention
1002+
• sys_email_template.organization_id — the ceremony leaves it nullable: its rows are mirrors the ceremony deletes, and the table retires (ADR-0131 D10 fate 2, D13), so the ceremony does not tighten it
1003+
None of them is one the organization-ownership ceremony tightens; each line says why it stays nullable.
1004+
```
1005+
1006+
When the ceremony would tighten any listed column, the section's last line names its
1007+
two commands instead. On a Turso remote database every listed column is one the
1008+
ceremony leaves, because that transport cannot rebuild a table. A fresh install creates
1009+
every tenant column `NOT NULL` and the section prints nothing. A column named
1010+
`organization_id` on an object with tenancy disabled (`sys_sso_provider`) is not a
1011+
tenant column and is not listed.
1012+
1013+
The section is advisory only. It is not drift: it proposes no change, adds nothing to
1014+
the change count, and does not change the exit code. Under `--json` it is the separate
1015+
`nullableTenantColumns` field: `{ status: "read", adr, remedy, columns }`, where each
1016+
column is `{ table, column, database?, ceremony: "tightens" | "leaves", reason? }`. It is
1017+
`{ status: "unreadable", detail }` when the plan could not read it; `columns: []` means
1018+
it read and found none.
1019+
9891020
#### Nothing is applied before you confirm
9901021
9911022
Both commands boot your app to read its metadata. That boot writes no row and no

‎content/docs/deployment/troubleshooting.mdx‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,21 @@ Solutions to the most common issues encountered when working with ObjectStack.
1212

1313
### "Cannot read properties of null (reading 'edgesOut')" from npm install
1414

15-
**Symptom:** npm exits 1 with `npm error Cannot read properties of null (reading 'edgesOut')` while installing `@objectstack/cli` or `@objectstack/plugin-auth`. This includes the install step of `npm create objectstack` and `npm install -g @objectstack/cli`.
15+
**Symptom:** npm exits 1 with `npm error Cannot read properties of null (reading 'edgesOut')` while installing `@objectstack/cli` or `@objectstack/plugin-auth`. This includes `npm install -g @objectstack/cli`, and `npm install` in a project whose `package.json` has no `vitest` override (fix 2).
1616

17-
**Cause:** npm 10, the npm that Node 22 ships, crashes on an optional `vitest` peer in the auth library's dependency tree: the newest vitest 4 has no matching `@vitest/browser-webdriverio` release. npm 11 and pnpm install the same packages, and `npm ci` from a committed lockfile is not affected.
17+
**Cause:** npm 10, the npm that Node 22 ships, crashes on the newest vitest 4, 4.1.12. Its optional peer `@vitest/browser-webdriverio ^4.1.11` has one matching release, 4.1.11, and that release requires exactly vitest 4.1.11. npm 10 reaches vitest through an optional `vitest` peer in the auth library's dependency tree, or through your project's own `vitest`. npm 11 and pnpm install the same packages, and `npm ci` from a committed lockfile is not affected.
1818

1919
**Fix** (either one):
2020

2121
1. **Use npm 11** (it needs Node 22.9+): run `npm install -g npm@11`, then repeat the install. This is the only fix for `npm install -g`, because a global install reads no `overrides`. In a Dockerfile, install under a pinned npm 11, as the [self-built runtime recipe](/docs/deployment/self-hosting#docker-official-image--the-standard-path) does.
22-
2. **Stay on npm 10 and pin vitest** in your project's root `package.json`. Then run `npm install` again and commit the lockfile:
22+
2. **Stay on npm 10 and pin vitest** in your project's root `package.json`. The override puts every `vitest` in the tree, optional peers included, on one range, and npm 10 installs it. Then run `npm install` again and commit the lockfile:
2323

2424
```json title="package.json"
25-
"overrides": { "vitest": "~4.0.0" }
25+
"overrides": { "vitest": "~4.1.12" }
2626
```
2727

28+
If `devDependencies` lists `vitest`, set it to the same `~4.1.12`: npm refuses an override that differs from a direct dependency (`EOVERRIDE`). A project created by `npm create objectstack`, `os init` or `os create plugin` already has this block. If yours reads `~4.0.0`, an earlier scaffold's value, change both to `~4.1.12`: `~4.0.0` installs vitest 4.0.18, which is affected by security advisory GHSA-qxg6-89mg-rqv8.
29+
2830
This entry, the npm 11 steps in [Self-hosting](/docs/deployment/self-hosting), and the npm 10 notes in [Getting Started](/docs/getting-started#prerequisites) and [Your First Project](/docs/getting-started/your-first-project#prerequisites) can be removed once `@vitest/browser-webdriverio` publishes the version that `npm view vitest dist-tags` lists as `V4`, and a plain npm 10 install of `@objectstack/plugin-auth` passes.
2931

3032
---

‎content/docs/upgrading.mdx‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,11 @@ ceremony, under any tenancy posture. A v18 server serves a database it created
354354
itself or one the ceremony certified, and refuses any other at boot, naming the
355355
command.
356356

357+
**A database the published 18.0.0 created** carries a birth marker and is certified
358+
with its tenant columns (`organization_id`) still nullable. `os migrate plan` lists
359+
them, and the ceremony, which such a database does not require, tightens them to
360+
`NOT NULL` when you run it.
361+
357362
**The sequence:**
358363

359364
1. Before the first 18.0 boot, run `os migrate security-catalog-overlays`. It

‎packages/cli/src/commands/init.ts‎

Lines changed: 59 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -344,9 +344,10 @@ export const SCAFFOLD_PNPM_RANGE = '>=10.15';
344344
// `^4.0.0` over `^4.0.18`: no recorded decision exists for either, both were
345345
// written in the same 2026-02-07 commit, and `^4.0.18` claims a PATCH-level
346346
// floor nothing justifies while being strictly the narrower of the two. (The
347-
// vitest range has since moved to `~4.0.0` because `^4.0.0` stopped
348-
// installing on npm 10. That was an install failure, not a support decision;
349-
// the comment on `SCAFFOLD_VITEST_RANGE` holds the reading.)
347+
// vitest range has since moved twice: to `~4.0.0` when `^4.0.0` stopped
348+
// installing on npm 10, then to `~4.1.12` for a security advisory on
349+
// `@vitest/mocker`. Neither was a support decision; the comment on
350+
// `SCAFFOLD_VITEST_RANGE` holds the reading.)
350351
//
351352
// ⚠️ Neither choice changes what a scaffolded project INSTALLS. Measured
352353
// against the registry: `^5.3.0` and `^5.8.0` both resolve to typescript
@@ -386,39 +387,52 @@ export const SCAFFOLD_TYPESCRIPT_RANGE = '^5.3.0';
386387
/**
387388
* The vitest range a scaffolded project declares when its template tests.
388389
*
389-
* `~4.0.0`, not `^4.0.0`, because `^4.0.0` does not INSTALL on npm 10, the npm
390-
* that Node 22 bundles. Measured 2026-10-12, in an empty directory with a fresh
391-
* `--cache` and a manifest holding only this one devDependency (the full matrix
392-
* is in the PR that set this value): on npm 10.9.4 and 10.9.9, `npm install`
393-
* exits 1 with `Cannot read properties of null (reading 'edgesOut')` from
394-
* arborist's `#loadPeerSet`, and npm 11.21.0 installs it. `~4.0.0` installed on
395-
* all three (vitest 4.0.18, vite 7.3.7), and so did both CLI-emitted plugin
396-
* manifests. The cost: a scaffold gets vitest 4.0.18 rather than 4.1.x.
390+
* `~4.1.12`: the floor is the first 4.x release outside GHSA-qxg6-89mg-rqv8,
391+
* which affects `@vitest/mocker` from 2.1.0-beta.7 up to the fix in 4.1.12.
392+
* vitest depends on its own `@vitest/mocker` at the exact same version, so the
393+
* vitest floor IS the mocker floor. The tilde admits patch releases only, as
394+
* the previous `~4.0.0` did, because both npm 10 failures recorded below
395+
* arrived with a new minor's peer graph. ⛔ Never move the floor back below
396+
* 4.1.12 to get around a future npm 10 failure: no 4.0.x fix exists, and
397+
* `test/scaffold-npm-overrides.test.ts` pins the floor.
397398
*
398-
* ⚠️ The range alone stopped being enough, so it is also emitted as an npm
399-
* OVERRIDE — see {@link SCAFFOLD_NPM_OVERRIDES}, which is built from this
400-
* constant, so the declared range and the override cannot disagree.
399+
* The range installs on npm 10, the npm Node 22 bundles, only TOGETHER with
400+
* the npm override — see {@link SCAFFOLD_NPM_OVERRIDES}, built from this
401+
* constant, so the declared range and the override cannot disagree. vitest
402+
* 4.1.12 itself peers `@vitest/browser-webdriverio ^4.1.11`, whose only match,
403+
* 4.1.11, peers `vitest: 4.1.11` exactly, and npm 10's arborist crashes on
404+
* that set. Measured 2026-10-22, each manifest in an empty directory with a
405+
* fresh `--cache`, the `@objectstack/*` ranges resolving to the published
406+
* 18.0.0 (blank template, `os init -t plugin`, `os create plugin`):
401407
*
402-
* The cause is upstream, in the peer graph. Every vitest 4.1.x admits vite 8.
403-
* vite 8 declares an optional `@vitejs/devtools` peer, which reaches
404-
* `@vitejs/devtools-vitest`, which peers `vitest@*`. npm 10 resolves that peer
405-
* to vitest's `latest` release, whose exact-version peers then contradict the
406-
* vitest at the root, and npm 10's arborist crashes on it. vitest 4.0.x admits
407-
* only vite `^6 || ^7`, and no vite 6 or 7 release declares the devtools peer,
408-
* so the `vitest@*` peer never enters the graph.
408+
* `~4.1.12` + override npm 10.9.4, 10.9.9, exit 0; one vitest and one
409+
* 11.21.1 `@vitest/mocker`, both 4.1.12;
410+
* `npm audit` lists no vitest or
411+
* `@vitest/*` entry
412+
* `~4.1.12`, no override npm 10.9.9 exit 1, `Cannot read properties
413+
* of null (reading 'edgesOut')`
414+
* `~4.0.0` + override npm 10.9.9, 11.21.1 exit 0 on vitest and
415+
* `@vitest/mocker` 4.0.18, which
416+
* `npm audit` reports
409417
*
410-
* ⛔ Not `^5.0.0`, although it installs today. It installs only because it
411-
* resolves to exactly the version `latest` names: pinned to 5.0.0 or 5.0.2 it
412-
* crashes the same way. So it would break again, with no change here, the day
413-
* `latest` moves past 5.x. ⛔ Not a shipped `.npmrc` with `legacy-peer-deps`
414-
* either, which changes peer resolution for every npm user of the scaffold.
418+
* The install does not depend on where vitest's `latest` tag points: `latest`
419+
* is 5.0.5, outside this range, and the range installs. The override routes
420+
* every `vitest` edge in the npm graph to this range, the optional peers deep
421+
* in the closure included.
415422
*
416-
* The re-check that lets this move back: run `npm install` of
417-
* `{"devDependencies":{"vitest":"^4.0.0"}}` (or the range you want) in an empty
418-
* directory with a fresh `--cache`, on the newest npm 10 and on npm 11. When
419-
* both exit 0, widen this.
423+
* ⛔ Not the 5.x line, although `^5.0.5` + override installed on the same
424+
* matrix and cleared the advisory too. vitest 5 declares `engines.node`
425+
* `^22.12.0 || ^24.0.0 || >=26.0.0`, narrower than the `>=22.0.0` the CLI and
426+
* `create-objectstack` declare, and this repo's own suites run vitest 4.
427+
* Moving a scaffold to a new major is its own change.
428+
* ⛔ Not a shipped `.npmrc` with `legacy-peer-deps` either, which changes peer
429+
* resolution for every npm user of the scaffold.
430+
*
431+
* The re-check before moving this: `npm install` of each emitted manifest in
432+
* an empty directory with a fresh `--cache`, on the newest npm 10 and on npm
433+
* 11, then `npm audit` of the installed tree.
420434
*/
421-
export const SCAFFOLD_VITEST_RANGE = '~4.0.0';
435+
export const SCAFFOLD_VITEST_RANGE = '~4.1.12';
422436

423437
/**
424438
* The npm `overrides` block every STANDALONE scaffold emits: `os init`'s three
@@ -429,11 +443,16 @@ export const SCAFFOLD_VITEST_RANGE = '~4.0.0';
429443
* Why it exists. vitest 4.1.12 (published 2026-10-15) peers
430444
* `@vitest/browser-webdriverio ^4.1.11`, and that package's only match, 4.1.11,
431445
* peers `vitest: 4.1.11` exactly — a peer set that contradicts itself. A
432-
* scaffold reaches it even with vitest pinned to `~4.0.0` at its root, through
433-
* OPTIONAL `vitest` peers deeper in the closure (`@objectstack/metadata-core`
434-
* peers `^4.0.0`, better-auth peers `^2 || ^3 || ^4`), which npm 10 resolves to
435-
* vitest's newest 4.x. Measured 2026-10-16, each manifest in an empty
436-
* directory with a fresh `--cache`, against the published `18.0.0-next.2`:
446+
* scaffold reaches it through its own root vitest, now that the range is
447+
* `~4.1.12`, and, in a template that declares no vitest, through OPTIONAL
448+
* `vitest` peers deeper in the closure (better-auth peers vitest optionally),
449+
* which npm 10 resolves to vitest's newest 4.x. Re-measured 2026-10-22 against
450+
* the published 18.0.0 with the range at `~4.1.12`: WITHOUT this block, `os
451+
* init -t app`, `-t plugin` and `os create plugin` each exit 1 on npm 10.9.9
452+
* with the crash below; WITH it, those three and `-t empty` exit 0 on npm
453+
* 10.9.9 and 11.21.1 (the full matrix is on `SCAFFOLD_VITEST_RANGE`). The
454+
* first reading, 2026-10-16, each manifest in an empty directory with a fresh
455+
* `--cache`, against the published `18.0.0-next.2`, with the range at `~4.0.0`:
437456
*
438457
* without the override npm 10.9.9 `os init -t app`, `-t plugin` and
439458
* `-t empty` all exit 1 with `Cannot
@@ -450,10 +469,10 @@ export const SCAFFOLD_VITEST_RANGE = '~4.0.0';
450469
* vitest, the override equals that declaration — npm refuses an override that
451470
* differs from the direct dependency's own spec (`EOVERRIDE`).
452471
*
453-
* The cost, measured on an emitted `-t app` (npm 10.9.9): a later bare `npm
454-
* install -D vitest` is refused with `EOVERRIDE: Override for vitest@*
455-
* conflicts with direct dependency`; `npm install -D vitest@~4.0.0` (this
456-
* range) installs. Loud and self-describing, where the alternative was a
472+
* The cost, measured on an emitted `-t app` (npm 10.9.9, re-measured with the
473+
* range at `~4.1.12`): a later bare `npm install -D vitest` is refused with
474+
* `EOVERRIDE: Override for vitest@* conflicts with direct dependency`; `npm
475+
* install -D vitest@~4.1.12` (this range) installs. Loud and self-describing, where the alternative was a
457476
* project that could not install at all. pnpm ignores npm's `overrides` field,
458477
* so pnpm users see no change.
459478
*

0 commit comments

Comments
 (0)