cursedbelt 4.1.1 → 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 (108) 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/CanvasVideoPlayer.d.ts +3 -2
  8. package/dist/react/media/CanvasVideoPlayer.d.ts.map +1 -1
  9. package/dist/react/media/CanvasVideoPlayer.js +4 -3
  10. package/dist/react/media/CanvasVideoPlayer.js.map +1 -1
  11. package/dist/react/media/FrameGrabber.d.ts +16 -2
  12. package/dist/react/media/FrameGrabber.d.ts.map +1 -1
  13. package/dist/react/media/FrameGrabber.js +68 -43
  14. package/dist/react/media/FrameGrabber.js.map +1 -1
  15. package/dist/react/media/PictureEditor.d.ts +17 -2
  16. package/dist/react/media/PictureEditor.d.ts.map +1 -1
  17. package/dist/react/media/PictureEditor.js +33 -7
  18. package/dist/react/media/PictureEditor.js.map +1 -1
  19. package/dist/react/media/VideoChapterEditor.d.ts +17 -2
  20. package/dist/react/media/VideoChapterEditor.d.ts.map +1 -1
  21. package/dist/react/media/VideoChapterEditor.js +21 -3
  22. package/dist/react/media/VideoChapterEditor.js.map +1 -1
  23. package/dist/react/media/VideoCutEditor.d.ts +16 -2
  24. package/dist/react/media/VideoCutEditor.d.ts.map +1 -1
  25. package/dist/react/media/VideoCutEditor.js +21 -3
  26. package/dist/react/media/VideoCutEditor.js.map +1 -1
  27. package/dist/react/media/VideoPlayer.d.ts +37 -9
  28. package/dist/react/media/VideoPlayer.d.ts.map +1 -1
  29. package/dist/react/media/VideoPlayer.js +130 -39
  30. package/dist/react/media/VideoPlayer.js.map +1 -1
  31. package/dist/react/media/hlsSource.d.ts +24 -3
  32. package/dist/react/media/hlsSource.d.ts.map +1 -1
  33. package/dist/react/media/hlsSource.js +109 -8
  34. package/dist/react/media/hlsSource.js.map +1 -1
  35. package/dist/react/media/pictureEditor.css +2 -0
  36. package/dist/react/media/sourceFault.d.ts +146 -0
  37. package/dist/react/media/sourceFault.d.ts.map +1 -0
  38. package/dist/react/media/sourceFault.js +139 -0
  39. package/dist/react/media/sourceFault.js.map +1 -0
  40. package/dist/react/media/videoChapterEditor.css +19 -0
  41. package/dist/react/media/videoCutEditor.css +22 -0
  42. package/dist/react/media-gallery/MediaGallery.d.ts +21 -3
  43. package/dist/react/media-gallery/MediaGallery.d.ts.map +1 -1
  44. package/dist/react/media-gallery/MediaGallery.js +178 -17
  45. package/dist/react/media-gallery/MediaGallery.js.map +1 -1
  46. package/dist/react/media-gallery/galleryItemMedia.d.ts +4 -0
  47. package/dist/react/media-gallery/galleryItemMedia.d.ts.map +1 -1
  48. package/dist/react/media-gallery/galleryItemMedia.js +27 -3
  49. package/dist/react/media-gallery/galleryItemMedia.js.map +1 -1
  50. package/dist/react/media-gallery/mediaGalleryModel.d.ts.map +1 -1
  51. package/dist/react/media-gallery/mediaGalleryModel.js +5 -0
  52. package/dist/react/media-gallery/mediaGalleryModel.js.map +1 -1
  53. package/dist/react/media-gallery/types.d.ts +23 -0
  54. package/dist/react/media-gallery/types.d.ts.map +1 -1
  55. package/dist/scripts/guardrailsEnforce.d.ts +1 -0
  56. package/dist/scripts/guardrailsEnforce.d.ts.map +1 -1
  57. package/dist/styles-areas/analytics.css +1 -1
  58. package/dist/styles-areas/data-table.css +1 -1
  59. package/dist/styles-areas/fields.css +1 -1
  60. package/dist/styles-areas/media-gallery.css +1 -1
  61. package/dist/styles-areas/media.css +1 -1
  62. package/dist/styles-areas/wizard.css +1 -1
  63. package/package.json +51 -6
  64. package/scripts/demoServer.ts +211 -0
  65. package/scripts/gate.ts +101 -0
  66. package/scripts/guardrailsEnforce.spec.ts +313 -1
  67. package/scripts/guardrailsEnforce.ts +283 -26
  68. package/scripts/paths.ts +1 -1
  69. package/src/demoFixture.spec.ts +54 -10
  70. package/src/demoStaticServer.spec.ts +169 -0
  71. package/src/publishShape.spec.ts +79 -0
  72. package/src/react/components/Autocomplete.tsx +11 -1
  73. package/src/react/components/ComboboxField.spec.tsx +12 -0
  74. package/src/react/components/ComboboxField.tsx +11 -1
  75. package/src/react/media/CanvasVideoPlayer.tsx +11 -6
  76. package/src/react/media/FrameGrabber.spec.tsx +57 -1
  77. package/src/react/media/FrameGrabber.tsx +138 -84
  78. package/src/react/media/PictureEditor.spec.tsx +90 -0
  79. package/src/react/media/PictureEditor.tsx +78 -23
  80. package/src/react/media/VideoChapterEditor.spec.tsx +86 -0
  81. package/src/react/media/VideoChapterEditor.tsx +64 -18
  82. package/src/react/media/VideoCutEditor.spec.tsx +44 -0
  83. package/src/react/media/VideoCutEditor.tsx +63 -18
  84. package/src/react/media/VideoPlayer.spec.tsx +101 -0
  85. package/src/react/media/VideoPlayer.tsx +221 -73
  86. package/src/react/media/hlsSource.answer.spec.ts +255 -0
  87. package/src/react/media/hlsSource.ts +136 -8
  88. package/src/react/media/pictureEditor.css +2 -0
  89. package/src/react/media/sourceFault.spec.tsx +224 -0
  90. package/src/react/media/sourceFault.tsx +220 -0
  91. package/src/react/media/videoChapterEditor.css +19 -0
  92. package/src/react/media/videoCutEditor.css +22 -0
  93. package/src/react/media-gallery/MediaGallery.spec.tsx +311 -0
  94. package/src/react/media-gallery/MediaGallery.tsx +382 -54
  95. package/src/react/media-gallery/galleryItemMedia.tsx +27 -4
  96. package/src/react/media-gallery/mediaGalleryModel.spec.ts +36 -0
  97. package/src/react/media-gallery/mediaGalleryModel.ts +4 -0
  98. package/src/react/media-gallery/types.ts +23 -0
  99. package/src/styles-areas/analytics.css +1 -1
  100. package/src/styles-areas/data-table.css +1 -1
  101. package/src/styles-areas/fields.css +1 -1
  102. package/src/styles-areas/media-gallery.css +1 -1
  103. package/src/styles-areas/media.css +1 -1
  104. package/src/styles-areas/wizard.css +1 -1
  105. package/src/testFilesRunInParallel.spec.ts +3 -3
  106. package/src/typecheckCachesAreSeparate.spec.ts +2 -2
  107. package/src/verifyGraph.spec.ts +75 -108
  108. package/scripts/verify.ts +0 -296
@@ -1,8 +1,13 @@
1
+ // 🔴 `package.json`'s `test` script globs `src scripts`, not `src` — this file is the only
2
+ // spec outside `src`, and until 2026-09-18 nothing ran it. The gate was green while every
3
+ // assertion in here (129 of them, the scanner's entire behavioural contract) was inert, so a
4
+ // rule could rot and `bun run verify` would still say ok. If you narrow that glob back, the
5
+ // tests below stop being checks and become a document.
1
6
  import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
2
7
  import { tmpdir } from 'node:os';
3
8
  import { join } from 'node:path';
4
9
  import { describe, expect, test } from 'bun:test';
5
- import { parseFileAllows, RULES, setReimplementedNames, walk } from './guardrailsEnforce';
10
+ import { beltExportedComponentNames, parseFileAllows, RULES, setReimplementedNames, walk } from './guardrailsEnforce';
6
11
 
7
12
  function check(rule: keyof typeof RULES, source: string) {
8
13
  return RULES[rule].check('test.tsx', source);
@@ -107,6 +112,70 @@ describe('opening-tag extraction (comment-aware)', () => {
107
112
  });
108
113
  });
109
114
 
115
+ // Filed 2026-09-18, found while proving the fix above: the first negative test came back
116
+ // green for the WRONG reason. Making the walker comment-aware widened what it returns, and
117
+ // the opt-out checks then read prose with regexes that look for a real ATTRIBUTE — so a
118
+ // comment merely QUOTING `role="radio"` suppressed no-raw-action-button on a raw button
119
+ // that had no presentational role at all. The rule could not be trusted to read zero in
120
+ // exactly the well-documented files it should be strictest about. The input was wrong, not
121
+ // the pattern: the comment check reads the tag as written, the attribute check reads it
122
+ // comment-blanked.
123
+ describe('a presentational marker must be an ATTRIBUTE, not prose about one', () => {
124
+ test('role="radio" only inside a comment does NOT opt the button out', () => {
125
+ const source = `<button
126
+ // A radio-like list: \`role="radio"\` + \`aria-checked\` says so to a screen reader.
127
+ type="button"
128
+ onClick={() => onPick(id)}
129
+ >Move here</button>`;
130
+ expect(check('no-raw-action-button', source)).toHaveLength(1);
131
+ });
132
+
133
+ test('aria-selected= only inside a block comment does NOT opt the button out', () => {
134
+ const source = `<button
135
+ /* The row is the control here; aria-selected= would be the tab-ish spelling. */
136
+ type="button"
137
+ >Move here</button>`;
138
+ expect(check('no-raw-action-button', source)).toHaveLength(1);
139
+ });
140
+
141
+ test('the guardrails-ignore comment still opts out beside that same prose', () => {
142
+ const source = `<button
143
+ // A radio-like list: \`role="radio"\` + \`aria-checked\` says so to a screen reader.
144
+ // guardrails-ignore no-raw-action-button: presentational row in a radio-like list
145
+ type="button"
146
+ >Move here</button>`;
147
+ expect(check('no-raw-action-button', source)).toHaveLength(0);
148
+ });
149
+
150
+ test('the REAL attributes still pass silently, prose and all', () => {
151
+ const source = `<button
152
+ // A radio-like list: \`role="radio"\` + \`aria-checked\` says so to a screen reader.
153
+ role="radio"
154
+ aria-checked={selected}
155
+ type="button"
156
+ >Move here</button>`;
157
+ expect(check('no-raw-action-button', source)).toHaveLength(0);
158
+ });
159
+
160
+ // The same shape on no-raw-title-attr, which fires the other way: there the attribute
161
+ // regex is what CREATES the violation, so prose mentioning `title=` invented one.
162
+ test('a title= written only in a comment is not a title attribute', () => {
163
+ const source = `<span
164
+ // Not a title= hover hint — the expansion is rendered beside it.
165
+ className="tabular"
166
+ >UTC</span>`;
167
+ expect(check('no-raw-title-attr', source)).toHaveLength(0);
168
+ });
169
+
170
+ test('a real title attribute below that prose is still flagged', () => {
171
+ const source = `<span
172
+ // Reads as a title= hover hint, and that is the bug.
173
+ title="Coordinated Universal Time"
174
+ >UTC</span>`;
175
+ expect(check('no-raw-title-attr', source)).toHaveLength(1);
176
+ });
177
+ });
178
+
110
179
  describe('no-raw-overflow', () => {
111
180
  test('flags inline style overflow: auto', () => {
112
181
  expect(check('no-raw-overflow', '<div style={{ overflow: "auto" }} />')).toHaveLength(1);
@@ -864,3 +933,246 @@ describe('no-fast-tooltip', () => {
864
933
  ).toHaveLength(0);
865
934
  });
866
935
  });
936
+
937
+ // ── the CLI's two ratchets, and the flags that let an app use one ──────────────
938
+ // Nine apps each carried an identical ~140-line wrapper (`apps/*/scripts/guardrails.ts`)
939
+ // because this CLI could only read the baseline shipped INSIDE the package. These
940
+ // exercise the flags that replaced them, as a subprocess — the wrappers' only contract
941
+ // was this script's exit code and its verdict lines, and that is what an app's `verify`
942
+ // still depends on.
943
+
944
+ const CLI = join(import.meta.dir, 'guardrailsEnforce.ts');
945
+
946
+ function withFixture(files: Record<string, string>, body: (dir: string) => void): void {
947
+ const root = mkdtempSync(join(tmpdir(), 'guardrails-cli-'));
948
+ try {
949
+ for (const [rel, contents] of Object.entries(files)) {
950
+ const full = join(root, rel);
951
+ mkdirSync(join(full, '..'), { recursive: true });
952
+ writeFileSync(full, contents);
953
+ }
954
+ body(root);
955
+ } finally {
956
+ rmSync(root, { recursive: true, force: true });
957
+ }
958
+ }
959
+
960
+ function runCli(cwd: string, args: string[]): { code: number; report: string } {
961
+ const run = Bun.spawnSync([process.execPath, CLI, ...args], { cwd });
962
+ return { code: run.exitCode, report: `${run.stdout.toString()}${run.stderr.toString()}` };
963
+ }
964
+
965
+ /** Two hex colours and one raw <input>: counts a baseline row can be wrong about. */
966
+ const FIXTURE = {
967
+ 'src/ui/theme.css': '.a { color: #ff0000; }\n.b { background: #00ff00; }\n',
968
+ 'src/ui/Form.tsx': 'export const Form = () => <input name="q" />;\n',
969
+ };
970
+
971
+ const rowsFor = (colors: number, inputs: number) =>
972
+ JSON.stringify({ 'no-hardcoded-colors': colors, 'no-raw-inputs': inputs });
973
+
974
+ describe('--baseline (a consuming repo\'s ratchet)', () => {
975
+ test('passes when every row equals the count that is actually there', () => {
976
+ withFixture({ ...FIXTURE, 'scripts/baseline.json': rowsFor(2, 1) }, (dir) => {
977
+ const { code, report } = runCli(dir, [
978
+ 'no-hardcoded-colors,no-raw-inputs',
979
+ '--baseline',
980
+ 'scripts/baseline.json',
981
+ 'src',
982
+ ]);
983
+ expect(report).toContain('✓ no-hardcoded-colors: 2 violation(s)');
984
+ expect(report).toContain('✓ no-raw-inputs: 1 violation(s)');
985
+ expect(code).toBe(0);
986
+ });
987
+ });
988
+
989
+ test('🔴 a row ABOVE the real count FAILS, printing the number to paste', () => {
990
+ // The half a one-sided ratchet gets wrong: fix a violation, leave the row where it
991
+ // was, and the slack is available for the next one to grow back into silently.
992
+ withFixture({ ...FIXTURE, 'scripts/baseline.json': rowsFor(9, 1) }, (dir) => {
993
+ const { code, report } = runCli(dir, [
994
+ 'no-hardcoded-colors,no-raw-inputs',
995
+ '--baseline',
996
+ 'scripts/baseline.json',
997
+ 'src',
998
+ ]);
999
+ expect(code).toBe(1);
1000
+ expect(report).toContain('✗ no-hardcoded-colors: 2 violation(s), below the baseline of 9');
1001
+ expect(report).toContain('lower "no-hardcoded-colors" to 2 in scripts/baseline.json');
1002
+ });
1003
+ });
1004
+
1005
+ test('a count above its row fails, the way it always did', () => {
1006
+ withFixture({ ...FIXTURE, 'scripts/baseline.json': rowsFor(2, 0) }, (dir) => {
1007
+ const { code, report } = runCli(dir, [
1008
+ 'no-hardcoded-colors,no-raw-inputs',
1009
+ '--baseline',
1010
+ 'scripts/baseline.json',
1011
+ 'src',
1012
+ ]);
1013
+ expect(code).toBe(1);
1014
+ expect(report).toContain('✗ no-raw-inputs: 1 violation(s) — exceeds baseline of 0');
1015
+ });
1016
+ });
1017
+
1018
+ test('a row for a rule nobody scans is refused — coverage that is not there', () => {
1019
+ withFixture(
1020
+ { ...FIXTURE, 'scripts/baseline.json': JSON.stringify({ 'no-hardcoded-colors': 2, 'no-raw-zindex': 0 }) },
1021
+ (dir) => {
1022
+ const { code, report } = runCli(dir, ['no-hardcoded-colors', '--baseline', 'scripts/baseline.json', 'src']);
1023
+ expect(code).toBe(2);
1024
+ expect(report).toContain('row(s) for rules nobody scans: no-raw-zindex');
1025
+ },
1026
+ );
1027
+ });
1028
+
1029
+ test('a scanned rule with no row is refused — a silent 0 demands the whole pile at once', () => {
1030
+ withFixture({ ...FIXTURE, 'scripts/baseline.json': JSON.stringify({ 'no-hardcoded-colors': 2 }) }, (dir) => {
1031
+ const { code, report } = runCli(dir, [
1032
+ 'no-hardcoded-colors,no-raw-inputs',
1033
+ '--baseline',
1034
+ 'scripts/baseline.json',
1035
+ 'src',
1036
+ ]);
1037
+ expect(code).toBe(2);
1038
+ expect(report).toContain('no row for rule(s) being scanned: no-raw-inputs');
1039
+ });
1040
+ });
1041
+
1042
+ test('"$comment" carries the why; any OTHER non-number is a typo in a row', () => {
1043
+ withFixture(
1044
+ {
1045
+ ...FIXTURE,
1046
+ 'scripts/baseline.json': JSON.stringify({ $comment: ['the why', 'over several lines'], 'no-hardcoded-colors': 2 }),
1047
+ },
1048
+ (dir) => {
1049
+ expect(runCli(dir, ['no-hardcoded-colors', '--baseline', 'scripts/baseline.json', 'src']).code).toBe(0);
1050
+ },
1051
+ );
1052
+ withFixture({ ...FIXTURE, 'scripts/baseline.json': JSON.stringify({ 'no-hardcoded-colors': '2' }) }, (dir) => {
1053
+ const { code, report } = runCli(dir, ['no-hardcoded-colors', '--baseline', 'scripts/baseline.json', 'src']);
1054
+ expect(code).toBe(2);
1055
+ expect(report).toContain('"no-hardcoded-colors" is "2", not a violation count');
1056
+ });
1057
+ });
1058
+
1059
+ test('a missing baseline file stops the run rather than defaulting every row to 0', () => {
1060
+ withFixture(FIXTURE, (dir) => {
1061
+ const { code, report } = runCli(dir, ['no-hardcoded-colors', '--baseline', 'scripts/nope.json', 'src']);
1062
+ expect(code).toBe(2);
1063
+ expect(report).toContain('no such file');
1064
+ });
1065
+ });
1066
+
1067
+ test('is not the packaged --scope ratchet, and says so rather than picking one', () => {
1068
+ withFixture({ ...FIXTURE, 'scripts/baseline.json': rowsFor(2, 1) }, (dir) => {
1069
+ const { code, report } = runCli(dir, [
1070
+ 'no-hardcoded-colors',
1071
+ '--scope',
1072
+ 'cursedbelt',
1073
+ '--baseline',
1074
+ 'scripts/baseline.json',
1075
+ 'src',
1076
+ ]);
1077
+ expect(code).toBe(2);
1078
+ expect(report).toContain('two different ratchets');
1079
+ });
1080
+ });
1081
+
1082
+ test('--scope stays ONE-sided: below its row passes and only suggests', () => {
1083
+ // The packaged baseline lives inside node_modules, so a consumer cannot lower a row
1084
+ // in it. Failing them for a number only a cursedbelt release can change would be a
1085
+ // gate nobody can turn green — which is why only --baseline is two-sided.
1086
+ withFixture(FIXTURE, (dir) => {
1087
+ const { code, report } = runCli(dir, ['no-hardcoded-colors', '--scope', 'cursedbelt', 'src']);
1088
+ expect(code).toBe(1); // 2 found against the packaged cursedbelt row of 0
1089
+ expect(report).toContain('exceeds baseline of 0');
1090
+ });
1091
+ });
1092
+ });
1093
+
1094
+ describe('--reimplements-from-belt', () => {
1095
+ test('flags a local component named after one this package exports', () => {
1096
+ // Read off this package's own react index files, so the list is the truth of the
1097
+ // version an app installed rather than a copy it maintains.
1098
+ withFixture(
1099
+ {
1100
+ 'src/Button.tsx': 'export function Button() { return null; }\n',
1101
+ 'scripts/baseline.json': JSON.stringify({ 'no-local-reimplementation': 0 }),
1102
+ },
1103
+ (dir) => {
1104
+ const { code, report } = runCli(dir, [
1105
+ 'no-local-reimplementation',
1106
+ '--baseline',
1107
+ 'scripts/baseline.json',
1108
+ '--reimplements-from-belt',
1109
+ 'src',
1110
+ ]);
1111
+ expect(code).toBe(1);
1112
+ expect(report).toContain('✗ no-local-reimplementation: 1 violation(s)');
1113
+ expect(report).toContain('src/Button.tsx');
1114
+ },
1115
+ );
1116
+ });
1117
+
1118
+ test('the derived list is this package\'s real exports, not a handful', () => {
1119
+ const names = beltExportedComponentNames();
1120
+ expect(names.length).toBeGreaterThan(100);
1121
+ expect(names).toContain('Button');
1122
+ expect(names).toContain('DataTable');
1123
+ });
1124
+
1125
+ test('🔴 fails CLOSED on an empty list rather than scoring an inert rule 0', () => {
1126
+ // Without this, a moved react tree makes `no-local-reimplementation` print
1127
+ // "0 violation(s)" for a check that never ran, and its 0 row looks earned.
1128
+ const empty = mkdtempSync(join(tmpdir(), 'guardrails-belt-'));
1129
+ try {
1130
+ expect(beltExportedComponentNames(empty)).toEqual([]);
1131
+ } finally {
1132
+ rmSync(empty, { recursive: true, force: true });
1133
+ }
1134
+ });
1135
+ });
1136
+
1137
+ describe('--exclude', () => {
1138
+ test('drops build output that lives under a source root', () => {
1139
+ // desk's `src/generated/assets.ts` is the compiled browser app inlined as a module:
1140
+ // 282 double-counted violations that move with the bundle, reddening a two-sided
1141
+ // ratchet over work nobody did.
1142
+ const files = {
1143
+ ...FIXTURE,
1144
+ 'src/generated/assets.css': '.x { color: #123456; }\n.y { color: #654321; }\n',
1145
+ 'scripts/baseline.json': rowsFor(2, 1),
1146
+ };
1147
+ withFixture(files, (dir) => {
1148
+ const scanned = runCli(dir, ['no-hardcoded-colors,no-raw-inputs', '--baseline', 'scripts/baseline.json', 'src']);
1149
+ expect(scanned.code).toBe(1); // 4 colours, not 2 — the artifact counted as source
1150
+
1151
+ const { code, report } = runCli(dir, [
1152
+ 'no-hardcoded-colors,no-raw-inputs',
1153
+ '--baseline',
1154
+ 'scripts/baseline.json',
1155
+ '--exclude',
1156
+ 'src/generated',
1157
+ 'src',
1158
+ ]);
1159
+ expect(report).toContain('✓ no-hardcoded-colors: 2 violation(s)');
1160
+ expect(code).toBe(0);
1161
+ });
1162
+ });
1163
+
1164
+ test('an exclusion matching nothing is refused, never read as handled', () => {
1165
+ withFixture({ ...FIXTURE, 'scripts/baseline.json': rowsFor(2, 1) }, (dir) => {
1166
+ const { code, report } = runCli(dir, [
1167
+ 'no-hardcoded-colors,no-raw-inputs',
1168
+ '--baseline',
1169
+ 'scripts/baseline.json',
1170
+ '--exclude',
1171
+ 'src/generated',
1172
+ 'src',
1173
+ ]);
1174
+ expect(code).toBe(2);
1175
+ expect(report).toContain('--exclude matched no scanned file');
1176
+ });
1177
+ });
1178
+ });