Skip to content

Commit 60fd853

Browse files
committed
release(ui-merge): the VS Code extension stays in the one fixed group and the QUICK_REFERENCE release sync retires (objectstack#25711, WIP)
Tesla's rulings of 2026-10-25: decision 17b, ruling B, keeps the private VS Code extension `object-ui` in the fixed group. The root check-changeset-fixed now accepts a declared private member (PRIVATE_FIXED_MEMBERS) while privatePackages.version is true, and check:lockstep-package-count counts published members only. The ui-side patch drops its private-arm change to objectui's check-changeset-fixed and keeps the extension rows of check-unused-dependencies. Decision 17c, ruling B, is carried in the same patch: QUICK_REFERENCE.md's synced "Current Release" rows, the sync script and its two tests retire. The patch is regenerated against verify head 9e37acd. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0149BHSXfCdQywGaLdpRAtWe
1 parent a5f4c93 commit 60fd853

7 files changed

Lines changed: 295 additions & 297 deletions

File tree

‎.changeset/25711-console-built-in-workspace.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@ Metadata-protocol: none
1010

1111
- **The package.** `@objectstack/console` ships the same `dist/` it shipped at 18.1.0: the built Console (`index.html`, `assets/`, the manifests it served). Two files leave the tarball: `dist/.objectui-sha`, which named the objectui commit the Console was built from, and `dist/.objectstack-injection.json`, which recorded the spec and client dists injected into that build. Both described a pin that no longer exists: the Console is built from the `ui/` tree of the same commit as the framework, so its version is the framework's version.
1212
- **The CLI.** When a framework checkout has no built Console, `objectstack serve` / `objectstack dev` and the 503 at `/_console/` now name `pnpm console:build`, which builds `ui/apps/console` and copies its dist into `packages/console`. The remedy used to be `pnpm objectui:build`, which cloned objectui at the pinned commit.
13-
- **One version.** The `@object-ui/*` packages join the `@objectstack/*` fixed group, so every public package of both scopes publishes at one version.
13+
- **One version.** The `@object-ui/*` packages and the private VS Code extension join the `@objectstack/*` fixed group, so every public package of both scopes publishes at one version and the extension ships at it.
1414

1515
What to do: nothing as a consumer; install `@objectstack/console` as before. In a framework checkout, run `pnpm console:build` where you ran `pnpm objectui:build`.

‎.changeset/config.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@
119119
"@object-ui/react-runtime",
120120
"@object-ui/runner",
121121
"@object-ui/sdui-parser",
122-
"@object-ui/types"
122+
"@object-ui/types",
123+
"object-ui"
123124
]
124125
],
125126
"linked": [],

‎scripts/check-changeset-fixed.mjs‎

Lines changed: 76 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99
* Exits with code 1 (and a clear diff) if:
1010
* - A public (non-private) workspace package is missing from the
1111
* `fixed` group in .changeset/config.json
12-
* - A name listed in the `fixed` group no longer exists in the workspace
12+
* - A name listed in the `fixed` group no longer exists in the workspace,
13+
* or names a private package that `PRIVATE_FIXED_MEMBERS` does not declare
14+
* - A declared private member is missing from the group, is not a private
15+
* workspace package, or sits there while `privatePackages.version` is not
16+
* true (Changesets would then leave it unversioned inside a lockstep group)
1317
*
1418
* The script intentionally has zero third-party dependencies so it can run
1519
* in minimal CI environments before `pnpm install`. The workspace walk comes
@@ -20,11 +24,32 @@
2024
import { readFileSync } from 'node:fs';
2125
import { dirname, resolve } from 'node:path';
2226
import { fileURLToPath } from 'node:url';
27+
import { isEntrypoint } from './invoked-as.mjs';
2328
import { workspacePackages } from './workspace-enumerator.mjs';
2429

2530
const __dirname = dirname(fileURLToPath(import.meta.url));
2631
const repoRoot = resolve(__dirname, '..');
2732

33+
/**
34+
* Private workspace packages that belong in the `fixed` group anyway, each with
35+
* the reason it is there. Every other name in the group must be public.
36+
*
37+
* `object-ui` is the VS Code extension (`ui/packages/vscode-extension`). It is
38+
* never published to npm, but it ships at the release's version: Tesla's ruling
39+
* of 2026-10-25 (decision 17b, ruling B, objectstack#25711) keeps it in the one
40+
* version group 「同一个版本号组」 the objectui merge created, as objectui's own
41+
* group had. A private package outside this list is still refused in the group,
42+
* so a package marked private by mistake cannot hide in it.
43+
*
44+
* Readers that count "published packages" (`check-lockstep-package-count.mjs`)
45+
* subtract these names.
46+
*
47+
* @type {Readonly<Record<string, string>>}
48+
*/
49+
export const PRIVATE_FIXED_MEMBERS = Object.freeze({
50+
'object-ui': 'the VS Code extension: never on npm, versioned with the release (decision 17b, objectstack#25711)',
51+
});
52+
2853
/**
2954
* Names of all non-private workspace packages.
3055
*
@@ -43,35 +68,60 @@ const repoRoot = resolve(__dirname, '..');
4368
*
4469
* @returns {string[]}
4570
*/
46-
function listPublicPackageNames() {
47-
const names = new Set();
71+
function listPackageNames() {
72+
const publicNames = new Set();
73+
const privateNames = new Set();
4874
for (const { manifest } of workspacePackages(repoRoot)) {
49-
if (!manifest.name || manifest.private === true) continue;
50-
names.add(manifest.name);
75+
if (!manifest.name) continue;
76+
(manifest.private === true ? privateNames : publicNames).add(manifest.name);
5177
}
52-
return [...names].sort();
78+
return { publicNames: [...publicNames].sort(), privateNames };
5379
}
5480

55-
function readFixedGroups() {
81+
function readConfig() {
5682
const configPath = resolve(repoRoot, '.changeset/config.json');
57-
const config = JSON.parse(readFileSync(configPath, 'utf8'));
58-
if (!Array.isArray(config.fixed)) return [];
59-
return config.fixed;
83+
return JSON.parse(readFileSync(configPath, 'utf8'));
6084
}
6185

62-
function main() {
63-
const publicPackages = listPublicPackageNames();
64-
const fixedGroups = readFixedGroups();
65-
const fixed = new Set(fixedGroups.flat());
86+
/**
87+
* Judges a config against the workspace. Pure, so the declared-private rules
88+
* can be read without a tree.
89+
*
90+
* @param {{ fixed?: string[][]; privatePackages?: { version?: boolean } }} config
91+
* @param {{ publicNames: string[]; privateNames: Set<string> }} packages
92+
* @param {Readonly<Record<string, string>>} [declared]
93+
*/
94+
export function judge(config, { publicNames, privateNames }, declared = PRIVATE_FIXED_MEMBERS) {
95+
const fixed = new Set(Array.isArray(config.fixed) ? config.fixed.flat() : []);
96+
const isDeclaredPrivate = (name) => Object.hasOwn(declared, name) && privateNames.has(name);
97+
98+
const missing = publicNames.filter((name) => !fixed.has(name));
99+
const stale = [...fixed].filter((name) => !publicNames.includes(name) && !isDeclaredPrivate(name)).sort();
100+
const declaredProblems = [];
101+
for (const name of Object.keys(declared).sort()) {
102+
if (publicNames.includes(name)) {
103+
declaredProblems.push(`${name} is declared a private member but its manifest is public; drop it from PRIVATE_FIXED_MEMBERS`);
104+
} else if (!privateNames.has(name)) {
105+
// No such package in this tree: `stale` already names it when the group does.
106+
continue;
107+
} else if (!fixed.has(name)) {
108+
declaredProblems.push(`${name} (${declared[name]}) is missing from "fixed"`);
109+
} else if (config.privatePackages?.version !== true) {
110+
declaredProblems.push(`${name} sits in "fixed" while privatePackages.version is not true, so Changesets would not version it`);
111+
}
112+
}
113+
return { missing, stale, declaredProblems };
114+
}
66115

67-
const missing = publicPackages.filter((name) => !fixed.has(name));
68-
const stale = [...fixed]
69-
.filter((name) => !publicPackages.includes(name))
70-
.sort();
116+
function main() {
117+
const packages = listPackageNames();
118+
const { missing, stale, declaredProblems } = judge(readConfig(), packages);
71119

72-
if (missing.length === 0 && stale.length === 0) {
120+
if (missing.length === 0 && stale.length === 0 && declaredProblems.length === 0) {
121+
const privateCount = Object.keys(PRIVATE_FIXED_MEMBERS).filter((name) => packages.privateNames.has(name)).length;
73122
console.log(
74-
`✓ .changeset/config.json "fixed" group is in sync with ${publicPackages.length} public workspace packages.`,
123+
`✓ .changeset/config.json "fixed" group is in sync with ${packages.publicNames.length} public workspace packages` +
124+
(privateCount > 0 ? ` and ${privateCount} declared private member(s).` : '.'),
75125
);
76126
return;
77127
}
@@ -85,15 +135,18 @@ function main() {
85135
}
86136
if (stale.length > 0) {
87137
console.error(
88-
'\nNames in "fixed" that no longer exist in the workspace (remove them):',
138+
'\nNames in "fixed" that are no public workspace package and no declared private member (remove them, or declare a private one in PRIVATE_FIXED_MEMBERS):',
89139
);
90140
for (const name of stale) console.error(` - ${name}`);
91141
}
142+
if (declaredProblems.length > 0) {
143+
console.error('\nDeclared private members (PRIVATE_FIXED_MEMBERS):');
144+
for (const problem of declaredProblems) console.error(` - ${problem}`);
145+
}
92146
console.error(
93147
'\nEdit .changeset/config.json so the "fixed" group matches the public workspace, then re-run this script.',
94148
);
95149
process.exit(1);
96150
}
97151

98-
main();
99-
152+
if (isEntrypoint(import.meta.url)) main();

‎scripts/check-lockstep-package-count.mjs‎

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,18 +68,25 @@ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'nod
6868
import * as os from 'node:os';
6969
import { dirname, join, resolve } from 'node:path';
7070
import { fileURLToPath } from 'node:url';
71+
import { PRIVATE_FIXED_MEMBERS } from './check-changeset-fixed.mjs';
7172

7273
const __dirname = dirname(fileURLToPath(import.meta.url));
7374
const repoRoot = resolve(__dirname, '..');
7475

75-
/** Reads the one true count: the length of the lockstep `fixed` group. */
76-
function trueCount(root) {
76+
/**
77+
* Reads the one true count: the length of the lockstep `fixed` group, less the
78+
* private members `check-changeset-fixed.mjs` declares (`PRIVATE_FIXED_MEMBERS`,
79+
* the VS Code extension since objectstack#25711). Every prose occurrence counts
80+
* PUBLISHED packages, and those members ship at the release's version without
81+
* ever reaching npm.
82+
*/
83+
function trueCount(root, privateMembers = PRIVATE_FIXED_MEMBERS) {
7784
const configPath = resolve(root, '.changeset/config.json');
7885
const config = JSON.parse(readFileSync(configPath, 'utf8'));
7986
if (!Array.isArray(config.fixed) || !Array.isArray(config.fixed[0])) {
8087
throw new Error(`${configPath}: "fixed" is not an array containing a group array`);
8188
}
82-
return config.fixed[0].length;
89+
return config.fixed[0].filter((name) => !Object.hasOwn(privateMembers, name)).length;
8390
}
8491

8592
// Every prose restatement of the lockstep package count, as of #17039. Each
@@ -150,7 +157,7 @@ function run(root, { fix = false } = {}) {
150157

151158
function formatReport({ truth, results }) {
152159
const lines = [
153-
`lockstep package count (source: .changeset/config.json fixed[0].length) = ${truth}`,
160+
`lockstep package count (source: .changeset/config.json fixed[0].length, less the declared private members) = ${truth}`,
154161
];
155162
for (const r of results) {
156163
if (r.status === 'OK') {
@@ -268,6 +275,16 @@ function selfTest() {
268275
'duplicated anchor: reported as ROTTED_ANCHOR with matchCount 2',
269276
dupedRows.every((r) => r.status === 'ROTTED_ANCHOR' && r.matchCount === 2),
270277
);
278+
279+
// Case 5: a declared private member sits in the group but is not counted
280+
// (objectstack#25711); an undeclared name is counted like any other.
281+
write(
282+
join(tmp, '.changeset', 'config.json'),
283+
JSON.stringify({ fixed: [['@objectstack/pkg-0', '@objectstack/pkg-1', 'private-ext']] }),
284+
);
285+
t('declared private member: not counted', trueCount(tmp, { 'private-ext': 'fixture' }) === 2);
286+
t('the same name undeclared: counted', trueCount(tmp, {}) === 3);
287+
t('the real declaration is what the gate subtracts by default', trueCount(tmp) === 3);
271288
} finally {
272289
rmSync(tmp, { recursive: true, force: true });
273290
}

‎scripts/publish-smoke-pack.mjs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
* .changeset/config.json enumerates the same 112 names, but it is a DERIVED
3737
* declaration validated against the workspace by scripts/check-changeset-fixed.mjs
3838
* (which reddens both when a public package is missing from the group and
39-
* when a group name no longer exists) — deriving from the group would mean
39+
* when a group name no longer exists), and it also holds the private members
40+
* that gate declares, which are never packed here — deriving from the group would mean
4041
* reading a copy that a gate keeps honest, rather than the thing itself.
4142
*/
4243

0 commit comments

Comments
 (0)