synthesisui 0.16.389 → 0.16.394
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.
- package/dist/absorb-plan.js +77 -6
- package/dist/claude-md.js +1 -1
- package/dist/commands/absorb.js +44 -3
- package/dist/commands/add.js +2 -53
- package/dist/commands/doctor.js +100 -19
- package/dist/commands/init.js +3 -3
- package/dist/commands/mcp.js +1 -1
- package/dist/commands/use.js +1 -1
- package/dist/copy/connect.pt-BR.js +1 -1
- package/dist/doctor/scan.js +23 -3
- package/dist/doctor/their-compiled-names.js +121 -0
- package/dist/doctor/their-names.js +39 -4
- package/dist/doctor/tokens.js +37 -1
- package/dist/global-sheet.js +50 -1
- package/dist/install-marks.js +1 -1
- package/dist/setup-prompt.js +95 -0
- package/dist/skill-configure.js +1 -1
- package/dist/skills.js +1 -1
- package/dist/stack.js +30 -2
- package/dist/their-vars.js +38 -7
- package/package.json +1 -1
- package/dist/wiring-prompt.js +0 -34
package/dist/absorb-plan.js
CHANGED
|
@@ -10,7 +10,11 @@ const HOME = {
|
|
|
10
10
|
};
|
|
11
11
|
/** Uma palavra de categoria no começo do nome deles não é família - é a categoria repetida. */
|
|
12
12
|
const CATEGORY = /^(color|colour|radius|rounded|spacing|space|gap|font|type|text|duration|motion|ease|easing)-/;
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* O nome sem o sigilo. `$` e `@` entram porque o vocabulário dele pode estar num pré-processador -
|
|
15
|
+
* um `$gray_dark` que mantivesse o `$` viraria o caminho `color.$gray.dark`, que não é um caminho.
|
|
16
|
+
*/
|
|
17
|
+
const clean = (name) => name.replace(/^(--|[$@])/, "").toLowerCase();
|
|
14
18
|
/**
|
|
15
19
|
* QUAL DOS NOMES DELE, quando mais de um segura o mesmo valor - e aqui a natureza do ACHADO decide.
|
|
16
20
|
*
|
|
@@ -105,7 +109,31 @@ function nearestOwn(theirs, literal, kind) {
|
|
|
105
109
|
* existente com outro valor - isso não é absorver, é repintar.
|
|
106
110
|
*/
|
|
107
111
|
export function absorbPlan(d, theirs, have, cap = 40) {
|
|
108
|
-
|
|
112
|
+
/**
|
|
113
|
+
* A REPETIÇÃO É UM FILTRO DE RUÍDO, E RUÍDO É UMA PROPRIEDADE DA ESCALA - não do valor.
|
|
114
|
+
*
|
|
115
|
+
* Ordenar por repetição está certo onde a deriva se concentra: no `~/projects/web-subscribe` são
|
|
116
|
+
* 858 valores distintos escritos à mão, e mostrar os 40 mais repetidos é o que transforma isso numa
|
|
117
|
+
* fila de trabalho. Num projeto que acabou de nascer, o mesmo corte apaga o projeto inteiro.
|
|
118
|
+
*
|
|
119
|
+
* Medido em 07/09 sobre três populações:
|
|
120
|
+
*
|
|
121
|
+
* ~/projects/web-subscribe 858 distintos 484 repetidos 362 apareciam uma vez só
|
|
122
|
+
* ~/projects/web-onboarding 52 distintos 21 repetidos 29 apareciam uma vez só
|
|
123
|
+
* create-next-app real 4 distintos 0 repetidos 4 apareciam uma vez só
|
|
124
|
+
*
|
|
125
|
+
* Naquele último o `absorb` respondia que não havia nada a absorver, três linhas depois de o
|
|
126
|
+
* `doctor` dizer *"0 of 4 have a name waiting"*.
|
|
127
|
+
*
|
|
128
|
+
* A REGRA SAI DO TETO QUE JÁ EXISTE, e por isso não é mais um número escolhido por nós: o corte
|
|
129
|
+
* serve para ESCOLHER quando não cabe tudo. Quando tudo cabe, não há o que escolher, e esconder
|
|
130
|
+
* metade é esconder por hábito. Num repositório grande nada muda - 484 já estouram o teto sozinhos.
|
|
131
|
+
*/
|
|
132
|
+
const repeated = d.repeats.filter((r) => !r.token);
|
|
133
|
+
const single = d.once.filter((r) => !r.token);
|
|
134
|
+
const unnamed = repeated.length + single.length <= cap
|
|
135
|
+
? [...repeated, ...single]
|
|
136
|
+
: repeated;
|
|
109
137
|
const entries = [];
|
|
110
138
|
for (const r of unnamed) {
|
|
111
139
|
if (entries.length >= cap)
|
|
@@ -133,7 +161,25 @@ export function absorbPlan(d, theirs, have, cap = 40) {
|
|
|
133
161
|
* `var(--dashboard-white-500)` e o `absorb`, no mesmo dia, pedia que ele batizasse `#fff`. Dois
|
|
134
162
|
* comandos com conselhos opostos sobre o mesmo valor.
|
|
135
163
|
*/
|
|
136
|
-
|
|
164
|
+
/**
|
|
165
|
+
* O NOME DELE VEM DAS DUAS FORMAS - e sem a segunda os dois comandos voltam a se contradizer.
|
|
166
|
+
*
|
|
167
|
+
* É o mesmo defeito que esta função já corrigiu uma vez, reaberto por outro caminho: o `doctor`
|
|
168
|
+
* passou a dizer *"#555 · your code calls it $gray_dark"* e o `absorb`, no mesmo repositório,
|
|
169
|
+
* pedia que ele batizasse o `#555`. Dois comandos com conselhos opostos sobre o mesmo valor.
|
|
170
|
+
*
|
|
171
|
+
* A ORDEM É A MESMA DO RESTO DA ESTEIRA: primeiro o nome que o navegador recebe, e só depois o
|
|
172
|
+
* que o pré-processador resolve. Onde ele nomeia nas duas formas, ganha a custom property.
|
|
173
|
+
*
|
|
174
|
+
* E aqui o nome compilado NÃO é um beco: o caminho que sai dele vira um token de verdade na
|
|
175
|
+
* fundação, que o navegador recebe - então as ocorrências em `.tsx`, que nunca poderiam receber
|
|
176
|
+
* um `$`, ficam trocáveis pelo caminho que já existe (`absorb` -> `upgrade` -> `doctor --fix`).
|
|
177
|
+
* Medido em 07/09 no `~/projects/web-subscribe`: 323 ocorrências têm nome dele, e só 52 delas
|
|
178
|
+
* estão num arquivo que poderia escrever o `$` diretamente.
|
|
179
|
+
*/
|
|
180
|
+
const value = normalizeValue(r.literal, theirs.rootPx);
|
|
181
|
+
const theirName = nameOf(r.kind, theirs.byValue.get(value)) ??
|
|
182
|
+
nameOf(r.kind, theirs.compiledAway.get(value)?.map((c) => c.name));
|
|
137
183
|
const path = pathFor(r.kind, theirName);
|
|
138
184
|
/** "≈ perto de um token seu" só interessa a quem não tem um EXATO - com o nome na mão, a dica
|
|
139
185
|
* vira ruído, e no arquivo de proposta ela vira uma segunda opção que não é opção. */
|
|
@@ -151,7 +197,11 @@ export function absorbPlan(d, theirs, have, cap = 40) {
|
|
|
151
197
|
...(near ? { near } : {}),
|
|
152
198
|
});
|
|
153
199
|
}
|
|
154
|
-
return {
|
|
200
|
+
return {
|
|
201
|
+
entries,
|
|
202
|
+
more: Math.max(0, unnamed.length - entries.length),
|
|
203
|
+
unnamed: d.repeats.length + d.once.length,
|
|
204
|
+
};
|
|
155
205
|
}
|
|
156
206
|
/** Quantas linhas da proposta ainda precisam de um nome humano. */
|
|
157
207
|
export const needingName = (plan) => plan.entries.filter((e) => !e.path);
|
|
@@ -166,9 +216,21 @@ export function describeAbsorb(plan) {
|
|
|
166
216
|
const ready = plan.entries.filter((e) => e.path);
|
|
167
217
|
const pending = needingName(plan);
|
|
168
218
|
const lines = [];
|
|
219
|
+
/**
|
|
220
|
+
* VAZIO POR DOIS MOTIVOS OPOSTOS, e a frase afirmava só um.
|
|
221
|
+
*
|
|
222
|
+
* "every repeated value already has a name" é notícia boa. Mas a lista também fica vazia quando não
|
|
223
|
+
* há valor NENHUM escrito à mão - e aí a frase de cima descreve um repositório que não é o dele.
|
|
224
|
+
* Medido em 07/09 numa cópia de `create-next-app`: 4 valores sem nome, e esta era a resposta.
|
|
225
|
+
*
|
|
226
|
+
* A distinção não é cosmética: um manda seguir em frente, o outro diz que o comando não tem
|
|
227
|
+
* trabalho porque o projeto não tem deriva. Duas conversas diferentes.
|
|
228
|
+
*/
|
|
169
229
|
if (plan.entries.length === 0)
|
|
170
230
|
return [
|
|
171
|
-
|
|
231
|
+
plan.unnamed > 0
|
|
232
|
+
? `Nothing to absorb here: the ${plan.unnamed} value${plan.unnamed === 1 ? "" : "s"} written by hand ${plan.unnamed === 1 ? "already has" : "already have"} a name in your system.`
|
|
233
|
+
: "Nothing to absorb: no design value in this project is written by hand.",
|
|
172
234
|
];
|
|
173
235
|
/**
|
|
174
236
|
* A SEÇÃO DOS QUE JÁ TÊM NOME SÓ EXISTE SE ALGUM TIVER - e no dia 1 nenhum tem.
|
|
@@ -187,9 +249,18 @@ export function describeAbsorb(plan) {
|
|
|
187
249
|
const close = pending.filter((e) => e.near);
|
|
188
250
|
if (ready.length > 0)
|
|
189
251
|
lines.push("");
|
|
252
|
+
/**
|
|
253
|
+
* "REPEATED" SÓ QUANDO ELES SE REPETEM - e desde que o corte por repetição deixou de valer para
|
|
254
|
+
* projeto pequeno, essa palavra passou a descrever um repositório que não é o dele.
|
|
255
|
+
*
|
|
256
|
+
* Medido em 07/09 numa cópia de `create-next-app`: a linha dizia *"4 repeated values"* sobre
|
|
257
|
+
* quatro valores que aparecem uma vez cada. Uma palavra errada na primeira linha do comando gasta
|
|
258
|
+
* a credibilidade das outras dez.
|
|
259
|
+
*/
|
|
260
|
+
const everyOneRepeats = pending.every((e) => e.count > 1);
|
|
190
261
|
lines.push(ready.length > 0
|
|
191
262
|
? `${pending.length} nobody names yet. Naming is a design decision, so those wait for a word from you:`
|
|
192
|
-
: `${pending.length} repeated values, and none of them has a name yet. Naming is a design decision, so each waits for a word from you:`);
|
|
263
|
+
: `${pending.length} ${everyOneRepeats ? "repeated values" : "values written by hand"}, and none of them has a name yet. Naming is a design decision, so each waits for a word from you:`);
|
|
193
264
|
for (const e of pending.slice(0, 8))
|
|
194
265
|
lines.push(` ${e.value.padEnd(24)} ${`<${e.kind}, ${e.files} file${e.files === 1 ? "" : "s"}>`.padEnd(28)} ${e.near ? `≈ your ${e.near.name} (${e.near.away})` : 'path: ""'}`);
|
|
195
266
|
if (pending.length > 8)
|
package/dist/claude-md.js
CHANGED
|
@@ -329,7 +329,7 @@ async function renderRegion(projectRoot, installed) {
|
|
|
329
329
|
START,
|
|
330
330
|
"## Design system",
|
|
331
331
|
"",
|
|
332
|
-
"This project
|
|
332
|
+
"This project has the synthesisui agent set up and **no design system yet** - so there is",
|
|
333
333
|
"no token vocabulary to follow, and nothing here to obey.",
|
|
334
334
|
"",
|
|
335
335
|
"To turn this repository into one, run `/sui-import-ds` and I will read what is",
|
package/dist/commands/absorb.js
CHANGED
|
@@ -3,8 +3,10 @@ import { dirname, join, resolve } from "node:path";
|
|
|
3
3
|
import { absorbPlan, describeAbsorb, needingName, } from "../absorb-plan.js";
|
|
4
4
|
import { readProjectConfig, readToken, resolveRegistry } from "../config.js";
|
|
5
5
|
import { diagnose, scanSource } from "../doctor/scan.js";
|
|
6
|
+
import { withTheirNames } from "../doctor/their-names.js";
|
|
6
7
|
import { measuredScope, scopePaths } from "../measured-scope.js";
|
|
7
8
|
import { body, paint, section, snippet } from "../output.js";
|
|
9
|
+
import { resolveDeps, tailwindMajor } from "../stack.js";
|
|
8
10
|
import { loadSystem, walkAll } from "./doctor.js";
|
|
9
11
|
/**
|
|
10
12
|
* `synthesisui absorb` - O SISTEMA APRENDE O DESIGN QUE O CÓDIGO JÁ TEM.
|
|
@@ -66,12 +68,28 @@ export async function absorb(opts) {
|
|
|
66
68
|
* ideia primeiro e ficou com a implementação de todo mundo.
|
|
67
69
|
*/
|
|
68
70
|
const theirs = installed.theirs;
|
|
71
|
+
/**
|
|
72
|
+
* SEM SISTEMA NOSSO, A TABELA DA VARREDURA É A DELE - a mesma queda que o `doctor` já faz.
|
|
73
|
+
*
|
|
74
|
+
* O QUE ACONTECIA: este comando varria com `installed.table`, que sem sistema instalado é a tabela
|
|
75
|
+
* VAZIA. Então nada tinha nome, nem o que o próprio código dele nomeia - e a linha
|
|
76
|
+
* `--background: #ffffff`, que é onde o token dele NASCE, entrava na proposta como um valor solto
|
|
77
|
+
* a ser batizado. Medido em 07/09 numa cópia de `create-next-app`: 4 das 8 entradas eram as
|
|
78
|
+
* declarações dos tokens dele, e duas delas já traziam `theirName` preenchido - a proposta pedindo
|
|
79
|
+
* um nome para o que ela mesma acabara de dizer que já tem um.
|
|
80
|
+
*
|
|
81
|
+
* O `doctor` resolve isto há tempos, e a linha é literalmente a dele: quando não há nada nosso, o
|
|
82
|
+
* vocabulário dele é o único que existe, e é contra ele que a medição acontece.
|
|
83
|
+
*/
|
|
84
|
+
const table = installed.table.byName.size > 0
|
|
85
|
+
? installed.table
|
|
86
|
+
: withTheirNames(theirs, theirs);
|
|
69
87
|
const reports = [];
|
|
70
88
|
for await (const file of walkAll(roots)) {
|
|
71
89
|
const source = await readFile(file, "utf8").catch(() => null);
|
|
72
90
|
if (source === null)
|
|
73
91
|
continue;
|
|
74
|
-
reports.push(scanSource(file.slice(root.length + 1), source,
|
|
92
|
+
reports.push(scanSource(file.slice(root.length + 1), source, table));
|
|
75
93
|
}
|
|
76
94
|
const plan = absorbPlan(diagnose(reports), theirs, pathsInSystem(installed.documents), opts.cap ?? 40);
|
|
77
95
|
console.log(section(hasSystem
|
|
@@ -173,13 +191,36 @@ async function sendProposal(root, path, registry) {
|
|
|
173
191
|
console.log(body(paint.dim(proposal.slug
|
|
174
192
|
? "Your config says absorbed values belong in your CSS, so nothing was sent."
|
|
175
193
|
: "You have no system yet, so these belong in your own CSS - nothing was sent, and nothing needed an account.")));
|
|
194
|
+
/**
|
|
195
|
+
* NUM TAILWIND v4, `:root` DECLARA A VARIÁVEL E NÃO GERA A CLASSE.
|
|
196
|
+
*
|
|
197
|
+
* O QUE ELE VIVIA: nomeava `#383838` de `color.ink.700`, colava o bloco que este comando
|
|
198
|
+
* imprime, e `text-ink-700` continuava não existindo. A variável estava lá, o doctor a
|
|
199
|
+
* reconhecia - e o código dele não tinha como usá-la. A metade visível da promessa não chegava.
|
|
200
|
+
*
|
|
201
|
+
* É a mesma lei que a `INV-VOLTA-13` já enuncia do outro lado da esteira: *"no Tailwind v4 só
|
|
202
|
+
* `@theme` gera utilitário"*. A folha que a plataforma instala sabe disso desde 23/08; o bloco
|
|
203
|
+
* que a plataforma manda ELE colar não sabia.
|
|
204
|
+
*
|
|
205
|
+
* E A SINTAXE SAI DO PROJETO, nunca de um padrão nosso - `tailwindMajor` já existe e o
|
|
206
|
+
* comentário dele diz por quê: *"gerar a sintaxe de uma para um projeto da outra produz um
|
|
207
|
+
* arquivo que o build DELE não entende, e é o pior tipo de saída, porque parece certa até
|
|
208
|
+
* alguém compilar"*. Sem Tailwind, ou na v3, `:root` continua sendo a resposta certa: lá o
|
|
209
|
+
* utilitário nasce do `tailwind.config`, não do CSS.
|
|
210
|
+
*/
|
|
211
|
+
const major = tailwindMajor(await resolveDeps(root));
|
|
212
|
+
const block = major === 4 ? "@theme" : ":root";
|
|
176
213
|
console.log("");
|
|
177
|
-
console.log(
|
|
214
|
+
console.log(` ${block} {`);
|
|
178
215
|
for (const e of ready)
|
|
179
216
|
console.log(` --${e.path.replace(/\./g, "-")}: ${e.value};`);
|
|
180
217
|
console.log(" }");
|
|
181
218
|
console.log("");
|
|
182
|
-
console.log(body(
|
|
219
|
+
console.log(body(major === 4
|
|
220
|
+
? "In `@theme` and not `:root`: on Tailwind v4 only `@theme` generates the utility, so this is what makes `text-ink-700` exist in your code."
|
|
221
|
+
: "The next import or `sync` reads these back as tokens you declared, and from then on the doctor holds your code to them by your own names."));
|
|
222
|
+
if (major === 4)
|
|
223
|
+
console.log(body("The next import or `sync` reads these back as tokens you declared, and from then on the doctor holds your code to them by your own names."));
|
|
183
224
|
return;
|
|
184
225
|
}
|
|
185
226
|
const token = await readToken();
|
package/dist/commands/add.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { access, mkdir,
|
|
1
|
+
import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { syncClaudeMd } from "../claude-md.js";
|
|
4
4
|
import { readProjectConfig, readToken, resolveRegistry } from "../config.js";
|
|
5
5
|
import { customFontFamilies, googleFontsHref, nextFontSnippet, } from "../fonts.js";
|
|
6
|
-
import { globalSheetOf, prefixFrom } from "../global-sheet.js";
|
|
6
|
+
import { detectAppDirs, globalSheetOf, prefixFrom } from "../global-sheet.js";
|
|
7
7
|
import { lockReference } from "../group-role.js";
|
|
8
8
|
import { buildGuide } from "../guide.js";
|
|
9
9
|
import { censusScope } from "../measured-scope.js";
|
|
@@ -131,57 +131,6 @@ async function readRootLock(path) {
|
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
const exists = (path) => access(path).then(() => true, () => false);
|
|
134
|
-
/**
|
|
135
|
-
* ONDE AS ROTAS DELE MORAM DE VERDADE - e num monorepo elas não moram na raiz.
|
|
136
|
-
*
|
|
137
|
-
* Olhava só `app/` e `src/app/` na raiz. O `codelevel-monorepo` tem `apps/web/app` e
|
|
138
|
-
* `apps/landing/app`, então a busca voltava null, o `appDir` caía no default `"app"`, e TODO caminho
|
|
139
|
-
* impresso no setup apontava para uma pasta que não existe: `app/globals.css`, `app/layout.tsx`,
|
|
140
|
-
* `app/fonts.ts`. O cliente lê três instruções e nenhuma serve para o repositório dele.
|
|
141
|
-
*
|
|
142
|
-
* O que faz uma pasta ser raiz de app é o `layout` do App Router morar dentro dela - por isso o
|
|
143
|
-
* `apps/api` do Nest, que também é um workspace, não entra na lista.
|
|
144
|
-
*
|
|
145
|
-
* DEVOLVE TODAS, em ordem determinística: o setup é "once per app" e num monorepo isso é literal.
|
|
146
|
-
* Escolher uma calada seria acertar uma e errar a outra sem dizer qual.
|
|
147
|
-
*/
|
|
148
|
-
export async function detectAppDirs(root, pagesDir) {
|
|
149
|
-
const isAppRoot = async (dir) => {
|
|
150
|
-
if (!(await exists(join(root, dir))))
|
|
151
|
-
return false;
|
|
152
|
-
for (const ext of ["tsx", "jsx", "ts", "js"])
|
|
153
|
-
if (await exists(join(root, dir, `layout.${ext}`)))
|
|
154
|
-
return true;
|
|
155
|
-
return false;
|
|
156
|
-
};
|
|
157
|
-
const here = [pagesDir, `src/${pagesDir}`];
|
|
158
|
-
const nested = [];
|
|
159
|
-
for (const group of ["apps", "packages"]) {
|
|
160
|
-
let entries;
|
|
161
|
-
try {
|
|
162
|
-
entries = await readdir(join(root, group));
|
|
163
|
-
}
|
|
164
|
-
catch {
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
for (const entry of entries.sort())
|
|
168
|
-
nested.push(`${group}/${entry}/${pagesDir}`, `${group}/${entry}/src/${pagesDir}`);
|
|
169
|
-
}
|
|
170
|
-
const found = [];
|
|
171
|
-
for (const dir of [...here, ...nested])
|
|
172
|
-
if (await isAppRoot(dir))
|
|
173
|
-
found.push(dir);
|
|
174
|
-
/**
|
|
175
|
-
* A RAIZ QUE EXISTE MAS NÃO TEM LAYOUT ainda é o melhor palpite de um app avulso - é o caso de um
|
|
176
|
-
* `create-next-app` no meio de uma migração. Sem isto, um projeto de app único perderia a detecção
|
|
177
|
-
* que já funcionava.
|
|
178
|
-
*/
|
|
179
|
-
if (found.length === 0)
|
|
180
|
-
for (const dir of here)
|
|
181
|
-
if (await exists(join(root, dir)))
|
|
182
|
-
return [dir];
|
|
183
|
-
return found;
|
|
184
|
-
}
|
|
185
134
|
/**
|
|
186
135
|
* Materializes a published DS into `_synthesisui/ds/<slug>/v<version>/`, points
|
|
187
136
|
* stable root re-exports (tokens.css/theme.css) and a `.lock` at it, and updates
|
package/dist/commands/doctor.js
CHANGED
|
@@ -17,12 +17,13 @@ import { findSelfConflicts, forbiddenProps, isReset, propMatchesLabel, } from ".
|
|
|
17
17
|
import { withTheirNames } from "../doctor/their-names.js";
|
|
18
18
|
import { buildTable, EMPTY_TABLE, nearestToken, } from "../doctor/tokens.js";
|
|
19
19
|
import { FAMILY_SEAM_PREFIX } from "../fonts.js";
|
|
20
|
+
import { detectAppDirs, globalSheetOf, prefixFrom } from "../global-sheet.js";
|
|
20
21
|
import { groupRole } from "../group-role.js";
|
|
21
22
|
import { actingSlug, describeScope, measuredScope, scopePaths, } from "../measured-scope.js";
|
|
22
23
|
import { body, paint, section, snippet } from "../output.js";
|
|
24
|
+
import { setupPrompt } from "../setup-prompt.js";
|
|
23
25
|
import { resolveDeps } from "../stack.js";
|
|
24
26
|
import { danglingTheirVars } from "../their-vars.js";
|
|
25
|
-
import { wiringPrompt } from "../wiring-prompt.js";
|
|
26
27
|
/**
|
|
27
28
|
* `synthesisui doctor` - the check nobody else ships.
|
|
28
29
|
*
|
|
@@ -399,14 +400,20 @@ async function harvestOwnTokens(roots,
|
|
|
399
400
|
* e `4px` deixaria de casar com `0.25rem` de um lado só. */
|
|
400
401
|
measured) {
|
|
401
402
|
let css = "";
|
|
403
|
+
/** UMA A UMA, e não só concatenadas: a regra de escopo de `their-compiled-names.ts` decide pelo
|
|
404
|
+
* `@import` de um arquivo no outro, e a concatenação apaga de qual arquivo cada linha veio. */
|
|
405
|
+
const sheets = new Map();
|
|
402
406
|
for await (const file of walkAll(roots)) {
|
|
403
407
|
if (!/\.(css|scss|sass|less)$/i.test(file))
|
|
404
408
|
continue;
|
|
405
|
-
|
|
409
|
+
const source = await readFile(file, "utf8").catch(() => "");
|
|
410
|
+
sheets.set(file, source);
|
|
411
|
+
css += `\n${source}`;
|
|
406
412
|
}
|
|
407
413
|
return buildTable({
|
|
408
414
|
css,
|
|
409
415
|
source: "yours",
|
|
416
|
+
sheets,
|
|
410
417
|
rootPx: measured?.px,
|
|
411
418
|
rootFrom: measured?.from ?? null,
|
|
412
419
|
});
|
|
@@ -463,10 +470,27 @@ function verdict(d, hasSystem, overruled, conflicts) {
|
|
|
463
470
|
*/
|
|
464
471
|
if (!hasSystem) {
|
|
465
472
|
const distinct = new Set(d.findings.map((f) => f.literal.toLowerCase()));
|
|
473
|
+
/**
|
|
474
|
+
* O QUE O CÓDIGO DELE JÁ NOMEIA - e sem esta linha o relatório se contradiz.
|
|
475
|
+
*
|
|
476
|
+
* "none of them has a name yet" é o fecho de um projeto sem sistema, e ele era verdade enquanto
|
|
477
|
+
* a plataforma só lia custom properties. Agora o detalhe imprime `#555 · your code calls it
|
|
478
|
+
* $gray_dark` três linhas acima: o resumo dizendo que nada tem nome, sobre um valor que a linha
|
|
479
|
+
* de cima acabou de nomear, é o defeito que faz alguém desconfiar do relatório inteiro.
|
|
480
|
+
*
|
|
481
|
+
* E o número muda a decisão dele: quem já batizou metade dos valores no Sass não está no dia
|
|
482
|
+
* zero - ele está a um `absorb` de trazer esses nomes para um vocabulário que o navegador
|
|
483
|
+
* recebe, o que é uma conversa diferente de "escolha o sistema de alguém".
|
|
484
|
+
*/
|
|
485
|
+
const spoken = new Set(d.findings
|
|
486
|
+
.filter((f) => f.theirCompiledName)
|
|
487
|
+
.map((f) => f.literal.toLowerCase()));
|
|
466
488
|
return [
|
|
467
489
|
...head,
|
|
468
490
|
body(`${distinct.size} distinct design values are written by hand here.`),
|
|
469
|
-
|
|
491
|
+
spoken.size === 0
|
|
492
|
+
? body("No design system is installed, so none of them has a name yet.")
|
|
493
|
+
: body(`No design system is installed, but your own code already names ${spoken.size} of them - in Sass or Less, which the browser never receives.`),
|
|
470
494
|
"",
|
|
471
495
|
body("Name them yourself - this reads them and asks you for the words:"),
|
|
472
496
|
snippet(["npx synthesisui@latest absorb"]),
|
|
@@ -698,7 +722,9 @@ export async function doctor(opts) {
|
|
|
698
722
|
* grupo está migrando para ela - e antes disto a referência existia na
|
|
699
723
|
* plataforma e o comando que ordena a lista nunca ficava sabendo.
|
|
700
724
|
*/
|
|
701
|
-
|
|
725
|
+
/** Lida uma vez: o `intent` ordena a lista e o setup abaixo deriva a folha e o nº de imports. */
|
|
726
|
+
const config = await readProjectConfig(root);
|
|
727
|
+
const intent = intentOf(config, opts.intent, groupRole(installed.groupLocks, installed.table.slug));
|
|
702
728
|
const { recipes, documents } = installed;
|
|
703
729
|
/**
|
|
704
730
|
* A TABELA JÁ VEM COM O VOCABULÁRIO DELE DENTRO - ver `loadSystem` e `their-names.ts`.
|
|
@@ -961,7 +987,7 @@ export async function doctor(opts) {
|
|
|
961
987
|
(!wiring.imported || !wiring.scoped || fontsPending);
|
|
962
988
|
if (unwired) {
|
|
963
989
|
console.log("");
|
|
964
|
-
console.log(body(`${table.name ?? table.slug} is installed - but not
|
|
990
|
+
console.log(body(`${table.name ?? table.slug} is installed - but this project does not load it yet.`));
|
|
965
991
|
console.log("");
|
|
966
992
|
console.log(body(wiring.imported
|
|
967
993
|
? ` ✓ some stylesheet imports _synthesisui/ds/${table.slug}/tokens.css`
|
|
@@ -1005,19 +1031,50 @@ export async function doctor(opts) {
|
|
|
1005
1031
|
* aqui já importou, materializou e rodou upgrade: mandá-lo ao `init` é devolver ao começo
|
|
1006
1032
|
* alguém que está a um passo do fim.
|
|
1007
1033
|
*
|
|
1008
|
-
* E O PASSO JÁ EXISTIA PRONTO. `
|
|
1034
|
+
* E O PASSO JÁ EXISTIA PRONTO. `setupPrompt` mora em `setup-prompt.ts`, exportado, e era
|
|
1009
1035
|
* usado só pelo `init`. Imprimi-lo aqui não é texto novo: é a MESMA fonte chegando na tela
|
|
1010
1036
|
* onde a pergunta nasce. Duas redações do mesmo passo divergiriam no primeiro conserto.
|
|
1011
1037
|
*
|
|
1012
|
-
* O PROMPT
|
|
1013
|
-
*
|
|
1014
|
-
*
|
|
1038
|
+
* O PROMPT LEVA A MEDIÇÃO, e até 07/09 ele era FIXO - o defeito que o dono leu na própria tela.
|
|
1039
|
+
*
|
|
1040
|
+
* As três linhas acima acabaram de imprimir `✗ ✓ ✓`: só o import faltava. O texto seguinte
|
|
1041
|
+
* mandava *"Do the ONE-TIME SETUP it names, all of it"* e listava as três, então um agente
|
|
1042
|
+
* obediente reescreve o escopo e o mapa de tipografia que já estavam corretos - contra a última
|
|
1043
|
+
* linha do próprio texto, que pede para não mexer nos estilos dele. Um comando que mede e depois
|
|
1044
|
+
* diz outra coisa gasta a medição.
|
|
1045
|
+
*
|
|
1046
|
+
* E A FOLHA TEM NOME, ao contrário do que este comentário dizia antes. `globalSheetOf` +
|
|
1047
|
+
* `prefixFrom` são as mesmas funções que o `add` usa para achar a folha que os apps REALMENTE
|
|
1048
|
+
* carregam - num monorepo não é o `globals.css` do app - e para contar o caminho relativo até a
|
|
1049
|
+
* raiz. Elas existiam e este comando não as chamava; então ele descrevia o arquivo em vez de
|
|
1050
|
+
* nomeá-lo, e o agente tinha que redescobrir o que a gente já sabia.
|
|
1051
|
+
*
|
|
1052
|
+
* Quando a derivação falha, `sheet` fica ausente e o texto volta a descrever - nenhum caminho é
|
|
1053
|
+
* inventado.
|
|
1015
1054
|
*/
|
|
1016
1055
|
if (table.slug) {
|
|
1056
|
+
const appDirs = await detectAppDirs(root, config.pagesDir);
|
|
1057
|
+
const sheet = appDirs[0]
|
|
1058
|
+
? await globalSheetOf(root, `${appDirs[0]}/globals.css`)
|
|
1059
|
+
: null;
|
|
1017
1060
|
console.log("");
|
|
1018
|
-
console.log(body("Paste this into your agent - it does the
|
|
1061
|
+
console.log(body("Paste this into your agent - it does the setup for you:"));
|
|
1019
1062
|
console.log("");
|
|
1020
|
-
console.log(snippet(
|
|
1063
|
+
console.log(snippet(setupPrompt(table.slug, {
|
|
1064
|
+
tokens: !wiring.imported,
|
|
1065
|
+
scope: !wiring.scoped,
|
|
1066
|
+
/** Só é passo quando o projeto TEM o arquivo de fontes - ver `readWiring`. */
|
|
1067
|
+
type: wiring.fontsWritten && !wiring.fontsMapped,
|
|
1068
|
+
...(sheet
|
|
1069
|
+
? { sheet: { path: sheet, prefix: prefixFrom(sheet) } }
|
|
1070
|
+
: {}),
|
|
1071
|
+
/**
|
|
1072
|
+
* DUAS LINHAS COM TAILWIND, UMA SEM - a mesma decisão que o `add` já toma pelo projeto.
|
|
1073
|
+
* Dizer "as duas linhas" a um projeto que precisa de uma manda o agente procurar o que
|
|
1074
|
+
* não existe.
|
|
1075
|
+
*/
|
|
1076
|
+
imports: config.styles === "css" ? 1 : 2,
|
|
1077
|
+
}).split("\n")));
|
|
1021
1078
|
}
|
|
1022
1079
|
}
|
|
1023
1080
|
/**
|
|
@@ -1500,9 +1557,19 @@ export async function doctor(opts) {
|
|
|
1500
1557
|
? `→ no ${x.kind} named for it · the value lives as ${x.token}`
|
|
1501
1558
|
: nameToWrite(x)
|
|
1502
1559
|
? `→ ${nameToWrite(x)}${alsoNamed(x)}`
|
|
1503
|
-
:
|
|
1504
|
-
|
|
1505
|
-
|
|
1560
|
+
: /**
|
|
1561
|
+
* O CÓDIGO DELE JÁ NOMEIA ISTO, e a linha dizia que nada nomeava.
|
|
1562
|
+
*
|
|
1563
|
+
* Vem antes de `near` porque um nome EXATO que ele escreveu vale mais que o degrau
|
|
1564
|
+
* mais próximo de outra coisa. Sem seta de troca: `$gray_dark` não existe num `.tsx`
|
|
1565
|
+
* e a troca segura depende de duas provas que esta camada não tem - ver
|
|
1566
|
+
* `TheirName.writable`.
|
|
1567
|
+
*/
|
|
1568
|
+
x.theirCompiledName
|
|
1569
|
+
? `· your code calls it ${x.theirCompiledName}`
|
|
1570
|
+
: near
|
|
1571
|
+
? `→ nearest is ${near.theirs ?? near.name} (${near.value})`
|
|
1572
|
+
: "→ no token holds this value yet";
|
|
1506
1573
|
say(` ${String(x.line).padStart(4)} ${x.literal} ${named}`);
|
|
1507
1574
|
}
|
|
1508
1575
|
if (f.findings.length > shown.length) {
|
|
@@ -1857,11 +1924,25 @@ export async function doctor(opts) {
|
|
|
1857
1924
|
* que NENHUM dos dois nomeia o valor. É isso que a linha passa a dizer, e é o que separa esta
|
|
1858
1925
|
* fila da anterior: aquela tem nome esperando, esta precisa de um.
|
|
1859
1926
|
*/
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1927
|
+
/**
|
|
1928
|
+
* O SEU CÓDIGO JÁ NOMEIA ISTO, e a linha dizia o contrário.
|
|
1929
|
+
*
|
|
1930
|
+
* "no name for it, here or in your CSS" é uma afirmação sobre o repositório DELE, e ela era
|
|
1931
|
+
* falsa sempre que o nome estava num pré-processador: medido em 07/09 no `web-subscribe`,
|
|
1932
|
+
* `#555` é `$gray_dark` em `css/colors.scss` e aparecia como "sem nome" em 27 arquivos.
|
|
1933
|
+
*
|
|
1934
|
+
* A linha DIZ o nome e não convida a trocar - `$gray_dark` não existe num `.tsx`, e a troca
|
|
1935
|
+
* segura depende de o arquivo de destino compilar aquele pré-processador e importar aquela
|
|
1936
|
+
* folha, que é o próximo passo desta frente. Dizer o que existe já muda a decisão dele; trocar
|
|
1937
|
+
* sem essas duas provas quebraria o build.
|
|
1938
|
+
*/
|
|
1939
|
+
what: r.theirCompiledName
|
|
1940
|
+
? `${r.literal} - your code calls it ${r.theirCompiledName} (no fix: it never reaches the browser)`
|
|
1941
|
+
: near
|
|
1942
|
+
? `${r.literal} - name it, or snap to ${near.theirs ?? near.name}`
|
|
1943
|
+
: r.crossFamily
|
|
1944
|
+
? `${r.literal} - no ${r.kind} named for it, and the value lives as ${r.token} in another family`
|
|
1945
|
+
: `${r.literal} - no name for it, here or in your CSS`,
|
|
1865
1946
|
size: `${r.files} file${r.files === 1 ? "" : "s"}`,
|
|
1866
1947
|
cheap: false,
|
|
1867
1948
|
});
|
package/dist/commands/init.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { DEFAULT_CONFIG, writeProjectConfig } from "../config.js";
|
|
2
2
|
import { body, section, snippet } from "../output.js";
|
|
3
|
+
import { setupPrompt } from "../setup-prompt.js";
|
|
3
4
|
import { resolveDeps, stylesFor, tailwindMajor } from "../stack.js";
|
|
4
5
|
import { formMix, measuredStyle } from "../styles-flavour.js";
|
|
5
|
-
import { wiringPrompt } from "../wiring-prompt.js";
|
|
6
6
|
import { add } from "./add.js";
|
|
7
7
|
/**
|
|
8
8
|
* Bootstraps a project for SynthesisUI: writes `_synthesisui/config.json`
|
|
@@ -141,9 +141,9 @@ export async function init(opts) {
|
|
|
141
141
|
console.log("");
|
|
142
142
|
console.log(snippet(["npx synthesisui@latest connect"]));
|
|
143
143
|
console.log("");
|
|
144
|
-
console.log(body("2. Open your agent and paste this once - it does the
|
|
144
|
+
console.log(body("2. Open your agent and paste this once - it does the setup for you:"));
|
|
145
145
|
console.log("");
|
|
146
|
-
console.log(snippet(
|
|
146
|
+
console.log(snippet(setupPrompt(opts.ds).split("\n")));
|
|
147
147
|
console.log("");
|
|
148
148
|
console.log(body("Then ask it for something real. The check introduces itself on the first"));
|
|
149
149
|
console.log(body("clean file and goes quiet after that."));
|
package/dist/commands/mcp.js
CHANGED
|
@@ -1286,7 +1286,7 @@ async function describeComponent(root, name) {
|
|
|
1286
1286
|
const r = recipe.runtime;
|
|
1287
1287
|
const lines = [];
|
|
1288
1288
|
if (r.stateOwner === "caller") {
|
|
1289
|
-
lines.push("State: the CALLER owns it - controlled only.
|
|
1289
|
+
lines.push("State: the CALLER owns it - controlled only. Pass the pair below; do not add internal state.");
|
|
1290
1290
|
}
|
|
1291
1291
|
else if (r.stateOwner === "self") {
|
|
1292
1292
|
lines.push("State: the component manages itself. Do not wire external state unless you need to read it.");
|
package/dist/commands/use.js
CHANGED
|
@@ -111,7 +111,7 @@ export async function use(slug, intent, opts) {
|
|
|
111
111
|
config.target === "next"
|
|
112
112
|
? '- Make sure `tokens.css` + `theme.css` are imported in the global CSS (see the GUIDE\'s "How to apply").'
|
|
113
113
|
: '- Make sure `tokens.css` is imported in the global CSS (see the GUIDE\'s "How to apply").',
|
|
114
|
-
"-
|
|
114
|
+
"- Implement the behavior yourself (open/close, focus, routing) - the system ships the looks, not the JS.",
|
|
115
115
|
"",
|
|
116
116
|
"Composition (make it look composed, not just correct):",
|
|
117
117
|
config.target === "next"
|
|
@@ -104,5 +104,5 @@ register("pt-BR", {
|
|
|
104
104
|
"the import, orchestrated": "o import, orquestrado",
|
|
105
105
|
"one component against the system": "um componente contra o sistema",
|
|
106
106
|
"build what the system does not have": "construir o que o sistema não tem",
|
|
107
|
-
"the tokens,
|
|
107
|
+
"the tokens, loaded by your app": "os tokens, carregados pelo seu app",
|
|
108
108
|
});
|
package/dist/doctor/scan.js
CHANGED
|
@@ -540,7 +540,18 @@ function scanCore(file, source, table) {
|
|
|
540
540
|
...(/(?<!r)em$/i.test(literal.trim())
|
|
541
541
|
? { fontRelative: true }
|
|
542
542
|
: {}),
|
|
543
|
-
...(theirs ? { theirToken: theirs.name } : {}),
|
|
543
|
+
...(theirs?.writable ? { theirToken: theirs.name } : {}),
|
|
544
|
+
/**
|
|
545
|
+
* O NOME QUE SÓ SE DIZ - `$gray_dark` do Sass, `@brand` do Less.
|
|
546
|
+
*
|
|
547
|
+
* Separado de `theirToken` porque aquele campo é o que o `--fix` ESCREVE, e um nome de
|
|
548
|
+
* pré-processador escrito num `.tsx` é código quebrado. Aqui ele serve ao relatório, que
|
|
549
|
+
* deixa de afirmar "no name for it, here or in your CSS" sobre um valor que o código dele
|
|
550
|
+
* nomeia - a afirmação era sobre o repositório DELE, e era falsa.
|
|
551
|
+
*/
|
|
552
|
+
...(theirs && !theirs.writable
|
|
553
|
+
? { theirCompiledName: theirs.name }
|
|
554
|
+
: {}),
|
|
544
555
|
...(theirs?.also.length ? { theirAlso: theirs.also } : {}),
|
|
545
556
|
/**
|
|
546
557
|
* A COINCIDÊNCIA VIAJA COM O ACHADO - ver `tokenMatch`.
|
|
@@ -737,6 +748,9 @@ export function diagnose(files) {
|
|
|
737
748
|
kind: f.kind,
|
|
738
749
|
token: f.token,
|
|
739
750
|
...(f.theirToken ? { theirToken: f.theirToken } : {}),
|
|
751
|
+
...(f.theirCompiledName
|
|
752
|
+
? { theirCompiledName: f.theirCompiledName }
|
|
753
|
+
: {}),
|
|
740
754
|
...(f.theirAlso?.length ? { theirAlso: f.theirAlso } : {}),
|
|
741
755
|
...(f.fontRelative ? { fontRelative: true } : {}),
|
|
742
756
|
count: 1,
|
|
@@ -745,25 +759,31 @@ export function diagnose(files) {
|
|
|
745
759
|
});
|
|
746
760
|
}
|
|
747
761
|
}
|
|
748
|
-
const
|
|
762
|
+
const everyLiteral = [...byLiteral.entries()]
|
|
749
763
|
.map(([key, v]) => ({
|
|
750
764
|
literal: key.slice(key.indexOf(":") + 1),
|
|
751
765
|
kind: v.kind,
|
|
752
766
|
token: v.token,
|
|
753
767
|
...(v.theirToken ? { theirToken: v.theirToken } : {}),
|
|
768
|
+
...(v.theirCompiledName
|
|
769
|
+
? { theirCompiledName: v.theirCompiledName }
|
|
770
|
+
: {}),
|
|
754
771
|
...(v.theirAlso?.length ? { theirAlso: v.theirAlso } : {}),
|
|
755
772
|
...(v.fontRelative ? { fontRelative: true } : {}),
|
|
756
773
|
count: v.count,
|
|
757
774
|
files: v.files.size,
|
|
758
775
|
...(v.crossFamily ? { crossFamily: true } : {}),
|
|
759
776
|
}))
|
|
760
|
-
.filter((r) => r.count > 1)
|
|
761
777
|
.sort((a, b) => b.count - a.count);
|
|
778
|
+
const repeats = everyLiteral.filter((r) => r.count > 1);
|
|
779
|
+
/** Ver `Diagnosis.once` - o que o corte por repetição deixa de fora. */
|
|
780
|
+
const once = everyLiteral.filter((r) => r.count === 1);
|
|
762
781
|
return {
|
|
763
782
|
// A file with a phantom name and no drift has nothing to say by the old
|
|
764
783
|
// measure and the worst thing to say by the new one. Both keep it.
|
|
765
784
|
files: files.filter((f) => f.findings.length > 0 || (f.phantoms?.length ?? 0) > 0),
|
|
766
785
|
findings: flat,
|
|
786
|
+
once,
|
|
767
787
|
counts,
|
|
768
788
|
/**
|
|
769
789
|
* QUANTOS JÁ TÊM NOME NESTE REPOSITÓRIO - e o dele conta.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { dirname, join, resolve } from "node:path";
|
|
2
|
+
/**
|
|
3
|
+
* OS NOMES DELE QUE SOMEM NA COMPILAÇÃO - e que a plataforma afirmava não existir.
|
|
4
|
+
*
|
|
5
|
+
* O QUE O CLIENTE VIA. Um projeto que declara `$gray_dark: #555` em `css/colors.scss` e escreve
|
|
6
|
+
* `#555` em dezenas de arquivos recebia, do nosso relatório: *"#555 - no name for it, here or in
|
|
7
|
+
* your CSS"*. A frase é uma afirmação sobre o repositório DELE, e ela era falsa. Medido em 07/09 no
|
|
8
|
+
* `~/projects/web-subscribe`: `doctor` dizia `13 tokens found` num projeto com 27 nomes de cor
|
|
9
|
+
* declarados e partilhados, e 323 ocorrências ficavam sem o nome que o próprio código já lhes dá.
|
|
10
|
+
*
|
|
11
|
+
* POR QUE A PLATAFORMA NÃO OS VIA, e não era descuido: a colheita lê custom properties (`--x`), que
|
|
12
|
+
* é o que o navegador recebe. Uma variável de pré-processador - `$x` no Sass, `@x` no Less - resolve
|
|
13
|
+
* em tempo de compilação e não chega ao CSS final. Ela é vocabulário dele para LER, nunca endereço
|
|
14
|
+
* para APONTAR: escrever `var($gray_dark)` não existe.
|
|
15
|
+
*
|
|
16
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
17
|
+
* A REGRA QUE SEPARA A FEATURE DO DEFEITO: ESCOPO.
|
|
18
|
+
*
|
|
19
|
+
* Uma custom property em `:root` é global por construção - é isso que a torna vocabulário. Um `$x` no
|
|
20
|
+
* topo de um arquivo é uma constante DAQUELE arquivo, e tratar as duas como a mesma coisa inventa
|
|
21
|
+
* vocabulário em vez de ler o que existe.
|
|
22
|
+
*
|
|
23
|
+
* Medido no mesmo repositório, e a diferença é a feature inteira:
|
|
24
|
+
*
|
|
25
|
+
* 323 ocorrências ganham o nome certo ($ declarado em folha que outra folha importa)
|
|
26
|
+
* 1085 ganhariam um nome FALSO ($color e $primary-color declarados dentro de um
|
|
27
|
+
* styles.module.scss de um componente - `$color: #FFF`
|
|
28
|
+
* existe em dois módulos diferentes, e `#fff` aparece
|
|
29
|
+
* 596 vezes no projeto inteiro)
|
|
30
|
+
*
|
|
31
|
+
* Então só conta o que é PARTILHADO: um arquivo que outro arquivo importa foi escrito para ser
|
|
32
|
+
* vocabulário comum, e quem decide isso é o `@import`/`@use`/`@forward` dele - não uma convenção de
|
|
33
|
+
* nome de arquivo nossa, que seria a forma de fixar o hábito de um repositório dentro do produto.
|
|
34
|
+
*/
|
|
35
|
+
/** As extensões cujo vocabulário some antes do navegador. */
|
|
36
|
+
const PREPROCESSED = /\.(scss|sass|less)$/i;
|
|
37
|
+
/** `$nome: valor;` no Sass, `@nome: valor;` no Less - no topo do arquivo, fora de qualquer bloco. */
|
|
38
|
+
const DECLARATION = /^[ \t]*([$@][a-zA-Z0-9_-]+)[ \t]*:[ \t]*([^;{}]+);/gm;
|
|
39
|
+
/**
|
|
40
|
+
* `!default` É A MARCA DA PRÓPRIA LINGUAGEM PARA "ISTO É O PADRÃO DE UMA BIBLIOTECA".
|
|
41
|
+
*
|
|
42
|
+
* Em Sass ele significa literalmente *este valor vale se quem me usa não definiu outro* - a forma
|
|
43
|
+
* como um pacote publica vocabulário configurável. Um valor que ELE decidiu não carrega `!default`,
|
|
44
|
+
* e é essa diferença que separa a decisão dele do default de um pacote que alguém copiou para
|
|
45
|
+
* dentro do repositório.
|
|
46
|
+
*
|
|
47
|
+
* Medido em 07/09 no `~/projects/web-subscribe`, que tem `bootstrap-sass` e `ionicons` copiados para
|
|
48
|
+
* `css/`: dos 1185 nomes que a varredura encontra, 1125 são desses dois pacotes e TODOS trazem
|
|
49
|
+
* `!default`. Sem esta porta, o relatório responderia que o `#555` dele "se chama
|
|
50
|
+
* `$navbar-default-link-active-color`" - o vocabulário de uma biblioteca apresentado como o dele,
|
|
51
|
+
* que é o oposto exato do que este leitor existe para fazer.
|
|
52
|
+
*
|
|
53
|
+
* O LADO SEGURO DO ERRO, declarado: um projeto que use `!default` no PRÓPRIO vocabulário perde esses
|
|
54
|
+
* nomes e volta a ver "no name for it". Deixar de nomear o que existe custa uma linha de relatório;
|
|
55
|
+
* batizar o valor dele com a palavra de um pacote custa a confiança na próxima linha.
|
|
56
|
+
*/
|
|
57
|
+
const LIBRARY_DEFAULT = /!\s*default\b/i;
|
|
58
|
+
/** `@import "colors"`, `@use "./colors" as c`, `@forward "colors"`. */
|
|
59
|
+
const REFERENCE = /@(?:import|use|forward)\s+["']([^"']+)["']/g;
|
|
60
|
+
/**
|
|
61
|
+
* QUAIS FOLHAS OUTRA FOLHA IMPORTA - a prova de que aquele arquivo é vocabulário comum.
|
|
62
|
+
*
|
|
63
|
+
* A resolução segue o que o Sass faz: a extensão é opcional e o parcial pode ter o `_` na frente.
|
|
64
|
+
* Um especificador que não aponta para nenhum arquivo do projeto é um pacote (`@import "bootstrap"`),
|
|
65
|
+
* e pacote não é vocabulário dele.
|
|
66
|
+
*/
|
|
67
|
+
export function sharedSheets(sheets) {
|
|
68
|
+
const known = new Set(sheets.keys());
|
|
69
|
+
const shared = new Set();
|
|
70
|
+
for (const [path, source] of sheets) {
|
|
71
|
+
const dir = dirname(path);
|
|
72
|
+
for (const [, spec] of source.matchAll(REFERENCE)) {
|
|
73
|
+
const bare = spec.replace(/\.(scss|sass|less|css)$/i, "");
|
|
74
|
+
const base = bare.split("/").pop() ?? bare;
|
|
75
|
+
const parent = bare.slice(0, bare.length - base.length);
|
|
76
|
+
for (const candidate of [
|
|
77
|
+
`${bare}.scss`,
|
|
78
|
+
`${bare}.sass`,
|
|
79
|
+
`${bare}.less`,
|
|
80
|
+
`${bare}.css`,
|
|
81
|
+
join(parent, `_${base}.scss`),
|
|
82
|
+
join(parent, `_${base}.sass`),
|
|
83
|
+
join(parent, `_${base}.less`),
|
|
84
|
+
]) {
|
|
85
|
+
const abs = resolve(dir, candidate);
|
|
86
|
+
if (known.has(abs)) {
|
|
87
|
+
shared.add(abs);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return shared;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* O VOCABULÁRIO PARTILHADO QUE O NAVEGADOR NUNCA VÊ.
|
|
97
|
+
*
|
|
98
|
+
* A primeira declaração vence, a mesma regra que a colheita de custom properties usa: uma folha
|
|
99
|
+
* posterior sobrescrevendo uma anterior é cascata, e este leitor não vê cascata.
|
|
100
|
+
*/
|
|
101
|
+
export function compiledNames(sheets) {
|
|
102
|
+
const shared = sharedSheets(sheets);
|
|
103
|
+
const out = [];
|
|
104
|
+
const seen = new Set();
|
|
105
|
+
for (const [path, source] of sheets) {
|
|
106
|
+
if (!PREPROCESSED.test(path) || !shared.has(path))
|
|
107
|
+
continue;
|
|
108
|
+
for (const [, name, value] of source.matchAll(DECLARATION)) {
|
|
109
|
+
if (seen.has(name) || LIBRARY_DEFAULT.test(value))
|
|
110
|
+
continue;
|
|
111
|
+
seen.add(name);
|
|
112
|
+
/** `!global` diz onde a atribuição vale, não o que ela vale - sai do valor e não da lista. */
|
|
113
|
+
out.push({
|
|
114
|
+
name,
|
|
115
|
+
value: value.replace(/!\s*global\b/i, "").trim(),
|
|
116
|
+
file: path,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
@@ -47,7 +47,12 @@ import { FAMILY_PREFIX, familySays, formDecides, normalizeValue, UNAMBIGUOUS, }
|
|
|
47
47
|
* que o próprio css dele nomeia `--dashboard-font-family`. É pouco, e é o único grupo que sobrou do
|
|
48
48
|
* item que eu tinha nomeado - os `em` saíram na leva anterior.
|
|
49
49
|
*/
|
|
50
|
-
|
|
50
|
+
/**
|
|
51
|
+
* O nome sem o sigilo, em pedaços. `$` e `@` entram porque o vocabulário dele pode estar num
|
|
52
|
+
* pré-processador (ver `their-compiled-names.ts`), e um `$gray-dark` que não perdesse o `$` nunca
|
|
53
|
+
* casaria segmento nenhum no desempate - o critério existiria e nunca alcançaria esses nomes.
|
|
54
|
+
*/
|
|
55
|
+
const segments = (name) => name.replace(/^(--|[$@])/, "").split("-");
|
|
51
56
|
/**
|
|
52
57
|
* QUAL DOS NOMES DELE, quando mais de um segura o mesmo valor - e são 18 no repo real.
|
|
53
58
|
*
|
|
@@ -132,13 +137,20 @@ roles) {
|
|
|
132
137
|
* sistema do dono, `#f59e0b` é `--color-tier-gold` E `--color-feedback-warning` - dois conceitos
|
|
133
138
|
* dele, gamificação e estado -, e o relatório dizia `→ {color.tier-gold}` como se fosse fato.
|
|
134
139
|
*/
|
|
135
|
-
function escolha(candidates, ours, convention, roles
|
|
140
|
+
function escolha(candidates, ours, convention, roles,
|
|
141
|
+
/** `false` para nome de pré-processador - ver `TheirName.writable`. */
|
|
142
|
+
writable = true) {
|
|
136
143
|
const name = pick(candidates, ours, convention, roles);
|
|
137
|
-
return { name, also: candidates.filter((c) => c !== name) };
|
|
144
|
+
return { name, also: candidates.filter((c) => c !== name), writable };
|
|
138
145
|
}
|
|
139
146
|
export function theirNames(ours, theirs) {
|
|
140
147
|
const out = new Map();
|
|
141
|
-
|
|
148
|
+
/**
|
|
149
|
+
* AS DUAS FONTES, e não só a primeira: um projeto que declara o vocabulário INTEIRO em Sass tem
|
|
150
|
+
* `byName` vazio e mesmo assim tem nomes para dizer. Sair aqui devolvia mapa vazio para ele, e o
|
|
151
|
+
* relatório voltava a afirmar que o repositório não nomeia um valor que o repositório nomeia.
|
|
152
|
+
*/
|
|
153
|
+
if (theirs.byName.size === 0 && theirs.compiledAway.size === 0)
|
|
142
154
|
return out;
|
|
143
155
|
/**
|
|
144
156
|
* OS PAPÉIS QUE ESTE SISTEMA DECLARA, derivados dos NOSSOS nomes - ver o critério 2 de `pick`.
|
|
@@ -205,6 +217,29 @@ export function theirNames(ours, theirs) {
|
|
|
205
217
|
out.set(key, escolha(candidates, null, convention, roles));
|
|
206
218
|
}
|
|
207
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* POR ÚLTIMO, O QUE SÓ SE DIZ - e ser o último é a regra, não a ordem do arquivo.
|
|
222
|
+
*
|
|
223
|
+
* Onde ele nomeia o mesmo valor nas duas formas, quem ganha é a custom property: ela resolve no
|
|
224
|
+
* navegador, o `--fix` pode escrevê-la e ela sobrevive a qualquer mudança de pré-processador. Só
|
|
225
|
+
* quando NENHUM nome que o navegador recebe segura aquele valor é que o nome compilado entra - e
|
|
226
|
+
* entra marcado como não escrevível.
|
|
227
|
+
*
|
|
228
|
+
* A mesma porta da forma vale aqui: `UNAMBIGUOUS` deixa passar o que a FORMA já classifica (cor,
|
|
229
|
+
* duração, pilha de fontes) e deixa o comprimento cru de fora. Sem isso, um `$mobile_gutter: 10px`
|
|
230
|
+
* seria oferecido como o nome de todo `10px` do projeto - medido em 07/09 no `web-subscribe`, onde
|
|
231
|
+
* `10px` aparece em 102 arquivos e quase nenhum é um gutter.
|
|
232
|
+
*/
|
|
233
|
+
for (const [value, names] of theirs.compiledAway) {
|
|
234
|
+
for (const [kind, form] of Object.entries(UNAMBIGUOUS)) {
|
|
235
|
+
if (!form.test(value))
|
|
236
|
+
continue;
|
|
237
|
+
const key = `${kind}:${value}`;
|
|
238
|
+
if (out.has(key))
|
|
239
|
+
continue;
|
|
240
|
+
out.set(key, escolha(names.map((n) => n.name), null, convention, roles, false));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
208
243
|
return out;
|
|
209
244
|
}
|
|
210
245
|
/**
|
package/dist/doctor/tokens.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { DEFAULT_ROOT_PX } from "./root-size.js";
|
|
15
15
|
import { parseSchemeBlocks } from "./scheme-blocks.js";
|
|
16
|
+
import { compiledNames } from "./their-compiled-names.js";
|
|
16
17
|
export const EMPTY_TABLE = {
|
|
17
18
|
source: null,
|
|
18
19
|
name: null,
|
|
@@ -24,6 +25,7 @@ export const EMPTY_TABLE = {
|
|
|
24
25
|
rootFrom: null,
|
|
25
26
|
aliases: new Map(),
|
|
26
27
|
declared: new Set(),
|
|
28
|
+
compiledAway: new Map(),
|
|
27
29
|
keyframes: new Set(),
|
|
28
30
|
};
|
|
29
31
|
const hex2 = (n) => Math.max(0, Math.min(255, Math.round(n)))
|
|
@@ -318,6 +320,17 @@ export function buildTable(input) {
|
|
|
318
320
|
const byName = source === "installed"
|
|
319
321
|
? parseTokens(input.css)
|
|
320
322
|
: parseRootTokens(input.css);
|
|
323
|
+
/**
|
|
324
|
+
* O VOCABULÁRIO DELE QUE NÃO CHEGA AO NAVEGADOR, somado ao que chega.
|
|
325
|
+
*
|
|
326
|
+
* Entra DEPOIS das custom properties e sem sobrescrever nenhuma: onde as duas formas nomeiam o
|
|
327
|
+
* mesmo valor, o nome que o navegador recebe é o que resolve, e é ele que a folha pode apontar.
|
|
328
|
+
*/
|
|
329
|
+
const compiled = source === "installed" || !input.sheets ? [] : compiledNames(input.sheets);
|
|
330
|
+
const compiledAway = new Map();
|
|
331
|
+
for (const one of compiled)
|
|
332
|
+
if (!byName.has(one.name))
|
|
333
|
+
compiledAway.set(one.name, one);
|
|
321
334
|
/**
|
|
322
335
|
* Built from the RESOLVED map, not from `byName`. A semantic role that
|
|
323
336
|
* aliases a primitive has to be findable by the primitive's value, or the
|
|
@@ -337,6 +350,24 @@ export function buildTable(input) {
|
|
|
337
350
|
*/
|
|
338
351
|
const raw = source === "installed" ? parseTokensWithAliases(input.css) : new Map();
|
|
339
352
|
const isAlias = (name) => /^var\(/.test((raw.get(name) ?? "").trim());
|
|
353
|
+
/**
|
|
354
|
+
* OS NOMES QUE SÓ SE DIZEM, indexados pelo MESMO valor normalizado das outras tabelas.
|
|
355
|
+
*
|
|
356
|
+
* Fora de `byName` e de `byValue` de propósito: aquelas duas respondem "o meu sistema nomeia
|
|
357
|
+
* isto?", que é a pergunta da cobertura e do `--fix`. Um nome que o navegador nunca recebe não
|
|
358
|
+
* pode entrar em nenhuma das duas - inflaria a contagem de tokens dele (medido em 07/09 no
|
|
359
|
+
* `web-subscribe`: 13 viraria 1198, e 1125 dos acrescentados eram bootstrap e ionicons copiados
|
|
360
|
+
* para dentro do repositório) e ofereceria ao `--fix` um nome que quebra um `.tsx`.
|
|
361
|
+
*/
|
|
362
|
+
const byCompiledValue = new Map();
|
|
363
|
+
for (const one of compiledAway.values()) {
|
|
364
|
+
const key = normalizeValue(one.value, rootPx);
|
|
365
|
+
const list = byCompiledValue.get(key);
|
|
366
|
+
if (list)
|
|
367
|
+
list.push(one);
|
|
368
|
+
else
|
|
369
|
+
byCompiledValue.set(key, [one]);
|
|
370
|
+
}
|
|
340
371
|
const byValue = new Map();
|
|
341
372
|
for (const [name, value] of resolved) {
|
|
342
373
|
const key = normalizeValue(value, rootPx);
|
|
@@ -360,6 +391,7 @@ export function buildTable(input) {
|
|
|
360
391
|
/** Vazio até `withTheirNames` ler o vocabulário dele - ver `their-names.ts`. */
|
|
361
392
|
aliases: new Map(),
|
|
362
393
|
declared: parseDeclaredNames(input.css),
|
|
394
|
+
compiledAway: byCompiledValue,
|
|
363
395
|
keyframes: new Set([...input.css.matchAll(/@keyframes\s+([a-zA-Z0-9_-]+)/g)].map((m) => m[1])),
|
|
364
396
|
};
|
|
365
397
|
}
|
|
@@ -418,7 +450,11 @@ kind) {
|
|
|
418
450
|
const theirs = kind
|
|
419
451
|
? table.aliases.get(`${kind}:${normalizeValue(best.value, table.rootPx)}`)
|
|
420
452
|
: undefined;
|
|
421
|
-
|
|
453
|
+
/**
|
|
454
|
+
* SÓ UM NOME ESCREVÍVEL - esta linha convida a trocar (`snap to X`), e um nome de pré-processador
|
|
455
|
+
* não pode ir para o código dele. Ver `TheirName.writable`.
|
|
456
|
+
*/
|
|
457
|
+
return theirs?.writable ? { ...best, theirs: theirs.name } : best;
|
|
422
458
|
}
|
|
423
459
|
/**
|
|
424
460
|
* The family a token belongs to, from the drift it was found in.
|
package/dist/global-sheet.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join, posix, relative, resolve } from "node:path";
|
|
3
|
+
import { exists } from "./agent-wiring.js";
|
|
3
4
|
/**
|
|
4
5
|
* A FOLHA GLOBAL QUE OS APPS DELE REALMENTE CARREGAM - e num monorepo ela quase nunca é a do app.
|
|
5
6
|
*
|
|
@@ -101,3 +102,51 @@ export function prefixFrom(sheet) {
|
|
|
101
102
|
const up = relative(dirname(resolve("/r", sheet)), "/r");
|
|
102
103
|
return up === "" ? "./" : `${up.split(/[\\/]/).join("/")}/`;
|
|
103
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* A RAIZ DE CADA APP DESTE REPOSITÓRIO - onde o escopo e a tipografia entram, um por app.
|
|
107
|
+
*
|
|
108
|
+
* MORA AQUI E NÃO NO `add` (07/09): o `doctor` precisa desta resposta para nomear a folha no texto
|
|
109
|
+
* que ele entrega, e importá-la de `commands/add.ts` puxou a rede para dentro de um comando local -
|
|
110
|
+
* `INV-COLETA-05` reprovou, e com razão. A pergunta *"quais pastas deste repositório são app?"* é
|
|
111
|
+
* leitura de disco e não tem nada a ver com publicar nem buscar: ela pertence ao módulo da folha.
|
|
112
|
+
*
|
|
113
|
+
* O que faz uma pasta ser app é o `layout` do App Router morar dentro dela. Um workspace sem
|
|
114
|
+
* `layout` (uma API, um pacote de config) não é app e não recebe escopo.
|
|
115
|
+
*/
|
|
116
|
+
export async function detectAppDirs(root, pagesDir) {
|
|
117
|
+
const isAppRoot = async (dir) => {
|
|
118
|
+
if (!(await exists(join(root, dir))))
|
|
119
|
+
return false;
|
|
120
|
+
for (const ext of ["tsx", "jsx", "ts", "js"])
|
|
121
|
+
if (await exists(join(root, dir, `layout.${ext}`)))
|
|
122
|
+
return true;
|
|
123
|
+
return false;
|
|
124
|
+
};
|
|
125
|
+
const here = [pagesDir, `src/${pagesDir}`];
|
|
126
|
+
const nested = [];
|
|
127
|
+
for (const group of ["apps", "packages"]) {
|
|
128
|
+
let entries;
|
|
129
|
+
try {
|
|
130
|
+
entries = await readdir(join(root, group));
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
for (const entry of entries.sort())
|
|
136
|
+
nested.push(`${group}/${entry}/${pagesDir}`, `${group}/${entry}/src/${pagesDir}`);
|
|
137
|
+
}
|
|
138
|
+
const found = [];
|
|
139
|
+
for (const dir of [...here, ...nested])
|
|
140
|
+
if (await isAppRoot(dir))
|
|
141
|
+
found.push(dir);
|
|
142
|
+
/**
|
|
143
|
+
* A RAIZ QUE EXISTE MAS NÃO TEM LAYOUT ainda é o melhor palpite de um app avulso - é o caso de um
|
|
144
|
+
* `create-next-app` no meio de uma migração. Sem isto, um projeto de app único perderia a detecção
|
|
145
|
+
* que já funcionava.
|
|
146
|
+
*/
|
|
147
|
+
if (found.length === 0)
|
|
148
|
+
for (const dir of here)
|
|
149
|
+
if (await exists(join(root, dir)))
|
|
150
|
+
return [dir];
|
|
151
|
+
return found;
|
|
152
|
+
}
|
package/dist/install-marks.js
CHANGED
|
@@ -170,7 +170,7 @@
|
|
|
170
170
|
* O que o cliente ganha ao rodar `upgrade`: o agente dele no Codex passa a poder PERGUNTAR ao
|
|
171
171
|
* sistema, em vez de só receber as regras e adivinhar o resto.
|
|
172
172
|
*/
|
|
173
|
-
export const MATERIALISER_SINCE = "0.16.
|
|
173
|
+
export const MATERIALISER_SINCE = "0.16.390";
|
|
174
174
|
/**
|
|
175
175
|
* A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
|
|
176
176
|
*
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* O TEXTO QUE TIRA AS EDIÇÕES DE SETUP DA MÃO DA PESSOA - e que agora USA a medição.
|
|
3
|
+
*
|
|
4
|
+
* O QUE O CLIENTE GANHA: um design system instalado não muda um pixel até que alguma folha que o app
|
|
5
|
+
* carrega importe os tokens e algum elemento carregue o escopo. Em 27/07 isso foi medido em 0% - a
|
|
6
|
+
* lista numerada de três passos existia e ninguém a executava. A pessoa que roda o comando tem um
|
|
7
|
+
* agente aberto ao lado, então o fecho deixou de ser tarefa e passou a ser um texto para colar.
|
|
8
|
+
*
|
|
9
|
+
* O DEFEITO QUE ISTO CORRIGE (dono, 07/09): o texto era FIXO. O `doctor` acabava de imprimir
|
|
10
|
+
* `✗ ✓ ✓` - só o import faltava, o escopo e a tipografia já estavam lá - e o parágrafo seguinte
|
|
11
|
+
* mandava *"Do the ONE-TIME SETUP it names, all of it"*, listando as três. Um agente obediente
|
|
12
|
+
* reescreve o que já estava correto, e o próprio texto termina com "não mexa nos meus estilos": ele
|
|
13
|
+
* se contradizia dentro de si mesmo.
|
|
14
|
+
*
|
|
15
|
+
* Então o que falta é ARGUMENTO. Sem ele o texto pede tudo, que é a verdade nos dois lugares onde
|
|
16
|
+
* ninguém mediu nada: o fecho do `init` (o projeto acabou de receber o sistema) e a tela do
|
|
17
|
+
* onboarding (a plataforma não vê o disco de ninguém). Com ele, o texto pede o que falta e NOMEIA o
|
|
18
|
+
* que já está pronto para o agente deixar em paz.
|
|
19
|
+
*
|
|
20
|
+
* GÊMEO POR SPEC com `apps/web/src/lib/ds/install-mission.ts`, que serve o mesmo texto na tela -
|
|
21
|
+
* `setup-prompt.spec.ts` reprova o drift. O CLI é publicado standalone e não pode importar de
|
|
22
|
+
* `apps/web`, então a cópia é inevitável; o que não é inevitável é ela divergir em silêncio, e dois
|
|
23
|
+
* textos diferentes para o mesmo setup ensinam dois setups.
|
|
24
|
+
*/
|
|
25
|
+
export function initCommand(slug) {
|
|
26
|
+
return `npx synthesisui@latest init --styles tailwind --ds ${slug}`;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A LINHA QUE DÁ SAÍDA QUANDO UMA VERSÃO ESTÁ PROPAGANDO (dono, 07/09).
|
|
30
|
+
*
|
|
31
|
+
* Ele rodou `npx synthesisui@latest doctor` e recebeu `ETARGET / No matching version found`: o
|
|
32
|
+
* `dist-tag` do npm já apontava para a versão nova e o tarball ainda não estava na réplica que a
|
|
33
|
+
* máquina dele consultou. O erro é do npm e acontece ANTES do nosso código rodar, então não há como
|
|
34
|
+
* interceptá-lo - o que está na nossa mão é a instrução não deixar quem lê sem saída, porque
|
|
35
|
+
* `ETARGET` não diz que é transitório nem o que fazer.
|
|
36
|
+
*/
|
|
37
|
+
const PROPAGATING = "If npm answers `ETARGET` or `No matching version found`, a release is still propagating - wait a minute and run it again, or pin the version the registry does have (`npm view synthesisui version`).";
|
|
38
|
+
const CONNECT_STEP = `Run: npx synthesisui@latest connect
|
|
39
|
+
${PROPAGATING}
|
|
40
|
+
If it answers "already had it" on both lines, everything is live and you are done.
|
|
41
|
+
If it WROTE either of them, then they are not live in this session, because hooks and tools are only read at startup. Stop there and tell me, in bold, on its own line: **RESTART THIS SESSION - the check is installed but not running yet.** Do not keep writing files after that; a session that cannot be checked is the state this whole setup exists to avoid.`;
|
|
42
|
+
/** As linhas exatas quando a folha é conhecida; a descrição do arquivo quando não é. */
|
|
43
|
+
function importStep(slug, gap) {
|
|
44
|
+
const lines = [
|
|
45
|
+
`@import "${gap.sheet ? gap.sheet.prefix : "<path to the repo root>/"}_synthesisui/ds/${slug}/tokens.css";`,
|
|
46
|
+
...(gap.imports === 2
|
|
47
|
+
? [
|
|
48
|
+
`@import "${gap.sheet ? gap.sheet.prefix : "<path to the repo root>/"}_synthesisui/ds/${slug}/theme.css";`,
|
|
49
|
+
]
|
|
50
|
+
: []),
|
|
51
|
+
];
|
|
52
|
+
const where = gap.sheet
|
|
53
|
+
? `\`${gap.sheet.path}\``
|
|
54
|
+
: "the global stylesheet this project's apps actually load (in a monorepo that is usually the shared package's, not the app's)";
|
|
55
|
+
return `Add ${gap.imports === 2 ? "these two lines" : "this line"} to ${where}, RIGHT AFTER its \`@import "tailwindcss"\` line:
|
|
56
|
+
|
|
57
|
+
${lines.map((l) => ` ${l}`).join("\n")}
|
|
58
|
+
|
|
59
|
+
The position is not cosmetic. Read there, this sheet comes BEFORE any \`@theme\` this project declares, so wherever both name the same variable the project's own value wins. Moved to the end of the file, ours would win instead - silently.`;
|
|
60
|
+
}
|
|
61
|
+
export function setupPrompt(slug, gap) {
|
|
62
|
+
const g = gap ?? {
|
|
63
|
+
tokens: true,
|
|
64
|
+
scope: true,
|
|
65
|
+
type: true,
|
|
66
|
+
imports: 2,
|
|
67
|
+
};
|
|
68
|
+
const done = [
|
|
69
|
+
g.tokens ? null : "a stylesheet already imports the system's tokens",
|
|
70
|
+
g.scope ? null : `\`data-ds="${slug}"\` is already on a root element`,
|
|
71
|
+
g.type
|
|
72
|
+
? null
|
|
73
|
+
: "this project's type is already mapped onto the system's family tokens",
|
|
74
|
+
].filter(Boolean);
|
|
75
|
+
const steps = [];
|
|
76
|
+
if (g.tokens)
|
|
77
|
+
steps.push(importStep(slug, g));
|
|
78
|
+
if (g.scope)
|
|
79
|
+
steps.push(`Put \`data-ds="${slug}"\` on a root element of each app - the element every page renders inside. Keep any classes it already has; a background they chose is a decision they made.`);
|
|
80
|
+
if (g.type)
|
|
81
|
+
steps.push(`Map this project's type onto the system's family tokens: import from the fonts file the install wrote and point the family variables at it, in the same stylesheet. This is the step people skip, and without it the pages render in the framework's default face.`);
|
|
82
|
+
steps.push(`Run: npx synthesisui@latest doctor
|
|
83
|
+
${PROPAGATING}
|
|
84
|
+
If it says "no system installed", the install never happened - run \`${initCommand(slug)}\` and then start over from step 1.
|
|
85
|
+
It must NOT say "this project does not load it yet". If it does, it names exactly which piece is still missing - fix that one and run it again.`);
|
|
86
|
+
steps.push(CONNECT_STEP);
|
|
87
|
+
const alreadyDone = done.length > 0
|
|
88
|
+
? `\nAlready in place - LEAVE THESE ALONE:\n${done.map((d) => `- ${d}`).join("\n")}\n`
|
|
89
|
+
: "";
|
|
90
|
+
return `Set up the "${slug}" design system in this project, so its tokens reach the browser. I already ran the install in my terminal.
|
|
91
|
+
${alreadyDone}
|
|
92
|
+
${steps.map((s, i) => `${i + 1}. ${s}`).join("\n")}
|
|
93
|
+
|
|
94
|
+
Do not change any of my existing styles.`;
|
|
95
|
+
}
|
package/dist/skill-configure.js
CHANGED
|
@@ -10,4 +10,4 @@
|
|
|
10
10
|
* e essa era a única parte da instalação que ninguém escrevia, só imprimia.
|
|
11
11
|
*/
|
|
12
12
|
export const CONFIGURE_SKILL_PATH = ".claude/skills/sui-configure-ds/SKILL.md";
|
|
13
|
-
export const CONFIGURE_SKILL = '---\nname: sui-configure-ds\ndescription:
|
|
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';
|
package/dist/skills.js
CHANGED
package/dist/stack.js
CHANGED
|
@@ -235,11 +235,39 @@ classStyle) {
|
|
|
235
235
|
*
|
|
236
236
|
* `null` quando o pacote não está lá ou quando a faixa não diz um número - um intervalo que não se
|
|
237
237
|
* resolve é ausência de resposta, e supor a v4 seria escolher o mais novo por conveniência nossa.
|
|
238
|
+
*
|
|
239
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
240
|
+
* O MAIOR SOZINHO É UMA FAIXA, e exigir o ponto apagava metade dos projetos v4.
|
|
241
|
+
*
|
|
242
|
+
* A primeira versão desta função casava `(\d+)\.` - um número SEGUIDO DE PONTO. `^4.1.2` respondia
|
|
243
|
+
* 4; `^4`, que é exatamente o que o `create-next-app` e o instalador do Tailwind v4 escrevem,
|
|
244
|
+
* respondia `null`. E `null` aqui não é neutro: é a plataforma dizendo *não sei qual versão* sobre um
|
|
245
|
+
* projeto que declara a versão na primeira linha do `package.json` dele.
|
|
246
|
+
*
|
|
247
|
+
* Medido em 07/09 nos sete projetos com Tailwind desta máquina:
|
|
248
|
+
*
|
|
249
|
+
* "^4" 3 projetos -> null test_ds_01, web-subscribe, web-dashboard
|
|
250
|
+
* "^3.4.1" 1 projeto -> 3
|
|
251
|
+
* "^3.3.5" 1 projeto -> 3
|
|
252
|
+
* "3.4.3" 1 projeto -> 3
|
|
253
|
+
* "4.1.11" 1 projeto -> 4
|
|
254
|
+
*
|
|
255
|
+
* Os três invisíveis são os três v4 - e não por acaso: é a v4 que se instala como `^4`. Quem paga é
|
|
256
|
+
* quem começa um projeto HOJE.
|
|
257
|
+
*
|
|
258
|
+
* A AMBIGUIDADE CONTINUA VALENDO `null`, e agora ela é medida em vez de suposta: cada faixa da
|
|
259
|
+
* expressão diz um maior, e a resposta só existe quando todas dizem o MESMO. `>=3 <5` continua sendo
|
|
260
|
+
* ausência de resposta, porque é; `^4` deixa de ser, porque não é.
|
|
238
261
|
*/
|
|
239
262
|
export function tailwindMajor(deps) {
|
|
240
263
|
const raw = deps.tailwindcss;
|
|
241
264
|
if (!raw)
|
|
242
265
|
return null;
|
|
243
|
-
const
|
|
244
|
-
|
|
266
|
+
const majors = new Set();
|
|
267
|
+
for (const part of raw.split(/\s*\|\|\s*|\s+|,/).filter(Boolean)) {
|
|
268
|
+
const m = /(\d+)/.exec(part.replace(/^[^0-9]*/, ""));
|
|
269
|
+
if (m)
|
|
270
|
+
majors.add(Number(m[1]));
|
|
271
|
+
}
|
|
272
|
+
return majors.size === 1 ? [...majors][0] : null;
|
|
245
273
|
}
|
package/dist/their-vars.js
CHANGED
|
@@ -101,7 +101,14 @@ export function pointAtTheirNames(css, theirs, resolvable) {
|
|
|
101
101
|
* buildou não tem como provar o que resolve, e um par afirmado sem prova é pior que par nenhum.
|
|
102
102
|
*/
|
|
103
103
|
if (!resolvable || theirs.byName.size === 0)
|
|
104
|
-
return {
|
|
104
|
+
return {
|
|
105
|
+
css,
|
|
106
|
+
pointed: 0,
|
|
107
|
+
pruned: 0,
|
|
108
|
+
compiledAway: 0,
|
|
109
|
+
cycles: 0,
|
|
110
|
+
pairs: [],
|
|
111
|
+
};
|
|
105
112
|
const ours = buildTable({ css, source: "installed" });
|
|
106
113
|
const alias = theirNames(ours, theirs);
|
|
107
114
|
/**
|
|
@@ -116,6 +123,7 @@ export function pointAtTheirNames(css, theirs, resolvable) {
|
|
|
116
123
|
bridged.set(theirName, ourName);
|
|
117
124
|
let pointed = 0;
|
|
118
125
|
let pruned = 0;
|
|
126
|
+
let compiledAway = 0;
|
|
119
127
|
let cycles = 0;
|
|
120
128
|
/** Ver `PointedAt.pairs`: o mapa sai do mesmo casamento que reescreve a linha. */
|
|
121
129
|
const pairs = [];
|
|
@@ -126,9 +134,19 @@ export function pointAtTheirNames(css, theirs, resolvable) {
|
|
|
126
134
|
const kind = Object.entries(FAMILY_KIND).find(([prefix]) => name.startsWith(prefix))?.[1];
|
|
127
135
|
if (!kind)
|
|
128
136
|
return line;
|
|
129
|
-
const
|
|
137
|
+
const theirs_ = alias.get(`${kind}:${normalizeValue(value.trim(), ours.rootPx)}`);
|
|
138
|
+
const theirName = theirs_?.name;
|
|
130
139
|
if (!theirName || theirName === name)
|
|
131
140
|
return line;
|
|
141
|
+
/**
|
|
142
|
+
* UM NOME DE PRÉ-PROCESSADOR NÃO É UM ENDEREÇO. `var($gray_dark)` não existe em CSS nenhum,
|
|
143
|
+
* então a linha fica com o valor - e a recusa se conta pela SUA causa, não como se o build
|
|
144
|
+
* dele tivesse podado um token que ele pode voltar a usar.
|
|
145
|
+
*/
|
|
146
|
+
if (!theirs_.writable) {
|
|
147
|
+
compiledAway += 1;
|
|
148
|
+
return line;
|
|
149
|
+
}
|
|
132
150
|
/**
|
|
133
151
|
* A PONTE JÁ APONTA PARA CÁ - então apontar de volta fecha um ciclo, e um ciclo não deixa o
|
|
134
152
|
* valor errado: deixa a propriedade INVÁLIDA. A utility dele já resolve pelo nosso token, que é
|
|
@@ -146,7 +164,7 @@ export function pointAtTheirNames(css, theirs, resolvable) {
|
|
|
146
164
|
pairs.push({ ours: name, theirs: theirName, value: value.trim() });
|
|
147
165
|
return `${indent}${name}${sep}var(${theirName});`;
|
|
148
166
|
});
|
|
149
|
-
return { css: out, pointed, pruned, cycles, pairs };
|
|
167
|
+
return { css: out, pointed, pruned, compiledAway, cycles, pairs };
|
|
150
168
|
}
|
|
151
169
|
/**
|
|
152
170
|
* O PREFIXO DA NOSSA VARIÁVEL -> A FAMÍLIA, e a chave que `theirNames` devolve é `<família>:<valor>`.
|
|
@@ -171,7 +189,14 @@ const FAMILY_KIND = {
|
|
|
171
189
|
export async function pointTokensAtTheirNames(root, payload) {
|
|
172
190
|
const css = payload.artifacts["tokens.css"] ?? "";
|
|
173
191
|
if (!css)
|
|
174
|
-
return {
|
|
192
|
+
return {
|
|
193
|
+
css,
|
|
194
|
+
pointed: 0,
|
|
195
|
+
pruned: 0,
|
|
196
|
+
compiledAway: 0,
|
|
197
|
+
cycles: 0,
|
|
198
|
+
pairs: [],
|
|
199
|
+
};
|
|
175
200
|
const [theirs, resolvable] = await Promise.all([
|
|
176
201
|
harvestTheirCss(root),
|
|
177
202
|
resolvableVars(root),
|
|
@@ -190,6 +215,9 @@ async function harvestTheirCss(root) {
|
|
|
190
215
|
"_synthesisui",
|
|
191
216
|
]);
|
|
192
217
|
let css = "";
|
|
218
|
+
/** As folhas UMA A UMA, porque a regra de escopo de `their-compiled-names.ts` precisa saber qual
|
|
219
|
+
* arquivo importa qual - a concatenação apaga exatamente essa informação. */
|
|
220
|
+
const sheets = new Map();
|
|
193
221
|
const walk = async (dir) => {
|
|
194
222
|
for (const entry of await readdir(dir, { withFileTypes: true }).catch(() => [])) {
|
|
195
223
|
if (skip.has(entry.name) || entry.name.startsWith("."))
|
|
@@ -197,12 +225,15 @@ async function harvestTheirCss(root) {
|
|
|
197
225
|
const path = join(dir, entry.name);
|
|
198
226
|
if (entry.isDirectory())
|
|
199
227
|
await walk(path);
|
|
200
|
-
else if (/\.(css|scss|sass|less)$/i.test(entry.name))
|
|
201
|
-
|
|
228
|
+
else if (/\.(css|scss|sass|less)$/i.test(entry.name)) {
|
|
229
|
+
const source = await readFile(path, "utf8").catch(() => "");
|
|
230
|
+
sheets.set(path, source);
|
|
231
|
+
css += `\n${source}`;
|
|
232
|
+
}
|
|
202
233
|
}
|
|
203
234
|
};
|
|
204
235
|
await walk(root);
|
|
205
|
-
return buildTable({ css, source: "yours" });
|
|
236
|
+
return buildTable({ css, source: "yours", sheets });
|
|
206
237
|
}
|
|
207
238
|
/**
|
|
208
239
|
* AS LINHAS QUE APONTAM PARA UM NOME DELE QUE NÃO EXISTE MAIS.
|
package/package.json
CHANGED
package/dist/wiring-prompt.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* O PASTE QUE TIRA AS QUATRO EDIÇÕES DA MÃO DA PESSOA.
|
|
3
|
-
*
|
|
4
|
-
* `init --ds` termina imprimindo uma seção "One-time setup" com três passos
|
|
5
|
-
* numerados para o humano fazer à mão: os dois `@import`, o `data-ds` na raiz
|
|
6
|
-
* e a fiação das fontes. Foi exatamente aí que 27/07 mediu 0% de fiação -
|
|
7
|
-
* ninguém fazia, e o sistema que acabara de chegar não aparecia na tela.
|
|
8
|
-
*
|
|
9
|
-
* A pessoa que roda esse comando tem um agente aberto ao lado. Então o fecho
|
|
10
|
-
* deixa de ser uma lista de tarefas e passa a ser uma coisa para colar.
|
|
11
|
-
*
|
|
12
|
-
* GÊMEO POR SPEC com `apps/web/src/lib/ds/install-mission.ts`, que serve o
|
|
13
|
-
* mesmo texto na tela do onboarding - `wiring-prompt.spec.ts` reprova o drift.
|
|
14
|
-
* O CLI é publicado standalone e não pode importar de `apps/web`, então a
|
|
15
|
-
* cópia é inevitável; o que não é inevitável é ela divergir em silêncio, e um
|
|
16
|
-
* prompt que diverge entre a tela e o terminal ensina duas fiações diferentes
|
|
17
|
-
* para o mesmo sistema.
|
|
18
|
-
*/
|
|
19
|
-
export function initCommand(slug) {
|
|
20
|
-
return `npx synthesisui@latest init --styles tailwind --ds ${slug}`;
|
|
21
|
-
}
|
|
22
|
-
export function wiringPrompt(slug) {
|
|
23
|
-
return `Wire the "${slug}" design system into this project. I already ran the install in my terminal.
|
|
24
|
-
|
|
25
|
-
1. Run: npx synthesisui@latest doctor
|
|
26
|
-
If it says "no system installed", I skipped the install - run ${initCommand(slug)} first, then carry on.
|
|
27
|
-
2. Do the ONE-TIME SETUP it names, all of it: the two @import lines in the project's global stylesheet (the path is relative to that file), data-ds="${slug}" on the root element, and the font wiring - importing from the fonts file it writes and mapping those variables in the stylesheet. The type is the step people skip.
|
|
28
|
-
3. Run doctor again. It must NOT say "not wired up yet". If it does, it names exactly which piece is missing - fix that and run it again.
|
|
29
|
-
4. Run: npx synthesisui@latest connect
|
|
30
|
-
If it answers "already had it" on both lines, everything is live and you are done.
|
|
31
|
-
If it WROTE either of them, then they are not live in this session, because hooks and tools are only read at startup. Stop there and tell me, in bold, on its own line: **RESTART THIS SESSION - the check is installed but not running yet.** Do not keep writing files after that; a session that cannot be checked is the state this whole setup exists to avoid.
|
|
32
|
-
|
|
33
|
-
Do not change any of my existing styles.`;
|
|
34
|
-
}
|