@softize/opus 12.10.0 → 13.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 (154) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/bin/lib/check.mjs +1098 -310
  3. package/bin/lib/copy.mjs +12 -5
  4. package/docs/adr/0003-dictionary-presentation-is-declared.md +3 -0
  5. package/docs/adr/0004-page-content-state-is-composed.md +65 -0
  6. package/docs/adr/0005-structural-surfaces-share-an-explicit-anatomy.md +180 -0
  7. package/docs/adr/0006-semantic-context-precedes-visual-variant.md +182 -0
  8. package/docs/adr/0007-toast-actions-form-an-ordered-collection.md +63 -0
  9. package/docs/adr/0008-hierarchical-navigation-is-composed-at-the-consumer-boundary.md +71 -0
  10. package/docs/radius-scale.md +1 -1
  11. package/package.json +1 -1
  12. package/registry/instructions/opus.md +5 -0
  13. package/registry/skills/build-opus-ui/SKILL.md +27 -16
  14. package/registry/skills/build-opus-ui/references/evaluations.md +16 -5
  15. package/registry/skills/build-opus-ui/references/ui-patterns.md +38 -15
  16. package/registry/skills/maintain-opus-docs/SKILL.md +83 -0
  17. package/registry/skills/maintain-opus-docs/agents/openai.yaml +4 -0
  18. package/registry/skills/maintain-opus-docs/references/editorial-standard.md +85 -0
  19. package/registry/skills/maintain-opus-docs/references/evaluations.md +34 -0
  20. package/registry/skills/maintain-opus-docs/scripts/audit-docs.mjs +81 -0
  21. package/registry/skills/model-opus-dictionary/SKILL.md +4 -2
  22. package/registry/skills/model-opus-dictionary/references/evaluations.md +4 -3
  23. package/registry/templates/app/src/App.tsx +1 -1
  24. package/src/core/dictionary.ts +52 -14
  25. package/src/core/index.ts +10 -0
  26. package/src/core/ui-context.ts +29 -0
  27. package/src/schema/drivers/zod.ts +17 -8
  28. package/src/ui/components/patterns/action-form-card.tsx +18 -12
  29. package/src/ui/components/patterns/confirm.tsx +163 -40
  30. package/src/ui/components/patterns/content-header.tsx +335 -61
  31. package/src/ui/components/patterns/data-state.tsx +23 -10
  32. package/src/ui/components/patterns/list.tsx +1097 -783
  33. package/src/ui/components/patterns/page-state.tsx +115 -0
  34. package/src/ui/components/patterns/page.tsx +231 -41
  35. package/src/ui/components/patterns/sidebar.tsx +357 -83
  36. package/src/ui/components/patterns/trigger.tsx +37 -30
  37. package/src/ui/components/patterns/view.tsx +7 -11
  38. package/src/ui/components/primitives/alert.tsx +298 -110
  39. package/src/ui/components/primitives/ask.tsx +2 -1
  40. package/src/ui/components/primitives/badge.tsx +91 -30
  41. package/src/ui/components/primitives/button.tsx +99 -60
  42. package/src/ui/components/primitives/calendar.tsx +39 -39
  43. package/src/ui/components/primitives/card.tsx +96 -23
  44. package/src/ui/components/primitives/detail.tsx +2 -2
  45. package/src/ui/components/primitives/dialog.tsx +196 -39
  46. package/src/ui/components/primitives/dictionary-value.tsx +9 -14
  47. package/src/ui/components/primitives/dot.tsx +74 -21
  48. package/src/ui/components/primitives/drawer.tsx +40 -24
  49. package/src/ui/components/primitives/empty.tsx +3 -3
  50. package/src/ui/components/primitives/item.tsx +135 -79
  51. package/src/ui/components/primitives/menu.tsx +11 -3
  52. package/src/ui/components/primitives/metric-card.tsx +133 -0
  53. package/src/ui/components/primitives/sonner.tsx +187 -8
  54. package/src/ui/components/primitives/table.tsx +2 -2
  55. package/src/ui/docs/DocBrowser.tsx +104 -25
  56. package/src/ui/docs/changelog.tsx +1 -1
  57. package/src/ui/docs/content/accordion.md +22 -16
  58. package/src/ui/docs/content/action-form-card.md +8 -8
  59. package/src/ui/docs/content/action-form-dialog.md +9 -9
  60. package/src/ui/docs/content/action-form.md +28 -34
  61. package/src/ui/docs/content/action-list-dialog.md +11 -6
  62. package/src/ui/docs/content/action-list.md +64 -39
  63. package/src/ui/docs/content/action-trigger.md +21 -14
  64. package/src/ui/docs/content/action-view.md +8 -8
  65. package/src/ui/docs/content/actions.md +9 -9
  66. package/src/ui/docs/content/ai.md +3 -3
  67. package/src/ui/docs/content/alert.md +54 -28
  68. package/src/ui/docs/content/aspect-ratio.md +4 -4
  69. package/src/ui/docs/content/audit.md +2 -2
  70. package/src/ui/docs/content/auth.md +3 -3
  71. package/src/ui/docs/content/avatar.md +34 -14
  72. package/src/ui/docs/content/badge.md +21 -22
  73. package/src/ui/docs/content/breadcrumb.md +13 -8
  74. package/src/ui/docs/content/button.md +93 -15
  75. package/src/ui/docs/content/calendar.md +5 -5
  76. package/src/ui/docs/content/card.md +6 -6
  77. package/src/ui/docs/content/carousel.md +16 -11
  78. package/src/ui/docs/content/chat.md +3 -3
  79. package/src/ui/docs/content/checkbox.md +7 -7
  80. package/src/ui/docs/content/cli.md +5 -5
  81. package/src/ui/docs/content/collapsible.md +8 -8
  82. package/src/ui/docs/content/command.md +16 -8
  83. package/src/ui/docs/content/composer.md +2 -2
  84. package/src/ui/docs/content/content.md +44 -0
  85. package/src/ui/docs/content/copyable.md +4 -3
  86. package/src/ui/docs/content/customization.md +7 -7
  87. package/src/ui/docs/content/cycle.md +3 -3
  88. package/src/ui/docs/content/data-state.md +11 -12
  89. package/src/ui/docs/content/data.md +26 -33
  90. package/src/ui/docs/content/detail.md +8 -5
  91. package/src/ui/docs/content/dialog.md +339 -31
  92. package/src/ui/docs/content/dictionary-value.md +19 -18
  93. package/src/ui/docs/content/dock.md +3 -3
  94. package/src/ui/docs/content/dot.md +7 -7
  95. package/src/ui/docs/content/drawer.md +32 -16
  96. package/src/ui/docs/content/empty-value.md +2 -2
  97. package/src/ui/docs/content/empty.md +19 -12
  98. package/src/ui/docs/content/events.md +4 -4
  99. package/src/ui/docs/content/field.md +34 -12
  100. package/src/ui/docs/content/getting-started.md +1 -1
  101. package/src/ui/docs/content/icon-picker.md +8 -4
  102. package/src/ui/docs/content/input-otp.md +20 -12
  103. package/src/ui/docs/content/input.md +121 -9
  104. package/src/ui/docs/content/item.md +64 -24
  105. package/src/ui/docs/content/kbd.md +19 -11
  106. package/src/ui/docs/content/label.md +5 -3
  107. package/src/ui/docs/content/log.md +4 -4
  108. package/src/ui/docs/content/markdown.md +7 -6
  109. package/src/ui/docs/content/mcp.md +13 -15
  110. package/src/ui/docs/content/menu.md +36 -17
  111. package/src/ui/docs/content/metric-card.md +41 -0
  112. package/src/ui/docs/content/observability.md +2 -2
  113. package/src/ui/docs/content/page.md +93 -10
  114. package/src/ui/docs/content/pagination.md +22 -17
  115. package/src/ui/docs/content/popover.md +16 -8
  116. package/src/ui/docs/content/progress.md +7 -5
  117. package/src/ui/docs/content/queue.md +5 -5
  118. package/src/ui/docs/content/radio-group.md +20 -12
  119. package/src/ui/docs/content/router.md +11 -6
  120. package/src/ui/docs/content/scheduler.md +4 -5
  121. package/src/ui/docs/content/scroll-area.md +12 -7
  122. package/src/ui/docs/content/select.md +42 -29
  123. package/src/ui/docs/content/semantic-context.md +63 -0
  124. package/src/ui/docs/content/separator.md +5 -5
  125. package/src/ui/docs/content/sidebar.md +325 -56
  126. package/src/ui/docs/content/skeleton.md +5 -4
  127. package/src/ui/docs/content/slider.md +8 -7
  128. package/src/ui/docs/content/spinner.md +8 -8
  129. package/src/ui/docs/content/split.md +8 -5
  130. package/src/ui/docs/content/storage.md +6 -8
  131. package/src/ui/docs/content/switch.md +8 -7
  132. package/src/ui/docs/content/table.md +16 -6
  133. package/src/ui/docs/content/tabs.md +28 -14
  134. package/src/ui/docs/content/testing.md +9 -11
  135. package/src/ui/docs/content/textarea.md +5 -4
  136. package/src/ui/docs/content/toast.md +47 -13
  137. package/src/ui/docs/content/toggle.md +75 -7
  138. package/src/ui/docs/content/tokens.md +31 -3
  139. package/src/ui/docs/content/tooltip.md +19 -11
  140. package/src/ui/docs/content/truncate.md +7 -8
  141. package/src/ui/docs/content/ui.md +10 -9
  142. package/src/ui/docs/content/upgrading.md +7 -8
  143. package/src/ui/docs/doc-client.tsx +2 -2
  144. package/src/ui/docs/registry.tsx +580 -229
  145. package/src/ui/lib/semantic-context.ts +30 -0
  146. package/src/ui/meta.ts +278 -286
  147. package/src/ui/react.tsx +377 -111
  148. package/src/ui/theme.css +116 -0
  149. package/src/ui/components/primitives/alert-dialog.tsx +0 -190
  150. package/src/ui/docs/content/alert-dialog.md +0 -73
  151. package/src/ui/docs/content/button-group.md +0 -71
  152. package/src/ui/docs/content/confirm.md +0 -120
  153. package/src/ui/docs/content/input-group.md +0 -78
  154. package/src/ui/docs/content/toggle-group.md +0 -81
package/bin/lib/check.mjs CHANGED
@@ -21,66 +21,282 @@
21
21
  * É a régua do reviewer e a métrica de padrão.
22
22
  */
23
23
 
24
- import path from 'node:path'
25
- import ts from 'typescript'
24
+ import path from "node:path";
25
+ import ts from "typescript";
26
26
  import {
27
27
  canonicalProjectDirectory,
28
28
  readProjectDirectory,
29
29
  readProjectFile,
30
30
  safeProjectPath,
31
- } from '@softize/base/project-path'
31
+ } from "@softize/base/project-path";
32
32
 
33
33
  // Ordem canônica (grupos do ActionBase em src/core/types.ts). Campos kind-específicos
34
34
  // (fields/paginate/filters/expand/background/emits/successStatus…) NÃO entram no mapa —
35
35
  // são ignorados na checagem de ordem (não geram falso-positivo).
36
36
  const FIELD_GROUP = {
37
- name: 0, kind: 0,
38
- label: 1, title: 1, summary: 1, description: 1, icon: 1, color: 1, messages: 1, tags: 1, examples: 1, errors: 1,
39
- input: 2, output: 2,
40
- public: 3, requires: 3, authorize: 3, loads: 3,
41
- handler: 4, mockHandler: 4,
42
- audit: 5, confirm: 5, invalidates: 5, rateLimit: 5, idempotency: 5,
43
- ai: 6, automatable: 6, internal: 6,
44
- }
37
+ name: 0,
38
+ kind: 0,
39
+ label: 1,
40
+ title: 1,
41
+ summary: 1,
42
+ description: 1,
43
+ icon: 1,
44
+ color: 1,
45
+ messages: 1,
46
+ tags: 1,
47
+ examples: 1,
48
+ errors: 1,
49
+ input: 2,
50
+ output: 2,
51
+ public: 3,
52
+ requires: 3,
53
+ authorize: 3,
54
+ loads: 3,
55
+ handler: 4,
56
+ mockHandler: 4,
57
+ audit: 5,
58
+ confirm: 5,
59
+ invalidates: 5,
60
+ rateLimit: 5,
61
+ idempotency: 5,
62
+ ai: 6,
63
+ automatable: 6,
64
+ internal: 6,
65
+ };
45
66
 
46
67
  // <resource>.<verb> com namespacing opcional: <resource>(.<sub>)*.<verb> — ≥2 segmentos,
47
68
  // minúsculo+hífen. Ex.: client.list, user.set-password, ticket.sla.check.
48
- const SEG = '[a-z][a-z0-9]*(-[a-z0-9]+)*'
49
- const ACTION_NAME_RE = new RegExp(`^${SEG}(\\.${SEG})+$`)
50
- const KINDS = new Set(['simple', 'form', 'list', 'view'])
69
+ const SEG = "[a-z][a-z0-9]*(-[a-z0-9]+)*";
70
+ const ACTION_NAME_RE = new RegExp(`^${SEG}(\\.${SEG})+$`);
71
+ const KINDS = new Set(["simple", "form", "list", "view"]);
51
72
 
52
73
  // Tokens públicos removidos cuja ausência no CSS não quebra typecheck/build: sem uma
53
74
  // migração executável, o consumidor perde borda/sombra em silêncio. O check fica no Opus
54
75
  // (dono do contrato visual), não no Maestro. Uma ocorrência por token/arquivo basta.
55
76
  const REMOVED_UI_TOKENS = new Map([
56
- ['ring-edge', 'use `border border-border` para a aresta da superfície'],
57
- ['border-edge', 'use `border-border`'],
58
- ['shadow-card', 'use `shadow-sm`'],
59
- ['shadow-popover', 'use `shadow-md`'],
60
- ['shadow-dialog', 'use `shadow-lg`'],
61
- ['rounded-card', 'use `rounded-xl`'],
62
- ['rounded-popover', 'use `rounded-md`'],
63
- ['rounded-dialog', 'use `rounded-xl`'],
64
- ['--edge-value', 'remova o override; a aresta agora usa `--border`'],
65
- ['--color-edge', 'remova o token; a aresta agora usa `--border`'],
66
- ['--elevation-card', 'use/ajuste `--shadow-sm`'],
67
- ['--elevation-popover', 'use/ajuste `--shadow-md`'],
68
- ['--elevation-dialog', 'use/ajuste `--shadow-lg`'],
69
- ['--radius-card', 'use/ajuste `--radius-xl`'],
70
- ['--radius-popover', 'use/ajuste `--radius-md`'],
71
- ['--radius-dialog', 'use/ajuste `--radius-xl`'],
72
- ])
77
+ ["ring-edge", "use `border border-border` para a aresta da superfície"],
78
+ ["border-edge", "use `border-border`"],
79
+ ["shadow-card", "use `shadow-sm`"],
80
+ ["shadow-popover", "use `shadow-md`"],
81
+ ["shadow-dialog", "use `shadow-lg`"],
82
+ ["rounded-card", "use `rounded-xl`"],
83
+ ["rounded-popover", "use `rounded-md`"],
84
+ ["rounded-dialog", "use `rounded-xl`"],
85
+ ["--edge-value", "remova o override; a aresta agora usa `--border`"],
86
+ ["--color-edge", "remova o token; a aresta agora usa `--border`"],
87
+ ["--elevation-card", "use/ajuste `--shadow-sm`"],
88
+ ["--elevation-popover", "use/ajuste `--shadow-md`"],
89
+ ["--elevation-dialog", "use/ajuste `--shadow-lg`"],
90
+ ["--radius-card", "use/ajuste `--radius-xl`"],
91
+ ["--radius-popover", "use/ajuste `--radius-md`"],
92
+ ["--radius-dialog", "use/ajuste `--radius-xl`"],
93
+ ]);
73
94
 
74
95
  // Superfícies com foreground próprio formam um par indivisível. Exigir as duas
75
96
  // utilities no mesmo fragmento de classes torna a regra local, revisável e evita que a
76
97
  // igualdade acidental com `--foreground` esconda um tema quebrado.
77
98
  const UI_SURFACE_PAIRS = new Map([
78
- ['bg-card', 'text-card-foreground'],
79
- ['bg-popover', 'text-popover-foreground'],
80
- ])
99
+ ["bg-card", "text-card-foreground"],
100
+ ["bg-popover", "text-popover-foreground"],
101
+ ]);
102
+
103
+ // Anatomia pública das superfícies compostas (ADR 0005). O lint olha somente nomes
104
+ // importados de @softize/opus/ui/react, evitando atribuir regras da biblioteca a componentes
105
+ // locais homônimos.
106
+ const UI_STRUCTURAL_PARENTS = new Map([
107
+ ["PageHeader", new Set(["Page"])],
108
+ ["PageBody", new Set(["Page"])],
109
+ ["PageTitle", new Set(["PageHeader"])],
110
+ ["PageDescription", new Set(["PageHeader"])],
111
+ ["PageMeta", new Set(["PageHeader"])],
112
+ ["PageActions", new Set(["PageHeader"])],
113
+ ["ContentHeader", new Set(["Content"])],
114
+ ["ContentBody", new Set(["Content"])],
115
+ ["ContentTitle", new Set(["ContentHeader"])],
116
+ ["ContentDescription", new Set(["ContentHeader"])],
117
+ ["ContentMeta", new Set(["ContentHeader"])],
118
+ ["ContentActions", new Set(["ContentHeader"])],
119
+ ["CardHeader", new Set(["Card"])],
120
+ ["CardBody", new Set(["Card"])],
121
+ ["CardFooter", new Set(["Card"])],
122
+ ["CardTitle", new Set(["CardHeader"])],
123
+ ["CardDescription", new Set(["CardHeader"])],
124
+ ["CardAction", new Set(["CardHeader"])],
125
+ ["DialogHeader", new Set(["DialogContent"])],
126
+ ["DialogBody", new Set(["DialogContent"])],
127
+ ["DialogFooter", new Set(["DialogContent"])],
128
+ ["DialogMedia", new Set(["DialogHeader"])],
129
+ ["DialogTitle", new Set(["DialogHeader"])],
130
+ ["DialogDescription", new Set(["DialogHeader"])],
131
+ ["DrawerHeader", new Set(["DrawerContent"])],
132
+ ["DrawerBody", new Set(["DrawerContent"])],
133
+ ["DrawerFooter", new Set(["DrawerContent"])],
134
+ ["DrawerTitle", new Set(["DrawerHeader"])],
135
+ ["DrawerDescription", new Set(["DrawerHeader"])],
136
+ ["PaneHeader", new Set(["Pane", "Sidebar"])],
137
+ ["PaneBody", new Set(["Pane", "Sidebar"])],
138
+ ["PaneFooter", new Set(["Pane", "Sidebar"])],
139
+ ["AlertMedia", new Set(["Alert"])],
140
+ ["AlertHeader", new Set(["Alert"])],
141
+ ["AlertActions", new Set(["Alert"])],
142
+ ["AlertTitle", new Set(["AlertHeader"])],
143
+ ["AlertDescription", new Set(["AlertHeader"])],
144
+ ["ItemMedia", new Set(["Item"])],
145
+ ["ItemHeader", new Set(["Item"])],
146
+ ["ItemBody", new Set(["Item"])],
147
+ ["ItemContent", new Set(["Item"])],
148
+ ["ItemActions", new Set(["Item"])],
149
+ ["ItemFooter", new Set(["Item"])],
150
+ ["ItemTitle", new Set(["ItemHeader"])],
151
+ ["ItemDescription", new Set(["ItemHeader"])],
152
+ ["EmptyHeader", new Set(["Empty"])],
153
+ ["EmptyActions", new Set(["Empty"])],
154
+ ["EmptyContent", new Set(["Empty"])],
155
+ ["EmptyMedia", new Set(["EmptyHeader"])],
156
+ ["EmptyTitle", new Set(["EmptyHeader"])],
157
+ ["EmptyDescription", new Set(["EmptyHeader"])],
158
+ ["PopoverHeader", new Set(["PopoverContent"])],
159
+ ["PopoverTitle", new Set(["PopoverHeader"])],
160
+ ["PopoverDescription", new Set(["PopoverHeader"])],
161
+ ]);
162
+
163
+ const UI_STRUCTURAL_ROOTS = new Map([
164
+ [
165
+ "Page",
166
+ {
167
+ header: "PageHeader",
168
+ body: "PageBody",
169
+ shorthand: new Set(["title", "description", "count", "actions"]),
170
+ },
171
+ ],
172
+ [
173
+ "Content",
174
+ {
175
+ header: "ContentHeader",
176
+ body: "ContentBody",
177
+ shorthand: new Set(["title", "description", "meta", "actions"]),
178
+ },
179
+ ],
180
+ ]);
181
+
182
+ const UI_STRICT_DIRECT_COMPONENTS = new Set([
183
+ "PageHeader",
184
+ "PageBody",
185
+ "PageTitle",
186
+ "PageDescription",
187
+ "PageMeta",
188
+ "PageActions",
189
+ "ContentHeader",
190
+ "ContentBody",
191
+ "ContentTitle",
192
+ "ContentDescription",
193
+ "ContentMeta",
194
+ "ContentActions",
195
+ "AlertMedia",
196
+ "AlertHeader",
197
+ "AlertActions",
198
+ ]);
199
+
200
+ const UI_STRUCTURAL_HEADERS = new Map([
201
+ [
202
+ "PageHeader",
203
+ {
204
+ title: "PageTitle",
205
+ optional: new Set(["PageDescription", "PageMeta", "PageActions"]),
206
+ shorthand: new Set(),
207
+ },
208
+ ],
209
+ [
210
+ "ContentHeader",
211
+ {
212
+ title: "ContentTitle",
213
+ optional: new Set([
214
+ "ContentDescription",
215
+ "ContentMeta",
216
+ "ContentActions",
217
+ ]),
218
+ shorthand: new Set(["title", "description", "meta", "actions"]),
219
+ },
220
+ ],
221
+ [
222
+ "AlertHeader",
223
+ {
224
+ title: "AlertTitle",
225
+ optional: new Set(["AlertDescription"]),
226
+ shorthand: new Set(),
227
+ titleRequired: false,
228
+ allowOpaque: true,
229
+ },
230
+ ],
231
+ [
232
+ "ItemHeader",
233
+ {
234
+ title: "ItemTitle",
235
+ optional: new Set(["ItemDescription"]),
236
+ shorthand: new Set(),
237
+ },
238
+ ],
239
+ ]);
240
+
241
+ // API semântica canônica (ADR 0006). Os aliases continuam no runtime somente para que uma
242
+ // atualização não quebre o app antes da migração; código-fonte novo deve separar significado
243
+ // (`context`) de tratamento visual (`variant`).
244
+ const UI_LEGACY_VARIANTS = new Map([
245
+ ["Button", new Set(["default", "secondary", "destructive"])],
246
+ [
247
+ "Badge",
248
+ new Set([
249
+ "default",
250
+ "secondary",
251
+ "destructive",
252
+ "info",
253
+ "success",
254
+ "warning",
255
+ "danger",
256
+ ]),
257
+ ],
258
+ [
259
+ "DictionaryValue",
260
+ new Set([
261
+ "default",
262
+ "secondary",
263
+ "destructive",
264
+ "info",
265
+ "success",
266
+ "warning",
267
+ "danger",
268
+ ]),
269
+ ],
270
+ [
271
+ "Alert",
272
+ new Set([
273
+ "default",
274
+ "destructive",
275
+ "info",
276
+ "success",
277
+ "warning",
278
+ "danger",
279
+ ]),
280
+ ],
281
+ [
282
+ "Dot",
283
+ new Set([
284
+ "default",
285
+ "secondary",
286
+ "destructive",
287
+ "info",
288
+ "success",
289
+ "warning",
290
+ "danger",
291
+ ]),
292
+ ],
293
+ ["MetricCard", new Set()],
294
+ ["MenuItem", new Set(["default", "destructive"])],
295
+ ["ActionTrigger", new Set(["default", "secondary", "destructive"])],
296
+ ]);
81
297
 
82
298
  /** Os três nomes que denotam action/contrato — o arquivo sem nenhum deles é pulado. */
83
- export const ACTION_MARKERS = ['defineAction', 'defineContract', 'bindAction']
299
+ export const ACTION_MARKERS = ["defineAction", "defineContract", "bindAction"];
84
300
 
85
301
  /**
86
302
  * Extrai `defineAction`/`defineContract`/`bindAction` de um source. Puro/sintático —
@@ -88,71 +304,86 @@ export const ACTION_MARKERS = ['defineAction', 'defineContract', 'bindAction']
88
304
  * da const, se houver) e, no bindAction, `contractRef` (o identificador do 1º arg).
89
305
  */
90
306
  export function parseActions(fileName, sourceText) {
91
- const sf = ts.createSourceFile(fileName, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
92
- const actions = []
307
+ const sf = ts.createSourceFile(
308
+ fileName,
309
+ sourceText,
310
+ ts.ScriptTarget.Latest,
311
+ true,
312
+ ts.ScriptKind.TS,
313
+ );
314
+ const actions = [];
93
315
 
94
- const lineOf = (node) => sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1
316
+ const lineOf = (node) =>
317
+ sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
95
318
 
96
319
  function constIdent(callNode) {
97
- const decl = callNode.parent
98
- if (!decl || !ts.isVariableDeclaration(decl)) return null
99
- return ts.isIdentifier(decl.name) ? decl.name.text : null
320
+ const decl = callNode.parent;
321
+ if (!decl || !ts.isVariableDeclaration(decl)) return null;
322
+ return ts.isIdentifier(decl.name) ? decl.name.text : null;
100
323
  }
101
324
 
102
325
  function isExported(callNode) {
103
326
  // call → VariableDeclaration → VariableDeclarationList → VariableStatement (com export)
104
- const decl = callNode.parent
105
- if (!decl || !ts.isVariableDeclaration(decl)) return false
106
- const list = decl.parent
107
- const stmt = list && list.parent
108
- if (!stmt || !ts.isVariableStatement(stmt)) return false
109
- return (stmt.modifiers ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
327
+ const decl = callNode.parent;
328
+ if (!decl || !ts.isVariableDeclaration(decl)) return false;
329
+ const list = decl.parent;
330
+ const stmt = list && list.parent;
331
+ if (!stmt || !ts.isVariableStatement(stmt)) return false;
332
+ return (stmt.modifiers ?? []).some(
333
+ (m) => m.kind === ts.SyntaxKind.ExportKeyword,
334
+ );
110
335
  }
111
336
 
112
337
  function strProp(obj, key) {
113
338
  const p = obj.properties.find(
114
- (pr) => ts.isPropertyAssignment(pr) && pr.name && pr.name.getText(sf) === key,
115
- )
116
- if (!p || !ts.isStringLiteral(p.initializer)) return null
117
- return p.initializer.text
339
+ (pr) =>
340
+ ts.isPropertyAssignment(pr) && pr.name && pr.name.getText(sf) === key,
341
+ );
342
+ if (!p || !ts.isStringLiteral(p.initializer)) return null;
343
+ return p.initializer.text;
118
344
  }
119
345
 
120
346
  function objKeys(obj) {
121
347
  return obj.properties
122
- .filter((p) => ts.isPropertyAssignment(p) || ts.isMethodDeclaration(p) || ts.isShorthandPropertyAssignment(p))
123
- .map((p) => (p.name ? p.name.getText(sf) : ''))
124
- .filter(Boolean)
348
+ .filter(
349
+ (p) =>
350
+ ts.isPropertyAssignment(p) ||
351
+ ts.isMethodDeclaration(p) ||
352
+ ts.isShorthandPropertyAssignment(p),
353
+ )
354
+ .map((p) => (p.name ? p.name.getText(sf) : ""))
355
+ .filter(Boolean);
125
356
  }
126
357
 
127
358
  function visit(node) {
128
359
  if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
129
- const callee = node.expression.text
360
+ const callee = node.expression.text;
130
361
 
131
362
  if (
132
- (callee === 'defineAction' || callee === 'defineContract') &&
363
+ (callee === "defineAction" || callee === "defineContract") &&
133
364
  node.arguments.length > 0 &&
134
365
  ts.isObjectLiteralExpression(node.arguments[0])
135
366
  ) {
136
- const obj = node.arguments[0]
367
+ const obj = node.arguments[0];
137
368
  actions.push({
138
369
  form: callee,
139
- name: strProp(obj, 'name'),
140
- kind: strProp(obj, 'kind'),
370
+ name: strProp(obj, "name"),
371
+ kind: strProp(obj, "kind"),
141
372
  keys: objKeys(obj),
142
373
  ident: constIdent(node),
143
374
  exported: isExported(node),
144
375
  line: lineOf(node),
145
- })
376
+ });
146
377
  }
147
378
 
148
379
  if (
149
- callee === 'bindAction' &&
380
+ callee === "bindAction" &&
150
381
  node.arguments.length >= 2 &&
151
382
  ts.isObjectLiteralExpression(node.arguments[1])
152
383
  ) {
153
- const ref = node.arguments[0]
384
+ const ref = node.arguments[0];
154
385
  actions.push({
155
- form: 'bindAction',
386
+ form: "bindAction",
156
387
  name: null,
157
388
  kind: null,
158
389
  keys: objKeys(node.arguments[1]),
@@ -160,67 +391,90 @@ export function parseActions(fileName, sourceText) {
160
391
  contractRef: ts.isIdentifier(ref) ? ref.text : ref.getText(sf),
161
392
  exported: isExported(node),
162
393
  line: lineOf(node),
163
- })
394
+ });
164
395
  }
165
396
  }
166
- ts.forEachChild(node, visit)
397
+ ts.forEachChild(node, visit);
167
398
  }
168
- visit(sf)
169
- return actions
399
+ visit(sf);
400
+ return actions;
170
401
  }
171
402
 
172
403
  /** Aplica as regras locais (por arquivo) a um item → findings. */
173
404
  export function lintAction(a) {
174
- const out = []
175
- const label = a.name ?? (a.form === 'bindAction' ? `bindAction(${a.contractRef})` : '(sem name)')
176
- const at = (rule, message) => out.push({ rule, level: 'error', action: label, line: a.line, message })
405
+ const out = [];
406
+ const label =
407
+ a.name ??
408
+ (a.form === "bindAction" ? `bindAction(${a.contractRef})` : "(sem name)");
409
+ const at = (rule, message) =>
410
+ out.push({ rule, level: "error", action: label, line: a.line, message });
177
411
 
178
- if (a.form === 'bindAction') {
412
+ if (a.form === "bindAction") {
179
413
  // O binding não repete name/kind/ordem — quem carrega isso é o contrato.
180
- if (!a.exported) at('export', 'bindAction precisa ser `export const` (senão não dá pra registrar)')
181
- return out
414
+ if (!a.exported)
415
+ at(
416
+ "export",
417
+ "bindAction precisa ser `export const` (senão não dá pra registrar)",
418
+ );
419
+ return out;
182
420
  }
183
421
 
184
- if (!a.name) at('action-name', `${a.form} sem \`name\``)
185
- else if (!ACTION_NAME_RE.test(a.name)) at('action-name', `name "${a.name}" não é <resource>.<verb> (minúsculo, ponto)`)
422
+ if (!a.name) at("action-name", `${a.form} sem \`name\``);
423
+ else if (!ACTION_NAME_RE.test(a.name))
424
+ at(
425
+ "action-name",
426
+ `name "${a.name}" não é <resource>.<verb> (minúsculo, ponto)`,
427
+ );
186
428
 
187
- if (!a.kind) at('kind', `${a.form} sem \`kind\``)
188
- else if (!KINDS.has(a.kind)) at('kind', `kind "${a.kind}" inválido (use simple|form|list|view)`)
429
+ if (!a.kind) at("kind", `${a.form} sem \`kind\``);
430
+ else if (!KINDS.has(a.kind))
431
+ at("kind", `kind "${a.kind}" inválido (use simple|form|list|view)`);
189
432
 
190
433
  // field-order: os campos conhecidos devem ter group-index não-decrescente
191
- let lastGroup = -1
192
- let lastKey = ''
434
+ let lastGroup = -1;
435
+ let lastKey = "";
193
436
  for (const k of a.keys) {
194
- const g = FIELD_GROUP[k]
195
- if (g === undefined) continue // kind-específico/desconhecido → lenient
437
+ const g = FIELD_GROUP[k];
438
+ if (g === undefined) continue; // kind-específico/desconhecido → lenient
196
439
  if (g < lastGroup) {
197
- at('field-order', `campo "${k}" fora de ordem (depois de "${lastKey}") — siga identidade→docs→input/output→authorize→handler→comportamento`)
198
- break
440
+ at(
441
+ "field-order",
442
+ `campo "${k}" fora de ordem (depois de "${lastKey}") — siga identidade→docs→input/output→authorize→handler→comportamento`,
443
+ );
444
+ break;
199
445
  }
200
- lastGroup = g
201
- lastKey = k
446
+ lastGroup = g;
447
+ lastKey = k;
202
448
  }
203
449
 
204
- if (!a.exported) at('export', `${a.form} precisa ser \`export const\` (senão não dá pra registrar)`)
450
+ if (!a.exported)
451
+ at(
452
+ "export",
453
+ `${a.form} precisa ser \`export const\` (senão não dá pra registrar)`,
454
+ );
205
455
 
206
456
  // requires sem authorize: no defineAction o par mora no mesmo objeto — acusa direto.
207
457
  // No defineContract a regra é cross-file (o bind pode trazer o authorize row-level);
208
458
  // fica pro join do checkProject.
209
- if (a.form === 'defineAction' && a.keys.includes('requires') && !a.keys.includes('authorize')) {
459
+ if (
460
+ a.form === "defineAction" &&
461
+ a.keys.includes("requires") &&
462
+ !a.keys.includes("authorize")
463
+ ) {
210
464
  at(
211
- 'requires-sem-authorize',
212
- '`requires` é declarativo (o runtime não o executa) — sem `authorize`, a action fica ABERTA. Declare o authorize.',
213
- )
465
+ "requires-sem-authorize",
466
+ "`requires` é declarativo (o runtime não o executa) — sem `authorize`, a action fica ABERTA. Declare o authorize.",
467
+ );
214
468
  }
215
469
 
216
- return out
470
+ return out;
217
471
  }
218
472
 
219
473
  /** Checa um source isolado → { actions, findings }. Regras cross-file: checkProject. */
220
474
  export function checkSource(fileName, sourceText) {
221
- const actions = parseActions(fileName, sourceText)
222
- const findings = actions.flatMap(lintAction)
223
- return { actions, findings }
475
+ const actions = parseActions(fileName, sourceText);
476
+ const findings = actions.flatMap(lintAction);
477
+ return { actions, findings };
224
478
  }
225
479
 
226
480
  /**
@@ -231,343 +485,877 @@ export function checkSource(fileName, sourceText) {
231
485
  * Referência não encontrada ou identificador duplicado não acusa.
232
486
  */
233
487
  export function checkProject(sources) {
234
- let actions = 0
235
- let contracts = 0
236
- const findings = []
237
- const contractsByIdent = new Map() // ident → item | 'ambiguous'
238
- const binds = [] // { item, file }
488
+ let actions = 0;
489
+ let contracts = 0;
490
+ const findings = [];
491
+ const contractsByIdent = new Map(); // ident → item | 'ambiguous'
492
+ const binds = []; // { item, file }
239
493
 
240
494
  for (const { file, text } of sources) {
241
495
  for (const item of parseActions(file, text)) {
242
- if (item.form === 'bindAction') {
243
- actions += 1
244
- binds.push({ item, file })
245
- } else if (item.form === 'defineContract') {
246
- contracts += 1
496
+ if (item.form === "bindAction") {
497
+ actions += 1;
498
+ binds.push({ item, file });
499
+ } else if (item.form === "defineContract") {
500
+ contracts += 1;
247
501
  if (item.ident !== null) {
248
- contractsByIdent.set(item.ident, contractsByIdent.has(item.ident) ? 'ambiguous' : item)
502
+ contractsByIdent.set(
503
+ item.ident,
504
+ contractsByIdent.has(item.ident) ? "ambiguous" : item,
505
+ );
249
506
  }
250
507
  } else {
251
- actions += 1
508
+ actions += 1;
252
509
  }
253
- for (const f of lintAction(item)) findings.push({ ...f, file })
510
+ for (const f of lintAction(item)) findings.push({ ...f, file });
254
511
  }
255
512
  }
256
513
 
257
514
  for (const { item, file } of binds) {
258
- const contract = contractsByIdent.get(item.contractRef)
259
- if (contract === undefined || contract === 'ambiguous') continue
260
- const protectedSomewhere = contract.keys.includes('authorize') || item.keys.includes('authorize')
261
- if (contract.keys.includes('requires') && !protectedSomewhere) {
515
+ const contract = contractsByIdent.get(item.contractRef);
516
+ if (contract === undefined || contract === "ambiguous") continue;
517
+ const protectedSomewhere =
518
+ contract.keys.includes("authorize") || item.keys.includes("authorize");
519
+ if (contract.keys.includes("requires") && !protectedSomewhere) {
262
520
  findings.push({
263
- rule: 'requires-sem-authorize',
264
- level: 'error',
521
+ rule: "requires-sem-authorize",
522
+ level: "error",
265
523
  action: contract.name ?? item.contractRef,
266
524
  line: item.line,
267
525
  file,
268
526
  message:
269
- '`requires` do contrato é declarativo (o runtime não o executa) — sem `authorize` no contrato ou no binding, a action fica ABERTA.',
270
- })
527
+ "`requires` do contrato é declarativo (o runtime não o executa) — sem `authorize` no contrato ou no binding, a action fica ABERTA.",
528
+ });
271
529
  }
272
530
  }
273
531
 
274
- return { actions, contracts, findings }
532
+ return { actions, contracts, findings };
275
533
  }
276
534
 
277
535
  /** Migrações de UI que falhariam apenas visualmente. Linhas que são só comentário não
278
536
  * contam; documentação vive fora do sourceRoot e também não entra no walk. */
279
537
  function stripComments(text) {
280
- let output = ''
281
- let state = 'code'
282
- let escaped = false
538
+ let output = "";
539
+ let state = "code";
540
+ let escaped = false;
283
541
  for (let index = 0; index < text.length; index += 1) {
284
- const char = text[index]
285
- const next = text[index + 1]
286
- if (state === 'line-comment') {
287
- if (char === '\n') { state = 'code'; output += char } else output += ' '
288
- continue
542
+ const char = text[index];
543
+ const next = text[index + 1];
544
+ if (state === "line-comment") {
545
+ if (char === "\n") {
546
+ state = "code";
547
+ output += char;
548
+ } else output += " ";
549
+ continue;
289
550
  }
290
- if (state === 'block-comment') {
291
- if (char === '*' && next === '/') { output += ' '; index += 1; state = 'code' }
292
- else output += char === '\n' ? '\n' : ' '
293
- continue
551
+ if (state === "block-comment") {
552
+ if (char === "*" && next === "/") {
553
+ output += " ";
554
+ index += 1;
555
+ state = "code";
556
+ } else output += char === "\n" ? "\n" : " ";
557
+ continue;
294
558
  }
295
- if (state === 'html-comment') {
296
- if (char === '-' && text.slice(index, index + 3) === '-->') {
297
- output += ' '; index += 2; state = 'code'
298
- } else output += char === '\n' ? '\n' : ' '
299
- continue
559
+ if (state === "html-comment") {
560
+ if (char === "-" && text.slice(index, index + 3) === "-->") {
561
+ output += " ";
562
+ index += 2;
563
+ state = "code";
564
+ } else output += char === "\n" ? "\n" : " ";
565
+ continue;
300
566
  }
301
- if (state === 'code' && text.slice(index, index + 4) === '<!--') {
302
- output += ' '; index += 3; state = 'html-comment'; continue
567
+ if (state === "code" && text.slice(index, index + 4) === "<!--") {
568
+ output += " ";
569
+ index += 3;
570
+ state = "html-comment";
571
+ continue;
303
572
  }
304
- if (state === 'code' && char === '/' && next === '/') {
305
- output += ' '; index += 1; state = 'line-comment'; continue
573
+ if (state === "code" && char === "/" && next === "/") {
574
+ output += " ";
575
+ index += 1;
576
+ state = "line-comment";
577
+ continue;
306
578
  }
307
- if (state === 'code' && char === '/' && next === '*') {
308
- output += ' '; index += 1; state = 'block-comment'; continue
579
+ if (state === "code" && char === "/" && next === "*") {
580
+ output += " ";
581
+ index += 1;
582
+ state = "block-comment";
583
+ continue;
309
584
  }
310
- if (state === 'code' && (char === "'" || char === '"' || char === '`')) {
311
- state = char; output += char; escaped = false; continue
585
+ if (state === "code" && (char === "'" || char === '"' || char === "`")) {
586
+ state = char;
587
+ output += char;
588
+ escaped = false;
589
+ continue;
312
590
  }
313
- if (state !== 'code') {
314
- output += char
315
- if (escaped) escaped = false
316
- else if (char === '\\') escaped = true
317
- else if (char === state) state = 'code'
318
- continue
591
+ if (state !== "code") {
592
+ output += char;
593
+ if (escaped) escaped = false;
594
+ else if (char === "\\") escaped = true;
595
+ else if (char === state) state = "code";
596
+ continue;
319
597
  }
320
- output += char
598
+ output += char;
321
599
  }
322
- return output
600
+ return output;
323
601
  }
324
602
 
325
603
  function quotedValues(text) {
326
- const values = []
327
- let line = 1
604
+ const values = [];
605
+ let line = 1;
328
606
  const startsRegex = (index) => {
329
- const before = text.slice(0, index).trimEnd()
330
- if (before === '') return true
331
- const previous = before.at(-1)
332
- return /[=(:,![{;?&|+*%^~<>-]/.test(previous) ||
333
- /\b(?:return|case|throw|typeof|instanceof|in|of|yield|await)\s*$/.test(before)
334
- }
607
+ const before = text.slice(0, index).trimEnd();
608
+ if (before === "") return true;
609
+ const previous = before.at(-1);
610
+ return (
611
+ /[=(:,![{;?&|+*%^~<>-]/.test(previous) ||
612
+ /\b(?:return|case|throw|typeof|instanceof|in|of|yield|await)\s*$/.test(
613
+ before,
614
+ )
615
+ );
616
+ };
335
617
  for (let index = 0; index < text.length; index += 1) {
336
- const quote = text[index]
337
- if (quote === '\n') { line += 1; continue }
338
- if (quote === '/' && startsRegex(index)) {
339
- let escaped = false
340
- let inCharacterClass = false
618
+ const quote = text[index];
619
+ if (quote === "\n") {
620
+ line += 1;
621
+ continue;
622
+ }
623
+ if (quote === "/" && startsRegex(index)) {
624
+ let escaped = false;
625
+ let inCharacterClass = false;
341
626
  for (index += 1; index < text.length; index += 1) {
342
- const char = text[index]
343
- if (char === '\n') line += 1
344
- if (escaped) { escaped = false; continue }
345
- if (char === '\\') { escaped = true; continue }
346
- if (char === '[') { inCharacterClass = true; continue }
347
- if (char === ']') { inCharacterClass = false; continue }
348
- if (char === '/' && !inCharacterClass) break
627
+ const char = text[index];
628
+ if (char === "\n") line += 1;
629
+ if (escaped) {
630
+ escaped = false;
631
+ continue;
632
+ }
633
+ if (char === "\\") {
634
+ escaped = true;
635
+ continue;
636
+ }
637
+ if (char === "[") {
638
+ inCharacterClass = true;
639
+ continue;
640
+ }
641
+ if (char === "]") {
642
+ inCharacterClass = false;
643
+ continue;
644
+ }
645
+ if (char === "/" && !inCharacterClass) break;
349
646
  }
350
- while (/[a-z]/i.test(text[index + 1] ?? '')) index += 1
351
- continue
647
+ while (/[a-z]/i.test(text[index + 1] ?? "")) index += 1;
648
+ continue;
352
649
  }
353
- if (quote !== "'" && quote !== '"' && quote !== '`') continue
354
- let value = ''
355
- let escaped = false
356
- const startLine = line
357
- const startIndex = index
650
+ if (quote !== "'" && quote !== '"' && quote !== "`") continue;
651
+ let value = "";
652
+ let escaped = false;
653
+ const startLine = line;
654
+ const startIndex = index;
358
655
  for (index += 1; index < text.length; index += 1) {
359
- const char = text[index]
360
- if (char === '\n') line += 1
361
- if (escaped) { value += char; escaped = false; continue }
362
- if (char === '\\') { value += char; escaped = true; continue }
363
- if (char === quote) break
364
- value += char
656
+ const char = text[index];
657
+ if (char === "\n") line += 1;
658
+ if (escaped) {
659
+ value += char;
660
+ escaped = false;
661
+ continue;
662
+ }
663
+ if (char === "\\") {
664
+ value += char;
665
+ escaped = true;
666
+ continue;
667
+ }
668
+ if (char === quote) break;
669
+ value += char;
365
670
  }
366
- values.push({ value, startLine, startIndex, endIndex: index })
671
+ values.push({ value, startLine, startIndex, endIndex: index });
367
672
  }
368
- return values
673
+ return values;
369
674
  }
370
675
 
371
- const SIMPLE_TAILWIND_CLASSES = new Set(['flex', 'grid', 'block', 'inline', 'hidden', 'border', 'shadow', 'ring'])
676
+ const SIMPLE_TAILWIND_CLASSES = new Set([
677
+ "flex",
678
+ "grid",
679
+ "block",
680
+ "inline",
681
+ "hidden",
682
+ "border",
683
+ "shadow",
684
+ "ring",
685
+ ]);
372
686
 
373
687
  function looksLikeUtilityValue(value, token, matchesToken) {
374
- if (!matchesToken(value, token)) return false
375
- const parts = value.trim().split(/\s+/).filter(Boolean)
376
- return parts.length > 1 && parts.every((part) =>
377
- matchesToken(part, token) || SIMPLE_TAILWIND_CLASSES.has(part) || /[-:[\]/!@.%]/.test(part),
378
- )
688
+ if (!matchesToken(value, token)) return false;
689
+ const parts = value.trim().split(/\s+/).filter(Boolean);
690
+ return (
691
+ parts.length > 1 &&
692
+ parts.every(
693
+ (part) =>
694
+ matchesToken(part, token) ||
695
+ SIMPLE_TAILWIND_CLASSES.has(part) ||
696
+ /[-:[\]/!@.%]/.test(part),
697
+ )
698
+ );
379
699
  }
380
700
 
381
701
  function isInsideUtilityHelper(beforeValue) {
382
- const matches = [...beforeValue.matchAll(/\b(?:cn|cva|clsx|twMerge)\s*\(/g)]
702
+ const matches = [...beforeValue.matchAll(/\b(?:cn|cva|clsx|twMerge)\s*\(/g)];
383
703
  for (const match of matches.reverse()) {
384
- const call = beforeValue.slice(match.index)
385
- let depth = 0
386
- let quote = null
387
- let escaped = false
704
+ const call = beforeValue.slice(match.index);
705
+ let depth = 0;
706
+ let quote = null;
707
+ let escaped = false;
388
708
  for (const char of call) {
389
709
  if (quote !== null) {
390
- if (escaped) escaped = false
391
- else if (char === '\\') escaped = true
392
- else if (char === quote) quote = null
393
- continue
710
+ if (escaped) escaped = false;
711
+ else if (char === "\\") escaped = true;
712
+ else if (char === quote) quote = null;
713
+ continue;
394
714
  }
395
- if (char === "'" || char === '"' || char === '`') { quote = char; continue }
396
- if (char === '(') depth += 1
397
- else if (char === ')') depth -= 1
715
+ if (char === "'" || char === '"' || char === "`") {
716
+ quote = char;
717
+ continue;
718
+ }
719
+ if (char === "(") depth += 1;
720
+ else if (char === ")") depth -= 1;
721
+ }
722
+ if (depth > 0) return true;
723
+ }
724
+ return false;
725
+ }
726
+
727
+ function uiImports(sourceFile) {
728
+ const imports = new Map();
729
+ for (const statement of sourceFile.statements) {
730
+ if (
731
+ !ts.isImportDeclaration(statement) ||
732
+ !ts.isStringLiteral(statement.moduleSpecifier) ||
733
+ statement.moduleSpecifier.text !== "@softize/opus/ui/react"
734
+ )
735
+ continue;
736
+ const bindings = statement.importClause?.namedBindings;
737
+ if (!bindings || !ts.isNamedImports(bindings)) continue;
738
+ for (const element of bindings.elements) {
739
+ imports.set(
740
+ element.name.text,
741
+ element.propertyName?.text ?? element.name.text,
742
+ );
398
743
  }
399
- if (depth > 0) return true
400
744
  }
401
- return false
745
+ return imports;
746
+ }
747
+
748
+ function jsxLocalName(node) {
749
+ const tag = ts.isJsxElement(node)
750
+ ? node.openingElement.tagName
751
+ : node.tagName;
752
+ return ts.isIdentifier(tag) ? tag.text : null;
753
+ }
754
+
755
+ function jsxOpening(node) {
756
+ return ts.isJsxElement(node) ? node.openingElement : node;
757
+ }
758
+
759
+ function directJsxParentName(node, imports) {
760
+ let parent = node.parent;
761
+ while (
762
+ ts.isParenthesizedExpression(parent) ||
763
+ ts.isConditionalExpression(parent) ||
764
+ ts.isBinaryExpression(parent) ||
765
+ ts.isJsxExpression(parent)
766
+ ) {
767
+ parent = parent.parent;
768
+ }
769
+ if (!ts.isJsxElement(parent)) return null;
770
+ const local = jsxLocalName(parent);
771
+ return local === null ? null : (imports.get(local) ?? null);
772
+ }
773
+
774
+ function structuralJsxAncestorName(node, imports) {
775
+ let current = node.parent;
776
+ while (current !== undefined) {
777
+ if (ts.isJsxElement(current) || ts.isJsxSelfClosingElement(current)) {
778
+ const local = jsxLocalName(current);
779
+ const imported = local === null ? undefined : imports.get(local);
780
+ if (
781
+ imported !== undefined &&
782
+ (UI_STRUCTURAL_ROOTS.has(imported) ||
783
+ UI_STRUCTURAL_PARENTS.has(imported) ||
784
+ [...UI_STRUCTURAL_PARENTS.values()].some((parents) =>
785
+ parents.has(imported),
786
+ ))
787
+ )
788
+ return imported;
789
+ }
790
+ current = current.parent;
791
+ }
792
+ return null;
793
+ }
794
+
795
+ function directStructuralChildren(node, imports) {
796
+ if (!ts.isJsxElement(node)) return { names: [], hasOpaque: false };
797
+ const names = [];
798
+ let hasOpaque = false;
799
+ for (const child of node.children) {
800
+ if (ts.isJsxText(child) && child.text.trim() === "") continue;
801
+ if (ts.isJsxExpression(child) && child.expression === undefined) continue;
802
+ if (ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child)) {
803
+ const local = jsxLocalName(child);
804
+ const imported = local === null ? null : imports.get(local);
805
+ if (imported !== undefined) names.push(imported);
806
+ else hasOpaque = true;
807
+ continue;
808
+ }
809
+ hasOpaque = true;
810
+ }
811
+ return { names, hasOpaque };
812
+ }
813
+
814
+ /** Verifica a anatomia JSX pública definida na ADR 0005. */
815
+ export function checkUiStructure(file, text) {
816
+ if (!/\.[cm]?[jt]sx$/.test(file)) return [];
817
+ const sourceFile = ts.createSourceFile(
818
+ file,
819
+ text,
820
+ ts.ScriptTarget.Latest,
821
+ true,
822
+ ts.ScriptKind.TSX,
823
+ );
824
+ const imports = uiImports(sourceFile);
825
+ if (imports.size === 0) return [];
826
+ const findings = [];
827
+ const lineOf = (node) =>
828
+ sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line +
829
+ 1;
830
+ const add = (node, action, message, rule = "ui-structure") =>
831
+ findings.push({
832
+ rule,
833
+ level: "error",
834
+ action,
835
+ line: lineOf(node),
836
+ file,
837
+ message,
838
+ });
839
+
840
+ const visit = (node) => {
841
+ if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) {
842
+ const local = jsxLocalName(node);
843
+ const component = local === null ? null : imports.get(local);
844
+ const parents =
845
+ component === null ? undefined : UI_STRUCTURAL_PARENTS.get(component);
846
+ if (parents !== undefined) {
847
+ const strict = UI_STRICT_DIRECT_COMPONENTS.has(component);
848
+ const parent = strict
849
+ ? directJsxParentName(node, imports)
850
+ : structuralJsxAncestorName(node, imports);
851
+ // Slots das famílias históricas podem ser a raiz de um componente wrapper. Quando há
852
+ // uma superfície estrutural visível no mesmo JSX, porém, ela precisa ser a família certa.
853
+ if (
854
+ (strict || parent !== null) &&
855
+ (parent === null || !parents.has(parent))
856
+ ) {
857
+ add(
858
+ node,
859
+ component,
860
+ `${component} deve estar ${strict ? "como filho direto" : "dentro"} de ${[...parents].join(" ou ")}.`,
861
+ );
862
+ }
863
+ }
864
+
865
+ const header =
866
+ component === null ? undefined : UI_STRUCTURAL_HEADERS.get(component);
867
+ if (header !== undefined) {
868
+ const opening = jsxOpening(node);
869
+ const attributes = new Set(
870
+ opening.attributes.properties.flatMap((attribute) =>
871
+ ts.isJsxAttribute(attribute) && ts.isIdentifier(attribute.name)
872
+ ? [attribute.name.text]
873
+ : [],
874
+ ),
875
+ );
876
+ const shorthand = [...header.shorthand].some((attribute) =>
877
+ attributes.has(attribute),
878
+ );
879
+ const children = directStructuralChildren(node, imports);
880
+ const structural =
881
+ children.names.includes(header.title) ||
882
+ children.names.some((name) => header.optional.has(name));
883
+ if (shorthand && structural) {
884
+ add(
885
+ node,
886
+ component,
887
+ `${component} não permite misturar propriedades de shorthand com seus slots explícitos.`,
888
+ "ui-structure-mode",
889
+ );
890
+ } else if (!shorthand) {
891
+ const titles = children.names.filter(
892
+ (name) => name === header.title,
893
+ ).length;
894
+ const unexpected = children.names.some(
895
+ (name) => name !== header.title && !header.optional.has(name),
896
+ );
897
+ const duplicatedOptional = [...header.optional].some(
898
+ (name) =>
899
+ children.names.filter((child) => child === name).length > 1,
900
+ );
901
+ const titleValid =
902
+ header.titleRequired === false ? titles <= 1 : titles === 1;
903
+ const recognizedContent =
904
+ titles +
905
+ children.names.filter((name) => header.optional.has(name)).length;
906
+ if (
907
+ !titleValid ||
908
+ (header.titleRequired === false &&
909
+ recognizedContent === 0 &&
910
+ !(header.allowOpaque && children.hasOpaque)) ||
911
+ unexpected ||
912
+ duplicatedOptional ||
913
+ (children.hasOpaque && !header.allowOpaque)
914
+ ) {
915
+ add(
916
+ node,
917
+ component,
918
+ `${component} explícito ${header.titleRequired === false ? `aceita no máximo um ${header.title}` : `exige um ${header.title}`} e aceita no máximo um de cada slot opcional como filhos diretos.`,
919
+ );
920
+ }
921
+ }
922
+ }
923
+
924
+ const root =
925
+ component === null ? undefined : UI_STRUCTURAL_ROOTS.get(component);
926
+ if (root !== undefined) {
927
+ const opening = jsxOpening(node);
928
+ const attributes = new Set(
929
+ opening.attributes.properties.flatMap((attribute) =>
930
+ ts.isJsxAttribute(attribute) && ts.isIdentifier(attribute.name)
931
+ ? [attribute.name.text]
932
+ : [],
933
+ ),
934
+ );
935
+ const shorthand = [...root.shorthand].some((attribute) =>
936
+ attributes.has(attribute),
937
+ );
938
+ const children = directStructuralChildren(node, imports);
939
+ const structural =
940
+ children.names.includes(root.header) ||
941
+ children.names.includes(root.body);
942
+ if (shorthand && structural) {
943
+ add(
944
+ node,
945
+ component,
946
+ `${component} não permite misturar propriedades de shorthand com ${root.header}/${root.body}.`,
947
+ "ui-structure-mode",
948
+ );
949
+ } else if (!shorthand) {
950
+ const headers = children.names.filter(
951
+ (name) => name === root.header,
952
+ ).length;
953
+ const bodies = children.names.filter(
954
+ (name) => name === root.body,
955
+ ).length;
956
+ const unexpected = children.names.some(
957
+ (name) => name !== root.header && name !== root.body,
958
+ );
959
+ if (
960
+ headers !== 1 ||
961
+ bodies !== 1 ||
962
+ unexpected ||
963
+ children.hasOpaque
964
+ ) {
965
+ add(
966
+ node,
967
+ component,
968
+ `${component} explícito exige exatamente um ${root.header} e um ${root.body} como filhos diretos.`,
969
+ );
970
+ }
971
+ }
972
+ }
973
+ }
974
+ ts.forEachChild(node, visit);
975
+ };
976
+ visit(sourceFile);
977
+ return findings;
978
+ }
979
+
980
+ function jsxAttribute(opening, name) {
981
+ return opening.attributes.properties.find(
982
+ (candidate) =>
983
+ ts.isJsxAttribute(candidate) &&
984
+ ts.isIdentifier(candidate.name) &&
985
+ candidate.name.text === name,
986
+ );
987
+ }
988
+
989
+ function jsxStringAttributeValues(opening, name) {
990
+ const attribute = jsxAttribute(opening, name);
991
+ if (!attribute || !ts.isJsxAttribute(attribute) || !attribute.initializer)
992
+ return [];
993
+ if (ts.isStringLiteral(attribute.initializer))
994
+ return [attribute.initializer.text];
995
+ if (!ts.isJsxExpression(attribute.initializer)) return [];
996
+ const values = [];
997
+ const collect = (node) => {
998
+ if (ts.isStringLiteral(node)) values.push(node.text);
999
+ else ts.forEachChild(node, collect);
1000
+ };
1001
+ if (attribute.initializer.expression)
1002
+ collect(attribute.initializer.expression);
1003
+ return [...new Set(values)];
1004
+ }
1005
+
1006
+ /** Reprova aliases de migração da API semântica e `tone` em novos dicionários. */
1007
+ export function checkUiSemantics(file, text) {
1008
+ if (!/\.[cm]?[jt]sx?$/.test(file)) return [];
1009
+ const sourceFile = ts.createSourceFile(
1010
+ file,
1011
+ text,
1012
+ ts.ScriptTarget.Latest,
1013
+ true,
1014
+ file.endsWith("x") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
1015
+ );
1016
+ const imports = uiImports(sourceFile);
1017
+ const schemaAliases = new Set();
1018
+ const variableInitializers = new Map();
1019
+ for (const statement of sourceFile.statements) {
1020
+ if (ts.isVariableStatement(statement)) {
1021
+ for (const declaration of statement.declarationList.declarations) {
1022
+ if (ts.isIdentifier(declaration.name) && declaration.initializer)
1023
+ variableInitializers.set(
1024
+ declaration.name.text,
1025
+ declaration.initializer,
1026
+ );
1027
+ }
1028
+ }
1029
+ if (
1030
+ !ts.isImportDeclaration(statement) ||
1031
+ !ts.isStringLiteral(statement.moduleSpecifier) ||
1032
+ statement.moduleSpecifier.text !== "@softize/opus/schema/zod"
1033
+ )
1034
+ continue;
1035
+ const bindings = statement.importClause?.namedBindings;
1036
+ if (!bindings || !ts.isNamedImports(bindings)) continue;
1037
+ for (const element of bindings.elements) {
1038
+ if ((element.propertyName?.text ?? element.name.text) === "t")
1039
+ schemaAliases.add(element.name.text);
1040
+ }
1041
+ }
1042
+
1043
+ const findings = [];
1044
+ const lineOf = (node) =>
1045
+ sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line +
1046
+ 1;
1047
+ const add = (node, action, message) =>
1048
+ findings.push({
1049
+ rule: "ui-semantic-api",
1050
+ level: "error",
1051
+ action,
1052
+ line: lineOf(node),
1053
+ file,
1054
+ message,
1055
+ });
1056
+
1057
+ const visit = (node) => {
1058
+ if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) {
1059
+ const local = jsxLocalName(node);
1060
+ const component = local === null ? null : imports.get(local);
1061
+ if (component !== null && UI_LEGACY_VARIANTS.has(component)) {
1062
+ const opening = jsxOpening(node);
1063
+ if (jsxAttribute(opening, "tone") !== undefined) {
1064
+ add(
1065
+ opening,
1066
+ `${component}.tone`,
1067
+ `${component} usa \`context\` para significado semântico; \`tone\` é apenas um alias temporário.`,
1068
+ );
1069
+ }
1070
+ const legacyVariants = jsxStringAttributeValues(
1071
+ opening,
1072
+ "variant",
1073
+ ).filter((variant) => UI_LEGACY_VARIANTS.get(component).has(variant));
1074
+ for (const variant of legacyVariants) {
1075
+ add(
1076
+ opening,
1077
+ `${component}.variant=${variant}`,
1078
+ "Separe o significado em `context` e use `variant` somente para tratamento visual.",
1079
+ );
1080
+ }
1081
+ }
1082
+ }
1083
+
1084
+ if (
1085
+ ts.isCallExpression(node) &&
1086
+ ts.isPropertyAccessExpression(node.expression) &&
1087
+ schemaAliases.has(node.expression.expression.getText(sourceFile)) &&
1088
+ node.expression.name.text === "dict"
1089
+ ) {
1090
+ const argument = node.arguments[0];
1091
+ const entries =
1092
+ argument && ts.isIdentifier(argument)
1093
+ ? variableInitializers.get(argument.text)
1094
+ : argument;
1095
+ if (entries) {
1096
+ const inspectEntry = (candidate) => {
1097
+ if (
1098
+ ts.isPropertyAssignment(candidate) &&
1099
+ candidate.name.getText(sourceFile).replaceAll(/["']/g, "") ===
1100
+ "tone"
1101
+ ) {
1102
+ add(
1103
+ candidate,
1104
+ "t.dict.tone",
1105
+ "Entradas de status e estágio usam `context`; `tone` é apenas um alias temporário.",
1106
+ );
1107
+ }
1108
+ ts.forEachChild(candidate, inspectEntry);
1109
+ };
1110
+ inspectEntry(entries);
1111
+ }
1112
+ }
1113
+ ts.forEachChild(node, visit);
1114
+ };
1115
+ visit(sourceFile);
1116
+ return findings;
402
1117
  }
403
1118
 
404
1119
  export function checkUiMigrations(file, text) {
405
- const findings = []
406
- const seen = new Set()
1120
+ const findings = [];
1121
+ const seen = new Set();
407
1122
  const matchesToken = (line, token) => {
408
- const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
409
- return new RegExp(`(^|[^A-Za-z0-9_-])${escaped}(?=$|[^A-Za-z0-9_-])`).test(line)
410
- }
411
- const extension = path.extname(file)
412
- const source = stripComments(text)
1123
+ const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1124
+ return new RegExp(`(^|[^A-Za-z0-9_-])${escaped}(?=$|[^A-Za-z0-9_-])`).test(
1125
+ line,
1126
+ );
1127
+ };
1128
+ const extension = path.extname(file);
1129
+ const source = stripComments(text);
413
1130
  const addFinding = (token, replacement, line) => {
414
- seen.add(token)
1131
+ seen.add(token);
415
1132
  findings.push({
416
- rule: 'removed-ui-token',
417
- level: 'error',
1133
+ rule: "removed-ui-token",
1134
+ level: "error",
418
1135
  action: token,
419
1136
  line,
420
1137
  file,
421
1138
  message: `token removido — ${replacement}.`,
422
- })
423
- }
1139
+ });
1140
+ };
424
1141
  for (const [index, raw] of source.split(/\r?\n/).entries()) {
425
- const line = raw.trim()
426
- if (line === '') continue
1142
+ const line = raw.trim();
1143
+ if (line === "") continue;
427
1144
  for (const [token, replacement] of REMOVED_UI_TOKENS) {
428
- if (seen.has(token)) continue
429
- const inStylesheet = extension === '.css' && matchesToken(line, token)
430
- const inSvelteDirective = extension === '.svelte' && new RegExp(`\\bclass:${token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?=[=\\s]|$)`).test(line)
431
- if (inStylesheet || inSvelteDirective) addFinding(token, replacement, index + 1)
1145
+ if (seen.has(token)) continue;
1146
+ const inStylesheet = extension === ".css" && matchesToken(line, token);
1147
+ const inSvelteDirective =
1148
+ extension === ".svelte" &&
1149
+ new RegExp(
1150
+ `\\bclass:${token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?=[=\\s]|$)`,
1151
+ ).test(line);
1152
+ if (inStylesheet || inSvelteDirective)
1153
+ addFinding(token, replacement, index + 1);
432
1154
  }
433
1155
  }
434
- for (const { value, startLine, startIndex, endIndex } of quotedValues(source)) {
435
- const beforeValue = source.slice(Math.max(0, startIndex - 500), startIndex)
436
- const classAttribute = /\bclass(?:Name)?\s*=\s*\{?\s*$/.test(beforeValue)
437
- const classExpression = /\bclass(?:Name)?\s*=\s*\{[^{}]*$/.test(beforeValue)
438
- const utilityHelper = isInsideUtilityHelper(beforeValue)
439
- const propertyKey = /^\s*:/.test(source.slice(endIndex + 1, endIndex + 12))
440
- const classBinding = /\b(?:class(?:Name|Names)?|classes|\w+(?:Class|Classes))\s*=\s*[^;\n]*$/.test(beforeValue)
441
- const objectProperty = /\b(?!aria(?:Label|Description)\b|label\b|title\b|description\b|text\b|message\b|note\b|content\b)[A-Za-z_$][\w$-]*\s*:\s*$/.test(beforeValue)
442
- const classListCall = /\bclassList\.(?:add|remove|toggle|replace|contains)\s*\([^)]*$/.test(beforeValue)
443
- const stylePropertyCall = /\b(?:style\.)?setProperty\s*\([^)]*$/.test(beforeValue)
444
- const utilityContext = classAttribute || classExpression || utilityHelper || classBinding || objectProperty || classListCall
445
- const utilities = value.trim().split(/\s+/).filter(Boolean)
1156
+ for (const { value, startLine, startIndex, endIndex } of quotedValues(
1157
+ source,
1158
+ )) {
1159
+ const beforeValue = source.slice(Math.max(0, startIndex - 500), startIndex);
1160
+ const classAttribute = /\bclass(?:Name)?\s*=\s*\{?\s*$/.test(beforeValue);
1161
+ const classExpression = /\bclass(?:Name)?\s*=\s*\{[^{}]*$/.test(
1162
+ beforeValue,
1163
+ );
1164
+ const utilityHelper = isInsideUtilityHelper(beforeValue);
1165
+ const propertyKey = /^\s*:/.test(source.slice(endIndex + 1, endIndex + 12));
1166
+ const classBinding =
1167
+ /\b(?:class(?:Name|Names)?|classes|\w+(?:Class|Classes))\s*=\s*[^;\n]*$/.test(
1168
+ beforeValue,
1169
+ );
1170
+ const objectProperty =
1171
+ /\b(?!aria(?:Label|Description)\b|label\b|title\b|description\b|text\b|message\b|note\b|content\b)[A-Za-z_$][\w$-]*\s*:\s*$/.test(
1172
+ beforeValue,
1173
+ );
1174
+ const classListCall =
1175
+ /\bclassList\.(?:add|remove|toggle|replace|contains)\s*\([^)]*$/.test(
1176
+ beforeValue,
1177
+ );
1178
+ const stylePropertyCall = /\b(?:style\.)?setProperty\s*\([^)]*$/.test(
1179
+ beforeValue,
1180
+ );
1181
+ const utilityContext =
1182
+ classAttribute ||
1183
+ classExpression ||
1184
+ utilityHelper ||
1185
+ classBinding ||
1186
+ objectProperty ||
1187
+ classListCall;
1188
+ const utilities = value.trim().split(/\s+/).filter(Boolean);
446
1189
  for (const [background, foreground] of UI_SURFACE_PAIRS) {
447
- const pairKey = `surface:${background}`
448
- const hasUtility = (expected) => utilities.some((utility) => {
449
- const normalized = utility.replace(/^!/, '').replace(/!$/, '')
450
- return normalized === expected || normalized.startsWith(`${expected}/`)
451
- })
452
- const hasBackground = hasUtility(background)
453
- const hasForeground = hasUtility(foreground)
454
- if (!seen.has(pairKey) && utilityContext && hasBackground && !hasForeground) {
455
- seen.add(pairKey)
1190
+ const pairKey = `surface:${background}`;
1191
+ const hasUtility = (expected) =>
1192
+ utilities.some((utility) => {
1193
+ const normalized = utility.replace(/^!/, "").replace(/!$/, "");
1194
+ return (
1195
+ normalized === expected || normalized.startsWith(`${expected}/`)
1196
+ );
1197
+ });
1198
+ const hasBackground = hasUtility(background);
1199
+ const hasForeground = hasUtility(foreground);
1200
+ if (
1201
+ !seen.has(pairKey) &&
1202
+ utilityContext &&
1203
+ hasBackground &&
1204
+ !hasForeground
1205
+ ) {
1206
+ seen.add(pairKey);
456
1207
  findings.push({
457
- rule: 'unpaired-ui-surface',
458
- level: 'error',
1208
+ rule: "unpaired-ui-surface",
1209
+ level: "error",
459
1210
  action: background,
460
1211
  line: startLine,
461
1212
  file,
462
1213
  message: `superfície sem o foreground correspondente — use \`${background} ${foreground}\` no mesmo fragmento de classes.`,
463
- })
1214
+ });
464
1215
  }
465
1216
  }
466
1217
  for (const [token, replacement] of REMOVED_UI_TOKENS) {
467
- const usedAsUtility = matchesToken(value, token) && (
468
- classAttribute || classExpression || utilityHelper || classBinding || objectProperty ||
469
- (classListCall && !token.startsWith('--')) ||
470
- (stylePropertyCall && token.startsWith('--')) ||
471
- (propertyKey && token.startsWith('--')) ||
472
- looksLikeUtilityValue(value, token, matchesToken)
473
- )
474
- if (seen.has(token) || !usedAsUtility) continue
475
- const tokenIndex = value.indexOf(token)
476
- const tokenLine = startLine + value.slice(0, tokenIndex).split('\n').length - 1
477
- addFinding(token, replacement, tokenLine)
1218
+ const usedAsUtility =
1219
+ matchesToken(value, token) &&
1220
+ (classAttribute ||
1221
+ classExpression ||
1222
+ utilityHelper ||
1223
+ classBinding ||
1224
+ objectProperty ||
1225
+ (classListCall && !token.startsWith("--")) ||
1226
+ (stylePropertyCall && token.startsWith("--")) ||
1227
+ (propertyKey && token.startsWith("--")) ||
1228
+ looksLikeUtilityValue(value, token, matchesToken));
1229
+ if (seen.has(token) || !usedAsUtility) continue;
1230
+ const tokenIndex = value.indexOf(token);
1231
+ const tokenLine =
1232
+ startLine + value.slice(0, tokenIndex).split("\n").length - 1;
1233
+ addFinding(token, replacement, tokenLine);
478
1234
  }
479
1235
  }
480
- return findings.sort((left, right) => left.line - right.line)
1236
+ findings.push(...checkUiStructure(file, text));
1237
+ findings.push(...checkUiSemantics(file, text));
1238
+ return findings.sort((left, right) => left.line - right.line);
481
1239
  }
482
1240
 
483
- const SKIP_DIRS = new Set(['node_modules', 'dist', 'build', '.git', '.next', 'coverage'])
484
- const UI_SKIP_DIRS = new Set([...SKIP_DIRS, '__tests__', '__fixtures__', 'fixtures', 'test', 'tests'])
485
- const UI_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs', '.css', '.html', '.svelte', '.vue'])
1241
+ const SKIP_DIRS = new Set([
1242
+ "node_modules",
1243
+ "dist",
1244
+ "build",
1245
+ ".git",
1246
+ ".next",
1247
+ "coverage",
1248
+ ]);
1249
+ const UI_SKIP_DIRS = new Set([
1250
+ ...SKIP_DIRS,
1251
+ "__tests__",
1252
+ "__fixtures__",
1253
+ "fixtures",
1254
+ "test",
1255
+ "tests",
1256
+ ]);
1257
+ const UI_EXTENSIONS = new Set([
1258
+ ".ts",
1259
+ ".tsx",
1260
+ ".mts",
1261
+ ".cts",
1262
+ ".js",
1263
+ ".jsx",
1264
+ ".mjs",
1265
+ ".cjs",
1266
+ ".css",
1267
+ ".html",
1268
+ ".svelte",
1269
+ ".vue",
1270
+ ]);
486
1271
 
487
1272
  export async function walkTsFiles(dir, acc = []) {
488
- const root = canonicalProjectDirectory(dir)
489
- const walk = (local = '.') => {
490
- const entries = readProjectDirectory(root, local).entries
1273
+ const root = canonicalProjectDirectory(dir);
1274
+ const walk = (local = ".") => {
1275
+ const entries = readProjectDirectory(root, local).entries;
491
1276
  for (const e of entries) {
492
- if (e.name.startsWith('.') && e.name !== '.') continue
493
- if (SKIP_DIRS.has(e.name)) continue
494
- const relative = path.join(local, e.name)
495
- const full = path.join(root, relative)
1277
+ if (e.name.startsWith(".") && e.name !== ".") continue;
1278
+ if (SKIP_DIRS.has(e.name)) continue;
1279
+ const relative = path.join(local, e.name);
1280
+ const full = path.join(root, relative);
496
1281
  if (e.isSymbolicLink()) {
497
1282
  // Dirent não revela se o link aponta para arquivo ou diretório. Ignorá-lo pela
498
1283
  // extensão permitiria ocultar uma árvore de sources sob um nome de asset.
499
- safeProjectPath(root, relative, { mustExist: true })
1284
+ safeProjectPath(root, relative, { mustExist: true });
500
1285
  }
501
1286
  if (e.isDirectory()) {
502
- safeProjectPath(root, relative, { mustExist: true })
503
- walk(relative)
1287
+ safeProjectPath(root, relative, { mustExist: true });
1288
+ walk(relative);
504
1289
  } else if (
505
- (e.name.endsWith('.ts') || e.name.endsWith('.tsx')) &&
506
- !e.name.endsWith('.d.ts') &&
507
- !e.name.endsWith('.test.ts')
1290
+ (e.name.endsWith(".ts") || e.name.endsWith(".tsx")) &&
1291
+ !e.name.endsWith(".d.ts") &&
1292
+ !e.name.endsWith(".test.ts")
508
1293
  ) {
509
- safeProjectPath(root, relative, { mustExist: true })
510
- acc.push(full)
1294
+ safeProjectPath(root, relative, { mustExist: true });
1295
+ acc.push(full);
511
1296
  }
512
1297
  }
513
- }
514
- walk()
515
- return acc
1298
+ };
1299
+ walk();
1300
+ return acc;
516
1301
  }
517
1302
 
518
1303
  async function walkUiFiles(dir, acc = []) {
519
- const root = canonicalProjectDirectory(dir)
520
- const walk = (local = '.') => {
521
- const entries = readProjectDirectory(root, local).entries
1304
+ const root = canonicalProjectDirectory(dir);
1305
+ const walk = (local = ".") => {
1306
+ const entries = readProjectDirectory(root, local).entries;
522
1307
  for (const e of entries) {
523
- if (e.name.startsWith('.')) continue
524
- if (UI_SKIP_DIRS.has(e.name)) continue
525
- const relative = path.join(local, e.name)
526
- const full = path.join(root, relative)
1308
+ if (e.name.startsWith(".")) continue;
1309
+ if (UI_SKIP_DIRS.has(e.name)) continue;
1310
+ const relative = path.join(local, e.name);
1311
+ const full = path.join(root, relative);
527
1312
  if (e.isSymbolicLink()) {
528
1313
  // Um link com nome de asset ainda pode esconder um diretório com UI analisável.
529
- safeProjectPath(root, relative, { mustExist: true })
1314
+ safeProjectPath(root, relative, { mustExist: true });
530
1315
  }
531
1316
  if (e.isDirectory()) {
532
- safeProjectPath(root, relative, { mustExist: true })
533
- walk(relative)
534
- } else if (UI_EXTENSIONS.has(path.extname(e.name)) && !/\.(?:test|spec)\.[^.]+$/.test(e.name)) {
535
- safeProjectPath(root, relative, { mustExist: true })
536
- acc.push(full)
1317
+ safeProjectPath(root, relative, { mustExist: true });
1318
+ walk(relative);
1319
+ } else if (
1320
+ UI_EXTENSIONS.has(path.extname(e.name)) &&
1321
+ !/\.(?:test|spec)\.[^.]+$/.test(e.name)
1322
+ ) {
1323
+ safeProjectPath(root, relative, { mustExist: true });
1324
+ acc.push(full);
537
1325
  }
538
1326
  }
539
- }
540
- walk()
541
- return acc
1327
+ };
1328
+ walk();
1329
+ return acc;
542
1330
  }
543
1331
 
544
1332
  /** Escaneia um diretório → { files, actions, contracts, findings, hasMarker }.
545
1333
  * `hasMarker` = o diretório é um projeto opus (tem opus.json na raiz). */
546
1334
  export async function scanDir(rootDir) {
547
- rootDir = canonicalProjectDirectory(rootDir)
548
- const files = await walkTsFiles(rootDir)
549
- const sources = []
1335
+ rootDir = canonicalProjectDirectory(rootDir);
1336
+ const files = await walkTsFiles(rootDir);
1337
+ const sources = [];
550
1338
  for (const file of files) {
551
- const text = readProjectFile(rootDir, path.relative(rootDir, file)).content
552
- if (!ACTION_MARKERS.some((m) => text.includes(m))) continue
553
- sources.push({ file: path.relative(rootDir, file), text })
1339
+ const text = readProjectFile(rootDir, path.relative(rootDir, file)).content;
1340
+ if (!ACTION_MARKERS.some((m) => text.includes(m))) continue;
1341
+ sources.push({ file: path.relative(rootDir, file), text });
554
1342
  }
555
- const checked = checkProject(sources)
556
- const uiFindings = []
1343
+ const checked = checkProject(sources);
1344
+ const uiFindings = [];
557
1345
  // UI pode morar em src/, app/, pages/ ou na raiz. O walk cobre o projeto inteiro e
558
1346
  // exclui dependências, builds, testes e fixtures para não interpretar exemplos como uso.
559
1347
  for (const file of await walkUiFiles(rootDir)) {
560
- const text = readProjectFile(rootDir, path.relative(rootDir, file)).content
561
- uiFindings.push(...checkUiMigrations(path.relative(rootDir, file), text))
1348
+ const text = readProjectFile(rootDir, path.relative(rootDir, file)).content;
1349
+ uiFindings.push(...checkUiMigrations(path.relative(rootDir, file), text));
562
1350
  }
563
- const hasMarker = safeProjectPath(rootDir, 'opus.json').exists
1351
+ const hasMarker = safeProjectPath(rootDir, "opus.json").exists;
564
1352
  return {
565
1353
  files: files.length,
566
1354
  actions: checked.actions,
567
1355
  contracts: checked.contracts,
568
1356
  findings: [...checked.findings, ...uiFindings],
569
1357
  hasMarker,
570
- }
1358
+ };
571
1359
  }
572
1360
 
573
1361
  /**
@@ -579,11 +1367,11 @@ export async function scanDir(rootDir) {
579
1367
  */
580
1368
  export function emptyScanVerdict({ hasMarker, contracts }) {
581
1369
  if (hasMarker) {
582
- return { ok: true, message: 'projeto opus sem actions — nada a checar.' }
1370
+ return { ok: true, message: "projeto opus sem actions — nada a checar." };
583
1371
  }
584
1372
  const hint =
585
1373
  contracts > 0
586
1374
  ? `${contracts} contrato(s) (defineContract) sem bindAction no diretório — o check roda na raiz do app, onde os binds moram.`
587
- : `procuro defineAction, defineContract e bindAction — confira o diretório.`
588
- return { ok: false, message: hint }
1375
+ : `procuro defineAction, defineContract e bindAction — confira o diretório.`;
1376
+ return { ok: false, message: hint };
589
1377
  }