cursedbelt 3.0.2 → 4.0.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 (250) hide show
  1. package/dist/react/components/ListToolbar.d.ts +94 -0
  2. package/dist/react/components/ListToolbar.d.ts.map +1 -0
  3. package/dist/react/components/ListToolbar.js +66 -0
  4. package/dist/react/components/ListToolbar.js.map +1 -0
  5. package/dist/react/file-tree/FileTree.d.ts +282 -3
  6. package/dist/react/file-tree/FileTree.d.ts.map +1 -1
  7. package/dist/react/file-tree/FileTree.js +799 -131
  8. package/dist/react/file-tree/FileTree.js.map +1 -1
  9. package/dist/react/file-tree/fileTree.css +151 -0
  10. package/dist/react/file-tree/fileTreeSelection.d.ts +187 -0
  11. package/dist/react/file-tree/fileTreeSelection.d.ts.map +1 -0
  12. package/dist/react/file-tree/fileTreeSelection.js +264 -0
  13. package/dist/react/file-tree/fileTreeSelection.js.map +1 -0
  14. package/dist/react/file-tree/index.d.ts +1 -0
  15. package/dist/react/file-tree/index.d.ts.map +1 -1
  16. package/dist/react/file-tree/index.js +8 -0
  17. package/dist/react/file-tree/index.js.map +1 -1
  18. package/dist/react/index.d.ts +1 -0
  19. package/dist/react/index.d.ts.map +1 -1
  20. package/dist/react/index.js +1 -0
  21. package/dist/react/index.js.map +1 -1
  22. package/dist/react/lib/form.d.ts +3 -0
  23. package/dist/react/lib/form.d.ts.map +1 -1
  24. package/dist/react/lib/form.js +43 -5
  25. package/dist/react/lib/form.js.map +1 -1
  26. package/dist/react/media/DenseGalleryShell.d.ts +19 -3
  27. package/dist/react/media/DenseGalleryShell.d.ts.map +1 -1
  28. package/dist/react/media/DenseGalleryShell.js +55 -15
  29. package/dist/react/media/DenseGalleryShell.js.map +1 -1
  30. package/dist/react/media/FocusViewer.d.ts.map +1 -1
  31. package/dist/react/media/FocusViewer.js +14 -14
  32. package/dist/react/media/FocusViewer.js.map +1 -1
  33. package/dist/react/media/FrameGrabber.d.ts +23 -0
  34. package/dist/react/media/FrameGrabber.d.ts.map +1 -1
  35. package/dist/react/media/FrameGrabber.js +67 -1
  36. package/dist/react/media/FrameGrabber.js.map +1 -1
  37. package/dist/react/media/MediaResize.d.ts +51 -19
  38. package/dist/react/media/MediaResize.d.ts.map +1 -1
  39. package/dist/react/media/MediaResize.js +20 -13
  40. package/dist/react/media/MediaResize.js.map +1 -1
  41. package/dist/react/media/VideoChapterEditor.d.ts +67 -0
  42. package/dist/react/media/VideoChapterEditor.d.ts.map +1 -0
  43. package/dist/react/media/VideoChapterEditor.js +148 -0
  44. package/dist/react/media/VideoChapterEditor.js.map +1 -0
  45. package/dist/react/media/VideoPlayer.d.ts +46 -4
  46. package/dist/react/media/VideoPlayer.d.ts.map +1 -1
  47. package/dist/react/media/VideoPlayer.js +124 -11
  48. package/dist/react/media/VideoPlayer.js.map +1 -1
  49. package/dist/react/media/VideoTurner.d.ts +15 -2
  50. package/dist/react/media/VideoTurner.d.ts.map +1 -1
  51. package/dist/react/media/VideoTurner.js +29 -2
  52. package/dist/react/media/VideoTurner.js.map +1 -1
  53. package/dist/react/media/denseGallery.css +104 -46
  54. package/dist/react/media/hlsSource.d.ts +57 -0
  55. package/dist/react/media/hlsSource.d.ts.map +1 -1
  56. package/dist/react/media/hlsSource.js +177 -1
  57. package/dist/react/media/hlsSource.js.map +1 -1
  58. package/dist/react/media/index.d.ts +2 -0
  59. package/dist/react/media/index.d.ts.map +1 -1
  60. package/dist/react/media/index.js +6 -0
  61. package/dist/react/media/index.js.map +1 -1
  62. package/dist/react/media/mediaResize.css +127 -54
  63. package/dist/react/media/pictureEditor.css +52 -6
  64. package/dist/react/media/videoChapterEditor.css +203 -0
  65. package/dist/react/media/videoChapters.d.ts +117 -0
  66. package/dist/react/media/videoChapters.d.ts.map +1 -0
  67. package/dist/react/media/videoChapters.js +95 -0
  68. package/dist/react/media/videoChapters.js.map +1 -0
  69. package/dist/react/media-gallery/GalleryTable.d.ts +52 -1
  70. package/dist/react/media-gallery/GalleryTable.d.ts.map +1 -1
  71. package/dist/react/media-gallery/GalleryTable.js +40 -9
  72. package/dist/react/media-gallery/GalleryTable.js.map +1 -1
  73. package/dist/react/media-gallery/MediaGallery.d.ts +107 -4
  74. package/dist/react/media-gallery/MediaGallery.d.ts.map +1 -1
  75. package/dist/react/media-gallery/MediaGallery.js +484 -104
  76. package/dist/react/media-gallery/MediaGallery.js.map +1 -1
  77. package/dist/react/media-gallery/MediaMetaEditor.d.ts +48 -0
  78. package/dist/react/media-gallery/MediaMetaEditor.d.ts.map +1 -0
  79. package/dist/react/media-gallery/MediaMetaEditor.js +66 -0
  80. package/dist/react/media-gallery/MediaMetaEditor.js.map +1 -0
  81. package/dist/react/media-gallery/index.d.ts +2 -0
  82. package/dist/react/media-gallery/index.d.ts.map +1 -1
  83. package/dist/react/media-gallery/index.js +5 -0
  84. package/dist/react/media-gallery/index.js.map +1 -1
  85. package/dist/react/media-gallery/mediaGallery.css +180 -4
  86. package/dist/react/media-gallery/mediaMeta.d.ts +62 -0
  87. package/dist/react/media-gallery/mediaMeta.d.ts.map +1 -0
  88. package/dist/react/media-gallery/mediaMeta.js +56 -0
  89. package/dist/react/media-gallery/mediaMeta.js.map +1 -0
  90. package/dist/react/media-gallery/playbackPreferences.d.ts +77 -0
  91. package/dist/react/media-gallery/playbackPreferences.d.ts.map +1 -1
  92. package/dist/react/media-gallery/playbackPreferences.js +42 -0
  93. package/dist/react/media-gallery/playbackPreferences.js.map +1 -1
  94. package/dist/react/media-gallery/types.d.ts +34 -0
  95. package/dist/react/media-gallery/types.d.ts.map +1 -1
  96. package/dist/scripts/guardrailsEnforce.d.ts +29 -0
  97. package/dist/scripts/guardrailsEnforce.d.ts.map +1 -0
  98. package/dist/styles-areas/activity-bar.css +30 -0
  99. package/dist/styles-areas/analytics.css +672 -0
  100. package/dist/styles-areas/auth.css +154 -0
  101. package/dist/styles-areas/calendar.css +416 -0
  102. package/dist/styles-areas/chart-table.css +41 -0
  103. package/dist/styles-areas/charts.css +89 -0
  104. package/dist/styles-areas/chat.css +364 -0
  105. package/dist/styles-areas/clipboard.css +26 -0
  106. package/dist/styles-areas/code-editor.css +130 -0
  107. package/dist/styles-areas/companion-link.css +70 -0
  108. package/dist/styles-areas/core.css +2503 -0
  109. package/dist/styles-areas/dashboard-grid.css +84 -0
  110. package/dist/styles-areas/data-table.css +657 -0
  111. package/dist/styles-areas/deep-link.css +26 -0
  112. package/dist/styles-areas/diff-viewer.css +30 -0
  113. package/dist/styles-areas/disk-usage.css +262 -0
  114. package/dist/styles-areas/emoji.css +154 -0
  115. package/dist/styles-areas/fields.css +582 -0
  116. package/dist/styles-areas/file-tree.css +33 -0
  117. package/dist/styles-areas/filter-rail.css +235 -0
  118. package/dist/styles-areas/folder-tree.css +219 -0
  119. package/dist/styles-areas/keep-awake.css +26 -0
  120. package/dist/styles-areas/layout-engine.css +239 -0
  121. package/dist/styles-areas/markdown.css +87 -0
  122. package/dist/styles-areas/master-detail.css +239 -0
  123. package/dist/styles-areas/master-lock.css +26 -0
  124. package/dist/styles-areas/media-gallery.css +351 -0
  125. package/dist/styles-areas/media.css +658 -0
  126. package/dist/styles-areas/nav.css +488 -0
  127. package/dist/styles-areas/notifications.css +245 -0
  128. package/dist/styles-areas/overlays.css +133 -0
  129. package/dist/styles-areas/palette.css +140 -0
  130. package/dist/styles-areas/pdf-viewer.css +47 -0
  131. package/dist/styles-areas/problem-detail.css +97 -0
  132. package/dist/styles-areas/rich-text.css +167 -0
  133. package/dist/styles-areas/sharing.css +153 -0
  134. package/dist/styles-areas/spreadsheet.css +37 -0
  135. package/dist/styles-areas/stats.css +281 -0
  136. package/dist/styles-areas/test-report.css +133 -0
  137. package/dist/styles-areas/virtual.css +30 -0
  138. package/dist/styles-areas/wizard.css +381 -0
  139. package/dist/styles-areas/workbench.css +543 -0
  140. package/dist/styles-areas/workbook-viewer.css +44 -0
  141. package/dist/styles-static.css +34 -23
  142. package/dist/styles.css +34 -23
  143. package/package.json +111 -31
  144. package/scripts/checkDistExports.ts +67 -0
  145. package/scripts/cssRules.ts +125 -0
  146. package/scripts/fixtureAppCss.ts +344 -0
  147. package/scripts/generateAreaStyles.ts +253 -0
  148. package/scripts/guardrailsEnforce.spec.ts +62 -0
  149. package/scripts/guardrailsEnforce.ts +67 -3
  150. package/scripts/styleAreas.ts +585 -0
  151. package/scripts/verify.ts +5 -0
  152. package/src/barrelsReachNoOptionalPeer.spec.ts +117 -9
  153. package/src/docsMatchTheSplit.spec.ts +98 -0
  154. package/src/fixtureAppCss.spec.ts +177 -0
  155. package/src/namedSubpathsResolve.spec.ts +35 -0
  156. package/src/publishShape.spec.ts +67 -0
  157. package/src/react/components/ListToolbar.spec.tsx +172 -0
  158. package/src/react/components/ListToolbar.tsx +151 -0
  159. package/src/react/file-tree/FileTree.spec.tsx +1009 -0
  160. package/src/react/file-tree/FileTree.tsx +1459 -309
  161. package/src/react/file-tree/fileTree.css +151 -0
  162. package/src/react/file-tree/fileTreeSelection.spec.ts +327 -0
  163. package/src/react/file-tree/fileTreeSelection.ts +321 -0
  164. package/src/react/file-tree/index.ts +21 -0
  165. package/src/react/index.ts +1 -0
  166. package/src/react/lib/form.ts +51 -8
  167. package/src/react/media/DenseGalleryShell.tsx +108 -36
  168. package/src/react/media/FocusViewer.tsx +15 -8
  169. package/src/react/media/FrameGrabber.spec.tsx +175 -0
  170. package/src/react/media/FrameGrabber.tsx +67 -0
  171. package/src/react/media/MediaResize.spec.tsx +84 -14
  172. package/src/react/media/MediaResize.tsx +51 -19
  173. package/src/react/media/VideoChapterEditor.tsx +428 -0
  174. package/src/react/media/VideoPlayer.spec.tsx +111 -1
  175. package/src/react/media/VideoPlayer.tsx +158 -11
  176. package/src/react/media/VideoTurner.spec.tsx +67 -0
  177. package/src/react/media/VideoTurner.tsx +70 -15
  178. package/src/react/media/denseGallery.css +104 -46
  179. package/src/react/media/hlsSource.recovery.spec.ts +213 -0
  180. package/src/react/media/hlsSource.spec.ts +8 -0
  181. package/src/react/media/hlsSource.ts +189 -1
  182. package/src/react/media/index.ts +14 -0
  183. package/src/react/media/mediaResize.css +127 -54
  184. package/src/react/media/pictureEditor.css +52 -6
  185. package/src/react/media/videoChapterEditor.css +203 -0
  186. package/src/react/media/videoChapters.spec.ts +176 -0
  187. package/src/react/media/videoChapters.ts +181 -0
  188. package/src/react/media-gallery/GalleryTable.tsx +93 -3
  189. package/src/react/media-gallery/MediaGallery.spec.tsx +1156 -10
  190. package/src/react/media-gallery/MediaGallery.tsx +813 -95
  191. package/src/react/media-gallery/MediaMetaEditor.tsx +237 -0
  192. package/src/react/media-gallery/galleryTable.spec.ts +41 -1
  193. package/src/react/media-gallery/index.ts +7 -0
  194. package/src/react/media-gallery/mediaGallery.css +180 -4
  195. package/src/react/media-gallery/mediaMeta.spec.ts +95 -0
  196. package/src/react/media-gallery/mediaMeta.ts +101 -0
  197. package/src/react/media-gallery/playbackPreferences.spec.ts +172 -0
  198. package/src/react/media-gallery/playbackPreferences.ts +100 -0
  199. package/src/react/media-gallery/types.ts +34 -0
  200. package/src/shippedFilesAreTracked.spec.ts +69 -0
  201. package/src/styles-areas/activity-bar.css +30 -0
  202. package/src/styles-areas/analytics.css +672 -0
  203. package/src/styles-areas/auth.css +154 -0
  204. package/src/styles-areas/calendar.css +416 -0
  205. package/src/styles-areas/chart-table.css +41 -0
  206. package/src/styles-areas/charts.css +89 -0
  207. package/src/styles-areas/chat.css +364 -0
  208. package/src/styles-areas/clipboard.css +26 -0
  209. package/src/styles-areas/code-editor.css +130 -0
  210. package/src/styles-areas/companion-link.css +70 -0
  211. package/src/styles-areas/core.css +2503 -0
  212. package/src/styles-areas/dashboard-grid.css +84 -0
  213. package/src/styles-areas/data-table.css +657 -0
  214. package/src/styles-areas/deep-link.css +26 -0
  215. package/src/styles-areas/diff-viewer.css +30 -0
  216. package/src/styles-areas/disk-usage.css +262 -0
  217. package/src/styles-areas/emoji.css +154 -0
  218. package/src/styles-areas/fields.css +582 -0
  219. package/src/styles-areas/file-tree.css +33 -0
  220. package/src/styles-areas/filter-rail.css +235 -0
  221. package/src/styles-areas/folder-tree.css +219 -0
  222. package/src/styles-areas/keep-awake.css +26 -0
  223. package/src/styles-areas/layout-engine.css +239 -0
  224. package/src/styles-areas/markdown.css +87 -0
  225. package/src/styles-areas/master-detail.css +239 -0
  226. package/src/styles-areas/master-lock.css +26 -0
  227. package/src/styles-areas/media-gallery.css +351 -0
  228. package/src/styles-areas/media.css +658 -0
  229. package/src/styles-areas/nav.css +488 -0
  230. package/src/styles-areas/notifications.css +245 -0
  231. package/src/styles-areas/overlays.css +133 -0
  232. package/src/styles-areas/palette.css +140 -0
  233. package/src/styles-areas/pdf-viewer.css +47 -0
  234. package/src/styles-areas/problem-detail.css +97 -0
  235. package/src/styles-areas/rich-text.css +167 -0
  236. package/src/styles-areas/sharing.css +153 -0
  237. package/src/styles-areas/spreadsheet.css +37 -0
  238. package/src/styles-areas/stats.css +281 -0
  239. package/src/styles-areas/test-report.css +133 -0
  240. package/src/styles-areas/virtual.css +30 -0
  241. package/src/styles-areas/wizard.css +381 -0
  242. package/src/styles-areas/workbench.css +543 -0
  243. package/src/styles-areas/workbook-viewer.css +44 -0
  244. package/src/styles-static.css +34 -23
  245. package/src/styles.css +34 -23
  246. package/src/stylesAreas.spec.ts +247 -0
  247. package/src/stylesUtilitiesMatches.spec.ts +6 -77
  248. package/src/testFilesRunInParallel.spec.ts +79 -0
  249. package/src/typecheckCachesAreSeparate.spec.ts +128 -0
  250. package/src/verifyGraph.spec.ts +6 -0
package/scripts/verify.ts CHANGED
@@ -76,6 +76,11 @@ export const STAGES: readonly Stage[] = [
76
76
  { script: "paths" },
77
77
  { script: "typecheck:root" },
78
78
  { script: "typecheck:specs" },
79
+ // 🔴 Added 2026-09-16 (task 172). `tsconfig.json` includes `src` only, so the eleven
80
+ // files under `scripts/` were compiled by nothing — including `guardrailsEnforce.ts`,
81
+ // which package.json EXPORTS as `cursedbelt/guardrails`. Four apps were typechecking a
82
+ // file this repo's own gate had never looked at, and inheriting its errors.
83
+ { script: "typecheck:scripts" },
79
84
  { script: "demo:check" },
80
85
  { script: "styles" },
81
86
  {
@@ -70,9 +70,18 @@ const REPO = fileURLToPath(new URL('..', import.meta.url));
70
70
  * task above is about; a `await import('sharp')` inside the function that needs it is
71
71
  * exactly the right shape and stays legal here.
72
72
  *
73
- * Limitation, stated rather than hidden: the externals are read out of the bundle's import
74
- * statements textually, so an import bun emits across several lines would be missed. That
75
- * makes this half a floor, not a ceiling — the runtime fixture above is the strict one.
73
+ * The externals are read out of the bundle's import statements textually.
74
+ *
75
+ * ⛔ **Corrected 2026-09-15 (task 149).** This paragraph used to admit the scan as "a floor,
76
+ * not a ceiling" because it only matched an import bun emitted on ONE line — and the
77
+ * admission was worth more than it looked. Bundling `./react` and diffing the result against
78
+ * the manifest showed the old pattern silently missing `cmdk` and `@tanstack/react-table`,
79
+ * both of which bun emits as a multi-line `import {\n … \n} from "pkg";`. Nothing was red at
80
+ * the time, because both are hard `dependencies` rather than optional peers — but the
81
+ * MEASUREMENT half of task 149 ran off this same scan, and a dependency graph with two
82
+ * packages missing from it is the kind of thing a decision gets made on. So the matcher is
83
+ * anchored on `from "…"` wherever it appears now, plus the bare `import "…";` form, and the
84
+ * limitation is gone rather than documented.
76
85
  */
77
86
  const NO_STATIC_OPTIONAL_PEERS = [
78
87
  {
@@ -92,8 +101,14 @@ const OPTIONAL_PEERS = new Set(
92
101
  .map(([name]) => name),
93
102
  );
94
103
 
95
- /** `import x from "pkg"`, `import "pkg"`, `export … from "pkg"` — one line each, as bun emits. */
96
- const STATIC_SPECIFIER = /^(?:import|export)\s[^\n]*?["']([^"'\n]+)["'];?$|^import\s*["']([^"'\n]+)["'];?$/gm;
104
+ /**
105
+ * `import x from "pkg"` · `export … from "pkg"` · the side-effect-only `import "pkg";`.
106
+ *
107
+ * 🔴 Anchored on the `from` clause, NOT on a line starting with `import`, so a multi-line
108
+ * named import still counts (see the correction above). A DYNAMIC `import("pkg")` matches
109
+ * neither branch and must not: that is the lazy shape this whole file exists to permit.
110
+ */
111
+ const STATIC_SPECIFIER = /\bfrom\s*["']([^"'\n]+)["']|^\s*import\s*["']([^"'\n]+)["'];?\s*$/gm;
97
112
 
98
113
  /**
99
114
  * 🔴 An OUTDIR, not stdout, and this repo is why (2026-09-15).
@@ -107,7 +122,7 @@ const STATIC_SPECIFIER = /^(?:import|export)\s[^\n]*?["']([^"'\n]+)["'];?$|^impo
107
122
  * `.js` it emits — a stylesheet cannot import a package, so the CSS output is
108
123
  * irrelevant to the question and only the JS is read.
109
124
  */
110
- const staticOptionalPeersOf = (entry: string): string[] => {
125
+ const staticExternalsOf = (entry: string): Set<string> => {
111
126
  const outdir = `${process.env.TMPDIR ?? '/tmp'}/cursedbelt-barrel-scan/${entry.replace(/[^a-z0-9]+/gi, '-')}`;
112
127
  const build = Bun.spawnSync(
113
128
  ['bun', 'build', entry, '--target=bun', '--packages=external', '--outdir', outdir],
@@ -123,13 +138,64 @@ const staticOptionalPeersOf = (entry: string): string[] => {
123
138
  const found = new Set<string>();
124
139
  for (const match of bundle.matchAll(STATIC_SPECIFIER)) {
125
140
  const specifier = match[1] ?? match[2];
126
- if (specifier === undefined || specifier.startsWith('.')) continue;
141
+ if (specifier === undefined || specifier.startsWith('.') || specifier.startsWith('bun:')) {
142
+ continue;
143
+ }
127
144
  // `@scope/name/deep` → `@scope/name`, `pkg/deep` → `pkg`.
128
145
  const parts = specifier.split('/');
129
146
  const name = specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
130
- if (name !== undefined && OPTIONAL_PEERS.has(name)) found.add(name);
147
+ if (name !== undefined) found.add(name);
131
148
  }
132
- return [...found].sort();
149
+ return found;
150
+ };
151
+
152
+ const staticOptionalPeersOf = (entry: string): string[] =>
153
+ [...staticExternalsOf(entry)].filter((name) => OPTIONAL_PEERS.has(name)).sort();
154
+
155
+ /**
156
+ * 🔴 The INVERSE of everything above, and the half task 149 had to add (2026-09-15).
157
+ *
158
+ * The rule the optional-peer check enforces — *the barrel must not drag a package a
159
+ * consumer may never call* — has an exact mirror that nothing was enforcing: **a hard
160
+ * `dependency` the barrel never touches is a package EVERY consumer installs and no
161
+ * consumer's `cursedbelt/react` import can reach.** Both are the same defect measured
162
+ * from opposite ends, and the second one is the expensive one, because it is invisible:
163
+ * the manifest is green, the import graph is clean, and the app just pays.
164
+ *
165
+ * Measured into an empty directory at 3.0.2, before this check existed: eight of the
166
+ * thirty-five runtime `dependencies` were in exactly that state — `emoji-mart` and its
167
+ * two siblings (already loaded through `await import()` in `emoji/EmojiPicker.tsx` and
168
+ * `emoji/emojiSearch.ts`), `@git-diff-view/react` + `diff` (only `./react/diff-viewer`),
169
+ * `marked` (only `./react/markdown-doc`), `react-grid-layout` (only
170
+ * `./react/dashboard-grid`) and `@hookform/resolvers` (only `lib/form.ts`). They cost a
171
+ * `Button` consumer **31 of 127 packages and 56 of 231 MB** and bought it nothing. All
172
+ * eight are optional peers at 4.0.0, and this test is what stops the ninth.
173
+ *
174
+ * 🔴 It is also the reason the twelve leaf Radix packages, `clsx`, `tailwind-merge` and
175
+ * `class-variance-authority` STAYED hard dependencies against task 149's own 23-row
176
+ * list: the barrel reaches every one of them, so by this rule they are correctly
177
+ * declared. `docs/STANDARDS.md` carries the measurement that says what moving them
178
+ * anyway would have bought (3 MB) and cost (`Button`, `Tooltip`, `cn`, and eleven more
179
+ * components out of `cursedbelt/react`). Read it before re-opening that question.
180
+ *
181
+ * The two `@atlaskit/pragmatic-drag-and-drop-{auto-scroll,hitbox}` packages are on that
182
+ * list too, and they are stuck for a different reason worth writing down once: `hitbox`'s
183
+ * `getReorderDestinationIndex` is called by `reorderByEdge`
184
+ * (`src/react/hooks/useDragReorder.tsx:112`), an exported **pure synchronous** function, and
185
+ * `attachClosestEdge` runs inside a drop target's `getData`, which the engine calls
186
+ * synchronously mid-drag. Making either lazy changes a public pure function's signature to
187
+ * async, for the 3 packages the pair costs. `DataTable`'s row drag puts the base engine in
188
+ * the barrel regardless, so moving the drag hooks behind a subpath would not free it either.
189
+ *
190
+ * Verified failing before it was trusted: reverting `emoji-mart` to a `dependency` reds
191
+ * with *"declared as runtime `dependencies` but `cursedbelt/react` never reaches them"*.
192
+ */
193
+ const DEPS_THE_BARREL_NEED_NOT_REACH: Record<string, string> = {
194
+ // The one honest exception, and it is not a JS dependency at all: `src/styles.css`
195
+ // pulls it with a CSS `@import`, so no JS bundle can ever contain it. Deleting it
196
+ // ships a stylesheet that cannot resolve — see `declaredDepsAreImported.spec.ts`,
197
+ // which made exactly that mistake on its first pass.
198
+ 'tw-animate-css': '@import-ed from src/styles.css; never reachable from a JS bundle',
133
199
  };
134
200
 
135
201
 
@@ -152,3 +218,45 @@ describe('barrels reach no optional peer statically', () => {
152
218
  });
153
219
  }
154
220
  });
221
+
222
+ describe('every runtime dependency is one the barrel actually needs', () => {
223
+ const reached = staticExternalsOf('src/react/index.ts');
224
+ const declared = Object.keys(
225
+ (pkg as { dependencies?: Record<string, string> }).dependencies ?? {},
226
+ );
227
+
228
+ it('has measured a real barrel', () => {
229
+ // Guards the assertion below the same way the optional-peer half guards its own: an
230
+ // empty `reached` set would make every declared dependency look unreachable, and an
231
+ // empty `declared` list would make the test pass having proved nothing.
232
+ expect(reached.size, 'the bundle scan found no external imports at all').toBeGreaterThan(20);
233
+ expect(declared.length, 'package.json declares no runtime dependencies').toBeGreaterThan(20);
234
+ });
235
+
236
+ it('declares no dependency `cursedbelt/react` cannot reach', () => {
237
+ const unreachable = declared
238
+ .filter((name) => !reached.has(name) && DEPS_THE_BARREL_NEED_NOT_REACH[name] === undefined)
239
+ .sort();
240
+ expect(
241
+ unreachable,
242
+ 'these are declared as runtime `dependencies` but `cursedbelt/react` never reaches them, ' +
243
+ 'so every consumer installs them to render a Button and no barrel import can ever use ' +
244
+ 'them. Make each one an OPTIONAL peer (`peerDependencies` + `peerDependenciesMeta.' +
245
+ '<name>.optional` + a `devDependencies` entry so this repo still typechecks it), which ' +
246
+ 'is what `recharts` and the CodeMirror set already are. If it genuinely cannot be ' +
247
+ 'reached from JS at all, add it to DEPS_THE_BARREL_NEED_NOT_REACH with the reason:\n ' +
248
+ unreachable.join('\n '),
249
+ ).toEqual([]);
250
+ });
251
+
252
+ it('keeps no excuse that has stopped being true', () => {
253
+ // An allowlist entry for a package that is no longer declared — or that the barrel now
254
+ // reaches anyway — is an excuse nobody re-reads. Same ratchet as `check-paths.ts`.
255
+ const stale = Object.keys(DEPS_THE_BARREL_NEED_NOT_REACH).filter(
256
+ (name) => !declared.includes(name) || reached.has(name),
257
+ );
258
+ expect(stale, 'DEPS_THE_BARREL_NEED_NOT_REACH excuses a dependency that needs no excuse').toEqual(
259
+ [],
260
+ );
261
+ });
262
+ });
@@ -0,0 +1,98 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { describe, expect, it } from 'bun:test';
4
+
5
+ /**
6
+ * 🔴 **The document a reader reads first may not claim a tier this package does not ship.**
7
+ *
8
+ * `cursedbelt` was one package and is now three: this React design system,
9
+ * `cursedbelt-core` beneath it, and the Bun/Hono server in `cursedbelt-server`.
10
+ * `package.json`'s `description` says so. `docs/API-REFERENCE.md` did not — until
11
+ * 2026-09-15 its "Foundation choices" list read:
12
+ *
13
+ * > - Server: **Hono on Bun** (one framework). … DB: SQLite (bun:sqlite) + a
14
+ * > migration runner + Kysely-typed plumbing.
15
+ *
16
+ * That cost a real session real time: a task to build a **server-side request-telemetry
17
+ * sink** (Analytics Engine when bound, SQLite when not) was written against `cursedbelt`
18
+ * and fenced to it, on the strength of that sentence. The seam belongs in — and landed
19
+ * in — `cursedbelt-server@1.1.0`. A stale ownership claim in a doc is not a typo; it
20
+ * routes work to the wrong repo, where the gate cannot prove it (generation rule 3).
21
+ *
22
+ * So this is the check rather than a corrected sentence. Two halves, and BOTH matter:
23
+ * the code really is server-free, and the doc really says so.
24
+ */
25
+
26
+ const ROOT = join(import.meta.dir, '..');
27
+ const read = (rel: string) => readFileSync(join(ROOT, rel), 'utf8');
28
+
29
+ /**
30
+ * Terms that name the server tier. If one appears in the doc, the SAME LINE must name the
31
+ * package that owns it — otherwise the line reads as a claim about this package.
32
+ */
33
+ const SERVER_TIER_TERMS = [
34
+ 'bun:sqlite',
35
+ 'Hono on Bun',
36
+ 'migration runner',
37
+ 'Kysely',
38
+ ] as const;
39
+
40
+ /** The packages a server-tier mention is allowed to be attributed to. */
41
+ const OWNERS = ['cursedbelt-server', 'cursedbelt-core'] as const;
42
+
43
+ /**
44
+ * The per-line excuse, because the rule has one honest false-positive class: a sentence
45
+ * that mentions a term in order to DENY it ("nothing here imports `bun:sqlite`") reads to
46
+ * a line matcher exactly like a claim. The generation's convention is to excuse the line
47
+ * and keep the rule — never to delete the rule — so this is spelled the same way
48
+ * `check-paths.ts` spells its own: an inline HTML comment carrying a reason.
49
+ *
50
+ * <!-- split:ignore the reason -->
51
+ */
52
+ const IGNORE = /<!--\s*split:ignore\b/;
53
+
54
+ describe('this package ships no server tier', () => {
55
+ it('no file under src/ imports bun:sqlite or hono', async () => {
56
+ const offenders: string[] = [];
57
+ const files = new Bun.Glob('**/*.{ts,tsx}').scanSync({ cwd: join(ROOT, 'src') });
58
+ for (const rel of files) {
59
+ const text = readFileSync(join(ROOT, 'src', rel), 'utf8');
60
+ // Import/require of the module itself — not a mention inside a comment, which is
61
+ // how this very file talks about the split.
62
+ if (/(?:from|require\()\s*['"](?:bun:sqlite|hono(?:\/[^'"]*)?)['"]/.test(text)) {
63
+ offenders.push(`src/${rel}`);
64
+ }
65
+ }
66
+ expect(offenders).toEqual([]);
67
+ });
68
+ });
69
+
70
+ describe('docs/API-REFERENCE.md does not claim the server tier', () => {
71
+ const doc = read('docs/API-REFERENCE.md');
72
+
73
+ it('attributes every server-tier mention to the package that owns it', () => {
74
+ const unattributed: string[] = [];
75
+ for (const [i, line] of doc.split('\n').entries()) {
76
+ if (!SERVER_TIER_TERMS.some((t) => line.includes(t))) continue;
77
+ if (OWNERS.some((owner) => line.includes(owner))) continue;
78
+ if (IGNORE.test(line)) continue;
79
+ unattributed.push(`docs/API-REFERENCE.md:${i + 1}: ${line.trim()}`);
80
+ }
81
+ // A line naming `bun:sqlite` or the migration runner WITHOUT naming
82
+ // `cursedbelt-server` is the 2026-09-15 defect, exactly.
83
+ expect(unattributed).toEqual([]);
84
+ });
85
+
86
+ it('says plainly, near the top, that the server tier is elsewhere', () => {
87
+ const top = doc.split('\n').slice(0, 20).join('\n');
88
+ expect(top).toContain('cursedbelt-server');
89
+ expect(top).toMatch(/ships NO server tier/i);
90
+ });
91
+
92
+ it("agrees with package.json's own description of the split", () => {
93
+ const { description } = JSON.parse(read('package.json')) as { description: string };
94
+ // The description is the claim the registry serves; the doc must not contradict it.
95
+ expect(description).toContain('cursedbelt-server');
96
+ expect(doc).toContain('cursedbelt-server');
97
+ });
98
+ });
@@ -0,0 +1,177 @@
1
+ /**
2
+ * 🔴 The check three apps, then four, then five, were paying for the absence of:
3
+ * what a consuming app EMITS, measured from inside the package, and refused when
4
+ * it grows.
5
+ *
6
+ * ## Why it has to be here, and why it has to be a fixture
7
+ *
8
+ * `flix`, `patterns`, `collections`, `music` and `family` all ship the union of
9
+ * this design system. Every measurement of that cost was taken in a consumer, in a
10
+ * repo that cannot change the thing causing it — and none of them was taken twice,
11
+ * so nothing could tell whether the number was going up. This generation's third
12
+ * rule forbids the obvious fix: a repo's gate proves that repo, so no check here
13
+ * may reach into `apps/*`.
14
+ *
15
+ * So the package keeps its own consumer. `demo/fixtures/lean-app` is a small
16
+ * releases console — header, stat tiles, a chart, a sortable table, a virtualized
17
+ * log — built entirely out of cursedbelt's public subpaths, and
18
+ * `demo/fixtures/lean-app/app.css` is the stylesheet an app copies. Not the
19
+ * kitchen-sink demo: the demo renders every component, so its CSS can never
20
+ * shrink and a ratchet on it would measure nothing.
21
+ *
22
+ * ## The two halves, because either one alone is a lie
23
+ *
24
+ * SIZE — the fixture's CSS on the area route is at or below the byte count in
25
+ * `fixtureAppCss.baseline`, and materially below the union route it
26
+ * replaces. `bun scripts/fixtureAppCss.ts --prune` lowers the baseline
27
+ * and refuses to raise it.
28
+ * TRUTH — the area route still emits every rule the fixture needs, and every
29
+ * cursedbelt module the fixture reaches is covered by an area its
30
+ * stylesheet declares. A stylesheet can always be made smaller by
31
+ * emitting less than the app renders; that is what "unstyled" IS, and it
32
+ * is the only failure mode this whole mechanism has.
33
+ *
34
+ * `scripts/fixtureAppCss.ts` owns the measurement and the baseline format; this
35
+ * file is the WHETHER.
36
+ */
37
+ import { describe, expect, it } from 'bun:test';
38
+ import pkg from '../package.json';
39
+ import { flatten, lostRules, referenced } from '../scripts/cssRules';
40
+ import {
41
+ BASELINE,
42
+ FIXTURE,
43
+ ROUTES,
44
+ areaRouteCss,
45
+ declaredAreas,
46
+ fixtureClosure,
47
+ fixtureImports,
48
+ measure,
49
+ missingNeeds,
50
+ readBaseline,
51
+ requiredCss,
52
+ uncoveredModules,
53
+ unionRouteCss,
54
+ } from '../scripts/fixtureAppCss';
55
+ import { REPO_ROOT } from '../scripts/generateStaticStyles';
56
+ import { CORE } from '../scripts/styleAreas';
57
+
58
+ const exportsMap = (pkg as { exports: Record<string, unknown> }).exports;
59
+ const declared = await declaredAreas();
60
+ const baseline = await readBaseline();
61
+ const { union, areas } = await measure();
62
+
63
+ /** The one command that changes any number below, as a line the owner can paste. */
64
+ const REPRUNE = `cd ${REPO_ROOT} && bun scripts/fixtureAppCss.ts --prune`;
65
+
66
+ describe('the fixture app measures a real consumer', () => {
67
+ it('imports cursedbelt the way an app does', async () => {
68
+ // Vacuity guard for the whole file: a fixture that imports nothing emits no
69
+ // cursedbelt CSS, so every size assertion below would pass triumphantly.
70
+ const imports = await fixtureImports();
71
+ expect(imports.length, `${FIXTURE} imports no cursedbelt subpath`).toBeGreaterThan(2);
72
+ const { modules, resolution } = await fixtureClosure(exportsMap);
73
+ expect(
74
+ resolution.unresolved,
75
+ `these imports in ${FIXTURE} cannot be attributed to a module, so no area can be\n` +
76
+ `checked against them:\n ${resolution.unresolved.join('\n ')}`,
77
+ ).toEqual([]);
78
+ expect(modules.size, 'the fixture reaches almost nothing — it is not a realistic app').toBeGreaterThan(30);
79
+ });
80
+
81
+ it('declares core first, and the `needs` of everything it declares', () => {
82
+ expect(declared[0], `${FIXTURE}/app.css must import styles-areas/${CORE}.css first`).toBe(CORE);
83
+ expect(new Set(declared).size, 'an area is imported twice').toBe(declared.length);
84
+ const missing = missingNeeds(declared);
85
+ expect(
86
+ missing,
87
+ `${FIXTURE}/app.css declares an area without the areas it composes, so the parts of\n` +
88
+ `it that are subtracted into those areas render unstyled:\n ${missing.join('\n ')}`,
89
+ ).toEqual([]);
90
+ });
91
+
92
+ it('declares an area for every cursedbelt module it reaches', async () => {
93
+ // 🔴 The assertion that stops the saving being fake. Under-declaring is the
94
+ // mistake an app makes — add a component, forget the `@import` — and its
95
+ // symptom is a component with no classes, three repos away, on a screen nobody
96
+ // opened during review.
97
+ const uncovered = await uncoveredModules(declared, exportsMap);
98
+ expect(
99
+ uncovered,
100
+ `${FIXTURE} renders these cursedbelt modules, and ${FIXTURE}/app.css imports no area\n` +
101
+ 'that ships their CSS — so they render UNSTYLED. Add the area named beside each:\n ' +
102
+ uncovered.join('\n '),
103
+ ).toEqual([]);
104
+ });
105
+ });
106
+
107
+ describe('the fixture app is materially smaller on the area route', () => {
108
+ it('has a baseline recording both routes', () => {
109
+ // A missing baseline must fail, not seed itself: a check that writes its own
110
+ // expectation the first time it runs can never fail again.
111
+ for (const route of ROUTES) {
112
+ expect(
113
+ baseline.counts.get(route),
114
+ `${BASELINE} records no \`${route}\` byte count. Seed it:\n ${REPRUNE}`,
115
+ ).toBeGreaterThan(0);
116
+ }
117
+ });
118
+
119
+ it('does not emit more CSS than the baseline allows', () => {
120
+ const grown = ROUTES.map((route) => ({ route, now: route === 'union' ? union : areas, was: baseline.counts.get(route) ?? 0 }))
121
+ .filter((r) => r.now > r.was)
122
+ .map((r) => `${r.route}: baseline allows ${r.was} B, it emits ${r.now} B now (+${r.now - r.was})`);
123
+ expect(
124
+ grown,
125
+ `the CSS a consuming app ships GREW. This is the check that exists because nothing\n` +
126
+ 'measured it while five apps paid for the union. If the growth is deliberate — a new\n' +
127
+ `primitive in \`${CORE}\`, a component the fixture started rendering — raise the number in\n` +
128
+ `${BASELINE} by hand, in a commit that says why; \`--prune\` only lowers.\n ` +
129
+ grown.join('\n '),
130
+ ).toEqual([]);
131
+ });
132
+
133
+ it('is a real saving, not a rounding error', () => {
134
+ // The ratio, not the absolute number: a ratchet on one count cannot tell a
135
+ // genuine saving from a design system that happened to shrink.
136
+ expect(union, 'the union route emitted almost nothing — the compile is broken').toBeGreaterThan(100_000);
137
+ expect(
138
+ areas / union,
139
+ `the area route emits ${areas} B against the union's ${union} B. If this stops being a\n` +
140
+ 'material saving, the area map has collapsed back into the union and the split is\n' +
141
+ 'carrying its cost for nothing.',
142
+ ).toBeLessThan(0.8);
143
+ });
144
+ });
145
+
146
+ describe('the smaller stylesheet is not an unstyled one', () => {
147
+ it('emits every rule the fixture actually needs', async () => {
148
+ // The half a byte count cannot see. `requiredCss` is the fixture's own classes
149
+ // plus exactly the classes of the cursedbelt modules it reaches — nothing else
150
+ // — so anything missing here is a rule that would not reach the browser.
151
+ const [required, actual] = await Promise.all([requiredCss(exportsMap), areaRouteCss(declared)]);
152
+ const need = flatten(required);
153
+ const have = flatten(actual);
154
+ expect(need.size, 'the required stylesheet is empty — the comparison would be vacuous').toBeGreaterThan(200);
155
+ const readByRequired = referenced(required);
156
+ const { rules, declarations } = lostRules(need, have, (prop) => !readByRequired.has(prop));
157
+ expect(
158
+ rules,
159
+ `${FIXTURE} renders these selectors and its own stylesheet does not emit them —\n` +
160
+ 'the app is UNSTYLED in exactly those places. Usually a missing area in\n' +
161
+ `${FIXTURE}/app.css:\n ${rules.slice(0, 20).join('\n ')}`,
162
+ ).toEqual([]);
163
+ expect(
164
+ declarations,
165
+ `${FIXTURE} emits these selectors but not these declarations, so those components\n` +
166
+ `render PARTLY styled:\n ${declarations.slice(0, 20).join('\n ')}`,
167
+ ).toEqual([]);
168
+ });
169
+
170
+ it('still emits everything the union route does for the fixture, minus what it does not render', async () => {
171
+ // The negative control for the assertion above: the union route is a superset
172
+ // by construction, so if the area route were somehow LARGER, `requiredCss` is
173
+ // measuring the wrong thing and the proof above is worthless.
174
+ const full = (await unionRouteCss()).length;
175
+ expect(areas).toBeLessThan(full);
176
+ });
177
+ });
@@ -54,6 +54,28 @@ const FILE_EXT = /\.(?!css$)[a-z0-9]+$/i;
54
54
 
55
55
  const exported = new Set(Object.keys(pkg.exports).map((k) => k.replace(/^\./, '')));
56
56
 
57
+ /**
58
+ * A mention that STOPS at a placeholder, inside a subpath family that exists
59
+ * (2026-09-15, the `styles-areas/*` split).
60
+ *
61
+ * `@import "cursedbelt/styles-areas/${name}.css"` and the prose form
62
+ * `cursedbelt/styles-areas/<area>.css` are not copy-pasteable imports — they name
63
+ * a family, and `NAMED` truncates at the `$`/`<` because neither is a subpath
64
+ * character. Flagging them would mean 32 `subpath-check:ignore` markers on the
65
+ * lines that DOCUMENT the feature, which is how a gate gets turned off.
66
+ *
67
+ * Narrow on purpose, so a removed family still fails: exempt only when the match
68
+ * is a strict prefix of something `exports` actually has AND the next character in
69
+ * the line is the `/` the match could not consume. `cursedbelt/tokens/${x}` is
70
+ * still an offence, because nothing under `./tokens/` is exported.
71
+ */
72
+ const familyPrefixes = new Set(
73
+ [...exported].flatMap((subpath) => {
74
+ const parts = subpath.split('/').filter(Boolean);
75
+ return parts.slice(0, -1).map((_, i) => `/${parts.slice(0, i + 1).join('/')}`);
76
+ }),
77
+ );
78
+
57
79
  // This file and the task/report prose quoting the removal are allowed to name them.
58
80
  const SELF = 'namedSubpathsResolve.spec.ts';
59
81
 
@@ -77,6 +99,8 @@ const offences = (): Promise<Offence[]> => {
77
99
  const subpath = m[1] ?? '';
78
100
  if (exported.has(subpath)) continue;
79
101
  if (REPO_DIRS.test(subpath) || FILE_EXT.test(subpath)) continue;
102
+ // A family name followed by the placeholder that truncated the match.
103
+ if (familyPrefixes.has(subpath) && line[(m.index ?? 0) + (m[0]?.length ?? 0)] === '/') continue;
80
104
  found.push({ file: `${root}/${rel}`, line: i + 1, subpath, text: line.trim() });
81
105
  }
82
106
  }
@@ -106,6 +130,17 @@ describe('subpaths named in this repo resolve', () => {
106
130
  expect(exported.has('/game')).toBe(false); // removed 2026-09-13
107
131
  });
108
132
 
133
+ it('excuses a placeholder only inside a family that exists', () => {
134
+ // The control for the allowance above: it must not become "anything with a
135
+ // slash in it is fine". `/styles-areas/core.css` is exported, so the family is;
136
+ // `/tokens` was removed in task 33 and nothing under it is.
137
+ expect(familyPrefixes.has('/styles-areas')).toBe(true);
138
+ expect(familyPrefixes.has('/react')).toBe(true);
139
+ expect(familyPrefixes.has('/tokens')).toBe(false);
140
+ // And a whole-file subpath is never a family — `/styles.css` has no children.
141
+ expect(familyPrefixes.has('/styles.css')).toBe(false);
142
+ });
143
+
109
144
  it('names no subpath package.json does not export', async () => {
110
145
  const dead = await offences();
111
146
  expect(
@@ -35,6 +35,23 @@ const emittedInSrc = (): string[] => {
35
35
  });
36
36
  };
37
37
 
38
+ /**
39
+ * Every `types` condition, as `subpath → target`. Pure, so the assertion below has a
40
+ * negative control that does not need a broken manifest on disk to prove it works.
41
+ */
42
+ const typesTargets = (exports: Record<string, unknown>): [string, string][] =>
43
+ Object.entries(exports).flatMap(([subpath, entry]) => {
44
+ if (typeof entry !== 'object' || entry === null) return [];
45
+ const target = (entry as Record<string, unknown>).types;
46
+ return typeof target === 'string' ? [[subpath, target] as [string, string]] : [];
47
+ });
48
+
49
+ /** A `types` that is not a declaration file — tsc COMPILES those instead of reading them. */
50
+ const typesThatAreNotDeclarations = (exports: Record<string, unknown>): string[] =>
51
+ typesTargets(exports)
52
+ .filter(([, target]) => !target.endsWith('.d.ts'))
53
+ .map(([subpath, target]) => `${subpath} → ${target}`);
54
+
38
55
  describe('publish shape', () => {
39
56
  it('ships every directory its exports resolve into', () => {
40
57
  const files = (pkg as { files?: string[] }).files ?? [];
@@ -86,4 +103,54 @@ describe('publish shape', () => {
86
103
  .map(([k]) => k);
87
104
  expect(wrong, `"types" must be the first condition:\n ${wrong.join('\n ')}`).toEqual([]);
88
105
  });
106
+
107
+ it('🔴 points every "types" condition at a .d.ts, never at raw source', () => {
108
+ // ── What this cost, found 2026-09-15, fixed in task 172 ────────────────────────
109
+ // `./guardrails` shipped `"types": "./scripts/guardrailsEnforce.ts"` — the SOURCE.
110
+ // A consumer's `tsc` does not READ that the way it reads a declaration file, it
111
+ // pulls it into the consumer's own program and COMPILES it, under the consumer's
112
+ // `@types/node` and the consumer's strictness. `skipLibCheck` cannot help: it skips
113
+ // `.d.ts`, and this is not one, so there is no flag an app can set to opt out.
114
+ //
115
+ // So one wrong line here — `let entries: ReturnType<typeof readdirSync>`, which
116
+ // silently changed meaning when @types/node 24 made `Dirent` generic — became five
117
+ // errors in four apps' gates, in a file none of them wrote, that this repo's own
118
+ // typecheck had never compiled. `apps/flix`'s `bun run typecheck` was red on `main`
119
+ // for a reason that had nothing to do with flix.
120
+ //
121
+ // The type surface a package publishes must be its own compiler's OUTPUT, not its
122
+ // input: `dist/**/*.d.ts` is already checked here, by us, before it is emitted.
123
+ const raw = typesThatAreNotDeclarations(pkg.exports as unknown as Record<string, unknown>);
124
+ expect(
125
+ raw,
126
+ 'these "types" conditions hand a consumer something tsc will COMPILE rather than read,\n' +
127
+ 'so every consumer inherits our type errors and cannot suppress one (skipLibCheck skips\n' +
128
+ '.d.ts, and these are not). Emit a declaration into dist and point at that:\n ' +
129
+ raw.join('\n '),
130
+ ).toEqual([]);
131
+ });
132
+
133
+ it('catches a raw-source "types" rather than passing on an empty manifest', () => {
134
+ // The negative control for the rule above, and it is the export that actually WAS
135
+ // broken. Without this, `typesThatAreNotDeclarations` could quietly stop matching
136
+ // anything — the assertion above would still be green, against nothing.
137
+ expect(
138
+ typesThatAreNotDeclarations({
139
+ './guardrails': { types: './scripts/guardrailsEnforce.ts' },
140
+ './button': { types: './dist/react/components/Button.d.ts' },
141
+ './styles.css': './src/styles.css',
142
+ }),
143
+ ).toEqual(['./guardrails → ./scripts/guardrailsEnforce.ts']);
144
+ // A `.tsx` component entry point is the same defect wearing the other extension,
145
+ // and a `.js` is one too — a consumer gets inferred-from-JS types or none at all.
146
+ expect(typesThatAreNotDeclarations({ './a': { types: './src/react/A.tsx' } })).toHaveLength(1);
147
+ expect(typesThatAreNotDeclarations({ './a': { types: './dist/a.js' } })).toHaveLength(1);
148
+ });
149
+
150
+ it('reads the real manifest, so the rule cannot pass vacuously', () => {
151
+ // `types` is what every conditional export in this package leads with; if the scan
152
+ // ever returns nothing, both assertions above are green about a manifest they never
153
+ // looked at.
154
+ expect(typesTargets(pkg.exports as unknown as Record<string, unknown>).length).toBeGreaterThan(40);
155
+ });
89
156
  });