synthesisui 0.16.421 → 0.16.423

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,317 +0,0 @@
1
- import { readFile } from "node:fs/promises";
2
- import { join } from "node:path";
3
- import { TAILWIND_THEME_VARS } from "./tailwind-theme-vars.js";
4
- /**
5
- * AS CLASSES QUE A FOLHA COMPILADA DECLARA COMO RECEITA - o nome, não o prefixo.
6
- *
7
- * A convenção viaja com o sistema (`ds-`, `sui-`, o que o projeto dele usa), então perguntar pelo
8
- * prefixo seria fixar a forma de um repositório. O que é estrutural é onde a regra MORA: o
9
- * compilador emite uma regra por receita dentro de `@layer components`, e é só essa camada que a
10
- * folha traz e o projeto dele não tem.
11
- *
12
- * A PRIMEIRA VERSÃO VARRIA O TEXTO INTEIRO, e a revisão do fecho mediu o preço na folha viva do
13
- * `codelevel` (143.409 bytes, versão instalada): **147 nomes extraídos, 3 deles não são receita** -
14
- * `dark`, que é a classe DELE em `@layer base`; `layer-3d`, um utilitário DELE espelhado em
15
- * `@layer utilities`; e `background`, que aparecia como VALOR de uma declaração e nem seletor era.
16
- * Rodada sobre o repositório dele, a função devolvia `["layer-3d", "dark"]` - duas classes DELE
17
- * sustentando a cobrança da NOSSA folha. É o defeito de 07/09 voltando pela porta do balde novo, e
18
- * o oposto exato do que esta medição existe para permitir.
19
- *
20
- * O ESCAPE CONTA como parte do nome, porque no CSS ele é como se escreve um caractere que o
21
- * seletor não aceita cru: `.ds-w-\[10px\]` é a classe `ds-w-[10px]`, e `.sm\:ds-hero` é a variante
22
- * `sm:` da classe `ds-hero` - que é como o código dele a escreve. Truncar no `\` produziria os
23
- * nomes `ds-w-` e `sm`, que ninguém escreve: uma dependência real deixaria de ser contada, e esse é
24
- * o único lado do erro que o cabeçalho deste módulo declara inaceitável.
25
- */
26
- export function classesTheSheetDeclares(css) {
27
- const out = new Set();
28
- for (const body of layerBodies(css, "components"))
29
- for (const prelude of selectorPreludes(body))
30
- for (const m of prelude.matchAll(/\.((?:\\.|[\w-])+)/g)) {
31
- const bare = m[1].replace(/\\(.)/g, "$1");
32
- /** A MESMA normalização que `classesIn` aplica ao que ele ESCREVE - `sm:ds-hero` é `ds-hero`. */
33
- out.add((bare.split(":").pop() ?? bare).replace(/^[!-]/, ""));
34
- }
35
- return [...out];
36
- }
37
- /** O corpo de cada `@layer <nome> { … }`, com as chaves de dentro contadas. */
38
- function layerBodies(css, name) {
39
- const out = [];
40
- for (const m of css.matchAll(new RegExp(`@layer\\s+${name}\\s*\\{`, "g"))) {
41
- let depth = 1;
42
- let i = (m.index ?? 0) + m[0].length;
43
- const start = i;
44
- for (; i < css.length && depth > 0; i += 1) {
45
- const c = css[i];
46
- if (c === "{")
47
- depth += 1;
48
- else if (c === "}")
49
- depth -= 1;
50
- }
51
- out.push(css.slice(start, i - 1));
52
- }
53
- return out;
54
- }
55
- /**
56
- * O PRELÚDIO DE CADA REGRA - o texto entre o fim da regra anterior e a `{` desta.
57
- *
58
- * É o que separa um SELETOR de um valor de declaração: `background: p.background` mora depois da
59
- * `{`, e nunca chega aqui. Um prelúdio que começa com `@` é uma regra de agrupamento (`@media`,
60
- * `@supports`) e não declara classe nenhuma - o corpo dela é varrido pela mesma volta do laço.
61
- */
62
- function selectorPreludes(body) {
63
- const out = [];
64
- const parts = body.split("{");
65
- for (const part of parts.slice(0, -1)) {
66
- const tail = part.slice(part.lastIndexOf("}") + 1).trim();
67
- if (tail && !tail.startsWith("@"))
68
- out.push(tail);
69
- }
70
- return out;
71
- }
72
- /** `--animate-shimmer` no `@theme` -> o nome `shimmer`, que é o que uma classe carrega. */
73
- function themeNames(themeCss) {
74
- const out = new Set();
75
- for (const block of themeCss.matchAll(/@theme[^{]*\{([\s\S]*?)\n\}/g))
76
- for (const m of block[1].matchAll(/^\s*--([a-z]+)-([a-zA-Z0-9_-]+)\s*:/gm))
77
- out.add(m[2]);
78
- return [...out];
79
- }
80
- /**
81
- * O VALOR DE UM ATRIBUTO DE CLASSE, QUANDO ELE É UMA EXPRESSÃO - e a regra é o ATRIBUTO, não a
82
- * função.
83
- *
84
- * O DEFEITO, medido em 08/09 nas duas árvores do `frontend-hub`: o leitor via `className="…"` e o
85
- * template direto, e não via `className={cn("…")}`.
86
- *
87
- * ```
88
- * packages/ui/src apps/web-dashboard
89
- * className=" literal 346 2639
90
- * className={cn( ou {clsx( 66 239 <- invisíveis
91
- * ```
92
- *
93
- * São **305 sítios** onde o projeto dele escreve classe e a pergunta *"algo aqui precisa da nossa
94
- * folha?"* era respondida sobre um texto que não continha a resposta. E o erro caía para o lado
95
- * ERRADO: o cabeçalho deste módulo declara que *"o lado seguro do erro é PRECISA"*, porque um falso
96
- * positivo mantém o setup sendo pedido e um falso NEGATIVO entrega um componente sem a animação que
97
- * ele veste. `cn()` é o idioma de quem parte do shadcn, então a metade que faltava era a maior parte
98
- * de quem chega.
99
- *
100
- * POR QUE A REGRA NÃO É O NOME DA FUNÇÃO. Casar `cn|clsx|twMerge` seria a lista dos casos de hoje: o
101
- * próximo cliente chama a dele de `classes`, `cx` ou nada - passa um ternário direto. O que é
102
- * estrutural é o ATRIBUTO: dentro do valor de um `class`/`className`, toda string literal é
103
- * candidata a classe, seja ela argumento de função, ramo de ternário ou pedaço de template.
104
- *
105
- * O QUE ISTO NÃO ALCANÇA, e está declarado com o número: a tabela de variantes que mora numa const
106
- * separada - `cva(...)` e `tv(...)`, medidos em 08/09 como **4 + 41 e 0 + 85** nas duas árvores. Ali
107
- * as classes chegam ao atributo por VARIÁVEL, e cobri-las exige seguir a variável até a declaração:
108
- * é outra frente, e o erro dela continua caindo para o lado de cobrar.
109
- */
110
- function attributeExpressions(source) {
111
- const out = [];
112
- const ATTR = /class(?:Name)?=\{/g;
113
- for (const m of source.matchAll(ATTR)) {
114
- let depth = 0;
115
- let i = (m.index ?? 0) + m[0].length - 1;
116
- const start = i;
117
- /** Aspas contam para NÃO deixar uma chave dentro de string fechar a expressão cedo. */
118
- let quote = null;
119
- for (; i < source.length; i += 1) {
120
- const c = source[i];
121
- if (quote) {
122
- if (c === "\\")
123
- i += 1;
124
- else if (c === quote)
125
- quote = null;
126
- continue;
127
- }
128
- if (c === '"' || c === "'" || c === "`")
129
- quote = c;
130
- else if (c === "{")
131
- depth += 1;
132
- else if (c === "}") {
133
- depth -= 1;
134
- if (depth === 0)
135
- break;
136
- }
137
- }
138
- if (depth === 0)
139
- out.push(source.slice(start + 1, i));
140
- }
141
- return out;
142
- }
143
- /**
144
- * A DECLARAÇÃO DE UMA CONST QUE O ATRIBUTO USA - o salto que faltava, e a mesma regra do atributo.
145
- *
146
- * O QUE FICAVA DE FORA, e estava declarado no contrato com o número: a tabela de variantes mora
147
- * numa const separada e chega ao atributo por VARIÁVEL. O caso real, medido no `frontend-hub`:
148
- *
149
- * ```
150
- * const buttonVariants = cva("… rounded-[var(--radius-md)] …", { variants: { … } });
151
- * …
152
- * className={twUtils.cn(buttonVariants({ variant, size, className }))}
153
- * ```
154
- *
155
- * As classes estão na declaração, não no atributo - então o leitor de 08/09, que já colhia toda
156
- * string DENTRO da expressão, via `buttonVariants(...)` e não via nenhuma classe. Medido nas duas
157
- * árvores: **4 + 41** declarações `cva(` e **0 + 85** `tv(`.
158
- *
159
- * A REGRA CONTINUA SENDO A MESMA, um salto adiante: o atributo diz quais identificadores importam,
160
- * e a declaração daqueles identificadores é lida no mesmo arquivo. Nada aqui conhece `cva`, `tv` ou
161
- * `cn` - o próximo cliente escreve a tabela dele num objeto pelado, e cai na mesma regra.
162
- *
163
- * E O ERRO CONTINUA CAINDO PARA O LADO SEGURO: uma const que não é classe - um rótulo, uma
164
- * mensagem - entrega palavras que o `@theme` não declara, então elas somem na peneira final. O
165
- * cabeçalho deste módulo diz que entre errar cobrando e errar calando, cobrar é o erro que a pessoa
166
- * percebe.
167
- */
168
- function declarationsBehind(source, exprs) {
169
- /**
170
- * TODO IDENTIFICADOR DA EXPRESSÃO, e não só os que ela CHAMA.
171
- *
172
- * A primeira versão pegava `nome(`, e o objeto pelado - `pick(byTone, tone)` - passa a tabela
173
- * como ARGUMENTO. O que decide é o atributo mencionar o identificador; se ele é chamado ou
174
- * passado é sintaxe, e sintaxe é o que muda de projeto para projeto.
175
- */
176
- const names = new Set();
177
- for (const expr of exprs)
178
- for (const m of expr.matchAll(/\b([A-Za-z_$][\w$]*)\b/g))
179
- names.add(m[1]);
180
- if (names.size === 0)
181
- return [];
182
- const out = [];
183
- for (const name of names) {
184
- /** `const <nome> = ` - e o valor vai até a linha que fecha no MESMO nível de indentação. */
185
- const at = source.search(new RegExp(`\\b(?:const|let|var)\\s+${name}\\s*=`));
186
- if (at < 0)
187
- continue;
188
- let depth = 0;
189
- let i = source.indexOf("=", at);
190
- let quote = null;
191
- let started = false;
192
- for (; i < source.length; i += 1) {
193
- const c = source[i];
194
- if (quote) {
195
- if (c === "\\")
196
- i += 1;
197
- else if (c === quote)
198
- quote = null;
199
- continue;
200
- }
201
- if (c === '"' || c === "'" || c === "`")
202
- quote = c;
203
- else if (c === "(" || c === "{" || c === "[") {
204
- depth += 1;
205
- started = true;
206
- }
207
- else if (c === ")" || c === "}" || c === "]") {
208
- depth -= 1;
209
- if (started && depth <= 0)
210
- break;
211
- }
212
- else if (c === ";" && !started)
213
- break;
214
- }
215
- out.push(source.slice(at, i + 1));
216
- }
217
- return out;
218
- }
219
- /** As classes escritas no código - `hover:animate-shimmer` conta como `animate-shimmer`. */
220
- function classesIn(source) {
221
- const out = new Set();
222
- const collect = (body) => {
223
- for (const raw of body.split(/\s+/)) {
224
- if (!raw)
225
- continue;
226
- const bare = raw.split(":").pop() ?? raw;
227
- out.add(bare.replace(/^[!-]/, ""));
228
- }
229
- };
230
- for (const m of source.matchAll(/class(?:Name)?=(?:"([^"]*)"|'([^']*)'|\{`([^`]*)`\}|\{"([^"]*)"\})/g))
231
- collect(m[1] ?? m[2] ?? m[3] ?? m[4] ?? "");
232
- /**
233
- * E TODA STRING DENTRO DA EXPRESSÃO - ver `attributeExpressions`. As três formas de escrever uma
234
- * string em JS/TS, porque quem escolhe a aspa é o formatador dele, não a gente.
235
- */
236
- const exprs = attributeExpressions(source);
237
- const strings = (text) => {
238
- for (const lit of text.matchAll(/"([^"]*)"|'([^']*)'|`([^`]*)`/g))
239
- collect(lit[1] ?? lit[2] ?? lit[3] ?? "");
240
- };
241
- for (const expr of exprs)
242
- strings(expr);
243
- /** E a declaração de quem o atributo chama - ver `declarationsBehind`. */
244
- for (const decl of declarationsBehind(source, exprs))
245
- strings(decl);
246
- return out;
247
- }
248
- /**
249
- * O QUE, NESTE TEXTO, SÓ A FOLHA RESOLVE.
250
- *
251
- * Puro de propósito: quem varre o disco é quem chama, e um spec precisa poder afirmar a regra sem
252
- * um diretório temporário.
253
- */
254
- export function whatOnlyTheSheetResolves(input) {
255
- const variables = [
256
- ...new Set([...input.source.matchAll(/var\(\s*(--ds-[a-zA-Z0-9_-]+)/g)].map((m) => m[1])),
257
- ];
258
- /**
259
- * `--animate-shimmer` declarado por ele -> o nome `shimmer` sai da conta.
260
- *
261
- * E O QUE O PRÓPRIO TAILWIND DECLARA sai junto, pela mesma razão e pela lista que já existe:
262
- * `animate-pulse` existe em qualquer projeto que importe o Tailwind, e acusá-la de exigir a nossa
263
- * folha é o mesmo aviso falso que `TAILWIND_THEME_VARS` foi escrito para acabar. Medido no
264
- * `codelevel` (07/09): sem esta linha, `animate-pulse` era a ÚNICA coisa sustentando a cobrança
265
- * do import naquele repositório.
266
- */
267
- const theirs = new Set();
268
- for (const name of [...TAILWIND_THEME_VARS, ...(input.theirNames ?? [])]) {
269
- const m = /^--[a-z]+-(.+)$/.exec(name.toLowerCase());
270
- if (m)
271
- theirs.add(m[1]);
272
- }
273
- const names = themeNames(input.themeCss).filter((n) => !theirs.has(n));
274
- const written = classesIn(input.source);
275
- const classes = [
276
- ...new Set([...written].filter((c) => names.some((n) => c === n || c.endsWith(`-${n}`)))),
277
- ];
278
- /** As classes que a folha declara e que ESTE texto veste - ver `recipeClasses`. */
279
- const declared = new Set(classesTheSheetDeclares(input.sheetCss));
280
- const recipeClasses = [
281
- ...new Set([...written].filter((c) => declared.has(c))),
282
- ];
283
- return {
284
- variables,
285
- classes,
286
- recipeClasses,
287
- needed: variables.length > 0 || classes.length > 0 || recipeClasses.length > 0,
288
- };
289
- }
290
- /** A folha compilada da versão instalada - as receitas moram nela, não no `theme.css`. */
291
- export async function installedTokensCss(root, slug) {
292
- return installedSheet(root, slug, "tokens.css");
293
- }
294
- /** O `theme.css` da versão instalada - a folha da raiz é um re-export de uma linha. */
295
- export async function installedThemeCss(root, slug) {
296
- return installedSheet(root, slug, "theme.css");
297
- }
298
- /**
299
- * UM ARQUIVO DA VERSÃO PINADA - o `.lock` diz qual é, e a folha da raiz é um re-export de uma linha.
300
- *
301
- * Uma leitura só para as duas folhas: duas cópias da mesma decisão de versão é a forma de uma delas
302
- * ficar atrás no dia em que o formato do `.lock` mudar.
303
- */
304
- async function installedSheet(root, slug, filename) {
305
- const dir = join(root, "_synthesisui", "ds", slug);
306
- const lock = await readFile(join(dir, ".lock"), "utf8").catch(() => null);
307
- let version = 0;
308
- if (lock) {
309
- try {
310
- version = JSON.parse(lock).version ?? 0;
311
- }
312
- catch {
313
- version = 0;
314
- }
315
- }
316
- return readFile(join(dir, `v${version}`, filename), "utf8").catch(() => "");
317
- }
@@ -1,13 +0,0 @@
1
- /**
2
- * O STUB DA SKILL - o corpo é SERVIDO, não embarcado (R1, dono 16/08).
3
- *
4
- * Mesma doutrina das outras quatro: o frontmatter fica inteiro aqui, porque é a `description` que
5
- * faz a skill ser invocada e ela não é segredo; a esteira mora em `apps/web/src/lib/ds/playbooks.ts`
6
- * e chega pelo catalogue autenticado.
7
- *
8
- * O QUE ELA RESOLVE: um sistema instalado e sincronizado pode não mudar um pixel na tela dele. Até
9
- * uma folha de estilo importar os tokens e um elemento carregar `data-ds`, nada chega ao navegador -
10
- * e essa era a única parte da instalação que ninguém escrevia, só imprimia.
11
- */
12
- export const CONFIGURE_SKILL_PATH = ".claude/skills/sui-configure-ds/SKILL.md";
13
- export const CONFIGURE_SKILL = '---\nname: sui-configure-ds\ndescription: Set up an installed design system in the app so its tokens actually reach the browser - the stylesheet import, the data-ds scope, and the type. Use when a system is installed and the app does not look themed, when `doctor` reports "installed - but this project does not load it yet", or when somebody asks to set up / connect / configure the design system in their app ("configura o design system aqui", "why aren\'t the tokens working", "/sui-configure-ds"). Asks the doctor first and does nothing when the three checks already pass.\n---\n\n# Setting the system up in the app - served live\n\nThis playbook is served from the platform, not shipped in this file - it is\nalways current, and your context only carries the step you are on.\n\n1. Call the `playbook` tool on the `synthesisui` MCP server with\n { "skill": "configure" } - you get the framing and a table of contents.\n2. Fetch ONLY the chapter for the step you are on:\n { "skill": "configure", "section": "<id from the toc>" }. Never fetch more\n than the current step needs.\n3. Follow it exactly. When the step is done, fetch the next chapter.\n\nStart by running `npx synthesisui doctor`: the three lines it prints are what\ndecide the work, and three ticks means there is nothing to do.\n\nIf the tool answers that you are not signed in, run `npx synthesisui login`\nin the terminal and call it again. If the `synthesisui` MCP server is not\navailable at all, run `npx synthesisui connect`, restart the session, and\ninvoke this skill again.\n';
@@ -1,212 +0,0 @@
1
- import { readFile } from "node:fs/promises";
2
- import { join } from "node:path";
3
- import { walk } from "./commands/doctor.js";
4
- import { FAMILY_SEAM_PREFIX } from "./fonts.js";
5
- import { detectAppDirs, sheetChainOf } from "./global-sheet.js";
6
- /**
7
- * A FIAÇÃO DESTE PROJETO, MEDIDA - e a mesma medição para todos os comandos que a citam.
8
- *
9
- * O QUE O CLIENTE VIA (medido em 08/09, nos dois apps do ato 2B): ele colou os dois `@import` no
10
- * `globals.css`, pôs `data-ds="codelevel"` no `<body>`, mapeou as quatro famílias - e o
11
- * `component` terminou imprimindo **One-time setup**, com os passos 1 e 2 para colar de novo. O
12
- * comando media "este arquivo PRECISA da folha?" (`sheet-needed.ts`) e nunca "a folha JÁ está
13
- * aqui?".
14
- *
15
- * As duas perguntas são diferentes e as duas importam: a primeira decide se o setup é necessário, e
16
- * a segunda decide se ele ainda está PENDENTE. Cobrar o que já foi feito gasta a confiança de quem
17
- * fez - ele fica sem saber se errou algo, e o texto que deveria orientar passa a ser ruído.
18
- *
19
- * POR QUE ESTE MÓDULO EXISTE, em vez de o `component` medir por conta: esta leitura já existia
20
- * dentro do `doctor`, com o histórico inteiro dela. Uma segunda medição da mesma coisa é uma
21
- * segunda medição livre de discordar da primeira - dois comandos dizendo coisas opostas sobre o
22
- * mesmo repositório é exatamente o defeito que este PR conserta.
23
- */
24
- /**
25
- * Wiring is a property of the PROJECT, never of the scope being read.
26
- *
27
- * This ran inside the scoped walk, so `doctor app/login` read only that folder
28
- * - and the import lives in `app/globals.css`. It concluded "installed, but not
29
- * wired up yet" about a project that was perfectly wired (my-test4, 27/07).
30
- *
31
- * Which would be a mild annoyance, except the managed block now tells the agent
32
- * to run `doctor <the file you just wrote>` after every edit. The false alarm
33
- * fires on every loop, and an agent that believes the system is unwired starts
34
- * repairing wiring that was already correct - editing the one file we most need
35
- * it to leave alone.
36
- *
37
- * The system itself was always resolved from the root. So is this now.
38
- */
39
- /**
40
- * O QUE NOS ESCREVEMOS NAO E' FIACAO DELE - e isto era um FALSO SILENCIO, o pior dos dois.
41
- *
42
- * MEDIDO EM 10/09: o cabecalho que `component-codegen.ts` poe em todo `.tsx` materializado diz,
43
- * em comentario, *"Global setup (once per app): import _synthesisui/ds/<slug>/tokens.css"* e
44
- * *"put data-ds=<slug> on a root element"*. A varredura abaixo procura exatamente essas duas
45
- * strings em QUALQUER arquivo do repositorio - entao, a partir do primeiro componente que nos
46
- * mesmos escrevemos, ela passava a responder `imported: true` e `scoped: true` sobre um projeto
47
- * que nao importa nada e nao tem escopo nenhum.
48
- *
49
- * O QUE O CLIENTE VIVIA: ele traz um componente, a tela diz *"This project already imports
50
- * <slug>'s tokens.css and carries data-ds - nothing to set up"*, ele cola o componente na pagina,
51
- * e ve' um bloco sem cor, sem tipografia e sem sombra. O comando acabou de garantir que estava
52
- * tudo certo. O `doctor` le' pela mesma porta, entao dizia o mesmo.
53
- *
54
- * A instrucao que ESTE arquivo carrega e' uma instrucao, nao um cumprimento dela. Um comentario
55
- * nosso nunca prova nada sobre o projeto dele.
56
- */
57
- const OURS = "Generated by SynthesisUI";
58
- export async function readWiring(root, slug) {
59
- const w = {
60
- imported: false,
61
- /**
62
- * O ADAPTADOR DO TAILWIND, e ele é uma pergunta SEPARADA de `imported`.
63
- *
64
- * O DEFEITO QUE ISTO CONSERTA, apontado na revisão deste PR: `imported` olha só o
65
- * `tokens.css`, e o bloco que consulta esta leitura fala dos UTILITÁRIOS - que só o `@theme`
66
- * do `theme.css` gera. Num projeto com um import e não o outro, o comando nomeava a falta
67
- * (`what needs it: the bg-canvas utility`) e na linha seguinte declarava que não faltava nada.
68
- *
69
- * Um falso silêncio é pior que o falso alarme que este PR veio consertar: o cliente cola o
70
- * componente, vê um bloco sem tipografia e sem sombra, e o comando acabou de dizer que estava
71
- * tudo certo.
72
- */
73
- themed: false,
74
- scoped: false,
75
- /** `init` wrote a fonts file for this project (Next targets only). */
76
- fontsWritten: false,
77
- /** …and the stylesheet actually maps it onto the system's type tokens. */
78
- fontsMapped: false,
79
- /**
80
- * THE FEATURE NOBODY KNEW WE SHIPPED.
81
- *
82
- * Every system compiles a `shadcn.css` mapping shadcn's whole variable
83
- * contract onto its own tokens, so shadcn components wear the system instead
84
- * of shadcn's defaults. It has been generated into every project since it
85
- * was built, and until 28/07 nothing mentioned it: not `add`, not the
86
- * managed block, not the GUIDE except as a filename in a list. The only
87
- * documentation was a comment inside the file, which is the same as none.
88
- *
89
- * The person it mattered most to is the one who asked "I don't get the use
90
- * case, I can just build a design system with shadcn" - and the answer was
91
- * sitting unimported in his own repo.
92
- *
93
- * Reported only when the project HAS shadcn. Telling everybody about a
94
- * bridge they will never use is how a report earns the skim.
95
- */
96
- hasShadcn: false,
97
- bridged: false,
98
- };
99
- if (!slug)
100
- return w;
101
- w.hasShadcn = await readFile(join(root, "components.json"), "utf8").then(() => true, () => false);
102
- for await (const file of walk(root)) {
103
- const src = await readFile(file, "utf8").catch(() => "");
104
- if (!src)
105
- continue;
106
- /** Ver `OURS`: o nosso proprio cabecalho respondia por ele. */
107
- if (src.slice(0, 300).includes(OURS))
108
- continue;
109
- if (src.includes(`_synthesisui/ds/${slug}/tokens.css`))
110
- w.imported = true;
111
- if (src.includes(`_synthesisui/ds/${slug}/theme.css`))
112
- w.themed = true;
113
- if (src.includes(`_synthesisui/ds/${slug}/shadcn.css`))
114
- w.bridged = true;
115
- if (src.includes(`data-ds="${slug}"`))
116
- w.scoped = true;
117
- // The requirement that stayed invisible. `init` writes a fonts file and
118
- // asks for two more edits; an agent told to check only the first two did
119
- // exactly that, stopped, and left the project rendering in the framework's
120
- // default face (my-test2, 27/07). What the checker checks is what gets done.
121
- if (src.includes("--font-ds-") && /next\/font/.test(src))
122
- w.fontsWritten = true;
123
- /** O prefixo vem da const única da costura - um literal aqui divergiria em silêncio (26/08). */
124
- if (new RegExp(`${FAMILY_SEAM_PREFIX}\\w+\\s*:\\s*var\\(\\s*--font-ds-`).test(src))
125
- w.fontsMapped = true;
126
- // The bridge joins the early exit, otherwise the walk can stop before the
127
- // stylesheet that imports it and report a wired project as unbridged.
128
- if (w.imported &&
129
- w.themed &&
130
- w.scoped &&
131
- w.fontsMapped &&
132
- (!w.hasShadcn || w.bridged))
133
- break;
134
- }
135
- return w;
136
- }
137
- /**
138
- * A FIAÇÃO DE CADA APP, e num monorepo é ela que decide - `readWiring` acima não decide.
139
- *
140
- * O QUE O CLIENTE VIA: `readWiring` varre da RAIZ e responde "existe, em algum lugar daqui". Num
141
- * monorepo com dois apps servidos, um fiado e outro não, o fiado respondia pelo outro: a pessoa
142
- * abria o app que não carrega nada e o comando dizia que estava tudo certo. O contrato já declarava
143
- * isso como lacuna (`INV-VOLTA-12`, "a resposta é do PROJETO, e num monorepo ela não decide") e o
144
- * comando dizia o que mediu em vez de afirmar - honesto, e ainda a resposta errada.
145
- *
146
- * COMO A RESPOSTA CERTA É ENCONTRADA, e cada metade vem de onde ela mora:
147
- *
148
- * os `@import` seguindo a CADEIA de folhas a partir da folha daquele app (`sheetChainOf`).
149
- * A folha que carrega os tokens quase nunca é a do app: num monorepo é a do
150
- * pacote compartilhado, e os dois apps chegam nela - então varrer pasta daria
151
- * "não" para os dois, e varrer a raiz dá "sim" para todos
152
- * o `data-ds` na árvore DAQUELE app, porque o escopo mora no layout dele
153
- * as fontes idem - `init` escreve o arquivo de fonte por app
154
- *
155
- * O QUE ESTA FUNÇÃO NÃO GENERALIZA, declarado com o N de hoje (medido em 09/09):
156
- *
157
- * `detectAppDirs` só procura em `apps/` e `packages/` cobre 4 de 4 monorepos medidos;
158
- * `pnpm-workspace.yaml` aparece em 0 de 10
159
- * e só reconhece app pelo `layout` do App Router cobre 7 de 7 raízes de app medidas;
160
- * `vite` aparece só em PACOTE de
161
- * biblioteca, corretamente ignorado
162
- *
163
- * As duas são estreitezas REAIS e as duas têm N=0 de evidência contrária hoje. Generalizá-las sem
164
- * uma população que as exija seria dimensionar pela imaginação - o erro que `INV-GERAL-07` nomeia
165
- * pelo avesso. Quando aparecer um repositório que caia fora, o número dele é que abre a frente.
166
- */
167
- export async function readWiringPerApp(root, slug, pagesDir) {
168
- if (!slug)
169
- return [];
170
- const apps = await detectAppDirs(root, pagesDir);
171
- const out = [];
172
- const hasShadcn = await readFile(join(root, "components.json"), "utf8").then(() => true, () => false);
173
- for (const app of apps) {
174
- const w = {
175
- imported: false,
176
- themed: false,
177
- scoped: false,
178
- fontsWritten: false,
179
- fontsMapped: false,
180
- hasShadcn,
181
- bridged: false,
182
- };
183
- /** Os imports: a cadeia daquele app, e não a pasta dele. */
184
- for (const sheet of await sheetChainOf(root, `${app}/globals.css`)) {
185
- const css = await readFile(join(root, sheet), "utf8").catch(() => "");
186
- if (!css)
187
- continue;
188
- if (css.includes(`_synthesisui/ds/${slug}/tokens.css`))
189
- w.imported = true;
190
- if (css.includes(`_synthesisui/ds/${slug}/theme.css`))
191
- w.themed = true;
192
- if (css.includes(`_synthesisui/ds/${slug}/shadcn.css`))
193
- w.bridged = true;
194
- }
195
- /** O escopo e as fontes: a árvore DAQUELE app - é onde o layout dele mora. */
196
- for await (const file of walk(join(root, app))) {
197
- const src = await readFile(file, "utf8").catch(() => "");
198
- if (!src)
199
- continue;
200
- if (src.includes(`data-ds="${slug}"`))
201
- w.scoped = true;
202
- if (src.includes("--font-ds-") && /next\/font/.test(src))
203
- w.fontsWritten = true;
204
- if (new RegExp(`${FAMILY_SEAM_PREFIX}\\w+\\s*:\\s*var\\(\\s*--font-ds-`).test(src))
205
- w.fontsMapped = true;
206
- if (w.scoped && w.fontsMapped)
207
- break;
208
- }
209
- out.push({ app, wiring: w });
210
- }
211
- return out;
212
- }