@ultimat3/cli 19.1.3 → 19.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,278 @@
1
+ // Two installed copies of one registry-holding framework package, found before either registry is
2
+ // asked anything. `@ultimat3/i18n`, `@ultimat3/policy` and `@ultimat3/entity` each keep their
3
+ // registry at MODULE scope, so a second module instance is a second, EMPTY registry: an app whose
4
+ // `packages/i18n` workspace pinned `@ultimat3/i18n@19.0.0` while its root had 19.1.0 registered
5
+ // every catalog into the workspace's copy, the CLI read the root's, and `x i18n check` answered
6
+ // `X_CATALOG_UNREGISTERED` with a fix that said to move a `defineCatalogs()` call that was already
7
+ // exactly where the fix said to put it (ai-maxxing, 2026-09-05). The same split is the one
8
+ // `local-cli.ts` hands over for — a global `x` is a second copy of `@ultimat3/entity` — except this
9
+ // one lives inside the app's own `node_modules`, where no hand-over can reach it.
10
+
11
+ // why: Bun ships no symlink-resolving stat and no path API of its own. `realpathSync` is the whole
12
+ // decision — two symlinks to one directory are one module instance, and two store entries at one
13
+ // version are still two — and `dirname`/`join` walk a resolved entry up to the package that owns it.
14
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
15
+ // why: Bun exposes no path API — `dirname` walks a resolved entry up to the package that owns it,
16
+ // `join` reaches its manifest, and `relative` is what turns an absolute hit into the repo-relative
17
+ // path a finding names.
18
+ import { dirname, join, relative } from 'node:path';
19
+ import { ERROR_DOCS_URL } from '@ultimat3/core';
20
+ import type { Finding } from './output';
21
+ import { readWorkspaceGraph } from './workspace-graph';
22
+
23
+ /**
24
+ * The packages whose registry is a module-scope table, and what that table is called in a finding.
25
+ * A second copy of any other `@ultimat3/*` package is a mixed-version install the CHANGELOG already
26
+ * warns against; a second copy of one of these is an app that registers into a registry nothing
27
+ * reads.
28
+ */
29
+ export const REGISTRY_PACKAGES: Readonly<Record<string, string>> = {
30
+ '@ultimat3/i18n': 'catalog registry',
31
+ '@ultimat3/policy': 'permission registry',
32
+ '@ultimat3/entity': 'entity registry',
33
+ };
34
+
35
+ /** One resolution of one package from one directory. `dir` is the package's REAL directory. */
36
+ export interface InstalledCopy {
37
+ readonly pkg: string;
38
+ /** Where the resolution started, app-root-relative: `.`, a workspace dir, or `CLI_ORIGIN`. */
39
+ readonly from: string;
40
+ readonly dir: string;
41
+ readonly version: string;
42
+ }
43
+
44
+ /** The `from` label of the copy the running CLI itself imports. */
45
+ export const CLI_ORIGIN = 'the x CLI';
46
+
47
+ /** One distinct copy of a duplicated package, and every origin that resolves to it. */
48
+ export interface DuplicateCopy {
49
+ readonly dir: string;
50
+ readonly version: string;
51
+ readonly from: readonly string[];
52
+ }
53
+
54
+ export interface DuplicateInstall {
55
+ readonly pkg: string;
56
+ /** Newest version first; ties in `dir` order, so a report is stable across runs. */
57
+ readonly copies: readonly DuplicateCopy[];
58
+ }
59
+
60
+ /**
61
+ * Semver order, through Bun's own comparator so `19.1.0-beta.1` sorts BELOW `19.1.0` and
62
+ * `19.10.0` above `19.9.0`; a string that is not a version falls back to its text, because the
63
+ * report still has to be stable over whatever a hand-edited manifest carries.
64
+ */
65
+ function compareVersions(left: string, right: string): number {
66
+ try {
67
+ return Bun.semver.order(left, right);
68
+ } catch {
69
+ return left.localeCompare(right);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * The pure half: every package that resolves to more than one real directory. Keyed by REALPATH,
75
+ * never by version — two workspace symlinks to one checkout are one instance whatever the labels
76
+ * say, and two store entries both stamped `19.1.0` are two instances all the same, because a
77
+ * module registry is per module instance and not per version string.
78
+ */
79
+ export function duplicateInstalls(copies: readonly InstalledCopy[]): readonly DuplicateInstall[] {
80
+ const byPackage = new Map<string, Map<string, { version: string; from: string[] }>>();
81
+ for (const copy of copies) {
82
+ const dirs = byPackage.get(copy.pkg) ?? new Map<string, { version: string; from: string[] }>();
83
+ const entry = dirs.get(copy.dir) ?? { version: copy.version, from: [] };
84
+ if (!entry.from.includes(copy.from)) entry.from.push(copy.from);
85
+ dirs.set(copy.dir, entry);
86
+ byPackage.set(copy.pkg, dirs);
87
+ }
88
+ const duplicates: DuplicateInstall[] = [];
89
+ for (const [pkg, dirs] of byPackage) {
90
+ if (dirs.size < 2) continue;
91
+ const sorted = [...dirs.entries()]
92
+ .map(([dir, entry]) => ({ dir, version: entry.version, from: [...entry.from].sort() }))
93
+ .sort(
94
+ (left, right) =>
95
+ compareVersions(right.version, left.version) || left.dir.localeCompare(right.dir),
96
+ );
97
+ duplicates.push({ pkg, copies: sorted });
98
+ }
99
+ return duplicates.sort((left, right) => left.pkg.localeCompare(right.pkg));
100
+ }
101
+
102
+ /** The filesystem this reader touches, injectable so a fixture needs no install. */
103
+ export interface DuplicateIo {
104
+ /** The resolved entry file, or throws — `Bun.resolveSync`'s contract. */
105
+ resolve(specifier: string, from: string): string;
106
+ realpath(path: string): string;
107
+ exists(path: string): boolean;
108
+ readText(path: string): string;
109
+ }
110
+
111
+ const nodeIo: DuplicateIo = {
112
+ resolve: (specifier, from) => Bun.resolveSync(specifier, from),
113
+ realpath: (path) => realpathSync(path),
114
+ exists: (path) => existsSync(path),
115
+ readText: (path) => readFileSync(path, 'utf8'),
116
+ };
117
+
118
+ /**
119
+ * Deep enough for `src/index.ts` and for any entry an `exports` map could point at, shallow enough
120
+ * that a resolver answering something unexpected stops rather than walking to `/` — the same bound
121
+ * `framework-scope.ts` walks under.
122
+ */
123
+ const MAX_DEPTH = 6;
124
+
125
+ /**
126
+ * The real directory owning `entry`, judged by the `package.json` that NAMES `pkg`. The entry alone
127
+ * is not enough: a workspace checkout resolves to `packages/i18n/src/index.ts` and a published
128
+ * install to `…/dist/index.js`, and the first `package.json` upward from either is the one that
129
+ * says which package it is — a stray manifest in a `src/` would otherwise be reported as a copy.
130
+ */
131
+ function packageDirOf(
132
+ pkg: string,
133
+ entry: string,
134
+ io: DuplicateIo,
135
+ ): { dir: string; version: string } | undefined {
136
+ let dir = dirname(entry);
137
+ for (let depth = 0; depth < MAX_DEPTH; depth += 1) {
138
+ const manifest = join(dir, 'package.json');
139
+ if (io.exists(manifest)) {
140
+ let parsed: unknown;
141
+ try {
142
+ parsed = JSON.parse(io.readText(manifest));
143
+ } catch {
144
+ return undefined;
145
+ }
146
+ if (typeof parsed !== 'object' || parsed === null) return undefined;
147
+ const fields = parsed as Record<string, unknown>;
148
+ if (fields['name'] !== pkg) return undefined;
149
+ const version = fields['version'];
150
+ return { dir: io.realpath(dir), version: typeof version === 'string' ? version : '0.0.0' };
151
+ }
152
+ const parent = dirname(dir);
153
+ if (parent === dir) return undefined;
154
+ dir = parent;
155
+ }
156
+ return undefined;
157
+ }
158
+
159
+ /**
160
+ * One resolution per (package, origin), for every origin that IMPORTS the package in a real boot:
161
+ * the app root, every workspace the root manifest claims, and this CLI's own directory — which is
162
+ * what `x i18n check` reads its registry through. A directory that cannot resolve the package
163
+ * contributes nothing: a workspace with no dependency on `@ultimat3/policy` is not a copy of it.
164
+ *
165
+ * `cliDir` is injectable so a test can stand the CLI somewhere with no framework under it;
166
+ * production passes nothing, and the only defensible base is this module's own directory.
167
+ */
168
+ export async function installedCopies(
169
+ root: string,
170
+ packages: readonly string[],
171
+ io: DuplicateIo = nodeIo,
172
+ cliDir: string = import.meta.dir,
173
+ ): Promise<readonly InstalledCopy[]> {
174
+ const origins: { from: string; dir: string }[] = [{ from: '.', dir: root }];
175
+ for (const node of await readWorkspaceGraph(root)) {
176
+ origins.push({ from: node.dir, dir: join(root, node.dir) });
177
+ }
178
+ origins.push({ from: CLI_ORIGIN, dir: cliDir });
179
+
180
+ const copies: InstalledCopy[] = [];
181
+ for (const pkg of packages) {
182
+ for (const origin of origins) {
183
+ let entry: string;
184
+ try {
185
+ entry = io.resolve(pkg, origin.dir);
186
+ } catch {
187
+ continue;
188
+ }
189
+ const owner = packageDirOf(pkg, entry, io);
190
+ if (owner === undefined) continue;
191
+ copies.push({ pkg, from: origin.from, dir: owner.dir, version: owner.version });
192
+ }
193
+ }
194
+ return copies;
195
+ }
196
+
197
+ /** The two steps composed — what `x i18n check` and the gate's `policy` step call. */
198
+ export async function findDuplicateInstalls(
199
+ root: string,
200
+ packages: readonly string[],
201
+ ): Promise<readonly DuplicateInstall[]> {
202
+ return duplicateInstalls(await installedCopies(root, packages));
203
+ }
204
+
205
+ /** What a caller injects in place of `findDuplicateInstalls`, so a fixture needs no install. */
206
+ export type DuplicateProbe = (
207
+ root: string,
208
+ packages: readonly string[],
209
+ ) => Promise<readonly DuplicateInstall[]>;
210
+
211
+ /** `<dir>@<version> (resolved from ., packages/i18n)` — one clause per copy. */
212
+ const renderCopy = (root: string, copy: DuplicateCopy): string => {
213
+ const shown = relative(root, copy.dir).replaceAll('\\', '/');
214
+ const dir = shown === '' || shown.startsWith('..') ? copy.dir : shown;
215
+ return `${dir}@${copy.version} (resolved from ${copy.from.join(', ')})`;
216
+ };
217
+
218
+ /** The manifest an origin pins its dependencies in — the file the fix names an edit to. */
219
+ const manifestOf = (from: string): string | undefined =>
220
+ from === CLI_ORIGIN ? undefined : from === '.' ? 'package.json' : `${from}/package.json`;
221
+
222
+ /**
223
+ * The one sentence both findings share: which copies, where each is read from, and why that is
224
+ * an empty registry rather than a version skew. `root` is what turns a store path into the
225
+ * app-relative one an agent can open.
226
+ */
227
+ export function duplicateCause(root: string, duplicate: DuplicateInstall): string {
228
+ const registry = REGISTRY_PACKAGES[duplicate.pkg] ?? 'registry';
229
+ const copies = duplicate.copies.map((copy) => renderCopy(root, copy)).join(' and ');
230
+ return (
231
+ `${duplicate.copies.length} copies of ${duplicate.pkg} are installed: ${copies} — each is ` +
232
+ `its own module instance with its own ${registry}, so what the app registers into one, the ` +
233
+ 'other never sees'
234
+ );
235
+ }
236
+
237
+ /**
238
+ * The command that leaves ONE copy, then the command that re-checks. Two shapes, because the two
239
+ * causes repair differently: copies at different versions are a workspace pinning an older
240
+ * range, and the fix names that manifest and the version to pin; copies at ONE version are two
241
+ * resolutions of one range — a stale nested `node_modules`, or two peer sets in the store — and
242
+ * the only edit that can collapse them is the install itself.
243
+ */
244
+ export function duplicateFix(duplicate: DuplicateInstall, recheck: string): string {
245
+ const newest = duplicate.copies[0];
246
+ const older = duplicate.copies.filter((copy) => copy.version !== newest?.version);
247
+ if (newest !== undefined && older.length > 0) {
248
+ const manifests = [
249
+ ...new Set(older.flatMap((copy) => copy.from.map(manifestOf)).filter(Boolean)),
250
+ ];
251
+ const where = manifests.length === 0 ? 'every package.json that pins it' : manifests.join(', ');
252
+ return `set "${duplicate.pkg}": "${newest.version}" in ${where}, then: bun install && ${recheck}`;
253
+ }
254
+ return `bun install --force # one resolution of ${duplicate.pkg} for every workspace; then: ${recheck}`;
255
+ }
256
+
257
+ /**
258
+ * The finding both steps report. Its own code rather than the registry's own (`X_CATALOG_UNREGISTERED`,
259
+ * `X_PERMISSION_UNKNOWN`): those name a symptom that has a source-level repair, and this names an
260
+ * install with no source-level repair at all — an agent handed the registry's fix edits a file that
261
+ * is already right, re-runs, and is red again.
262
+ */
263
+ export function duplicateFinding(
264
+ root: string,
265
+ duplicate: DuplicateInstall,
266
+ recheck: string,
267
+ ): Finding {
268
+ const at = duplicate.copies
269
+ .flatMap((copy) => copy.from.map(manifestOf))
270
+ .find((manifest): manifest is string => manifest !== undefined);
271
+ return {
272
+ code: 'X_PACKAGE_DUPLICATED',
273
+ cause: duplicateCause(root, duplicate),
274
+ fix: duplicateFix(duplicate, recheck),
275
+ docs: ERROR_DOCS_URL,
276
+ ...(at === undefined ? {} : { at }),
277
+ };
278
+ }
@@ -148,6 +148,11 @@ export const CLI_OWNED_ERROR_CODES = [
148
148
  'X_SECRETS_EDITOR_MISSING',
149
149
  'X_SECRETS_EDIT_FAILED',
150
150
  'X_WORKSPACE_DEP_UNDECLARED',
151
+ // Two module instances of a registry-holding package inside ONE app's `node_modules`. Its own
152
+ // code and not the registry's (`X_CATALOG_UNREGISTERED`, `X_PERMISSION_UNKNOWN`): those carry a
153
+ // source-level fix, and an agent following it here edits a file that is already right — the
154
+ // repair is an install, and only `duplicate-packages.ts` can see that.
155
+ 'X_PACKAGE_DUPLICATED',
151
156
  'X_SHOT_BROWSER_MISSING',
152
157
  // `x shot --island` — one code per way a component's named state fails to become a picture.
153
158
  // The last of the four is the one that gates: it is checked against the expansion computed
@@ -290,6 +295,7 @@ export const CLI_ERROR_TITLES: Readonly<Record<CliOwnedErrorCode, string>> = {
290
295
  X_SECRETS_EDITOR_MISSING: 'no $EDITOR to open the decrypted secrets in',
291
296
  X_SECRETS_EDIT_FAILED: 'the editor exited non-zero, so nothing was resealed',
292
297
  X_WORKSPACE_DEP_UNDECLARED: 'a workspace imports another workspace it does not declare',
298
+ X_PACKAGE_DUPLICATED: 'two copies of one registry-holding framework package are installed',
293
299
  X_SHOT_BROWSER_MISSING: 'x shot found no browser library in the app',
294
300
  X_SHOT_ISLAND_STATES_EMPTY: 'an island states file declares no manifest',
295
301
  X_SHOT_ISLAND_UNPHOTOGRAPHABLE: 'the island never reached a state worth photographing',
@@ -19,12 +19,17 @@ import {
19
19
  registeredLocales,
20
20
  } from '@ultimat3/i18n';
21
21
  import { loadApp } from './app-load';
22
+ import type { DuplicateProbe } from './duplicate-packages';
23
+ import { duplicateCause, duplicateFinding, findDuplicateInstalls } from './duplicate-packages';
22
24
  import { auditApp } from './i18n-audit';
23
25
  import { I18N_INDEX_PATH } from './i18n-index';
24
26
  import type { Finding } from './output';
25
27
  import { findingFrom } from './output';
26
28
  import { CATALOG_ROOT, catalogPath } from './templates/locales';
27
29
 
30
+ /** The one package this check probes for a duplicate: its registry is the one it reads. */
31
+ const I18N_PKG = '@ultimat3/i18n';
32
+
28
33
  /**
29
34
  * What this check needs of a boot. The seam is injected so a fixture can be exactly "the app
30
35
  * loaded and registered nothing" — the shipped shape of the bug — without a temp directory that
@@ -43,6 +48,8 @@ export interface RegistrationInput {
43
48
  readonly extraction: Extraction;
44
49
  readonly ignoreUnused: readonly string[];
45
50
  readonly load?: AppLoader;
51
+ /** The duplicate-install probe; `findDuplicateInstalls` is the production value. */
52
+ readonly duplicates?: DuplicateProbe;
46
53
  }
47
54
 
48
55
  export interface RegistrationReport {
@@ -74,17 +81,39 @@ function unresolvedUsedKeys(input: RegistrationInput, locale: Locale): readonly
74
81
  return report.locales[0]?.missing ?? [];
75
82
  }
76
83
 
84
+ /** What `X_PACKAGE_DUPLICATED`'s fix runs once the install is one copy again. */
85
+ const RECHECK = 'x i18n check --json';
86
+
77
87
  export async function checkRegistration(input: RegistrationInput): Promise<RegistrationReport> {
78
88
  // Importing the app's modules IS the registration, in this process exactly as in the server's.
79
89
  const app = await (input.load ?? loadApp)(input.root);
80
90
 
91
+ // Asked BEFORE the registry is: two copies of `@ultimat3/i18n` in one app are two registries,
92
+ // and every gap below is then a symptom of the install, not of where `defineCatalogs()` sits.
93
+ // Reported even with no gap — the CLI may share the app's index module's copy while a page in
94
+ // another workspace reads the other, which renders `⟦key⟧` under a green gate.
95
+ const duplicate = (await (input.duplicates ?? findDuplicateInstalls)(input.root, [I18N_PKG]))[0];
81
96
  const gaps = catalogRegistrationGaps(input.catalogs);
82
97
  const index = await indexSource(input.root);
83
- const findings: Finding[] = gaps.map((gap) => ({
84
- ...findingFrom(catalogUnregistered(gap)),
85
- ...unregisteredFix(gap.locale, index),
86
- at: catalogPath(gap.locale),
87
- }));
98
+ const findings: Finding[] =
99
+ duplicate === undefined ? [] : [duplicateFinding(input.root, duplicate, RECHECK)];
100
+ for (const gap of gaps) {
101
+ const finding = findingFrom(catalogUnregistered(gap));
102
+ findings.push(
103
+ duplicate === undefined
104
+ ? { ...finding, ...unregisteredFix(gap.locale, index), at: catalogPath(gap.locale) }
105
+ : // The registry's own fix names a source edit — move the `defineCatalogs()` call — that an
106
+ // agent following it performs on a file that is already right (ai-maxxing, 2026-09-05).
107
+ // With a duplicate on disk the cause IS the install, so the finding says so and the fix
108
+ // is the one the duplicate carries.
109
+ {
110
+ ...finding,
111
+ cause: `${duplicateCause(input.root, duplicate)}; ${gap.missing.length} of ${catalogPath(gap.locale)}'s ${gap.shipped} key(s) registered into the copy the CLI does not read`,
112
+ fix: findings[0]?.fix ?? finding.fix,
113
+ at: catalogPath(gap.locale),
114
+ },
115
+ );
116
+ }
88
117
  let unregistered = gaps.reduce((sum, gap) => sum + gap.missing.length, 0);
89
118
  let locales = gaps.length;
90
119
 
package/src/index.ts CHANGED
@@ -361,6 +361,8 @@ export {
361
361
  skipReasonFor,
362
362
  writeStaticReport,
363
363
  } from './static-report';
364
+ export type { StyleBundle, StyleChunk } from './style-bundle';
365
+ export { STYLE_BASE_PATH, styleBundle } from './style-bundle';
364
366
  export type { TestCounts } from './test-counts';
365
367
  export { countsOf } from './test-counts';
366
368
  export type { TestFile } from './test-select';
@@ -1,12 +1,13 @@
1
1
  // The island chunk table: every `*.island.tsx` in the app compiled as its OWN bundle entry point,
2
- // content-hashed, plus the resolver that turns a page's `src` specifier into the URL its
2
+ // addressed by a hash of its SOURCE GRAPH (`graphHash` — `Bun.build`'s minified output is not
3
+ // byte-deterministic), plus the resolver that turns a page's `src` specifier into the URL its
3
4
  // `data-x-entry` carries. One entry point per island is axiom 6 made mechanical — the page's graph
4
5
  // never reaches an island, so a `site/` document stays at 0kb whatever the island imports.
5
6
 
6
7
  // Bun ships no path API. `posix` does the specifier arithmetic (an app-relative route file is
7
8
  // POSIX by construction), `join`/`basename` the filesystem side.
8
9
  import { basename, join, posix, relative, sep } from 'node:path';
9
- import { renderThrowable } from '@ultimat3/core';
10
+ import { frameworkVersion, renderThrowable } from '@ultimat3/core';
10
11
  import { ISLAND_EXTENSION, IslandInvalidError, islandModuleId } from '@ultimat3/render';
11
12
  import { contentHash } from '@ultimat3/render/server';
12
13
  import { IslandBuildFailedError } from './errors';
@@ -32,7 +33,10 @@ export interface IslandChunk {
32
33
  readonly file: string;
33
34
  /** `islandModuleId` of the filename — the id the document, the budget and a finding all name. */
34
35
  readonly moduleId: string;
35
- /** Immutable, content-addressed URL. What `data-x-entry` carries and what a route serves. */
36
+ /**
37
+ * Immutable, source-addressed URL. What `data-x-entry` carries and what a route serves — stable
38
+ * for as long as the sources, the framework version and the Bun version are. See `graphHash`.
39
+ */
36
40
  readonly url: string;
37
41
  /** The built JavaScript. Held in memory so `x dev` and the container serve without a disk hop. */
38
42
  readonly code: string;
@@ -101,30 +105,138 @@ async function buildOne(root: string, file: string): Promise<IslandChunk> {
101
105
  // `x dev` serves the same chunk the container does, and bytes that depend on the ambient
102
106
  // NODE_ENV are a content hash and a byte budget measured on a build nobody ships.
103
107
  define: { 'process.env.NODE_ENV': '"production"' },
108
+ // The fourth, and it is asked for its INPUT list rather than its output: `sourcesContent` is
109
+ // the whole module graph this chunk was built from, which is the only stable identity a
110
+ // chunk has. See `graphHash`. Measured on 1.4.0 against a 131 kB island: 277ms with it and
111
+ // 276ms without, so the map costs nothing worth naming.
112
+ sourcemap: 'external',
104
113
  });
105
114
  } catch (error) {
106
115
  throw new IslandBuildFailedError({ file, logs: describeBuildError(error) });
107
116
  }
108
117
  const output = built.outputs.find((artifact) => artifact.kind === 'entry-point');
109
- if (!built.success || output === undefined) {
118
+ const map = built.outputs.find((artifact) => artifact.kind === 'sourcemap');
119
+ if (!built.success || output === undefined || map === undefined) {
110
120
  throw new IslandBuildFailedError({
111
121
  file,
112
122
  logs: built.logs.map((log) => String(log)).join('; '),
113
123
  });
114
124
  }
115
- const code = await output.text();
125
+ const code = stripDebugId(await output.text());
126
+ const hash = graphHash(file, await map.text());
116
127
  const moduleId = islandModuleId(basename(file));
117
128
  return {
118
129
  file,
119
130
  moduleId,
120
- // Hashed with the framework's own `contentHash`, the function that already stamps an ETag and
121
- // a precache revision — one identity for a byte string, not a third.
122
- url: `${ISLAND_BASE_PATH}/${moduleId}-${contentHash(code)}.js`,
123
- code,
131
+ url: `${ISLAND_BASE_PATH}/${moduleId}-${hash}.js`,
132
+ // The FIRST bytes this process emitted for these inputs, so a URL served `immutable` answers
133
+ // one byte string for as long as the process lives. Without it `x dev` re-mints the chunk on
134
+ // every watcher tick and a browser holding the previous one under `max-age=31536000` has two
135
+ // different files at one address.
136
+ code: stableCode(file, hash, code),
124
137
  bytes: new TextEncoder().encode(code).byteLength,
125
138
  };
126
139
  }
127
140
 
141
+ /**
142
+ * `sourcemap: 'external'` appends `//# debugId=<hex>` to the chunk. It is a pointer to a map this
143
+ * framework does not serve, so it is removed rather than shipped — and removing it makes the
144
+ * emitted bytes identical to what the same build produced before the map was asked for, which is
145
+ * what keeps `bytes` a budget number and not a build-flag artefact. `slice`, never a `replace` with
146
+ * an empty replacement — `bun run sql-literal-copies` refuses that shape anywhere but `db/sql.ts`.
147
+ */
148
+ const DEBUG_ID_COMMENT = '\n//# debugId=';
149
+
150
+ function stripDebugId(code: string): string {
151
+ const at = code.lastIndexOf(DEBUG_ID_COMMENT);
152
+ return at === -1 ? code : code.slice(0, at);
153
+ }
154
+
155
+ /**
156
+ * The chunk's identity, computed from what went IN rather than from what came out.
157
+ *
158
+ * `Bun.build` is not byte-deterministic under `minify`. Measured on 1.4.0, one entry point, no
159
+ * source file touched: a 131,589-byte island alternated between two outputs of IDENTICAL length
160
+ * differing only in minified identifier names (`var ca=Object.defineProperty` against
161
+ * `var la=…`) — roughly one build in ten, which is a race in the renamer and not anything a caller
162
+ * can order. Hashing that output made the URL flap: ten distinct `session-console-*.js` names in
163
+ * ten minutes, so a service worker's precache manifest named a chunk that already 404ed and a
164
+ * browser's `immutable` cache never hit on a 131 kB download. Twelve consecutive builds hash
165
+ * identically here.
166
+ *
167
+ * `sourcesContent`, hashed per file and SORTED, so the identity is independent of the order the
168
+ * bundler happened to visit the graph in. The PATHS are deliberately not in it: they are absolute
169
+ * on the build machine and would make a chunk built in a container disagree with the same chunk
170
+ * built on a laptop for no difference a browser could observe. `file` is, so two islands with
171
+ * byte-identical sources under different names stay two chunks; the framework version and the Bun
172
+ * version are, because both decide the emitted bytes while no source file moves — an upgrade must
173
+ * mint a new URL rather than leave a stale chunk pinned in a browser for a year.
174
+ *
175
+ * What this gives up, stated plainly: the URL is source-addressed, not byte-addressed, so two
176
+ * processes building the same sources can serve two byte-strings at one URL. They are the same
177
+ * program under different local identifier names. That is the trade a nondeterministic bundler
178
+ * forces, and the alternative — `minify: { identifiers: false }`, which IS deterministic — was
179
+ * measured at 193,590 bytes against 131,649, +47% raw and +20% gzipped, on every island of every
180
+ * app. Delete this the day `Bun.build` is deterministic.
181
+ */
182
+ function graphHash(file: string, map: string): string {
183
+ const parsed: unknown = JSON.parse(map);
184
+ const contents = sourcesContentOf(parsed);
185
+ if (contents === undefined) {
186
+ throw new IslandBuildFailedError({
187
+ file,
188
+ logs: 'the bundler emitted a source map with no sourcesContent, so the chunk has no stable identity',
189
+ });
190
+ }
191
+ const graph = contents.map((source) => contentHash(source)).sort();
192
+ return contentHash([file, frameworkVersion(), Bun.version, ...graph].join('\u0000'));
193
+ }
194
+
195
+ /**
196
+ * `sourcesContent`, read the way `aggregatedErrors` below reads `errors`: narrowed first,
197
+ * dereferenced inside a `try`, `undefined` for anything that is not a full list of strings. A
198
+ * partial list is refused rather than padded — a graph with holes in it hashes two different
199
+ * islands the same.
200
+ */
201
+ function sourcesContentOf(value: unknown): readonly string[] | undefined {
202
+ if (typeof value !== 'object' || value === null) return undefined;
203
+ try {
204
+ const held: unknown = (value as Record<string, unknown>)['sourcesContent'];
205
+ if (!Array.isArray(held) || held.length === 0) return undefined;
206
+ return held.every((one: unknown) => typeof one === 'string')
207
+ ? (held as readonly string[])
208
+ : undefined;
209
+ } catch {
210
+ return undefined;
211
+ }
212
+ }
213
+
214
+ /**
215
+ * The code this process already emitted for these inputs, or the code it just built.
216
+ *
217
+ * Keyed by PATH and validated by the input hash, `transformIslandTsx`'s cache's shape and for its
218
+ * reason: one entry per island bounds the map by the island count, which is the only quantity that
219
+ * should bound it, and an entry whose hash no longer matches is replaced rather than served.
220
+ */
221
+ const emitted = new Map<string, { readonly graph: string; readonly code: string }>();
222
+
223
+ /** Test seam: the table is process-global because the dev server it serves is too. */
224
+ export function clearIslandChunkCache(): void {
225
+ emitted.clear();
226
+ }
227
+
228
+ /**
229
+ * `graph`, never `hash`: `bun run secret-compare` reads the NAME of a comparison's operands, and a
230
+ * value called `hash` is a digest an attacker may be probing. This one is a build input's
231
+ * identity — the same reason `pr-threads.ts` calls a review state `wanted`.
232
+ */
233
+ function stableCode(file: string, graph: string, code: string): string {
234
+ const hit = emitted.get(file);
235
+ if (hit !== undefined && hit.graph === graph) return hit.code;
236
+ emitted.set(file, { graph, code });
237
+ return code;
238
+ }
239
+
128
240
  /**
129
241
  * The bundler's own diagnostics, kept verbatim. An `AggregateError` holds one entry per unresolved
130
242
  * import or syntax error, and flattening them is what puts the line number in the cause instead of
@@ -13,9 +13,9 @@ import {
13
13
  islandModuleId,
14
14
  SURFACES,
15
15
  } from '@ultimat3/render';
16
- import { stylesFor } from '@ultimat3/render/server';
17
16
  import type { IslandShotTarget, IslandState } from '@ultimat3/testing';
18
17
  import { harnessScript } from './island-harness-script';
18
+ import { styleBundle } from './style-bundle';
19
19
 
20
20
  /** Where the harness lives in `x dev`'s own namespace, so no app route can shadow it. */
21
21
  export const ISLAND_HARNESS_PATH = '/_x/island';
@@ -43,8 +43,13 @@ export function surfaceOf(island: string): Surface | null {
43
43
  * off because a picture taken mid-transition is a picture of a moment no user experiences; the
44
44
  * caret is invisible because a focused input blinks and two otherwise identical runs then differ.
45
45
  * Colours are semantic tokens, never literals — the app's own global layer defines them.
46
+ *
47
+ * Exported so `x dev` can hash it into `style-src`: it is emitted INLINE, so a policy that does
48
+ * not name it blocks the frame under an enforced CSP. It was never hashed at all until
49
+ * 2026-09-06 — invisible because `x dev` sends the policy report-only, which is exactly how the
50
+ * hydration runtime shipped blocked once already.
46
51
  */
47
- const FRAME_STYLE = `
52
+ export const FRAME_STYLE = `
48
53
  *,*::before,*::after{animation:none !important;transition:none !important;
49
54
  scroll-behavior:auto !important;caret-color:transparent !important}
50
55
  html{background:rgb(var(--color-bg) / 1)}
@@ -74,13 +79,15 @@ export function harnessPage(input: HarnessPageInput): string {
74
79
  entry: input.entry,
75
80
  props: input.state.props,
76
81
  };
77
- const css = stylesFor(surfaceOf(input.target.island));
82
+ // The same content-hashed file every real document links, so the picture is taken against the
83
+ // bytes a visitor gets — and the harness stops carrying 157 kB of inline CSS of its own.
84
+ const href = styleBundle().hrefFor(surfaceOf(input.target.island));
78
85
  return [
79
86
  '<!doctype html>',
80
87
  `<html lang="en" data-theme="${input.target.theme}">`,
81
88
  '<head><meta charset="utf-8">',
82
89
  `<title>${input.target.name} · ${input.target.state} · ${input.target.theme}</title>`,
83
- css.length === 0 ? '' : `<style>${css}</style>`,
90
+ href === undefined ? '' : `<link rel="stylesheet" href="${href}">`,
84
91
  `<style>${FRAME_STYLE}</style>`,
85
92
  // Before the body and before every module script, which is the only ordering in which the
86
93
  // seal can catch a component's first request.
package/src/mcp-errors.ts CHANGED
@@ -52,6 +52,8 @@ const CLI_FIXES: Readonly<Record<CliErrorCode, string>> = {
52
52
  'x verify --json # the finding names the fix line and the path it cites',
53
53
  X_WORKSPACE_DEP_UNDECLARED:
54
54
  'x verify --json # the package-shape finding carries the dependency line to add',
55
+ X_PACKAGE_DUPLICATED:
56
+ 'x i18n check --json # the finding names both copies and the package.json to pin',
55
57
  X_SHOT_BROWSER_MISSING: 'bun add -d puppeteer-core',
56
58
  // The four island-capture codes. Each one's real repair is an edit to the app's own states file
57
59
  // or component, which no command can perform — so each names the command that REPRODUCES it with
package/src/mcp-host.ts CHANGED
@@ -3,6 +3,9 @@
3
3
  // the gate. The description half is the framework's own `frameworkIntrospection`, so nothing here
4
4
  // is a second catalog of routes, entities, actions, queries or jobs.
5
5
 
6
+ // why: Bun exposes no path API. Every use here builds a path this host then hands to `Bun.file`
7
+ // or prints inside a `fix:` an operator runs — the dev log, a per-role log, the committed
8
+ // manifest — and string concatenation would answer a different path on a Windows checkout.
6
9
  import { join } from 'node:path';
7
10
  import { agentActor, isUltimateError, renderThrowable, UltimateError } from '@ultimat3/core';
8
11
  import type { DbClient } from '@ultimat3/db';