Skip to content

Commit 7f4e115

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-23849-npm10-install-docs
Claude-Session: https://claude.ai/code/session_016PVjjuVPte5XEJvaQx1E4t Co-authored-by: Claude <noreply@anthropic.com>
2 parents dc23702 + a31ff2e commit 7f4e115

33 files changed

Lines changed: 707 additions & 98 deletions

‎.changeset/23802-setup-copy.md‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/platform-objects': patch
3+
'@objectstack/metadata-core': patch
4+
'@objectstack/service-messaging': patch
5+
---
6+
7+
Clause-②: no
8+
9+
Setup pages describe their records in plain admin sentences. Business Units now reads "Your
10+
company structure — divisions, departments, regions and offices — used to scope data and
11+
reports.", Notification Events "Every notification the platform sent, one row per event.",
12+
Notification Preferences "Which topics and channels each user receives; admins set the
13+
defaults.", Notification Templates "The subject and body each notification topic sends, per
14+
channel and language." and HTTP Deliveries "Outgoing HTTP calls made by webhooks and flows, one
15+
row per call, with its delivery status." Field help on Users, Organizations, Invitations and SSO
16+
Providers, and the metadata-storage and notification-delivery object descriptions, no longer
17+
carry design-record numbers; those references stay in the code comments. The zh-CN, ja-JP and
18+
es-ES translations follow. Wording only: no field, permission or behaviour changes.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
Clause-②: no
6+
7+
`os init` (every template: `app`, `plugin`, `empty`) and `os create plugin` now emit
8+
`"overrides": { "vitest": "~4.0.0" }` in the scaffolded `package.json`, so the project installs on
9+
npm 10, the npm Node 22 bundles. Since vitest 4.1.12 shipped a peer set that contradicts itself,
10+
`npm install` of a fresh `os init` project crashed on npm 10.9.x with
11+
`Cannot read properties of null (reading 'edgesOut')`. The override routes every `vitest` edge in
12+
the npm graph to the range the scaffold declares; npm 11 installs the same tree, and pnpm ignores
13+
the field.
14+
15+
The override equals `devDependencies.vitest` wherever the manifest declares vitest directly,
16+
because npm refuses an override that differs from the direct dependency (`EOVERRIDE`). In an `app`
17+
or `empty` project, add vitest at the same range (`npm install -D vitest@~4.0.0`); a bare
18+
`npm install -D vitest` is refused with `EOVERRIDE` while the override is in place. Existing
19+
projects can add the same `overrides` block by hand. `os create plugin --in-repo` is unchanged.

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ import {
134134
NPM_PACKAGE_NAME_MAX_LENGTH,
135135
renderPnpmWorkspaceYaml,
136136
renderScaffoldTsconfig,
137+
SCAFFOLD_NPM_OVERRIDES,
137138
SCAFFOLD_PNPM_RANGE,
138139
SCAFFOLD_TSCONFIG_INCLUDE_SRC_ONLY,
139140
SCAFFOLD_TYPES_NODE_RANGE,
@@ -408,6 +409,13 @@ export const templates: Record<string, CreateTemplate> = {
408409
typescript: SCAFFOLD_TYPESCRIPT_RANGE,
409410
vitest: SCAFFOLD_VITEST_RANGE,
410411
},
412+
// The same npm override `os init` emits, from the same constant, so
413+
// it always equals `devDependencies.vitest` above (npm refuses one
414+
// that differs: `EOVERRIDE`) — see SCAFFOLD_NPM_OVERRIDES for the
415+
// npm 10 crash it prevents. Standalone only: npm reads `overrides`
416+
// from the ROOT manifest alone and pnpm never reads it, so inside
417+
// this monorepo's workspace the block would be inert.
418+
...(standalone ? { overrides: { ...SCAFFOLD_NPM_OVERRIDES } } : {}),
411419
}),
412420
'tsconfig.json': (name: string) =>
413421
standalone

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

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -390,9 +390,13 @@ export const SCAFFOLD_TYPESCRIPT_RANGE = '^5.3.0';
390390
* `--cache` and a manifest holding only this one devDependency (the full matrix
391391
* is in the PR that set this value): on npm 10.9.4 and 10.9.9, `npm install`
392392
* exits 1 with `Cannot read properties of null (reading 'edgesOut')` from
393-
* arborist's `#loadPeerSet`, and npm 11.21.0 installs it. `~4.0.0` installs on
394-
* all three (vitest 4.0.18, vite 7.3.7), and so do both CLI-emitted plugin
395-
* manifests. The cost: a scaffold gets vitest 4.0.18 rather than 4.1.11.
393+
* arborist's `#loadPeerSet`, and npm 11.21.0 installs it. `~4.0.0` installed on
394+
* all three (vitest 4.0.18, vite 7.3.7), and so did both CLI-emitted plugin
395+
* manifests. The cost: a scaffold gets vitest 4.0.18 rather than 4.1.x.
396+
*
397+
* ⚠️ The range alone stopped being enough, so it is also emitted as an npm
398+
* OVERRIDE — see {@link SCAFFOLD_NPM_OVERRIDES}, which is built from this
399+
* constant, so the declared range and the override cannot disagree.
396400
*
397401
* The cause is upstream, in the peer graph. Every vitest 4.1.x admits vite 8.
398402
* vite 8 declares an optional `@vitejs/devtools` peer, which reaches
@@ -415,6 +419,56 @@ export const SCAFFOLD_TYPESCRIPT_RANGE = '^5.3.0';
415419
*/
416420
export const SCAFFOLD_VITEST_RANGE = '~4.0.0';
417421

422+
/**
423+
* The npm `overrides` block every STANDALONE scaffold emits: `os init`'s three
424+
* templates, `os create plugin` outside the monorepo, and — by generation, not
425+
* import — `create-objectstack`'s bundled template (the `overrides.vitest` row
426+
* in `scripts/sync-scaffold-emission-policy.mjs`'s `POLICY_STAMPS`).
427+
*
428+
* Why it exists. vitest 4.1.12 (published 2026-10-15) peers
429+
* `@vitest/browser-webdriverio ^4.1.11`, and that package's only match, 4.1.11,
430+
* peers `vitest: 4.1.11` exactly — a peer set that contradicts itself. A
431+
* scaffold reaches it even with vitest pinned to `~4.0.0` at its root, through
432+
* OPTIONAL `vitest` peers deeper in the closure (`@objectstack/metadata-core`
433+
* peers `^4.0.0`, better-auth peers `^2 || ^3 || ^4`), which npm 10 resolves to
434+
* vitest's newest 4.x. Measured 2026-10-16, each manifest in an empty
435+
* directory with a fresh `--cache`, against the published `18.0.0-next.2`:
436+
*
437+
* without the override npm 10.9.9 `os init -t app`, `-t plugin` and
438+
* `-t empty` all exit 1 with `Cannot
439+
* read properties of null (reading
440+
* 'edgesOut')`; `os create plugin` (no
441+
* `@objectstack/cli` in its closure)
442+
* exits 0
443+
* with the override npm 10.9.9 all four exit 0
444+
* npm 11.21.1 all four exit 0
445+
*
446+
* So the override goes on EVERY template, not only the ones that declare
447+
* vitest: `-t app` and `-t empty` declare none and crashed all the same,
448+
* because what they share is `@objectstack/cli`. Where a manifest does declare
449+
* vitest, the override equals that declaration — npm refuses an override that
450+
* differs from the direct dependency's own spec (`EOVERRIDE`).
451+
*
452+
* The cost, measured on an emitted `-t app` (npm 10.9.9): a later bare `npm
453+
* install -D vitest` is refused with `EOVERRIDE: Override for vitest@*
454+
* conflicts with direct dependency`; `npm install -D vitest@~4.0.0` (this
455+
* range) installs. Loud and self-describing, where the alternative was a
456+
* project that could not install at all. pnpm ignores npm's `overrides` field,
457+
* so pnpm users see no change.
458+
*
459+
* ⛔ Not `"$vitest"`: it needs a direct dependency to point at (two of the
460+
* three `os init` templates have none), and where it does apply, npm stops
461+
* hoisting `@objectstack/metadata-core` and nests copies of it (measured on the
462+
* bundled template) — a duplicated module is a duplicated `instanceof` identity.
463+
*
464+
* The re-check that lets this go: the same empty-directory install of an
465+
* emitted `-t app` manifest WITHOUT this block, on the newest npm 10 and on npm
466+
* 11. When both exit 0, delete it here and its `POLICY_STAMPS` row together.
467+
*/
468+
export const SCAFFOLD_NPM_OVERRIDES: Readonly<Record<string, string>> = Object.freeze({
469+
vitest: SCAFFOLD_VITEST_RANGE,
470+
});
471+
418472
/** The `@types/node` range a scaffolded project declares. */
419473
export const SCAFFOLD_TYPES_NODE_RANGE = '^22.0.0';
420474

@@ -495,6 +549,9 @@ export function renderScaffoldPackageJson(
495549
scripts: template.scripts,
496550
dependencies: template.dependencies,
497551
devDependencies: template.devDependencies,
552+
// Every template, whether or not it declares vitest itself — without it
553+
// npm 10 cannot install any of them (see SCAFFOLD_NPM_OVERRIDES).
554+
overrides: { ...SCAFFOLD_NPM_OVERRIDES },
498555
};
499556
}
500557

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* PIN — every standalone manifest `os init` and `os create` emit carries the
5+
* npm `overrides` block, built from the one vitest constant.
6+
*
7+
* ## The defect
8+
*
9+
* vitest 4.1.12 shipped a self-contradicting peer set (it peers
10+
* `@vitest/browser-webdriverio ^4.1.11`, whose only release peers `vitest:
11+
* 4.1.11` exactly). npm 10 — the npm Node 22 bundles — reaches it through
12+
* OPTIONAL `vitest` peers in the `@objectstack/cli` closure, even with vitest
13+
* pinned to `~4.0.0` at the project root, and its arborist crashes with
14+
* `Cannot read properties of null (reading 'edgesOut')`. Measured 2026-10-16:
15+
* `os init -t app`, `-t plugin` and `-t empty` all failed `npm install` on npm
16+
* 10.9.9; with `"overrides": { "vitest": "~4.0.0" }` all of them, and `os
17+
* create plugin`, installed on npm 10.9.9 and npm 11.21.1. The full reading is
18+
* on `SCAFFOLD_NPM_OVERRIDES` in `src/commands/init.ts`.
19+
*
20+
* ## What is asserted, and why no range is written down here
21+
*
22+
* Every expectation is DERIVED from the exported constants, never a literal:
23+
* a pin comparing against `'~4.0.0'` would stay green on a tree where the
24+
* constant moved and one emission was left behind — the drift it exists to
25+
* catch.
26+
*
27+
* 1. Each emission's `overrides.vitest` IS `SCAFFOLD_VITEST_RANGE`.
28+
* 2. Where an emission also declares `vitest` directly, the two are equal —
29+
* npm refuses an override that differs from the direct dependency's own
30+
* spec (`EOVERRIDE`), so a mismatch is an install failure of its own.
31+
* 3. `os create plugin --in-repo` emits NO `overrides`: npm reads the field
32+
* only from the root manifest and pnpm never reads it, so inside the
33+
* workspace it would be a declaration nothing honours.
34+
*
35+
* That the emitted manifests really install is not decidable here; the npm
36+
* matrix above is the measurement, and `create-objectstack`'s bundled template
37+
* reaches the same constant through `scripts/sync-scaffold-emission-policy.mjs`
38+
* (`pnpm check:scaffold-emission-policy`).
39+
*/
40+
41+
import { describe, it, expect } from 'vitest';
42+
import {
43+
renderScaffoldPackageJson,
44+
SCAFFOLD_NPM_OVERRIDES,
45+
SCAFFOLD_VITEST_RANGE,
46+
TEMPLATES,
47+
} from '../src/commands/init.js';
48+
import { DEFAULT_PLACEMENT, templates } from '../src/commands/create.js';
49+
50+
const PROBE_NAME = 'overrides-probe';
51+
52+
type Manifest = {
53+
dependencies?: Record<string, string>;
54+
devDependencies?: Record<string, string>;
55+
overrides?: Record<string, string>;
56+
};
57+
58+
/** Every standalone manifest the two commands emit, derived from the live maps. */
59+
function standaloneManifests(): Array<{ id: string; manifest: Manifest }> {
60+
const out: Array<{ id: string; manifest: Manifest }> = [];
61+
for (const [key, template] of Object.entries(TEMPLATES)) {
62+
out.push({ id: `os init -t ${key}`, manifest: renderScaffoldPackageJson(PROBE_NAME, template) as Manifest });
63+
}
64+
for (const [key, template] of Object.entries(templates)) {
65+
const render = template.filesFor(DEFAULT_PLACEMENT)['package.json'];
66+
if (!render) continue;
67+
out.push({ id: `os create ${key}`, manifest: render(PROBE_NAME) as Manifest });
68+
}
69+
return out;
70+
}
71+
72+
describe('npm overrides in every standalone scaffold manifest', () => {
73+
it('harvests every emission, and the default placement IS standalone (control)', () => {
74+
expect(DEFAULT_PLACEMENT).toBe('standalone');
75+
const ids = standaloneManifests().map((m) => m.id);
76+
expect(ids).toEqual(expect.arrayContaining(['os init -t app', 'os init -t plugin', 'os create plugin']));
77+
// Assertion 2 below must range over something, or it certifies nothing.
78+
const direct = standaloneManifests().filter((m) => m.manifest.devDependencies?.vitest !== undefined);
79+
expect(direct.map((m) => m.id)).toEqual(expect.arrayContaining(['os init -t plugin', 'os create plugin']));
80+
});
81+
82+
it('the override map is built from the vitest constant', () => {
83+
expect(SCAFFOLD_NPM_OVERRIDES.vitest).toBe(SCAFFOLD_VITEST_RANGE);
84+
expect(Object.isFrozen(SCAFFOLD_NPM_OVERRIDES)).toBe(true);
85+
});
86+
87+
it.each(standaloneManifests())('$id emits overrides.vitest = SCAFFOLD_VITEST_RANGE', ({ manifest }) => {
88+
expect(manifest.overrides).toEqual({ ...SCAFFOLD_NPM_OVERRIDES });
89+
expect(manifest.overrides?.vitest).toBe(SCAFFOLD_VITEST_RANGE);
90+
});
91+
92+
it.each(standaloneManifests())(
93+
'$id: every override naming a direct dependency equals that dependency (npm EOVERRIDE)',
94+
({ manifest }) => {
95+
const direct = { ...(manifest.dependencies ?? {}), ...(manifest.devDependencies ?? {}) };
96+
for (const [name, range] of Object.entries(manifest.overrides ?? {})) {
97+
if (direct[name] === undefined) continue;
98+
expect(range, `overrides.${name} vs the direct ${name} dependency`).toBe(direct[name]);
99+
}
100+
},
101+
);
102+
103+
it('os create --in-repo emits no overrides block — npm and pnpm both ignore it in a workspace member', () => {
104+
const render = templates.plugin.filesFor('in-repo')['package.json'];
105+
const manifest = render(PROBE_NAME) as Manifest;
106+
// Control: the in-repo manifest is a real one, with vitest declared.
107+
expect(manifest.devDependencies?.vitest).toBe(SCAFFOLD_VITEST_RANGE);
108+
expect(manifest).not.toHaveProperty('overrides');
109+
});
110+
});

‎packages/create-objectstack/src/template-consistency.test.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,11 @@ describe('bundled template declared version surfaces', () => {
218218
// The literal costs one thing instead, and loudly: npm refuses an override
219219
// that differs from the direct dependency's own spec (`EOVERRIDE: Override
220220
// for vitest@^4.1.0 conflicts with direct dependency`, measured), so the two
221-
// move together. That is the second assertion. `devDependencies.vitest` is
222-
// stamped from `SCAFFOLD_VITEST_RANGE` at build; this override is not.
221+
// move together. That is the second assertion. Both are stamped from
222+
// `SCAFFOLD_VITEST_RANGE` at build (the `devDependencies.vitest` and
223+
// `overrides.vitest` rows of the emission-policy stamp script's
224+
// `POLICY_STAMPS`), the constant `os init` / `os create` emit their own
225+
// override from.
223226
// ⛔ Not a shipped `.npmrc` with `legacy-peer-deps`, which changes peer
224227
// resolution for every npm user of the scaffold.
225228
it('package.json overrides vitest to its own declared range, so npm 10 installs it', () => {

‎packages/metadata-core/src/objects/sys-metadata-audit.object.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ export const SysMetadataAuditObject = ObjectSchema.create({
4545
retention: { maxAge: '365d' },
4646
archive: { after: '365d', to: 'archive' },
4747
},
48-
description: 'Append-only audit trail of metadata write decisions (ADR-0010).',
48+
// ADR-0010 — the reference lives in the docblock above, not in the copy.
49+
description: 'Append-only audit trail of metadata write decisions.',
4950

5051
fields: {
5152
/** Primary Key (UUID) */

‎packages/metadata-core/src/objects/sys-metadata-commit.object.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ export const SysMetadataCommitObject = ObjectSchema.create({
2828
icon: 'git-commit',
2929
isSystem: true,
3030
managedBy: 'engine-owned',
31-
description: 'Package-scoped commit log grouping a turn’s metadata changes (ADR-0067).',
31+
// ADR-0067 — the reference lives in the docblock above, not in the copy.
32+
description: 'Package-scoped commit log grouping a turn’s metadata changes.',
3233

3334
fields: {
3435
/**

‎packages/metadata-core/src/objects/sys-metadata.object.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,8 @@ export const SysMetadataObject = ObjectSchema.create({
158158
environment_id: Field.lookup('sys_environment', {
159159
label: 'Environment (deprecated)',
160160
required: false,
161-
description: 'DEPRECATED. Overlays are environment rows (ADR-0131 D6); no tenant column.',
161+
// ADR-0131 D6 — overlays are environment rows.
162+
description: 'DEPRECATED. Overlays are environment rows; no tenant column.',
162163
}),
163164

164165
/** Version number for optimistic concurrency */

0 commit comments

Comments
 (0)