cursedbelt 3.0.2 → 4.1.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 (252) 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 +130 -32
  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/paths.ts +106 -0
  151. package/scripts/styleAreas.ts +585 -0
  152. package/scripts/verify.ts +5 -0
  153. package/src/barrelsReachNoOptionalPeer.spec.ts +117 -9
  154. package/src/docsMatchTheSplit.spec.ts +98 -0
  155. package/src/fixtureAppCss.spec.ts +177 -0
  156. package/src/namedSubpathsResolve.spec.ts +35 -0
  157. package/src/publishShape.spec.ts +67 -0
  158. package/src/react/components/ListToolbar.spec.tsx +172 -0
  159. package/src/react/components/ListToolbar.tsx +151 -0
  160. package/src/react/file-tree/FileTree.spec.tsx +1009 -0
  161. package/src/react/file-tree/FileTree.tsx +1459 -309
  162. package/src/react/file-tree/fileTree.css +151 -0
  163. package/src/react/file-tree/fileTreeSelection.spec.ts +327 -0
  164. package/src/react/file-tree/fileTreeSelection.ts +321 -0
  165. package/src/react/file-tree/index.ts +21 -0
  166. package/src/react/index.ts +1 -0
  167. package/src/react/lib/form.ts +51 -8
  168. package/src/react/markdown/MarkdownDoc.spec.tsx +1 -1
  169. package/src/react/media/DenseGalleryShell.tsx +108 -36
  170. package/src/react/media/FocusViewer.tsx +15 -8
  171. package/src/react/media/FrameGrabber.spec.tsx +175 -0
  172. package/src/react/media/FrameGrabber.tsx +67 -0
  173. package/src/react/media/MediaResize.spec.tsx +84 -14
  174. package/src/react/media/MediaResize.tsx +51 -19
  175. package/src/react/media/VideoChapterEditor.tsx +428 -0
  176. package/src/react/media/VideoPlayer.spec.tsx +111 -1
  177. package/src/react/media/VideoPlayer.tsx +158 -11
  178. package/src/react/media/VideoTurner.spec.tsx +67 -0
  179. package/src/react/media/VideoTurner.tsx +70 -15
  180. package/src/react/media/denseGallery.css +104 -46
  181. package/src/react/media/hlsSource.recovery.spec.ts +213 -0
  182. package/src/react/media/hlsSource.spec.ts +8 -0
  183. package/src/react/media/hlsSource.ts +189 -1
  184. package/src/react/media/index.ts +14 -0
  185. package/src/react/media/mediaResize.css +127 -54
  186. package/src/react/media/pictureEditor.css +52 -6
  187. package/src/react/media/videoChapterEditor.css +203 -0
  188. package/src/react/media/videoChapters.spec.ts +176 -0
  189. package/src/react/media/videoChapters.ts +181 -0
  190. package/src/react/media-gallery/GalleryTable.tsx +93 -3
  191. package/src/react/media-gallery/MediaGallery.spec.tsx +1156 -10
  192. package/src/react/media-gallery/MediaGallery.tsx +813 -95
  193. package/src/react/media-gallery/MediaMetaEditor.tsx +237 -0
  194. package/src/react/media-gallery/galleryTable.spec.ts +41 -1
  195. package/src/react/media-gallery/index.ts +7 -0
  196. package/src/react/media-gallery/mediaGallery.css +180 -4
  197. package/src/react/media-gallery/mediaMeta.spec.ts +95 -0
  198. package/src/react/media-gallery/mediaMeta.ts +101 -0
  199. package/src/react/media-gallery/playbackPreferences.spec.ts +172 -0
  200. package/src/react/media-gallery/playbackPreferences.ts +100 -0
  201. package/src/react/media-gallery/types.ts +34 -0
  202. package/src/shippedFilesAreTracked.spec.ts +69 -0
  203. package/src/styles-areas/activity-bar.css +30 -0
  204. package/src/styles-areas/analytics.css +672 -0
  205. package/src/styles-areas/auth.css +154 -0
  206. package/src/styles-areas/calendar.css +416 -0
  207. package/src/styles-areas/chart-table.css +41 -0
  208. package/src/styles-areas/charts.css +89 -0
  209. package/src/styles-areas/chat.css +364 -0
  210. package/src/styles-areas/clipboard.css +26 -0
  211. package/src/styles-areas/code-editor.css +130 -0
  212. package/src/styles-areas/companion-link.css +70 -0
  213. package/src/styles-areas/core.css +2503 -0
  214. package/src/styles-areas/dashboard-grid.css +84 -0
  215. package/src/styles-areas/data-table.css +657 -0
  216. package/src/styles-areas/deep-link.css +26 -0
  217. package/src/styles-areas/diff-viewer.css +30 -0
  218. package/src/styles-areas/disk-usage.css +262 -0
  219. package/src/styles-areas/emoji.css +154 -0
  220. package/src/styles-areas/fields.css +582 -0
  221. package/src/styles-areas/file-tree.css +33 -0
  222. package/src/styles-areas/filter-rail.css +235 -0
  223. package/src/styles-areas/folder-tree.css +219 -0
  224. package/src/styles-areas/keep-awake.css +26 -0
  225. package/src/styles-areas/layout-engine.css +239 -0
  226. package/src/styles-areas/markdown.css +87 -0
  227. package/src/styles-areas/master-detail.css +239 -0
  228. package/src/styles-areas/master-lock.css +26 -0
  229. package/src/styles-areas/media-gallery.css +351 -0
  230. package/src/styles-areas/media.css +658 -0
  231. package/src/styles-areas/nav.css +488 -0
  232. package/src/styles-areas/notifications.css +245 -0
  233. package/src/styles-areas/overlays.css +133 -0
  234. package/src/styles-areas/palette.css +140 -0
  235. package/src/styles-areas/pdf-viewer.css +47 -0
  236. package/src/styles-areas/problem-detail.css +97 -0
  237. package/src/styles-areas/rich-text.css +167 -0
  238. package/src/styles-areas/sharing.css +153 -0
  239. package/src/styles-areas/spreadsheet.css +37 -0
  240. package/src/styles-areas/stats.css +281 -0
  241. package/src/styles-areas/test-report.css +133 -0
  242. package/src/styles-areas/virtual.css +30 -0
  243. package/src/styles-areas/wizard.css +381 -0
  244. package/src/styles-areas/workbench.css +543 -0
  245. package/src/styles-areas/workbook-viewer.css +44 -0
  246. package/src/styles-static.css +34 -23
  247. package/src/styles.css +34 -23
  248. package/src/stylesAreas.spec.ts +247 -0
  249. package/src/stylesUtilitiesMatches.spec.ts +6 -77
  250. package/src/testFilesRunInParallel.spec.ts +79 -0
  251. package/src/typecheckCachesAreSeparate.spec.ts +128 -0
  252. package/src/verifyGraph.spec.ts +6 -0
@@ -147,7 +147,7 @@
147
147
  // run, so these stay few and visible. A malformed/unrecognized-category directive is
148
148
  // ignored (its violations still fire) and a warning is printed.
149
149
 
150
- import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
150
+ import { type Dirent, existsSync, readdirSync, readFileSync, statSync } from "node:fs";
151
151
  import { basename, extname, join, relative } from "node:path";
152
152
 
153
153
  const IGNORE_DIR_NAMES = new Set(["node_modules", "dist", "dist-deploy", ".git", "build", "coverage"]);
@@ -179,7 +179,15 @@ interface Rule {
179
179
  }
180
180
 
181
181
  export function walk(dir: string, out: string[] = []): string[] {
182
- let entries: ReturnType<typeof readdirSync>;
182
+ // 🔴 `Dirent[]`, never `ReturnType<typeof readdirSync>`. `readdirSync` is OVERLOADED, and
183
+ // `ReturnType` of an overloaded function is the LAST overload — which @types/node 24 made
184
+ // `Dirent<NonSharedBuffer>[]` when `Dirent` became generic, while the call below returns
185
+ // the string-encoding overload's `Dirent<string>[]`. The annotation was always a guess at
186
+ // an overload set somebody else maintains; a release reordered it and the guess became
187
+ // five compile errors that every consumer of `cursedbelt/guardrails` inherited (task 172).
188
+ // Bare `Dirent` is `Dirent<string>` under >=24's default type argument and is the plain
189
+ // non-generic class before that, so this spelling is correct on both.
190
+ let entries: Dirent[];
183
191
  try {
184
192
  entries = readdirSync(dir, { withFileTypes: true });
185
193
  } catch {
@@ -201,13 +209,69 @@ export function walk(dir: string, out: string[] = []): string[] {
201
209
  return out;
202
210
  }
203
211
 
212
+ /** Index just past the string/template literal opening at `start`. Quoted attribute values
213
+ * are skipped so a `//` inside one reads as the URL it is (`src="https://…"`) rather than
214
+ * as the start of a comment. `'`/`"` stop at a newline — an unterminated quote must not
215
+ * swallow the rest of the file; a backtick may span lines and nests `${…}`. */
216
+ function skipQuoted(source: string, start: number): number {
217
+ const quote = source[start];
218
+ let i = start + 1;
219
+ if (quote !== "`") {
220
+ while (i < source.length && source[i] !== quote && source[i] !== "\n") {
221
+ i += source[i] === "\\" ? 2 : 1;
222
+ }
223
+ return Math.min(i + 1, source.length);
224
+ }
225
+ let depth = 0;
226
+ while (i < source.length) {
227
+ if (source[i] === "\\") {
228
+ i += 2;
229
+ continue;
230
+ }
231
+ if (depth === 0 && source[i] === "`") return i + 1;
232
+ if (source.slice(i, i + 2) === "${") {
233
+ depth++;
234
+ i += 2;
235
+ continue;
236
+ }
237
+ if (depth > 0 && source[i] === "}") depth--;
238
+ i++;
239
+ }
240
+ return i;
241
+ }
242
+
204
243
  /** Slice from a `<button` (or similar) match to the end of its opening tag, tolerating
205
- * `{...}` JS expressions in attribute values that may themselves contain `>`. */
244
+ * `{...}` JS expressions in attribute values that may themselves contain `>`
245
+ * (`onClick={() => …}`, which works because that `>` sits inside braces).
246
+ *
247
+ * JSX permits line and block comments INSIDE an attribute list, and a comment is prose —
248
+ * so it may contain an angle bracket ("a radio-like list rather than a `<select>`").
249
+ * Those are skipped the same way `blankComments` skips them, because a `>` written in prose
250
+ * used to end the tag here and make every attribute below it invisible — the
251
+ * `guardrails-ignore` opt-out included. `blankComments` cannot simply be run first: the
252
+ * opt-out checks read this slice and must see the ORIGINAL comment text (see the
253
+ * no-raw-action-button check below), so the skip only suppresses TERMINATION; the comment
254
+ * stays in what is returned. */
206
255
  function extractOpeningTag(source: string, startIndex: number): string {
207
256
  let i = startIndex;
208
257
  let braceDepth = 0;
209
258
  while (i < source.length) {
259
+ const two = source.slice(i, i + 2);
260
+ if (two === "//") {
261
+ const end = source.indexOf("\n", i);
262
+ i = end === -1 ? source.length : end;
263
+ continue;
264
+ }
265
+ if (two === "/*") {
266
+ const end = source.indexOf("*/", i + 2);
267
+ i = end === -1 ? source.length : end + 2;
268
+ continue;
269
+ }
210
270
  const ch = source[i];
271
+ if (ch === '"' || ch === "'" || ch === "`") {
272
+ i = skipQuoted(source, i);
273
+ continue;
274
+ }
211
275
  if (ch === "{") braceDepth++;
212
276
  else if (ch === "}") braceDepth--;
213
277
  else if (ch === ">" && braceDepth <= 0) {
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * `bun run paths` — the generation's whole-tree laws, run over THIS repo's tree.
4
+ *
5
+ * {@link TOOLS} is the list: `check-paths.ts` (the one law — exactly one file in a generation
6
+ * may contain a literal path) and `check-doc-citations.ts` (every `docs/<name>.md` this repo
7
+ * points a reader at must exist, with the sections it names). Both are generation-scoped rules
8
+ * a repo proves about ITSELF — rule 3, a repo's gate proves that repo — both are located the
9
+ * same way, and both have to fail this step rather than be skipped.
10
+ *
11
+ * ## 🔴 Why this is a wrapper and not `bun ../../tools/check-paths.ts .`
12
+ *
13
+ * That spelling assumes this checkout sits at a fixed depth under the generation root, and a
14
+ * WORKTREE never does: the working agreement puts worktrees outside every generation by
15
+ * construction, under the machine-wide worktrees root. Measured 2026-09-17 in a worktree,
16
+ * with no changes to any script — `error: Module not found "../../tools/check-paths.ts"`,
17
+ * and the gate stops on its FIRST step. Seventeen packages spelled it that way, and the failure
18
+ * mode is not the error — it is the workaround: an agent that cannot run its repo's gate moves
19
+ * back into the shared checkout another agent is holding, or lands without a green gate — and
20
+ * the second one is invisible afterwards.
21
+ *
22
+ * {@link findForgeRoot} walks up for `forge.env`, the same marker `tools/check-paths.ts` and
23
+ * the runner both use, so the three cannot disagree. {@link viaWorktree} is the worktree case:
24
+ * a worktree's `.git` is a FILE pointing at the primary checkout's git directory, so the
25
+ * primary is discoverable with no environment variable and no guess.
26
+ *
27
+ * 🔴 **No generation, no pass.** Answering "fine" because the check could not be located is how
28
+ * a rule stops being enforced without anybody deciding to stop enforcing it — so every failure
29
+ * to locate exits 1, and says which of the two walks came back empty.
30
+ *
31
+ * `tools/check-paths.ts` is what keeps this spelling honest across the roster: a repo whose
32
+ * `paths` script reaches a tool through a relative `../` module path fails the whole-generation
33
+ * run, which is the check that this whole file is the fix for.
34
+ */
35
+ import { spawnSync } from "node:child_process";
36
+ import { existsSync } from "node:fs";
37
+ import { dirname, join, resolve } from "node:path";
38
+ import { fileURLToPath } from "node:url";
39
+
40
+ /** The generation's tools this repo runs over its own tree, in the order they should fail. */
41
+ const TOOLS = ["check-paths.ts", "check-doc-citations.ts"];
42
+
43
+ /** How far up either walk looks before giving up — a bound, so a misuse is `null` in ms. */
44
+ const MAX_HOPS = 16;
45
+
46
+ /** Walk up from `from` for a directory containing `marker`, or `null`. */
47
+ function walkUp(from: string, marker: string): string | null {
48
+ let dir = resolve(from);
49
+ for (let hops = 0; hops < MAX_HOPS; hops++) {
50
+ if (existsSync(join(dir, marker))) return dir;
51
+ const up = dirname(dir);
52
+ if (up === dir) return null;
53
+ dir = up;
54
+ }
55
+ return null;
56
+ }
57
+
58
+ /** The directory holding `forge.env` above `from`, or `null` when that is not inside a generation. */
59
+ function findForgeRoot(from: string): string | null {
60
+ return walkUp(from, "forge.env");
61
+ }
62
+
63
+ /** The generation above the checkout this WORKTREE was cut from, or `null` on anything unexpected. */
64
+ function viaWorktree(from: string): string | null {
65
+ try {
66
+ const git = spawnSync("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], {
67
+ cwd: from,
68
+ encoding: "utf8",
69
+ timeout: 5_000,
70
+ });
71
+ if (git.status !== 0) return null;
72
+ const gitDir = (git.stdout ?? "").trim();
73
+ if (!gitDir || !gitDir.startsWith("/")) return null;
74
+ // `<primary>/.git` → `<primary>`, then the ordinary walk up.
75
+ return findForgeRoot(dirname(gitDir));
76
+ } catch {
77
+ return null;
78
+ }
79
+ }
80
+
81
+ const here = dirname(fileURLToPath(import.meta.url));
82
+ /** This checkout — the nearest ancestor holding a `package.json`, so it is right in a worktree. */
83
+ const repo = walkUp(here, "package.json");
84
+ const root = findForgeRoot(here) ?? viaWorktree(here);
85
+
86
+ if (!repo) {
87
+ console.error(`✗ no package.json above ${here}, so there is no repo to check.`);
88
+ process.exit(1);
89
+ }
90
+ if (!root) {
91
+ console.error("✗ this checkout is not inside a generation and is not a worktree of one, so");
92
+ console.error(` tools/{${TOOLS.join(",")}} cannot be located. Run it from a checkout under a`);
93
+ console.error(" generation, or from a worktree cut from one — a skipped check is not a passed one.");
94
+ process.exit(1);
95
+ }
96
+
97
+ for (const name of TOOLS) {
98
+ const tool = join(root, "tools", name);
99
+ if (!existsSync(tool)) {
100
+ console.error(`✗ ${tool} does not exist. The generation at ${root} has no ${name},`);
101
+ console.error(" so this repo cannot prove it obeys that law. That is a failure, not a skip.");
102
+ process.exit(1);
103
+ }
104
+ const result = spawnSync("bun", [tool, repo, ...process.argv.slice(2)], { stdio: "inherit" });
105
+ if ((result.status ?? 1) !== 0) process.exit(result.status ?? 1);
106
+ }