cursedbelt 5.103.0 โ†’ 5.103.1

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.
@@ -1 +1 @@
1
- {"version":3,"file":"guardrailsEnforce.d.ts","sourceRoot":"","sources":["../../scripts/guardrailsEnforce.ts"],"names":[],"mappings":";AAsQA,UAAU,SAAS;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB;;;sEAGkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,UAAU,IAAI;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,SAAS,EAAE,CAAC;CACzD;AAED,wBAAgB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,EAAO,GAAG,MAAM,EAAE,CA6B9D;AA2yCD,iGAAiG;AACjG,wBAAgB,WAAW,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAE3D;AAoHD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAIpE;AA+6BD,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,SAAS,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,GAAG,IAAI,CAEpF;AAID,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,CAoBhE;AAyKD,kGAAkG;AAClG,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAatE;AAqDD,UAAU,MAAM;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACb;AA0BD,+FAA+F;AAC/F,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAgBpD;AAID,wEAAwE;AACxE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAO5E;AAiJD,eAAO,MAAM,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAoCtC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,mBAAmB,EAAE,WAAW,CAAC,MAAM,CAAiE,CAAC;AAEtH,0EAA0E;AAC1E,eAAO,MAAM,SAAS,EAAE,SAAS,MAAM,EAAwE,CAAC;AAShH,eAAO,MAAM,uBAAuB,aAAkD,CAAC;AAGvF,MAAM,WAAW,SAAS;IACzB,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACb;AAED;2FAC2F;AAC3F,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CA+C3F;AAED,YAAY,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;AAgHhC,wBAAgB,0BAA0B,CAAC,SAAS,GAAE,MAAwB,GAAG,MAAM,EAAE,CAyBxF"}
1
+ {"version":3,"file":"guardrailsEnforce.d.ts","sourceRoot":"","sources":["../../scripts/guardrailsEnforce.ts"],"names":[],"mappings":";AAsQA,UAAU,SAAS;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB;;;sEAGkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,UAAU,IAAI;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,SAAS,EAAE,CAAC;CACzD;AAED,wBAAgB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,EAAO,GAAG,MAAM,EAAE,CA6B9D;AAm1CD,iGAAiG;AACjG,wBAAgB,WAAW,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAE3D;AAoHD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAIpE;AA+6BD,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,SAAS,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,GAAG,IAAI,CAEpF;AAID,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,CAoBhE;AAyKD,kGAAkG;AAClG,wBAAgB,yBAAyB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAatE;AAqDD,UAAU,MAAM;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACb;AA0BD,+FAA+F;AAC/F,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAgBpD;AAID,wEAAwE;AACxE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAO5E;AAiJD,eAAO,MAAM,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAoCtC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,mBAAmB,EAAE,WAAW,CAAC,MAAM,CAAiE,CAAC;AAEtH,0EAA0E;AAC1E,eAAO,MAAM,SAAS,EAAE,SAAS,MAAM,EAAwE,CAAC;AAShH,eAAO,MAAM,uBAAuB,aAAkD,CAAC;AAGvF,MAAM,WAAW,SAAS;IACzB,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACb;AAED;2FAC2F;AAC3F,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CA+C3F;AAED,YAAY,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;AAgHhC,wBAAgB,0BAA0B,CAAC,SAAS,GAAE,MAAwB,GAAG,MAAM,EAAE,CAyBxF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursedbelt",
3
- "version": "5.103.0",
3
+ "version": "5.103.1",
4
4
  "license": "ISC",
5
5
  "type": "module",
6
6
  "description": "The React design system of the cursedbelt split โ€” components, styles, theme. cursedbelt-core below it; server tier in cursedbelt-server.",
@@ -77,9 +77,41 @@ describe('no-raw-action-button', () => {
77
77
  test('a CLOSED or self-closing group earlier in the file exempts nothing after it', () => {
78
78
  const closed = `<Tabs value={t}><TabsList /></Tabs>\n<button role="tab">Loose</button>`;
79
79
  expect(check('no-raw-action-button', closed)).toHaveLength(1);
80
- const selfClosed = `<RadioGroup options={o} />\n<button role="radio">Loose</button>`;
80
+ const selfClosed = `<SegmentedControl options={o} />\n<button role="radio">Loose</button>`;
81
81
  expect(check('no-raw-action-button', selfClosed)).toHaveLength(1);
82
82
  });
83
+
84
+ // Task 4098: the type argument's `>` ended the tag, so the real `/>` went unread and the
85
+ // control stayed open for the rest of the file. Desk's rewind list passed on exactly this.
86
+ test('๐Ÿ”ด a self-closing GENERIC group exempts nothing after it', () => {
87
+ const union = `<SegmentedControl<"a" | "b"> value={v} onChange={set} options={o} />
88
+ <button role="radio">Loose</button>`;
89
+ expect(check('no-raw-action-button', union)).toHaveLength(1);
90
+ const multiline = `<SegmentedControl<
91
+ Record<string, "a" | "b">
92
+ >
93
+ value={v}
94
+ onChange={(next) => set(next)}
95
+ />
96
+ <button role="tab">Loose</button>`;
97
+ expect(check('no-raw-action-button', multiline)).toHaveLength(1);
98
+ const functionType = `<Tabs<(next: string) => void> value={t} />\n<button role="switch">Loose</button>`;
99
+ expect(check('no-raw-action-button', functionType)).toHaveLength(1);
100
+ });
101
+
102
+ test('a group the belt does not ship exempts nothing, whatever an app names it', () => {
103
+ const source = `<RadioGroup value={v}>
104
+ <button role="radio">A</button>
105
+ </RadioGroup>`;
106
+ expect(check('no-raw-action-button', source)).toHaveLength(1);
107
+ });
108
+
109
+ test('a generic group left OPEN still exempts its own part', () => {
110
+ const source = `<SegmentedControl<"a" | "b"> value={v}>
111
+ <button role="radio" aria-checked={v === 'a'}>A</button>
112
+ </SegmentedControl>`;
113
+ expect(check('no-raw-action-button', source)).toHaveLength(0);
114
+ });
83
115
  });
84
116
 
85
117
  // Filed 2026-09-15 after three false positives in three repos, all one defect: the tag
@@ -122,11 +154,11 @@ describe('opening-tag extraction (comment-aware)', () => {
122
154
  });
123
155
 
124
156
  test('a prose angle bracket does not hide a role="radio" below it', () => {
125
- const source = `<RadioGroup value={v}><button
157
+ const source = `<SegmentedControl value={v}><button
126
158
  /* A radio-like list rather than a <select>. */
127
159
  role="radio"
128
160
  aria-checked={selected}
129
- >Move here</button></RadioGroup>`;
161
+ >Move here</button></SegmentedControl>`;
130
162
  expect(check('no-raw-action-button', source)).toHaveLength(0);
131
163
  });
132
164
 
@@ -206,12 +238,12 @@ describe('a presentational marker must be an ATTRIBUTE, not prose about one', ()
206
238
  });
207
239
 
208
240
  test('the REAL attributes still pass silently, prose and all', () => {
209
- const source = `<RadioGroup value={v}><button
241
+ const source = `<SegmentedControl value={v}><button
210
242
  // A radio-like list: \`role="radio"\` + \`aria-checked\` says so to a screen reader.
211
243
  role="radio"
212
244
  aria-checked={selected}
213
245
  type="button"
214
- >Move here</button></RadioGroup>`;
246
+ >Move here</button></SegmentedControl>`;
215
247
  expect(check('no-raw-action-button', source)).toHaveLength(0);
216
248
  });
217
249
 
@@ -210,8 +210,8 @@
210
210
  //
211
211
  // A presentational ARIA role/attribute on the tag (role="tab" | "switch" | "radio" |
212
212
  // "menuitemradio" | "presentation", or aria-selected) exempts a `<button>` from
213
- // `no-raw-action-button` ONLY inside a belt `Tabs`/`TabsList`/`SegmentedControl`/
214
- // `RadioGroup`/`ToggleGroup` (task 2381 โ€” a bare marker was the loophole; see the rule).
213
+ // `no-raw-action-button` ONLY inside a belt `Tabs`/`TabsList`/`SegmentedControl`
214
+ // (task 2381 โ€” a bare marker was the loophole; see the rule).
215
215
  //
216
216
  // ๐Ÿ”ด Every opt-out is COUNTED (`opt-out-count`) and must lead with its cause
217
217
  // (`opt-out-needs-cause`): `guardrails-ignore <rule>: <cause> โ€” <six+ words of why>`, the
@@ -356,6 +356,38 @@ interface OpeningTag {
356
356
  readonly code: string;
357
357
  }
358
358
 
359
+ function endOfTagName(source: string, startIndex: number): number {
360
+ const name = /<[\w$.:-]+/y;
361
+ name.lastIndex = startIndex;
362
+ return name.test(source) ? name.lastIndex : startIndex;
363
+ }
364
+
365
+ /** The index past a JSX type argument at `index` (`<SegmentedControl<"a" | "b"> โ€ฆ`), or
366
+ * `index` when none opens there or it never closes. Its `>` is not the end of the tag: read
367
+ * as one, a self-closing generic control looked OPEN, and with no `</SegmentedControl>` to
368
+ * shut it every role-marked button below it passed `no-raw-action-button` (forge task 4098).
369
+ * The `>` of a `=>` inside a function type closes nothing. */
370
+ function endOfTypeArguments(source: string, index: number): number {
371
+ if (source[index] !== "<") return index;
372
+ let depth = 0;
373
+ let i = index;
374
+ while (i < source.length) {
375
+ const ch = source[i];
376
+ if (ch === '"' || ch === "'" || ch === "`") {
377
+ i = skipQuoted(source, i);
378
+ continue;
379
+ }
380
+ if (source.slice(i, i + 2) === "=>") {
381
+ i += 2;
382
+ continue;
383
+ }
384
+ if (ch === "<") depth++;
385
+ else if (ch === ">" && --depth === 0) return i + 1;
386
+ i++;
387
+ }
388
+ return index;
389
+ }
390
+
359
391
  /** Read from a `<button` (or similar) match to the end of its opening tag, tolerating
360
392
  * `{...}` JS expressions in attribute values that may themselves contain `>`
361
393
  * (`onClick={() => โ€ฆ}`, which works because that `>` sits inside braces).
@@ -372,6 +404,12 @@ function readOpeningTag(source: string, startIndex: number): OpeningTag {
372
404
  let i = startIndex;
373
405
  let braceDepth = 0;
374
406
  let code = "";
407
+ const nameEnd = endOfTagName(source, startIndex);
408
+ const typeEnd = endOfTypeArguments(source, nameEnd);
409
+ if (typeEnd > nameEnd) {
410
+ code = source.slice(startIndex, nameEnd) + blank(source.slice(nameEnd, typeEnd));
411
+ i = typeEnd;
412
+ }
375
413
  while (i < source.length) {
376
414
  const two = source.slice(i, i + 2);
377
415
  if (two === "//") {
@@ -754,14 +792,16 @@ const PRESENTATIONAL_MARKER_RE =
754
792
  // Task 2381: a role marker used to exempt ANY raw button, so the cheapest way past this rule
755
793
  // was `role="switch"` โ€” `apps/family/src/App.tsx:1786-1800` (`EditModeToggle`) says in its
756
794
  // own comment the marker is there to satisfy it, beside the belt's `Switch`. A marker now
757
- // exempts only a button INSIDE a belt group that owns that role (`Tabs`, `SegmentedControl`,
758
- // `RadioGroup`, `ToggleGroup`) โ€” the one place a raw button with it is the group's own part.
759
- // Anywhere else it is `Switch` / `Tabs` / `SegmentedControl` / `RadioGroup`, or an opt-out
795
+ // exempts only a button INSIDE a belt group that owns that role (`Tabs`, `SegmentedControl`)
796
+ // โ€” the one place a raw button with it is the group's own part. `ChoiceGroup` takes no
797
+ // children, so nothing can sit inside it, and a `RadioGroup` or `ToggleGroup` is no belt
798
+ // component: naming them only exempted whatever an app happened to call that (forge task 4098).
799
+ // Anywhere else it is `Switch` / `Tabs` / `SegmentedControl` / `ChoiceGroup`, or an opt-out
760
800
  // with its cause.
761
801
  const BELT_OWN_SRC = `${join(import.meta.dir, "..", "src")}/`;
762
802
  const isBeltOwnSource = (filePath: string) =>
763
803
  resolve(filePath).startsWith(BELT_OWN_SRC) || /(^|\/)(?:packages|libs)\/cursedbelt\/src\//.test(filePath.replace(/\\/g, "/"));
764
- const BELT_CHOICE_GROUPS = ["Tabs", "TabsList", "SegmentedControl", "RadioGroup", "ToggleGroup"];
804
+ const BELT_CHOICE_GROUPS = ["Tabs", "TabsList", "SegmentedControl"];
765
805
 
766
806
  /** Is `index` inside an open (not yet closed) belt choice-group element in this file? */
767
807
  function insideBeltChoiceGroup(scannable: string, index: number): boolean {
@@ -778,7 +818,7 @@ function insideBeltChoiceGroup(scannable: string, index: number): boolean {
778
818
  const noRawActionButton: Rule = {
779
819
  name: "no-raw-action-button",
780
820
  description:
781
- 'DESIGN.md ยง2 (Component Architecture): no hand-rolled <button> โ€” use the cursedbelt Button family; a toggle is `Switch`, a tab `Tabs`, a choice `SegmentedControl`/`RadioGroup`. A role marker exempts a button only inside one of those groups.',
821
+ 'DESIGN.md ยง2 (Component Architecture): no hand-rolled <button> โ€” use the cursedbelt Button family; a toggle is `Switch`, a tab `Tabs`, a choice `SegmentedControl`/`ChoiceGroup`. A role marker exempts a button only inside `Tabs` or `SegmentedControl`.',
782
822
  check(filePath, source) {
783
823
  const violations: Violation[] = [];
784
824
  const scannable = blankNonMarkupText(source, filePath);