cursedbelt 4.3.0 β†’ 4.4.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.
Files changed (39) hide show
  1. package/dist/react/components/Autocomplete.d.ts.map +1 -1
  2. package/dist/react/components/Autocomplete.js +1 -1
  3. package/dist/react/components/Autocomplete.js.map +1 -1
  4. package/dist/react/components/ComboboxField.d.ts.map +1 -1
  5. package/dist/react/components/ComboboxField.js +1 -1
  6. package/dist/react/components/ComboboxField.js.map +1 -1
  7. package/dist/react/media-gallery/MediaGallery.d.ts +9 -2
  8. package/dist/react/media-gallery/MediaGallery.d.ts.map +1 -1
  9. package/dist/react/media-gallery/MediaGallery.js +12 -4
  10. package/dist/react/media-gallery/MediaGallery.js.map +1 -1
  11. package/dist/scripts/guardrailsEnforce.d.ts.map +1 -1
  12. package/dist/styles-areas/analytics.css +1 -1
  13. package/dist/styles-areas/data-table.css +1 -1
  14. package/dist/styles-areas/fields.css +1 -1
  15. package/dist/styles-areas/media-gallery.css +1 -1
  16. package/dist/styles-areas/wizard.css +1 -1
  17. package/package.json +50 -6
  18. package/scripts/demoServer.ts +211 -0
  19. package/scripts/gate.ts +101 -0
  20. package/scripts/guardrailsEnforce.spec.ts +69 -0
  21. package/scripts/guardrailsEnforce.ts +59 -19
  22. package/scripts/paths.ts +1 -1
  23. package/src/demoFixture.spec.ts +54 -10
  24. package/src/demoStaticServer.spec.ts +169 -0
  25. package/src/publishShape.spec.ts +79 -0
  26. package/src/react/components/Autocomplete.tsx +11 -1
  27. package/src/react/components/ComboboxField.spec.tsx +12 -0
  28. package/src/react/components/ComboboxField.tsx +11 -1
  29. package/src/react/media-gallery/MediaGallery.spec.tsx +23 -0
  30. package/src/react/media-gallery/MediaGallery.tsx +26 -9
  31. package/src/styles-areas/analytics.css +1 -1
  32. package/src/styles-areas/data-table.css +1 -1
  33. package/src/styles-areas/fields.css +1 -1
  34. package/src/styles-areas/media-gallery.css +1 -1
  35. package/src/styles-areas/wizard.css +1 -1
  36. package/src/testFilesRunInParallel.spec.ts +3 -3
  37. package/src/typecheckCachesAreSeparate.spec.ts +2 -2
  38. package/src/verifyGraph.spec.ts +75 -108
  39. package/scripts/verify.ts +0 -296
@@ -4,6 +4,8 @@ import { resolve } from 'node:path';
4
4
  import {
5
5
  DEMO_COMMAND,
6
6
  DEMO_DEV_SCRIPT,
7
+ DEMO_DIST_DIR,
8
+ DEMO_HOST,
7
9
  DEMO_PACKAGE_DIR,
8
10
  DEMO_PORT,
9
11
  DEMO_ROOT,
@@ -12,7 +14,8 @@ import {
12
14
  } from '../demo/devServer';
13
15
  import { DEMO_TSCONFIG, derivePaths, readDemoTsconfig, renderPathsBlock } from '../demo/generatePaths';
14
16
  import pkg from '../package.json';
15
- import { STAGES } from '../scripts/verify';
17
+ // The declared graph, read as data β€” there is no scheduler in this repo to import from.
18
+ const STAGES: readonly { script: string }[] = (pkg as { gate?: { stages?: { script: string }[] } }).gate?.stages ?? [];
16
19
 
17
20
  /**
18
21
  * πŸ”΄ The e2e suite must never be able to sit green while it drives nothing.
@@ -56,19 +59,38 @@ describe('the e2e fixture exists', () => {
56
59
  }
57
60
  expect(pkg.scripts.demo, 'the demo script must start vite on the demo config').toInclude('demo/vite.config.ts');
58
61
  // Playwright drives the BUILT bundle, not the dev server β€” see demo/devServer.ts.
59
- expect(pkg.scripts['demo:serve'], 'demo:serve must build before it previews').toInclude('demo:build');
60
- expect(pkg.scripts['demo:serve']).toInclude('vite preview');
62
+ expect(pkg.scripts['demo:serve'], 'demo:serve must build before it serves').toInclude('demo:build');
63
+ // πŸ”΄ And it serves it from OUR server, never `vite preview` β€” which under Bun 1.4.2
64
+ // exits 1 with ERR_STREAM_WRITE_AFTER_END when a Playwright worker hangs up
65
+ // mid-response, reddening this gate about one run in four (task 388, 2026-09-18).
66
+ // Going back to it is a one-word edit, so it is a one-line assertion.
67
+ expect(pkg.scripts['demo:serve'], 'demo:serve must run scripts/demoServer.ts').toInclude('scripts/demoServer.ts');
68
+ expect(pkg.scripts['demo:serve'], '`vite preview` dies on a client hang-up under Bun').not.toInclude(
69
+ 'vite preview',
70
+ );
61
71
  });
62
72
 
63
- it('both servers serve the port the specs are pointed at', () => {
73
+ it('every server serves the port the specs are pointed at', () => {
64
74
  // vite silently walks to the next free port unless `strictPort` is set, which is
65
75
  // the same outage wearing a costume: a server that starts perfectly well and a
66
- // suite that times out against the port nobody is listening on.
76
+ // suite that times out against the port nobody is listening on. `Bun.serve` throws
77
+ // EADDRINUSE rather than walking, and `demoStaticServer.spec.ts` proves it does.
67
78
  const viteConfig = readFileSync(resolve(DEMO_ROOT, 'vite.config.ts'), 'utf8');
68
- expect(DEMO_URL).toBe(`http://localhost:${DEMO_PORT}`);
69
- // Once for `server` (dev), once for `preview` (what e2e drives).
70
- expect(viteConfig.match(/port: DEMO_PORT/g) ?? [], 'both server and preview must pin the port').toHaveLength(2);
71
- expect(viteConfig.match(/strictPort: true/g) ?? [], 'both must set strictPort').toHaveLength(2);
79
+ expect(DEMO_URL).toBe(`http://${DEMO_HOST}:${DEMO_PORT}`);
80
+ // πŸ”΄ A literal address, never `localhost`. Measured 2026-09-18: two demo servers
81
+ // bound 4318 at the same time β€” one on `[::1]`, one on `127.0.0.1` β€” because
82
+ // `localhost` resolves to both and each process took whichever its resolver offered
83
+ // first. Neither bind collided, and which one a client reached was its own
84
+ // resolution order. A name here is a gate that can silently test a stale bundle.
85
+ expect(DEMO_HOST, 'the fixture must not be reached through a name that resolves twice').toBe('127.0.0.1');
86
+ expect(viteConfig, 'the dev server must bind the same one address').toInclude('host: DEMO_HOST');
87
+ // Once, for `server` β€” the dev server a human runs. There is no `preview` block any
88
+ // more, and a second match here would mean one grew back unread.
89
+ expect(viteConfig.match(/port: DEMO_PORT/g) ?? [], 'the dev server must pin the port').toHaveLength(1);
90
+ expect(viteConfig.match(/strictPort: true/g) ?? [], 'and must set strictPort').toHaveLength(1);
91
+ expect(viteConfig, 'the preview block is dead config now β€” e2e is served by scripts/demoServer.ts').not.toInclude(
92
+ 'preview:',
93
+ );
72
94
  });
73
95
 
74
96
  it('every page the specs navigate to is a real demo entry point', () => {
@@ -98,13 +120,35 @@ describe('the e2e fixture exists', () => {
98
120
  expect(unlisted, `demo/vite.config.ts's rollup input omits:\n ${unlisted.join('\n ')}`).toEqual([]);
99
121
  });
100
122
 
123
+ it('πŸ”΄ never reuses a server somebody else started', () => {
124
+ // The gate's meaning must not depend on what the previous agent in this checkout
125
+ // left running. `reuseExistingServer: !process.env.CI` meant exactly that: `CI` is
126
+ // unset on this Mac, so a stale listener on 4318 was silently adopted and the demo
127
+ // build was skipped. Two agents measured the same command at 34.8s and 45.0s on
128
+ // 2026-09-18 for that reason β€” the fast one tested a bundle nobody had rebuilt.
129
+ const config = readFileSync(resolve(e2eDir, 'playwright.config.ts'), 'utf8');
130
+ expect(config, 'the webServer must always be ours').toInclude('reuseExistingServer: false');
131
+ expect(config, 'reuse must not be conditional on an env var this machine never sets').not.toMatch(
132
+ /reuseExistingServer:\s*!?\s*process\.env/,
133
+ );
134
+ });
135
+
136
+ it('serves exactly the directory the demo build writes', () => {
137
+ // `scripts/demoServer.ts` serves DEMO_DIST_DIR; vite writes to `outDir` resolved
138
+ // against its `root`. A custom `outDir` would leave the server pointed at an empty
139
+ // directory and every spec 404ing against a 200-answering process.
140
+ const viteConfig = readFileSync(resolve(DEMO_ROOT, 'vite.config.ts'), 'utf8');
141
+ expect(DEMO_DIST_DIR).toBe(resolve(DEMO_ROOT, 'dist'));
142
+ expect(viteConfig, 'demoServer.ts assumes vite’s default outDir').not.toInclude('outDir');
143
+ });
144
+
101
145
  it('verify actually runs the suite', () => {
102
146
  // Everything above proves the fixture resolves. Only this proves anybody looks:
103
147
  // without it, `e2e` drops back out of the gate and this whole file becomes a
104
148
  // comment about a suite nothing runs.
105
149
  //
106
150
  // πŸ”΄ `verify` stopped being an `&&` chain on 2026-09-15 and became a dependency
107
- // graph (`scripts/verify.ts`), so "the script mentions e2e" is no longer where the
151
+ // graph (the `gate` block in `package.json`), so "the script mentions e2e" is no longer where the
108
152
  // answer lives. Asking the graph is strictly stronger than asking the string was:
109
153
  // `verifyGraph.spec.ts` additionally pins that every OTHER stage of the old chain
110
154
  // is still in it, which the `toInclude` never checked for any of them.
@@ -0,0 +1,169 @@
1
+ /**
2
+ * πŸ”΄ The demo's static server, and specifically the fault that used to end the gate.
3
+ *
4
+ * `bun run verify` went red about one run in four on an unmodified tree (task 388,
5
+ * 2026-09-18): `vite preview` β€” connect on Bun's `node:http` shim β€” threw
6
+ * `ERR_STREAM_WRITE_AFTER_END` out of `advanceResponsePipeline` when a Playwright worker
7
+ * hung up on a response that was still being written, and the throw landed after the
8
+ * handler had returned, where no middleware could catch it. The process exited 1, the
9
+ * `webServer` never came ready, and 138 innocent specs went red 120 seconds later.
10
+ *
11
+ * So the first assertion here is the one that matters: abort requests mid-flight and prove
12
+ * the server is still serving afterwards. Everything else in this file is the ordinary
13
+ * contract that makes the fixture pages reachable at all.
14
+ */
15
+ import { describe, expect, it } from 'bun:test';
16
+ import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
17
+ import { tmpdir } from 'node:os';
18
+ import { join, resolve } from 'node:path';
19
+ import { DEMO_HOST, DEMO_URL } from '../demo/devServer';
20
+ import { isPeerHangup, resolveRequestPath, startDemoServer } from '../scripts/demoServer';
21
+
22
+ /**
23
+ * A throwaway `dist` shaped like the one `bun run demo:build` writes. `$TMPDIR` is already
24
+ * redirected into this repo's scratch root by `scripts/testTmpRoot.ts` in the preload.
25
+ */
26
+ function fixtureRoot(): string {
27
+ const root = mkdtempSync(join(tmpdir(), 'demo-server-'));
28
+ mkdirSync(resolve(root, 'assets'), { recursive: true });
29
+ writeFileSync(resolve(root, 'index.html'), '<!doctype html><title>kitchen sink</title>');
30
+ writeFileSync(resolve(root, 'table-layout.html'), '<!doctype html><title>table layout</title>');
31
+ // Big enough that an abort lands mid-body rather than after the whole thing arrived.
32
+ writeFileSync(resolve(root, 'assets', 'app.js'), `export const pad = '${'x'.repeat(4_000_000)}';\n`);
33
+ writeFileSync(resolve(root, 'assets', 'app.css'), ':root { color-scheme: dark; }\n');
34
+ return root;
35
+ }
36
+
37
+ /** Runs `body` against a server on an ephemeral port, and always stops it. */
38
+ async function withServer(body: (base: string) => Promise<void>): Promise<void> {
39
+ const server = startDemoServer({ root: fixtureRoot(), port: 0, hostname: '127.0.0.1' });
40
+ try {
41
+ await body(`http://127.0.0.1:${server.port}`);
42
+ } finally {
43
+ await server.stop(true);
44
+ }
45
+ }
46
+
47
+ describe('πŸ”΄ a client that hangs up is a non-event', () => {
48
+ it('keeps serving after a burst of aborted requests', async () => {
49
+ await withServer(async (base) => {
50
+ expect((await fetch(`${base}/`)).status, 'the server must be up before we abort at it').toBe(200);
51
+
52
+ // Eight, because that is how many Playwright workers this repo runs, and each one
53
+ // abandons in-flight subresources on every navigation.
54
+ for (let i = 0; i < 8; i++) {
55
+ const control = new AbortController();
56
+ const inFlight = fetch(`${base}/assets/app.js`, { signal: control.signal });
57
+ // Abort on the next turn of the loop: late enough that the response is being
58
+ // written, early enough that the body has not finished.
59
+ queueMicrotask(() => control.abort());
60
+ await inFlight.then(
61
+ async (response) => {
62
+ // If it won the race, drain it and abort during the read instead.
63
+ control.abort();
64
+ await response.text().catch(() => undefined);
65
+ },
66
+ () => undefined,
67
+ );
68
+ }
69
+
70
+ // The whole point. Under `vite preview` the process was gone by now.
71
+ const after = await fetch(`${base}/`);
72
+ expect(after.status, 'the server died on a client hang-up β€” the exact task-388 bug').toBe(200);
73
+ expect(await after.text()).toInclude('kitchen sink');
74
+ });
75
+ });
76
+
77
+ it('names the peer faults it tolerates, and nothing else', () => {
78
+ // πŸ”΄ A blanket handler would keep a broken server alive and let the gate measure a
79
+ // corpse that answers 200. The list is closed; this is the negative control.
80
+ expect(isPeerHangup({ code: 'ERR_STREAM_WRITE_AFTER_END' })).toBe(true);
81
+ expect(isPeerHangup({ code: 'ECONNRESET' })).toBe(true);
82
+ expect(isPeerHangup({ code: 'EPIPE' })).toBe(true);
83
+ expect(isPeerHangup(Object.assign(new Error('aborted'), { name: 'AbortError' }))).toBe(true);
84
+ expect(isPeerHangup(new TypeError('x is not a function'))).toBe(false);
85
+ expect(isPeerHangup({ code: 'ENOSPC' })).toBe(false);
86
+ expect(isPeerHangup(null)).toBe(false);
87
+ expect(isPeerHangup('ECONNRESET')).toBe(false);
88
+ });
89
+ });
90
+
91
+ describe('the pages the specs navigate to', () => {
92
+ it('serves the fixture pages, with the right content type and no caching', async () => {
93
+ await withServer(async (base) => {
94
+ const index = await fetch(`${base}/`);
95
+ expect(index.status).toBe(200);
96
+ expect(index.headers.get('content-type')).toInclude('text/html');
97
+ // πŸ”΄ A cached bundle is a gate reading pre-checkout code, which is the trap this
98
+ // whole change exists to close.
99
+ expect(index.headers.get('cache-control')).toInclude('no-store');
100
+
101
+ const page = await fetch(`${base}/table-layout.html`);
102
+ expect(page.status).toBe(200);
103
+ expect(await page.text()).toInclude('table layout');
104
+
105
+ const css = await fetch(`${base}/assets/app.css`);
106
+ expect(css.status).toBe(200);
107
+ expect(css.headers.get('content-type')).toInclude('text/css');
108
+ });
109
+ });
110
+
111
+ it('404s what it does not have, rather than answering with index.html', async () => {
112
+ // A SPA fallback here would make `demoFixture.spec.ts`'s "every page the specs
113
+ // navigate to is real" assertion unfalsifiable at runtime: a typo'd fixture URL
114
+ // would load the kitchen sink and fail on a missing selector instead.
115
+ await withServer(async (base) => {
116
+ expect((await fetch(`${base}/no-such-page.html`)).status).toBe(404);
117
+ expect((await fetch(`${base}/assets/`)).status, 'no directory listings').toBe(404);
118
+ });
119
+ });
120
+ });
121
+
122
+ describe('what it refuses to serve', () => {
123
+ const root = '/tmp/demo-dist';
124
+
125
+ it('resolves the shapes the fixture actually uses', () => {
126
+ expect(resolveRequestPath(root, '/../../etc/passwd')).toBeNull();
127
+ expect(resolveRequestPath(root, '/%2e%2e/%2e%2e/etc/passwd')).toBeNull();
128
+ expect(resolveRequestPath(root, '/%zz')).toBeNull();
129
+ expect(resolveRequestPath(root, '/nope.html')).toBeNull();
130
+ });
131
+
132
+ it('cannot be walked out of its root even through an encoded traversal', async () => {
133
+ // The pure check above says `null`; this says the socket agrees, because a server
134
+ // that resolved correctly and then read the file anyway would pass the first.
135
+ await withServer(async (base) => {
136
+ for (const path of ['/../package.json', '/%2e%2e/package.json', '/..%2fpackage.json']) {
137
+ const response = await fetch(`${base}${path}`);
138
+ expect(await response.text(), `${path} escaped the demo dist`).not.toInclude('cursedbelt');
139
+ }
140
+ });
141
+ });
142
+ });
143
+
144
+ describe('the port', () => {
145
+ it('πŸ”΄ binds one literal address, so a second server cannot hide beside it', async () => {
146
+ // Measured 2026-09-18: with `hostname: 'localhost'` two processes bound 4318 at once,
147
+ // one on `[::1]` and one on `127.0.0.1`, and NEITHER raised EADDRINUSE β€” `localhost`
148
+ // resolves to both, so they were never competing for the same socket. The gate would
149
+ // then test whichever one the client happened to reach.
150
+ const server = startDemoServer({ root: fixtureRoot(), port: 0 });
151
+ try {
152
+ expect(server.hostname, 'a resolvable NAME here is two sockets pretending to be one').toBe(DEMO_HOST);
153
+ expect(DEMO_URL).toInclude(DEMO_HOST);
154
+ } finally {
155
+ await server.stop(true);
156
+ }
157
+ });
158
+
159
+ it('πŸ”΄ refuses a taken port instead of walking to the next one', async () => {
160
+ // The same rule `strictPort: true` gave the vite servers: a fixture that quietly
161
+ // moves to 4319 is a suite that times out against the port it was pointed at.
162
+ const first = startDemoServer({ root: fixtureRoot(), port: 0, hostname: '127.0.0.1' });
163
+ try {
164
+ expect(() => startDemoServer({ root: fixtureRoot(), port: first.port, hostname: '127.0.0.1' })).toThrow();
165
+ } finally {
166
+ await first.stop(true);
167
+ }
168
+ });
169
+ });
@@ -52,6 +52,31 @@ const typesThatAreNotDeclarations = (exports: Record<string, unknown>): string[]
52
52
  .filter(([, target]) => !target.endsWith('.d.ts'))
53
53
  .map(([subpath, target]) => `${subpath} β†’ ${target}`);
54
54
 
55
+ /**
56
+ * Every `[subpath, condition, target]` this manifest resolves OUTSIDE `dist/`.
57
+ *
58
+ * `./dist/` is deliberately excluded: those are `scripts/checkDistExports.ts`' job, and it
59
+ * has to be a build step because `dist/` is the one directory no spec here may open
60
+ * (`src/verifyGraph.spec.ts` asserts it, because `build` runs beside `test`). Everything
61
+ * else is a file that is already on disk when the tests run, so it belongs here.
62
+ *
63
+ * A string-valued export (`'./styles.css': './src/styles.css'`) is one target under the
64
+ * implicit `default` condition β€” 48 of this manifest's entries are that shape.
65
+ */
66
+ const targetsOutsideDist = (exports: Record<string, unknown>): [string, string, string][] => {
67
+ const out: [string, string, string][] = [];
68
+ for (const [subpath, entry] of Object.entries(exports)) {
69
+ const conditions: Record<string, unknown> =
70
+ typeof entry === 'string' ? { default: entry } : typeof entry === 'object' && entry !== null ? (entry as Record<string, unknown>) : {};
71
+ for (const [condition, target] of Object.entries(conditions)) {
72
+ if (typeof target === 'string' && target.startsWith('./') && !target.startsWith('./dist/')) {
73
+ out.push([subpath, condition, target]);
74
+ }
75
+ }
76
+ }
77
+ return out;
78
+ };
79
+
55
80
  describe('publish shape', () => {
56
81
  it('ships every directory its exports resolve into', () => {
57
82
  const files = (pkg as { files?: string[] }).files ?? [];
@@ -147,7 +172,61 @@ describe('publish shape', () => {
147
172
  expect(typesThatAreNotDeclarations({ './a': { types: './dist/a.js' } })).toHaveLength(1);
148
173
  });
149
174
 
175
+ it('πŸ”΄ points every non-dist export condition at a file that EXISTS', async () => {
176
+ // ── The third of three ways this manifest can lie, and the only one nothing watched ──
177
+ // `publishShape` above proves an export's ROOT directory is in `files`;
178
+ // `scripts/checkDistExports.ts` proves every `./dist/` target was emitted. Between
179
+ // them sits the condition a consumer actually RUNS β€” `bun`/`source`/`default`, which
180
+ // for this package is raw `./src/**` and `./scripts/guardrailsEnforce.ts` β€” and
181
+ // nothing proved those files are there. `files` ships whole directories, so renaming
182
+ // or moving one leaves the root shipped, the tarball valid, and the target dangling.
183
+ //
184
+ // It is not hypothetical and it is not ours to survive: since task 202 folded
185
+ // `--baseline` into the scanner, SIX apps run
186
+ // `bun node_modules/cursedbelt/scripts/guardrailsEnforce.ts …` inside their own
187
+ // `verify`. That path and this manifest's `./guardrails` conditions name the same
188
+ // file, so the day it moves, six repos go red in a file none of them wrote β€” the
189
+ // exact three-repos-away failure that tasks 172 and 219 each cost a session to find.
190
+ //
191
+ // The 48 string-valued CSS entries are the sharper half: they are GENERATED
192
+ // (`bun run styles:areas`) and have no second condition to fall through to, so a
193
+ // generator that stops emitting one is an unresolvable import for every consumer
194
+ // with nothing red on this side at all.
195
+ const repo = `${import.meta.dir}/..`;
196
+ const missing: string[] = [];
197
+ for (const [subpath, condition, target] of targetsOutsideDist(pkg.exports as unknown as Record<string, unknown>)) {
198
+ // πŸ”΄ Bun.file, NOT node:fs β€” the preload virtualizes node:fs and an existsSync
199
+ // here would answer from MOCK data. Same trap as the header of this file.
200
+ if (!(await Bun.file(`${repo}/${target.slice(2)}`).exists())) missing.push(`${subpath} [${condition}] β†’ ${target}`);
201
+ }
202
+ expect(
203
+ missing,
204
+ 'these export conditions name a file that is not on disk. A consumer does not get a\n' +
205
+ 'helpful error β€” the resolver falls through to whatever condition is left, or fails\n' +
206
+ 'outright for the CSS entries, which have none:\n ' +
207
+ missing.join('\n '),
208
+ ).toEqual([]);
209
+ });
210
+
211
+ it('catches a dangling non-dist target, and leaves dist to the build step', () => {
212
+ // The negative control for the rule above. Without it, `targetsOutsideDist` could
213
+ // quietly stop matching and the assertion would be green against nothing.
214
+ const found = targetsOutsideDist({
215
+ './guardrails': { types: './dist/scripts/guardrailsEnforce.d.ts', bun: './scripts/moved.ts' },
216
+ './styles.css': './src/styles.css',
217
+ './react': { types: './dist/react/index.d.ts', import: './dist/react/index.js' },
218
+ });
219
+ // The `./dist/` conditions are checkDistExports' to police, so neither appears here.
220
+ expect(found).toEqual([
221
+ ['./guardrails', 'bun', './scripts/moved.ts'],
222
+ ['./styles.css', 'default', './src/styles.css'],
223
+ ]);
224
+ });
225
+
150
226
  it('reads the real manifest, so the rule cannot pass vacuously', () => {
227
+ // The scan above must see the whole non-dist surface, not a handful of it: 97 targets
228
+ // across the `bun`/`source`/`default` conditions and the 48 bare CSS entries.
229
+ expect(targetsOutsideDist(pkg.exports as unknown as Record<string, unknown>).length).toBeGreaterThan(90);
151
230
  // `types` is what every conditional export in this package leads with; if the scan
152
231
  // ever returns nothing, both assertions above are green about a manifest they never
153
232
  // looked at.
@@ -138,7 +138,17 @@ export function AutocompleteField<S = string>({
138
138
  </Autocomplete.InputGroup>
139
139
 
140
140
  <Autocomplete.Portal>
141
- <Autocomplete.Positioner sideOffset={4} collisionPadding={FLOATING_COLLISION_PADDING}>
141
+ {/* πŸ”΄ The z-index goes on the POSITIONER, not only the popup. Base UI's positioner is
142
+ the element that is `position: absolute` (with a transform); the popup inside it is
143
+ `position: static`, so the popup's own `z-[var(--z-popover)]` does nothing and the
144
+ list painted UNDER any sibling with a z-index β€” measured 2026-09-22 in the
145
+ binary-server inspector, a sticky table header (z-index 1) covering the second option
146
+ of an open MultiSelectField. */}
147
+ <Autocomplete.Positioner
148
+ sideOffset={4}
149
+ collisionPadding={FLOATING_COLLISION_PADDING}
150
+ className='z-[var(--z-popover)]'
151
+ >
142
152
  <Autocomplete.Popup
143
153
  className={cn(
144
154
  FLOATING_CONTENT_BASE,
@@ -53,6 +53,18 @@ describe('ComboboxField (single)', () => {
53
53
  expect(getByText('Banana')).not.toBeNull();
54
54
  });
55
55
 
56
+ test('πŸ”΄ the POSITIONER carries the popover z-index β€” the popup is static inside it', () => {
57
+ const { container, getByRole } = render(<Controlled />);
58
+ const trigger = container.querySelector('[aria-label="Toggle options"]') as HTMLElement;
59
+ act(() => {
60
+ fireEvent.click(trigger);
61
+ });
62
+ const popup = getByRole('listbox').closest('[data-side]') as HTMLElement;
63
+ const positioner = popup.parentElement?.closest('[data-side]') as HTMLElement;
64
+ expect(positioner).not.toBeNull();
65
+ expect(positioner.className).toContain('z-[var(--z-popover)]');
66
+ });
67
+
56
68
  test('disabled prevents the trigger from responding', () => {
57
69
  const onChange = mock(() => {});
58
70
  const { container } = render(
@@ -291,7 +291,17 @@ export function ComboboxField(props: ComboboxFieldProps) {
291
291
  </Combobox.InputGroup>
292
292
 
293
293
  <Combobox.Portal>
294
- <Combobox.Positioner sideOffset={4} collisionPadding={FLOATING_COLLISION_PADDING}>
294
+ {/* πŸ”΄ The z-index goes on the POSITIONER, not only the popup. Base UI's positioner is
295
+ the element that is `position: absolute` (with a transform); the popup inside it is
296
+ `position: static`, so the popup's own `z-[var(--z-popover)]` does nothing and the
297
+ list painted UNDER any sibling with a z-index β€” measured 2026-09-22 in the
298
+ binary-server inspector, a sticky table header (z-index 1) covering the second option
299
+ of an open MultiSelectField. */}
300
+ <Combobox.Positioner
301
+ sideOffset={4}
302
+ collisionPadding={FLOATING_COLLISION_PADDING}
303
+ className='z-[var(--z-popover)]'
304
+ >
295
305
  <Combobox.Popup
296
306
  className={cn(
297
307
  FLOATING_CONTENT_BASE,
@@ -1027,6 +1027,14 @@ describe('MediaGallery β€” size and length', () => {
1027
1027
  expect(tileLine(container, 'notes.pdf')).toBe('5.0 MB');
1028
1028
  });
1029
1029
 
1030
+ test("'length-and-size' puts both facts on a film's tile, and a document keeps its size", () => {
1031
+ const { container } = render(
1032
+ <MediaGallery items={MEASURED} meta={{ tileMeasure: 'length-and-size' }} />,
1033
+ );
1034
+ expect(tileLine(container, 'holiday.mp4')).toBe('3:07 Β· 1.3 GB');
1035
+ expect(tileLine(container, 'notes.pdf')).toBe('5.0 MB');
1036
+ });
1037
+
1030
1038
  test('tags keep their line and the running time joins them on it', () => {
1031
1039
  // πŸ”΄ Never a fourth line: the caption block is a fixed 65px (`CBGD_META_HEIGHT`), and a
1032
1040
  // tile taller than its neighbour is what made rows lap each other across the fleet.
@@ -1116,6 +1124,21 @@ describe('MediaGallery β€” size and length', () => {
1116
1124
  { measure: true },
1117
1125
  ),
1118
1126
  ).toBe('1:05');
1127
+ // `length-and-size` keeps the size beside the running time β€” binary-server's ask, both
1128
+ // facts on one tile β€” and a clip nobody measured is just its size, never `β€” Β· 1.3 GB`.
1129
+ expect(galleryTileLine(film, { measure: 'length-and-size' })).toBe('3:07 Β· 1.3 GB');
1130
+ expect(
1131
+ galleryTileLine({ ...film, tags: ['collections'] }, { measure: 'length-and-size' }),
1132
+ ).toBe('collections Β· 3:07 Β· 1.3 GB');
1133
+ expect(
1134
+ galleryTileLine({ ...film, durationSeconds: undefined }, { measure: 'length-and-size' }),
1135
+ ).toBe('1.3 GB');
1136
+ expect(
1137
+ galleryTileLine({ id: 'v', name: 'bare.mp4', category: 'video' }, { measure: 'length-and-size' }),
1138
+ ).toBe('β€”');
1139
+ expect(
1140
+ galleryTileLine({ id: 'd', name: 'doc.pdf', sizeBytes: 900 }, { measure: 'length-and-size' }),
1141
+ ).toBe('900 B');
1119
1142
  // Off, nothing about the line moves β€” tags win, then the size, then nothing at all.
1120
1143
  expect(galleryTileLine({ ...film, tags: ['holiday'] })).toBe('holiday');
1121
1144
  expect(galleryTileLine({ id: 'x', name: 'bare.bin' })).toBeNull();
@@ -445,6 +445,10 @@ function isTurned(item: MediaGalleryItem): boolean {
445
445
  return item.orientation ? swapsAxes(item.orientation.turns) : false;
446
446
  }
447
447
 
448
+ /** What a tile's caption measures β€” see {@link galleryTileLine}. `true` = the running time on a
449
+ * time-based item (else its size); `'length-and-size'` = both, side by side. */
450
+ export type TileMeasure = boolean | 'length-and-size';
451
+
448
452
  type BytesCache = MutableRefObject<Map<string, Promise<ArrayBuffer>>>;
449
453
 
450
454
  /**
@@ -473,19 +477,28 @@ type BytesCache = MutableRefObject<Map<string, Promise<ArrayBuffer>>>;
473
477
  */
474
478
  export function galleryTileLine(
475
479
  item: MediaGalleryItem,
476
- options: { measure?: boolean } = {},
480
+ options: { measure?: TileMeasure } = {},
477
481
  ): string | null {
478
482
  const tags = item.tags && item.tags.length > 0 ? item.tags.slice(0, 3).join(' Β· ') : null;
479
483
  const size = item.sizeBytes != null ? formatBytes(item.sizeBytes) : null;
480
484
  if (!options.measure) return tags ?? size;
481
485
  const category = galleryItemCategory(item);
482
486
  const timeBased = category === 'video' || category === 'audio';
483
- const measure = timeBased ? (durationLabel(item.durationSeconds) ?? size ?? 'β€”') : size;
487
+ const length = timeBased ? durationLabel(item.durationSeconds) : null;
488
+ // `length-and-size` keeps the size BESIDE the running time instead of dropping it β€” the
489
+ // binary-server inspector's ask, which named both facts for the same tile. A clip with no
490
+ // measured length is then just its size, never `β€” Β· 1.3 GB`.
491
+ const measure =
492
+ options.measure === 'length-and-size'
493
+ ? ([length, size].filter(Boolean).join(' Β· ') || (timeBased ? 'β€”' : null))
494
+ : timeBased
495
+ ? (length ?? size ?? 'β€”')
496
+ : size;
484
497
  return tags && measure ? `${tags} Β· ${measure}` : (tags ?? measure);
485
498
  }
486
499
 
487
- function GalleryMeta({ item, measure }: { item: MediaGalleryItem; measure?: boolean }) {
488
- const line = galleryTileLine(item, { measure: measure === true });
500
+ function GalleryMeta({ item, measure }: { item: MediaGalleryItem; measure?: TileMeasure }) {
501
+ const line = galleryTileLine(item, { measure });
489
502
  // The date is its OWN line rather than appended to the one above, because that line is
490
503
  // already whichever of tags/size exists β€” folding a third fact into it would push the tags
491
504
  // out of a tile caption that is two lines tall by design.
@@ -647,7 +660,7 @@ function GalleryTile({
647
660
  /** Preview every video tile without hovering it β€” see `readThumbAutoplay`. */
648
661
  autoPreview?: boolean;
649
662
  /** Lead the caption with a running time / a size β€” see {@link galleryTileLine}. */
650
- measure?: boolean;
663
+ measure?: TileMeasure;
651
664
  /** See {@link TileShell}'s `stageMark` β€” the label for the tile that is ON the stage. */
652
665
  stageMark?: string;
653
666
  }) {
@@ -741,7 +754,7 @@ function RawGalleryTile({
741
754
  cache: MediaCache;
742
755
  onOpen: () => void;
743
756
  autoPreview?: boolean;
744
- measure?: boolean;
757
+ measure?: TileMeasure;
745
758
  }) {
746
759
  const category = galleryItemCategory(item);
747
760
  const { media } = useItemMedia(item, cache);
@@ -1749,8 +1762,12 @@ export interface MediaGalleryProps {
1749
1762
  * A tile's caption leads with the measurement that means something for its kind: the
1750
1763
  * RUNNING TIME on a film or a track, the size on everything else. Tags keep the line
1751
1764
  * they had and the measurement joins them on it β€” see {@link galleryTileLine}.
1765
+ *
1766
+ * `'length-and-size'` shows BOTH on a film or a track (`3:07 Β· 1.3 GB`) rather than the
1767
+ * running time instead of the size β€” `ideas/binary-server-09-15.md` asked for the two
1768
+ * facts on the same tile, where `collections` asked for one in place of the other.
1752
1769
  */
1753
- tileMeasure?: boolean;
1770
+ tileMeasure?: TileMeasure;
1754
1771
  /**
1755
1772
  * The table offers the exact byte count as its own column, beside the rounded Size one
1756
1773
  * rather than instead of it β€” see `exactBytesLabel` in `GalleryTable.tsx`. A rounded size cannot answer
@@ -3094,7 +3111,7 @@ export function MediaGallery({
3094
3111
  // …and so does the running time on a film's caption: the strip under an open
3095
3112
  // file is the same grid, and a tile that told you its length on one screen and
3096
3113
  // its size on the other would be two tiles.
3097
- measure={meta?.tileMeasure === true}
3114
+ measure={meta?.tileMeasure || undefined}
3098
3115
  // The one tile the stage is showing. Clicking it re-selects what is already
3099
3116
  // open, which `select` makes a no-op change β€” it stays a live tile rather than
3100
3117
  // a dead one, because a tile you cannot press reads as broken.
@@ -3292,7 +3309,7 @@ export function MediaGallery({
3292
3309
  selection={tileSelection(item)}
3293
3310
  drag={tileDrag(item)}
3294
3311
  autoPreview={thumbAutoplay}
3295
- measure={meta?.tileMeasure === true}
3312
+ measure={meta?.tileMeasure || undefined}
3296
3313
  />
3297
3314
  ) : null;
3298
3315
  }}
@@ -15,7 +15,7 @@
15
15
  *
16
16
  * An app that imports every area gets exactly what `cursedbelt/styles-utilities.css`
17
17
  * emits β€” proved rule by rule in src/stylesAreas.spec.ts, which is also what fails
18
- * when this file is stale. 1309 candidate(s) scanned.
18
+ * when this file is stale. 1312 candidate(s) scanned.
19
19
  *
20
20
  * Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
21
21
  * `bun run build` runs. The map of what is in which area, and why the areas are
@@ -15,7 +15,7 @@
15
15
  *
16
16
  * An app that imports every area gets exactly what `cursedbelt/styles-utilities.css`
17
17
  * emits β€” proved rule by rule in src/stylesAreas.spec.ts, which is also what fails
18
- * when this file is stale. 1345 candidate(s) scanned.
18
+ * when this file is stale. 1347 candidate(s) scanned.
19
19
  *
20
20
  * Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
21
21
  * `bun run build` runs. The map of what is in which area, and why the areas are
@@ -15,7 +15,7 @@
15
15
  *
16
16
  * An app that imports every area gets exactly what `cursedbelt/styles-utilities.css`
17
17
  * emits β€” proved rule by rule in src/stylesAreas.spec.ts, which is also what fails
18
- * when this file is stale. 610 candidate(s) scanned.
18
+ * when this file is stale. 613 candidate(s) scanned.
19
19
  *
20
20
  * Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
21
21
  * `bun run build` runs. The map of what is in which area, and why the areas are
@@ -15,7 +15,7 @@
15
15
  *
16
16
  * An app that imports every area gets exactly what `cursedbelt/styles-utilities.css`
17
17
  * emits β€” proved rule by rule in src/stylesAreas.spec.ts, which is also what fails
18
- * when this file is stale. 2113 candidate(s) scanned.
18
+ * when this file is stale. 2115 candidate(s) scanned.
19
19
  *
20
20
  * Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
21
21
  * `bun run build` runs. The map of what is in which area, and why the areas are
@@ -15,7 +15,7 @@
15
15
  *
16
16
  * An app that imports every area gets exactly what `cursedbelt/styles-utilities.css`
17
17
  * emits β€” proved rule by rule in src/stylesAreas.spec.ts, which is also what fails
18
- * when this file is stale. 386 candidate(s) scanned.
18
+ * when this file is stale. 389 candidate(s) scanned.
19
19
  *
20
20
  * Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
21
21
  * `bun run build` runs. The map of what is in which area, and why the areas are
@@ -13,7 +13,7 @@
13
13
  * | **`--parallel=6`** | **14.6s** | 2689 / 0 |
14
14
  * | `--parallel` (14) | 11.8s | 2689 / 0 |
15
15
  *
16
- * Six, not fourteen, and the reason is the one `scripts/verify.ts` gives for its own job
16
+ * Six, not fourteen, and the reason is the one `$FORGE/tools/gate.ts` gives for its own job
17
17
  * cap: `test` runs BESIDE `e2e` in the graph, and e2e is seven Playwright workers driving
18
18
  * seven browsers. Fourteen more processes on top converts parallelism into thrash, and
19
19
  * browser specs degrade worse than anything else under contention. The gain from 6 β†’ 14 is
@@ -57,7 +57,7 @@ describe('the unit suite runs on more than one core', () => {
57
57
  });
58
58
 
59
59
  test('the worker count stays inside the budget the verify graph assumes', () => {
60
- // `scripts/verify.ts` runs up to `jobLimit()` stages at once and `e2e` is seven
60
+ // `$FORGE/tools/gate.ts` runs up to `jobLimit()` stages at once and `e2e` is seven
61
61
  // browsers. A bare `--parallel` means one worker per core, which is that budget
62
62
  // ignored β€” so the number is written down, and it is written down here as well as in
63
63
  // package.json because the two have to agree about why.
@@ -71,7 +71,7 @@ describe('the unit suite runs on more than one core', () => {
71
71
  expect(workers).toBeGreaterThanOrEqual(2);
72
72
  expect(
73
73
  workers,
74
- 'more workers than a third of the cores is the over-subscription `scripts/verify.ts` ' +
74
+ 'more workers than a third of the cores is the over-subscription `$FORGE/tools/gate.ts` ' +
75
75
  'caps its own stage count to avoid; measured, 6 β†’ 14 workers buys 2.8s and is paid ' +
76
76
  'for by the browser stage running beside it.',
77
77
  ).toBeLessThanOrEqual(8);