synthesisui 0.16.348 → 0.16.351
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/census-pages.js +66 -0
- package/dist/commands/add.js +98 -8
- package/dist/commands/import.js +61 -29
- package/dist/commands/sync.js +40 -0
- package/dist/doctor/fragments.js +35 -0
- package/dist/global-sheet.js +103 -0
- package/dist/guide.js +56 -3
- package/dist/install-marks.js +37 -2
- package/dist/merge-census.js +20 -0
- package/dist/tracked.js +40 -0
- package/package.json +1 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* COMO ELE MONTA UMA PÁGINA - a régua única, num módulo só.
|
|
3
|
+
*
|
|
4
|
+
* O que o cliente ganha: o planejador de página oferece o esqueleto que ELE já usa - as peças
|
|
5
|
+
* dele, na ordem em que ele as escreve - em vez dos três layouts que NÓS escrevemos. Ver
|
|
6
|
+
* `INV-COLETA-12` em `contracts/camada-1-coleta.md`.
|
|
7
|
+
*
|
|
8
|
+
* POR QUE ISTO NÃO MORA EM `import.ts`: três lugares precisam da mesma resposta - a varredura do
|
|
9
|
+
* escopo, a varredura da evidência (`--usage`, que é onde as páginas moram em todo monorepo) e a
|
|
10
|
+
* FUSÃO de dois escopos medidos. Duas cópias da transcrição seriam duas réguas no dia em que uma
|
|
11
|
+
* fosse editada, e o teto declarado num arquivo e furado no outro seria um teto que não existe.
|
|
12
|
+
*/
|
|
13
|
+
import { sketchOf } from "./doctor/sketch.js";
|
|
14
|
+
import { transcribe } from "./doctor/transcribe.js";
|
|
15
|
+
/**
|
|
16
|
+
* QUANTAS PÁGINAS VIAJAM NO CENSO.
|
|
17
|
+
*
|
|
18
|
+
* O teto existe pela mesma razão que o de `skipped`: num monorepo o número estoura e o censo
|
|
19
|
+
* viraria um dump do repositório. Medido em 01/09, o `frontend-hub` tem 145 arquivos de página e
|
|
20
|
+
* o nosso app 65, então 400 cabe as duas populações inteiras e o corte só aparece em repositório
|
|
21
|
+
* bem maior que os que a gente mede.
|
|
22
|
+
*
|
|
23
|
+
* E QUANDO ELE CORTAR, `pagesTotal` diz quantas eram. Uma tela que contasse `pages.length` diria
|
|
24
|
+
* 400 e pareceria completa, que é a forma mais barata de um corte silencioso mentir.
|
|
25
|
+
*/
|
|
26
|
+
export const PAGES_MAX = 400;
|
|
27
|
+
/**
|
|
28
|
+
* UMA PÁGINA DELE, TRANSCRITA - a moldura da raiz e as peças que ela compõe.
|
|
29
|
+
*
|
|
30
|
+
* `from` VEM DO SKETCH e não de uma segunda leitura do arquivo: o nó já carrega o especificador
|
|
31
|
+
* que o import dele declara, e reabrir o arquivo para descobrir o mesmo fato é a falha de
|
|
32
|
+
* esteira que o dono nomeou em 01/08.
|
|
33
|
+
*/
|
|
34
|
+
export function pageCompositionOf(rel, src, name, declaredValues) {
|
|
35
|
+
const sketch = sketchOf(src, name);
|
|
36
|
+
const at = sketch[0];
|
|
37
|
+
return {
|
|
38
|
+
file: rel,
|
|
39
|
+
root: at?.classes
|
|
40
|
+
? transcribe(at.classes.split(/\s+/).filter(Boolean), declaredValues).base
|
|
41
|
+
: {},
|
|
42
|
+
composes: sketch
|
|
43
|
+
.filter((n) => /^[A-Z]/.test(n.tag))
|
|
44
|
+
.map((n) => ({
|
|
45
|
+
name: n.tag.split(".")[0],
|
|
46
|
+
...(n.from ? { from: n.from } : {}),
|
|
47
|
+
depth: n.depth,
|
|
48
|
+
})),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* AS PÁGINAS DE DOIS ESCOPOS MEDIDOS, SOMADAS - e o teto reaplicado sobre a soma.
|
|
53
|
+
*
|
|
54
|
+
* Cada escopo mediu arquivos DIFERENTES, então isto é soma e não desempate: nenhuma regra do
|
|
55
|
+
* primeiro-escopo-vence se aplica a um conjunto disjunto. O que se preserva é o denominador -
|
|
56
|
+
* `pagesTotal` conta quantas foram VISTAS em cada medição, inclusive as que aquela medição já
|
|
57
|
+
* havia cortado, senão a soma de dois cortes leria como o total do projeto.
|
|
58
|
+
*/
|
|
59
|
+
export function mergePages(list) {
|
|
60
|
+
const all = list.flatMap((c) => c.pages ?? []);
|
|
61
|
+
if (all.length === 0)
|
|
62
|
+
return {};
|
|
63
|
+
const seen = list.reduce((n, c) => n + (c.pagesTotal ?? (c.pages ?? []).length), 0);
|
|
64
|
+
const pages = all.slice(0, PAGES_MAX);
|
|
65
|
+
return { pages, ...(seen > pages.length ? { pagesTotal: seen } : {}) };
|
|
66
|
+
}
|
package/dist/commands/add.js
CHANGED
|
@@ -3,9 +3,11 @@ 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
7
|
import { lockReference } from "../group-role.js";
|
|
7
8
|
import { buildGuide } from "../guide.js";
|
|
8
9
|
import { censusScope } from "../measured-scope.js";
|
|
10
|
+
import { recallAvailable } from "../memory/availability.js";
|
|
9
11
|
import { body as line, section, snippet } from "../output.js";
|
|
10
12
|
import { fetchDesignSystem } from "../registry.js";
|
|
11
13
|
import { repoStateOf } from "../repo-state.js";
|
|
@@ -13,6 +15,7 @@ import { describeFiltered, ruleApplies, rulesForProject, } from "../rule-filter.
|
|
|
13
15
|
import { detectStack } from "../stack.js";
|
|
14
16
|
import { onlyWhatMatched } from "../their-theme.js";
|
|
15
17
|
import { pointTokensAtTheirNames } from "../their-vars.js";
|
|
18
|
+
import { tracksAnyOf } from "../tracked.js";
|
|
16
19
|
/**
|
|
17
20
|
* QUAL METADE DESTA PASTA UM TIME COMMITA.
|
|
18
21
|
*
|
|
@@ -184,6 +187,16 @@ export async function detectAppDirs(root, pagesDir) {
|
|
|
184
187
|
* stable root re-exports (tokens.css/theme.css) and a `.lock` at it, and updates
|
|
185
188
|
* CLAUDE.md. Older version folders are kept for rollback/diff.
|
|
186
189
|
*/
|
|
190
|
+
/**
|
|
191
|
+
* ESTE REPOSITÓRIO VESTE SHADCN? - `components.json` é o arquivo que o próprio shadcn escreve.
|
|
192
|
+
*
|
|
193
|
+
* Uma pergunta, uma resposta: ela decide se o adaptador é ESCRITO e se ele é MENCIONADO. Enquanto
|
|
194
|
+
* eram duas leituras, o terminal dizia a verdade (só citava para quem tem) e a escrita plantava o
|
|
195
|
+
* arquivo em todo mundo - a pior combinação, porque a prosa parecia certa.
|
|
196
|
+
*/
|
|
197
|
+
async function wearsShadcn(root) {
|
|
198
|
+
return access(join(root, "components.json")).then(() => true, () => false);
|
|
199
|
+
}
|
|
187
200
|
export async function add(slug, opts) {
|
|
188
201
|
const base = resolveRegistry(opts.registry);
|
|
189
202
|
const projectRoot = opts.dir ?? process.cwd();
|
|
@@ -219,7 +232,26 @@ export async function add(slug, opts) {
|
|
|
219
232
|
*/
|
|
220
233
|
const aligned = onlyWhatMatched(payload.artifacts["theme.css"] ?? "", new Set(theirVars.pairs.map((p) => p.ours)));
|
|
221
234
|
// 1. server artifacts (tokens.css, theme.css, …) → pinned version folder
|
|
235
|
+
/**
|
|
236
|
+
* O ADAPTADOR DO SHADCN SÓ VIAJA PARA QUEM TEM SHADCN - e o sinal já existia.
|
|
237
|
+
*
|
|
238
|
+
* O QUE O CLIENTE PERCEBIA. `shadcn.css` caía na pasta de todo mundo, inclusive de quem nunca
|
|
239
|
+
* usou shadcn: um arquivo de uma stack que não é a dele, que ele não pediu e que nada no
|
|
240
|
+
* repositório dele importa. MEDIDO em 01/09 no `codelevel` - nenhum `@radix-ui`, nenhum
|
|
241
|
+
* `components/ui`, nenhum `components.json` - e os 2,3 KB estavam lá.
|
|
242
|
+
*
|
|
243
|
+
* `components.json` É O SINAL, e ele já decidia se o `add` MENCIONA o adaptador no terminal
|
|
244
|
+
* (28/07). Só a escrita não perguntava - então o produto dizia a verdade em voz alta e plantava o
|
|
245
|
+
* arquivo em silêncio. Agora a mesma pergunta responde às duas.
|
|
246
|
+
*
|
|
247
|
+
* É a régua que o compilador já aplica aos tokens (`reachableTokens` descarta o que nada
|
|
248
|
+
* referencia), levada ao arquivo inteiro: quem, NESTE repositório, lê isto?
|
|
249
|
+
*/
|
|
250
|
+
const wantsShadcn = await wearsShadcn(projectRoot);
|
|
251
|
+
const skipped = !wantsShadcn && "shadcn.css" in payload.artifacts;
|
|
222
252
|
for (const [filename, content] of Object.entries(payload.artifacts)) {
|
|
253
|
+
if (filename === "shadcn.css" && !wantsShadcn)
|
|
254
|
+
continue;
|
|
223
255
|
await writeFile(join(versionDir, filename), filename === "tokens.css"
|
|
224
256
|
? theirVars.css
|
|
225
257
|
: filename === "theme.css"
|
|
@@ -227,12 +259,35 @@ export async function add(slug, opts) {
|
|
|
227
259
|
: content, "utf8");
|
|
228
260
|
}
|
|
229
261
|
// 2. canonical source of truth
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
262
|
+
/**
|
|
263
|
+
* O DOCUMENTO FICA - ele É a referência que o agente dele lê (decisão do dono, 01/09) - E ELE VAI
|
|
264
|
+
* COMPACTO.
|
|
265
|
+
*
|
|
266
|
+
* MEDIDO em 01/09 no `codelevel`, 62 componentes: `null, 2` custa **347 400 bytes**; sem a
|
|
267
|
+
* indentação são **213 470** - 134 KB de espaço em branco, 39% do arquivo, num arquivo que
|
|
268
|
+
* NENHUM humano abre para ler. Quem o consome são `loadSystem` e as quatro ferramentas do MCP, e
|
|
269
|
+
* `JSON.parse` não distingue os dois.
|
|
270
|
+
*
|
|
271
|
+
* O que o cliente ganha: o mesmo dado, com 39% a menos de diff em todo `upgrade` e 134 KB a menos
|
|
272
|
+
* no repositório dele. Quem quiser lê-lo com os olhos tem `describe_component`, que responde por
|
|
273
|
+
* componente em vez de por arquivo.
|
|
274
|
+
*/
|
|
275
|
+
await writeFile(join(versionDir, "design-system.json"), `${JSON.stringify(payload.document)}\n`, "utf8");
|
|
276
|
+
/**
|
|
277
|
+
* 3. O GUIA - e o catálogo só é COPIADO onde não há caminho de volta.
|
|
278
|
+
*
|
|
279
|
+
* Com o servidor MCP registrado, `list_components` e `describe_component` servem o mesmo catálogo
|
|
280
|
+
* por componente e sob demanda, então copiá-lo inteiro é peso e duplicação. MEDIDO em 01/09 no
|
|
281
|
+
* `codelevel`: 923 das 1225 linhas do guia (75%) eram o catálogo, num arquivo de 113 KB.
|
|
282
|
+
*
|
|
283
|
+
* É a MESMA pergunta que o manifesto do `CLAUDE.md` já faz desde 20/08, e por isso a MESMA
|
|
284
|
+
* função responde: cortar sem caminho de volta trocaria contexto por ignorância.
|
|
285
|
+
*/
|
|
286
|
+
const tools = await recallAvailable(projectRoot);
|
|
287
|
+
await writeFile(join(versionDir, "GUIDE.md"), buildGuide(payload, tools.available), "utf8");
|
|
233
288
|
// 4. stable root re-exports for each CSS artifact → always the active version,
|
|
234
289
|
// so the consumer's @import path never changes across updates
|
|
235
|
-
const cssArtifacts = Object.keys(payload.artifacts).filter((f) => f.endsWith(".css"));
|
|
290
|
+
const cssArtifacts = Object.keys(payload.artifacts).filter((f) => f.endsWith(".css") && !(f === "shadcn.css" && !wantsShadcn));
|
|
236
291
|
for (const filename of cssArtifacts) {
|
|
237
292
|
await writeFile(join(slugDir, filename), `/* Active version (v${payload.version}). Managed by synthesisui - do not edit. */\n` +
|
|
238
293
|
`@import "./v${payload.version}/${filename}";\n`, "utf8");
|
|
@@ -434,11 +489,34 @@ export async function add(slug, opts) {
|
|
|
434
489
|
console.log(`↺ ${payload.name} active version set to v${v} (was v${prev.version})`);
|
|
435
490
|
}
|
|
436
491
|
const files = [
|
|
437
|
-
...Object.keys(payload.artifacts),
|
|
492
|
+
...Object.keys(payload.artifacts).filter((f) => !(f === "shadcn.css" && skipped)),
|
|
438
493
|
"design-system.json",
|
|
439
494
|
"GUIDE.md",
|
|
440
495
|
];
|
|
441
496
|
console.log(` v${v}/: ${files.join(", ")}`);
|
|
497
|
+
/**
|
|
498
|
+
* O QUE NÃO FOI ESCRITO, DITO EM VOZ ALTA - lei 8, e ela vale nos dois sentidos.
|
|
499
|
+
*
|
|
500
|
+
* Deixar de plantar um arquivo é melhor que plantá-lo, mas fazê-lo em SILÊNCIO é como o produto
|
|
501
|
+
* ganha a fama de esconder coisas: alguém que já viu `shadcn.css` num outro repositório procuraria
|
|
502
|
+
* aqui e concluiria que a instalação falhou. A frase diz o que não veio, por quê, e o que muda a
|
|
503
|
+
* resposta.
|
|
504
|
+
*/
|
|
505
|
+
if (skipped)
|
|
506
|
+
console.log(` shadcn.css was not written: this repository has no \`components.json\`, so nothing here wears shadcn's variables. Add shadcn and run \`synthesisui add ${payload.slug}\` again to get the adapter.`);
|
|
507
|
+
/**
|
|
508
|
+
* O QUE FALTA PARA A PROMESSA DO `.gitignore` SER VERDADE - ver `tracksAnyOf`.
|
|
509
|
+
*
|
|
510
|
+
* O arquivo que a gente escreve dentro de `_synthesisui/` diz que a identidade e o CSS são
|
|
511
|
+
* commitados, e nenhum comando nunca verificou. MEDIDO em 01/09 no `codelevel`: zero arquivos
|
|
512
|
+
* rastreados depois de um `add` bem-sucedido.
|
|
513
|
+
*
|
|
514
|
+
* A frase lidera pelo EFEITO, porque é ele que faz alguém agir: "o build de um colega não tem
|
|
515
|
+
* tokens" move; "a pasta está untracked" não.
|
|
516
|
+
*/
|
|
517
|
+
const tracked = await tracksAnyOf(projectRoot, `_synthesisui/ds/${payload.slug}`);
|
|
518
|
+
if (tracked === false)
|
|
519
|
+
console.log(` none of this is in version control yet - a colleague who clones this repository gets no tokens, and their build breaks. \`git add _synthesisui/ds\` commits the identity and the CSS; the measurement and the local record are already ignored for you.`);
|
|
442
520
|
/**
|
|
443
521
|
* A FOLHA NÃO É A QUE O SERVIDOR COMPILOU, e calar isso é o pior dos dois mundos: o cliente vê um
|
|
444
522
|
* `var(--color-ink-500)` num arquivo "gerenciado pelo synthesisui" e não tem como saber de onde
|
|
@@ -519,7 +597,16 @@ export async function add(slug, opts) {
|
|
|
519
597
|
/** Nenhuma pasta encontrada: os caminhos viram exemplo, e o texto abaixo diz isso. */
|
|
520
598
|
const appDir = appDirs[0] ?? projectConfig.pagesDir;
|
|
521
599
|
const appDirFound = appDirs.length > 0;
|
|
522
|
-
|
|
600
|
+
/**
|
|
601
|
+
* A FOLHA GLOBAL QUE ELE DE FATO CARREGA - ver `globalSheetOf`.
|
|
602
|
+
*
|
|
603
|
+
* A instrução mandava editar `<appDir>/globals.css`. MEDIDO em 01/09 no `codelevel`: os dois apps
|
|
604
|
+
* dele fazem `@import "@repo/ui/styles/globals.css"`, e o `globals.css` do app diz em comentário
|
|
605
|
+
* *"Do not redeclare tokens or fonts here"*. O arquivo certo era o do pacote, e a gente apontava
|
|
606
|
+
* para o do app - com o caminho relativo do app, que dali não resolve.
|
|
607
|
+
*/
|
|
608
|
+
const sheet = await globalSheetOf(projectRoot, `${appDir}/globals.css`);
|
|
609
|
+
const importPrefix = prefixFrom(sheet);
|
|
523
610
|
console.log(section("One-time setup (once per app)"));
|
|
524
611
|
/**
|
|
525
612
|
* "ONCE PER APP" É LITERAL NUM MONOREPO, e calar os outros apps entrega metade da fiação. Os
|
|
@@ -528,7 +615,9 @@ export async function add(slug, opts) {
|
|
|
528
615
|
*/
|
|
529
616
|
if (appDirs.length > 1)
|
|
530
617
|
console.log(line(`(${appDirs.length} apps here: ${appDirs.join(", ")} - the paths below are for ${appDir}; repeat for the others.)`));
|
|
531
|
-
console.log(line(
|
|
618
|
+
console.log(line(sheet === `${appDir}/globals.css`
|
|
619
|
+
? `1. Import the system in your GLOBAL stylesheet, e.g. ${sheet}`
|
|
620
|
+
: `1. Import the system in ${sheet} - that is the sheet your apps load (${appDir}/globals.css only re-exports it, so tokens put there would not reach the package's own components):`));
|
|
532
621
|
console.log(line(` (the path is relative to that file - hence the leading ${importPrefix}):`));
|
|
533
622
|
console.log("");
|
|
534
623
|
// THE BRIDGE ONLY EXISTS FOR PEOPLE WHO ALREADY HAVE SHADCN, so it only
|
|
@@ -539,7 +628,8 @@ export async function add(slug, opts) {
|
|
|
539
628
|
// can just build a design system with shadcn", with the answer sitting
|
|
540
629
|
// unimported in his own repo. Naming it to everybody would be noise; naming it
|
|
541
630
|
// to whoever has `components.json` is the whole feature arriving.
|
|
542
|
-
|
|
631
|
+
/** A MESMA pergunta que decide a ESCRITA - ver `wantsShadcn`. Duas leituras seriam duas regras. */
|
|
632
|
+
const hasShadcn = await wearsShadcn(projectRoot);
|
|
543
633
|
console.log(snippet([
|
|
544
634
|
...(hasTheme ? [`@import "tailwindcss";`] : []),
|
|
545
635
|
`@import "${importPrefix}_synthesisui/ds/${payload.slug}/tokens.css";`,
|
package/dist/commands/import.js
CHANGED
|
@@ -3,6 +3,7 @@ import { basename, dirname, join, relative, sep } from "node:path";
|
|
|
3
3
|
import { anatomyFromSketch } from "../anatomy-from-sketch.js";
|
|
4
4
|
import { applyAnatomyPatch, hasEdits, } from "../anatomy-patch.js";
|
|
5
5
|
import { resolveAnatomy, resolveFlatParts, safePartName, } from "../anatomy-read.js";
|
|
6
|
+
import { PAGES_MAX, pageCompositionOf } from "../census-pages.js";
|
|
6
7
|
import { readCredentials, readToken, resolveRegistry, sameRegistry, } from "../config.js";
|
|
7
8
|
import { declaredElsewhere } from "../declared-elsewhere.js";
|
|
8
9
|
import { architectureGap, architectureRule, componentHome, describeArchitecture, describeChoice, describeGap, detectArchitectures, homeLine, packagingOf, proposeNewHome, resolvesAs, } from "../doctor/architecture.js";
|
|
@@ -68,17 +69,12 @@ import { walk, walkAll } from "./doctor.js";
|
|
|
68
69
|
* nunca o nome do arquivo - e é isso que a faz valer no projeto de alguém que a gente nunca viu.
|
|
69
70
|
*/
|
|
70
71
|
/**
|
|
71
|
-
*
|
|
72
|
+
* O TETO DE PÁGINAS, re-exportado de onde a régua inteira mora - ver `census-pages.ts`.
|
|
72
73
|
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* o nosso app 65, então 400 cabe as duas populações inteiras e o corte só aparece em repositório
|
|
76
|
-
* bem maior que os que a gente mede.
|
|
77
|
-
*
|
|
78
|
-
* E QUANDO ELE CORTAR, `pagesTotal` diz quantas eram. Uma tela que contasse `pages.length` diria
|
|
79
|
-
* 400 e pareceria completa, que é a forma mais barata de um corte silencioso mentir.
|
|
74
|
+
* Ele vive lá porque a FUSÃO de dois escopos precisa reaplicá-lo sobre a soma, e um teto
|
|
75
|
+
* declarado num arquivo e furado no outro é um teto que não existe.
|
|
80
76
|
*/
|
|
81
|
-
export
|
|
77
|
+
export { PAGES_MAX };
|
|
82
78
|
/**
|
|
83
79
|
* How many distinct values travel, PER KIND.
|
|
84
80
|
*
|
|
@@ -770,26 +766,7 @@ export async function takeCensus(root, opts) {
|
|
|
770
766
|
* que falta para alguém escrever a próxima (04/08).
|
|
771
767
|
*/
|
|
772
768
|
if (verdict.why === "route" || verdict.why === "screen") {
|
|
773
|
-
|
|
774
|
-
const root = sketch[0];
|
|
775
|
-
pages.push({
|
|
776
|
-
file: rel,
|
|
777
|
-
root: root?.classes
|
|
778
|
-
? transcribe(root.classes.split(/\s+/).filter(Boolean), declaredValues).base
|
|
779
|
-
: {},
|
|
780
|
-
/**
|
|
781
|
-
* `from` VEM DO SKETCH e não de uma segunda leitura do arquivo: o nó já carrega o
|
|
782
|
-
* especificador que o import dele declara, e reabrir o arquivo para descobrir o
|
|
783
|
-
* mesmo fato é a falha de esteira que o dono nomeou em 01/08.
|
|
784
|
-
*/
|
|
785
|
-
composes: sketch
|
|
786
|
-
.filter((n) => /^[A-Z]/.test(n.tag))
|
|
787
|
-
.map((n) => ({
|
|
788
|
-
name: n.tag.split(".")[0],
|
|
789
|
-
...(n.from ? { from: n.from } : {}),
|
|
790
|
-
depth: n.depth,
|
|
791
|
-
})),
|
|
792
|
-
});
|
|
769
|
+
pages.push(pageCompositionOf(rel, src, d.name, declaredValues));
|
|
793
770
|
}
|
|
794
771
|
skips.push({
|
|
795
772
|
name: d.name,
|
|
@@ -1456,6 +1433,11 @@ export async function takeCensus(root, opts) {
|
|
|
1456
1433
|
* not `reports`: an app's classes are choices made from a scale, and folding
|
|
1457
1434
|
* them into the census would put the average back where the declaration goes.
|
|
1458
1435
|
*
|
|
1436
|
+
* A ÚNICA COISA QUE ESTE LAÇO ESCREVE ALÉM DA CONTAGEM É `pages`, e a razão é que uma página
|
|
1437
|
+
* não é uma escolha de estilo: é a COMPOSIÇÃO dela - quais peças, em que ordem, sob que
|
|
1438
|
+
* moldura. Ela não entra em `defined`, não declara token e não vota em escala. Ver o bloco
|
|
1439
|
+
* `AS PÁGINAS MORAM AQUI` mais abaixo, com o número das duas populações.
|
|
1440
|
+
*
|
|
1459
1441
|
* Deliberately its own tally so the LIBRARY QUESTION is asked of the scope
|
|
1460
1442
|
* alone. Fold the app in first and every export has a count, `isLibrary` reads
|
|
1461
1443
|
* false, and the crosswalk starts substituting the components it was just
|
|
@@ -1548,6 +1530,41 @@ export async function takeCensus(root, opts) {
|
|
|
1548
1530
|
// source - so measuring nesting only on the defined loop reported zero
|
|
1549
1531
|
// companions on a 128-component library (e2e, 01/08).
|
|
1550
1532
|
readNesting(rel, src, nesting);
|
|
1533
|
+
/**
|
|
1534
|
+
* AS PÁGINAS MORAM AQUI, E ATÉ AGORA O CENSO NÃO GUARDAVA NENHUMA.
|
|
1535
|
+
*
|
|
1536
|
+
* `Census.pages` é como ELE monta uma tela - a moldura da raiz e a ordem das peças -, e é
|
|
1537
|
+
* o único dado a partir do qual alguém escreve a próxima página no vocabulário dele. Ele
|
|
1538
|
+
* só era colhido na varredura do ESCOPO, e o escopo é a biblioteca: uma `packages/ui` não
|
|
1539
|
+
* tem `app/` nem `pages/`, então a resposta era sempre zero. Medido em 01/09 nas duas
|
|
1540
|
+
* populações, com o mesmo `--scope`/`--usage` que o `import` e o `sync` passam:
|
|
1541
|
+
*
|
|
1542
|
+
* codelevel 0 páginas guardadas · 9 existem (apps/web 3, apps/landing 6)
|
|
1543
|
+
* frontend-hub 0 páginas guardadas · 156 existem (apps/web-dashboard)
|
|
1544
|
+
*
|
|
1545
|
+
* Isto NÃO afrouxa a lei da passagem de evidência. Uma página não entra em `defined`, não
|
|
1546
|
+
* doa token e não vota em escala: o que ela doa é COMPOSIÇÃO - quais peças dele seguram
|
|
1547
|
+
* quais telas -, que é exatamente a classe de fato que `--usage` existe para trazer.
|
|
1548
|
+
*
|
|
1549
|
+
* A régua é a mesma do escopo, de propósito: o portão recusa a rota (`route`/`screen`) e
|
|
1550
|
+
* é essa recusa que a identifica como página. Só o que o portão RECUSA por ser tela vira
|
|
1551
|
+
* página, então nada que já é componente é contado duas vezes.
|
|
1552
|
+
*/
|
|
1553
|
+
if (/\.(tsx|jsx)$/i.test(rel)) {
|
|
1554
|
+
for (const d of scanDefinitions(rel, src)) {
|
|
1555
|
+
const verdict = gateComponent({
|
|
1556
|
+
name: d.name,
|
|
1557
|
+
file: rel,
|
|
1558
|
+
source: src,
|
|
1559
|
+
internal: [...shared, ...uInternal],
|
|
1560
|
+
});
|
|
1561
|
+
if (verdict.ok)
|
|
1562
|
+
continue;
|
|
1563
|
+
if (verdict.why !== "route" && verdict.why !== "screen")
|
|
1564
|
+
continue;
|
|
1565
|
+
pages.push(pageCompositionOf(rel, src, d.name, declaredValues));
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1551
1568
|
// Which PROJECT composes it - the credibility panel's number, per root.
|
|
1552
1569
|
for (const m of src.matchAll(/<([A-Z][A-Za-z0-9_]*)/g)) {
|
|
1553
1570
|
const at = projectsOf.get(m[1]) ?? new Set();
|
|
@@ -2000,6 +2017,21 @@ export async function takeCensus(root, opts) {
|
|
|
2000
2017
|
]),
|
|
2001
2018
|
islandRead: islandClassesRead,
|
|
2002
2019
|
refused: new Set(skips.map((s) => s.file)),
|
|
2020
|
+
/**
|
|
2021
|
+
* AS ANIMAÇÕES QUE O CENSO CAPTUROU - ver o veredito em `judgeFragments`.
|
|
2022
|
+
*
|
|
2023
|
+
* `keyframes` é o objeto que já viaja no censo e que o compilador emite. Um frame de uma
|
|
2024
|
+
* delas chegou por essa porta, então cobrá-lo como lacuna conta a mesma coisa duas vezes.
|
|
2025
|
+
* A LISTA É A MESMA FUSÃO QUE O CENSO GRAVA, e não a metade de componente.
|
|
2026
|
+
*
|
|
2027
|
+
* `keyframes` sozinho é o que os componentes declaram; `globalKeyframes` é a folha dele, e
|
|
2028
|
+
* é ali que os 35 do codelevel moram - o censo funde os dois na hora de escrever. Ler só o
|
|
2029
|
+
* primeiro aqui declarava capturado um conjunto VAZIO e o conserto não movia nada: a
|
|
2030
|
+
* primeira medição depois de ligar isto deu exatamente os mesmos 130 de antes.
|
|
2031
|
+
*
|
|
2032
|
+
* Se a captura falhar, o frame volta a ser lacuna sozinho, que é a resposta certa.
|
|
2033
|
+
*/
|
|
2034
|
+
keyframes: new Set(Object.keys({ ...globalKeyframes, ...keyframes })),
|
|
2003
2035
|
},
|
|
2004
2036
|
/**
|
|
2005
2037
|
* AS FORMAS QUE ELE DECLAROU - e é aqui que a declaração dele muda o veredito.
|
package/dist/commands/sync.js
CHANGED
|
@@ -21,6 +21,25 @@ import { resolveReadParts, siblingProjects, takeCensus, } from "./import.js";
|
|
|
21
21
|
* after `closeRequest` runs here, so the person knows what happened and what
|
|
22
22
|
* (if anything) is theirs to do next.
|
|
23
23
|
*/
|
|
24
|
+
/**
|
|
25
|
+
* ONDE O SYNC FOI PARAR, E O QUE FALTA PARA CHEGAR NO DISCO DELE.
|
|
26
|
+
*
|
|
27
|
+
* O `sync` escreve no RASCUNHO, e o registry serve a última PUBLICADA - por lei, desde 29/07: se o
|
|
28
|
+
* rascunho fluísse para o repo, o botão Publish não seguraria nada. Então uma medição bem-sucedida
|
|
29
|
+
* termina com o repositório dele **inalterado**, e nada dizia isso.
|
|
30
|
+
*
|
|
31
|
+
* MEDIDO em 01/09 no `codelevel`: o `sync` escreveu 62 de 62 componentes na v4 (o rascunho, com 1276
|
|
32
|
+
* revisões), o `add` tinha instalado a v3 (a última publicada), e o output mandou o cliente para o
|
|
33
|
+
* Studio - uma tela, não o passo que falta.
|
|
34
|
+
*
|
|
35
|
+
* É A MESMA LIÇÃO DE 13/08, uma superfície adiante - ver `decisionLine`, caso `authored`. Lá, "Get
|
|
36
|
+
* it: upgrade" mandava rodar um comando que responde `already at the latest version`: a pessoa
|
|
37
|
+
* seguia a instrução, não recebia nada, e ficava sem saber se errou ou se a ferramenta falhou. A
|
|
38
|
+
* frase de lá nomeia os DOIS passos na ordem, e o primeiro é dele - esta faz o mesmo.
|
|
39
|
+
*/
|
|
40
|
+
export function draftLine(slug, base) {
|
|
41
|
+
return `this went into your DRAFT - your repository still has what you installed. It reaches the disk once you publish${base ? `: ${base}/dashboard/mine/${slug}/publish` : ""}\n then: npx synthesisui@latest upgrade ${slug}`;
|
|
42
|
+
}
|
|
24
43
|
export function decisionLine(d, slug,
|
|
25
44
|
/** Onde o sistema dele vive - o `publish` mora lá, e sem o endereço a frase manda procurar. */
|
|
26
45
|
base) {
|
|
@@ -563,6 +582,27 @@ export async function remeasure(args) {
|
|
|
563
582
|
console.log(body(paint.strong(said)));
|
|
564
583
|
if (out.url)
|
|
565
584
|
console.log(body(out.url));
|
|
585
|
+
/**
|
|
586
|
+
* ONDE ISSO FOI PARAR, E O QUE FALTA PARA CHEGAR NO DISCO DELE.
|
|
587
|
+
*
|
|
588
|
+
* O `sync` escreve no RASCUNHO, e o registry serve a última PUBLICADA - por lei, desde 29/07: se o
|
|
589
|
+
* rascunho fluísse para o repo, o botão Publish não seguraria nada. Então uma medição bem-sucedida
|
|
590
|
+
* termina com o repositório dele **inalterado**, e nada dizia isso.
|
|
591
|
+
*
|
|
592
|
+
* MEDIDO em 01/09 no `codelevel`: o `sync` escreveu 62 de 62 componentes na v4 (o rascunho, com
|
|
593
|
+
* 1276 revisões), o `add` tinha instalado a v3 (a última publicada), e o output mandou o cliente
|
|
594
|
+
* para o Studio - uma tela, não o passo que falta.
|
|
595
|
+
*
|
|
596
|
+
* É A MESMA LIÇÃO DE 13/08, uma superfície adiante. Lá, `authored` dizia "Get it: upgrade" e o
|
|
597
|
+
* `upgrade` respondia `already at the latest version`: a pessoa seguia a instrução, não recebia
|
|
598
|
+
* nada, e ficava sem saber se errou ou se a ferramenta falhou. A frase de lá nomeia os DOIS passos
|
|
599
|
+
* na ordem, e o primeiro é dele - esta faz o mesmo.
|
|
600
|
+
*
|
|
601
|
+
* Só aparece quando algo FOI escrito: um `sync` que não mudou nada não tem o que publicar, e
|
|
602
|
+
* cobrar um publish vazio é o aviso que ensina a ignorar o próximo.
|
|
603
|
+
*/
|
|
604
|
+
if ((out.written ?? 0) > 0)
|
|
605
|
+
console.log(body(paint.faint(draftLine(slug, base))));
|
|
566
606
|
/**
|
|
567
607
|
* O QUE MUDOU DESDE A ÚLTIMA VEZ - a linha que só uma RE-medição pode dar.
|
|
568
608
|
*
|
package/dist/doctor/fragments.js
CHANGED
|
@@ -302,12 +302,24 @@ kind) {
|
|
|
302
302
|
continue;
|
|
303
303
|
}
|
|
304
304
|
const selector = selectorOf(open);
|
|
305
|
+
/**
|
|
306
|
+
* DE QUAL ANIMAÇÃO ESTE FRAME É - ver `Fragment.inKeyframe`.
|
|
307
|
+
*
|
|
308
|
+
* `selectorOf` devolve `0%`, porque o bloco do frame é um seletor comum e o at-rule só
|
|
309
|
+
* vence quando não há nenhum. O nome fica na pilha e é a única coisa que liga esta
|
|
310
|
+
* declaração ao keyframe que o censo capturou.
|
|
311
|
+
*/
|
|
312
|
+
const inKeyframe = open
|
|
313
|
+
.map((b) => /^@keyframes\s+([^\s{]+)/.exec(b)?.[1])
|
|
314
|
+
.filter(Boolean)
|
|
315
|
+
.pop();
|
|
305
316
|
out.push({
|
|
306
317
|
shape: "css",
|
|
307
318
|
file,
|
|
308
319
|
line: i + 1,
|
|
309
320
|
text: `${selector ? `${selector} ` : ""}{ ${m[1]}: ${m[2].trim()} }`,
|
|
310
321
|
read: false,
|
|
322
|
+
...(inKeyframe ? { inKeyframe } : {}),
|
|
311
323
|
...(kind === "global"
|
|
312
324
|
? { reason: "shape-not-read" }
|
|
313
325
|
: kind === "dead"
|
|
@@ -440,7 +452,30 @@ forms = []) {
|
|
|
440
452
|
const wornGlobal = elsewhere?.wornGlobal ?? new Set();
|
|
441
453
|
const islandRead = elsewhere?.islandRead ?? new Map();
|
|
442
454
|
const refused = elsewhere?.refused ?? new Set();
|
|
455
|
+
const captured = elsewhere?.keyframes ?? new Set();
|
|
443
456
|
return seen.map((f) => {
|
|
457
|
+
/**
|
|
458
|
+
* UM FRAME DE UM KEYFRAME CAPTURADO CHEGOU - a mesma família de `wornGlobal` logo abaixo, e a
|
|
459
|
+
* mesma família de `INV-INTERP-15`: a régua acusava um fato que já estava no censo.
|
|
460
|
+
*
|
|
461
|
+
* `0% { opacity: 0 }` não tem leitor de RECEITA e nunca vai ter - um frame não é a decisão de
|
|
462
|
+
* um componente, é um instante de uma animação. Mas a animação inteira viaja em
|
|
463
|
+
* `census.keyframes`, com todos os seus frames, e o compilador a emite. Cobrar o frame como
|
|
464
|
+
* lacuna é contar duas vezes a mesma coisa e chamar a segunda de perda.
|
|
465
|
+
*
|
|
466
|
+
* MEDIDO EM 02/09, nas duas populações e com o mesmo `--scope`/`--usage` do produto:
|
|
467
|
+
*
|
|
468
|
+
* codelevel 35 keyframes capturados · 87 de 105 declarações sem leitor eram frames (83%)
|
|
469
|
+
* frontend-hub 1 keyframe capturado · 0 de 6 eram frames (0%)
|
|
470
|
+
*
|
|
471
|
+
* As duas discordam por larga margem, e é isso que decide o desenho: a pergunta não é sobre
|
|
472
|
+
* volume, é sobre a porta por onde a declaração passou. O nome tem que estar CAPTURADO - um
|
|
473
|
+
* frame de uma animação que o censo não colheu continua sendo lacuna, porque ali ela é real.
|
|
474
|
+
*/
|
|
475
|
+
if (f.inKeyframe && captured.has(f.inKeyframe)) {
|
|
476
|
+
const { reason: _unread, ...rest } = f;
|
|
477
|
+
return { ...rest, read: true };
|
|
478
|
+
}
|
|
444
479
|
/**
|
|
445
480
|
* A REGRA DE CLASSE GLOBAL QUE ALGUÉM VESTE FOI LIDA - antes do desvio de
|
|
446
481
|
* `binding`, porque lida é mais forte que ligada. `.root { isolation: isolate }`
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join, posix, relative, resolve } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* A FOLHA GLOBAL QUE OS APPS DELE REALMENTE CARREGAM - e num monorepo ela quase nunca é a do app.
|
|
5
|
+
*
|
|
6
|
+
* O QUE O CLIENTE PERCEBE SEM ISTO. O `add` manda importar os tokens em `apps/<x>/app/globals.css`.
|
|
7
|
+
* MEDIDO em 01/09 no `codelevel`: os DOIS apps dele fazem `@import "@repo/ui/styles/globals.css"`, e
|
|
8
|
+
* o `globals.css` do app diz, em comentário dele, *"Do not redeclare tokens or fonts here - extend in
|
|
9
|
+
* styles/"*. A instrução apontava para o arquivo errado, e a convenção certa estava escrita ali do
|
|
10
|
+
* lado.
|
|
11
|
+
*
|
|
12
|
+
* Ele seguiu a instrução no arquivo CERTO - o do pacote - e o caminho relativo que a gente imprimiu
|
|
13
|
+
* era o do app. Um monorepo com pacote de UI compartilhado não é exceção: é a forma comum, e a
|
|
14
|
+
* instrução tem que derivar de onde o CSS dele mora em vez de assumir.
|
|
15
|
+
*
|
|
16
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
17
|
+
* COMO A FOLHA REAL É ENCONTRADA, e nada aqui é palpite:
|
|
18
|
+
*
|
|
19
|
+
* 1. abre o `globals.css` do app
|
|
20
|
+
* 2. procura `@import "<spec>"` que aponte para um CSS DENTRO deste repositório
|
|
21
|
+
* 3. resolve o `<spec>`: caminho relativo, ou pacote do workspace pelo `exports`
|
|
22
|
+
* 4. repete na folha encontrada, até ela não re-exportar mais
|
|
23
|
+
*
|
|
24
|
+
* O QUE NÃO É SEGUIDO: `tailwindcss` e qualquer coisa que não resolva para um arquivo daqui. Um
|
|
25
|
+
* `@import "tailwindcss"` é a biblioteca, e mandar o cliente editá-la seria pior que a instrução que
|
|
26
|
+
* isto conserta.
|
|
27
|
+
*/
|
|
28
|
+
/** Quantos saltos seguir antes de desistir - um ciclo de imports não pode travar um `add`. */
|
|
29
|
+
const MAX_HOPS = 5;
|
|
30
|
+
const IMPORT = /@import\s+["']([^"']+)["']/g;
|
|
31
|
+
/** `@repo/ui/styles/globals.css` → `packages/ui/src/styles/globals.css`, pelo `exports` dele. */
|
|
32
|
+
async function fromWorkspace(root, spec) {
|
|
33
|
+
for (const group of ["packages", "apps", "libs"]) {
|
|
34
|
+
let entries;
|
|
35
|
+
try {
|
|
36
|
+
const { readdir } = await import("node:fs/promises");
|
|
37
|
+
entries = await readdir(join(root, group));
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
const pkgPath = join(root, group, entry, "package.json");
|
|
44
|
+
const raw = await readFile(pkgPath, "utf8").catch(() => null);
|
|
45
|
+
if (!raw)
|
|
46
|
+
continue;
|
|
47
|
+
let pkg;
|
|
48
|
+
try {
|
|
49
|
+
pkg = JSON.parse(raw);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (!pkg.name || !spec.startsWith(`${pkg.name}/`))
|
|
55
|
+
continue;
|
|
56
|
+
const sub = `./${spec.slice(pkg.name.length + 1)}`;
|
|
57
|
+
const target = pkg.exports?.[sub];
|
|
58
|
+
if (typeof target !== "string")
|
|
59
|
+
continue;
|
|
60
|
+
return posix.join(group, entry, target.replace(/^\.\//, ""));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* A folha onde os tokens dele devem entrar, relativa à raiz - `appSheet` quando nada a re-exporta.
|
|
67
|
+
*/
|
|
68
|
+
export async function globalSheetOf(root, appSheet) {
|
|
69
|
+
let current = appSheet;
|
|
70
|
+
for (let hop = 0; hop < MAX_HOPS; hop += 1) {
|
|
71
|
+
const raw = await readFile(join(root, current), "utf8").catch(() => null);
|
|
72
|
+
if (!raw)
|
|
73
|
+
return current;
|
|
74
|
+
let next = null;
|
|
75
|
+
for (const m of raw.matchAll(IMPORT)) {
|
|
76
|
+
const spec = m[1];
|
|
77
|
+
if (!spec.endsWith(".css"))
|
|
78
|
+
continue;
|
|
79
|
+
const candidate = spec.startsWith(".")
|
|
80
|
+
? posix.normalize(posix.join(posix.dirname(current), spec))
|
|
81
|
+
: await fromWorkspace(root, spec);
|
|
82
|
+
if (!candidate)
|
|
83
|
+
continue;
|
|
84
|
+
/** Fora da raiz não é folha dele para editar - e um `..` demais sai do repositório. */
|
|
85
|
+
if (candidate.startsWith(".."))
|
|
86
|
+
continue;
|
|
87
|
+
const readable = await readFile(join(root, candidate), "utf8").catch(() => null);
|
|
88
|
+
if (readable === null)
|
|
89
|
+
continue;
|
|
90
|
+
next = candidate;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
if (!next || next === current)
|
|
94
|
+
return current;
|
|
95
|
+
current = next;
|
|
96
|
+
}
|
|
97
|
+
return current;
|
|
98
|
+
}
|
|
99
|
+
/** O `../` que leva daquela folha até a raiz do repositório - o prefixo do `@import`. */
|
|
100
|
+
export function prefixFrom(sheet) {
|
|
101
|
+
const up = relative(dirname(resolve("/r", sheet)), "/r");
|
|
102
|
+
return up === "" ? "./" : `${up.split(/[\\/]/).join("/")}/`;
|
|
103
|
+
}
|
package/dist/guide.js
CHANGED
|
@@ -244,7 +244,42 @@ function componentEntry(cname, recipe) {
|
|
|
244
244
|
* components with claude-code" work: the tokens alone are not enough, the agent
|
|
245
245
|
* needs the rules and the real vocabulary (semantic token names and recipes).
|
|
246
246
|
*/
|
|
247
|
-
|
|
247
|
+
/**
|
|
248
|
+
* O QUE FICA NO LUGAR DO CATÁLOGO - e ele diz QUANTOS existem, nunca só que existem.
|
|
249
|
+
*
|
|
250
|
+
* Um guia que omite o catálogo em silêncio ensina o agente que o sistema não tem componente nenhum,
|
|
251
|
+
* que é o oposto do que ele precisa saber antes de escrever a primeira tela.
|
|
252
|
+
*/
|
|
253
|
+
function toolsPointer(count) {
|
|
254
|
+
return `This system defines **${count} component${count === 1 ? "" : "s"}**, and they are NOT listed here on purpose: the tools answer the same
|
|
255
|
+
question with the piece you actually need, at the moment you need it.
|
|
256
|
+
|
|
257
|
+
- \`list_components\` - every name, with what each one is for. Call it BEFORE writing any UI
|
|
258
|
+
element from scratch; if one covers the purpose, use it instead of inventing another.
|
|
259
|
+
- \`describe_component { name }\` - one component in full: its parts, its axes, the libraries it
|
|
260
|
+
needs, and the rules that govern it.
|
|
261
|
+
- \`add_blueprint { name }\` - the same component as typed code in your repository.
|
|
262
|
+
|
|
263
|
+
A name alone does not tell you that an editor needs \`@tiptap/react\`, or that a card is built out
|
|
264
|
+
of a metric card - so read the component before you use it, never the list alone.`;
|
|
265
|
+
}
|
|
266
|
+
export function buildGuide(payload,
|
|
267
|
+
/**
|
|
268
|
+
* AS FERRAMENTAS ESTÃO AO ALCANCE DESTE REPOSITÓRIO? - ver `recallAvailable`.
|
|
269
|
+
*
|
|
270
|
+
* O QUE O CLIENTE GANHA. O catálogo inteiro deixa de ser copiado para o disco dele quando existe
|
|
271
|
+
* caminho de volta: `list_components` e `describe_component` servem a MESMA coisa, por componente
|
|
272
|
+
* e no momento em que o agente precisa. MEDIDO em 01/09 no `codelevel`: a seção do catálogo é
|
|
273
|
+
* **923 de 1225 linhas do guia (75%)**, e o arquivo inteiro pesa 113 KB no repositório dele.
|
|
274
|
+
*
|
|
275
|
+
* POR QUE É A MESMA RÉGUA DO MANIFESTO. O corte de 20/08 (`readManifest`) já usa exatamente este
|
|
276
|
+
* sinal, pelo mesmo motivo: cortar sem caminho de volta troca contexto por ignorância. Aqui a
|
|
277
|
+
* pergunta é idêntica, e por isso a resposta vem de fora - quem sabe é quem leu o `.mcp.json`.
|
|
278
|
+
*
|
|
279
|
+
* `false` mantém o guia inteiro, que é o comportamento de todo repositório sem o servidor
|
|
280
|
+
* registrado.
|
|
281
|
+
*/
|
|
282
|
+
toolsReachable = false) {
|
|
248
283
|
const { document: doc, slug, name, version } = payload;
|
|
249
284
|
const { meta, foundations, motion, components } = doc;
|
|
250
285
|
const semanticRoles = Object.keys(foundations.color.semantic);
|
|
@@ -360,6 +395,21 @@ ${depLines.join("\n")}
|
|
|
360
395
|
const hasTailwind = "theme.css" in payload.artifacts;
|
|
361
396
|
const hasParts = Object.values(components).some((r) => r.parts && Object.keys(r.parts).length > 0);
|
|
362
397
|
const componentLines = Object.entries(components).map(([cname, recipe]) => componentEntry(cname, recipe));
|
|
398
|
+
/**
|
|
399
|
+
* O CORTE SÓ ACONTECE QUANDO ELE DE FATO CORTA - e este é o único jeito honesto de prometê-lo.
|
|
400
|
+
*
|
|
401
|
+
* O ponteiro para as ferramentas tem tamanho fixo. Num sistema de três componentes com descrições
|
|
402
|
+
* de uma linha, ele é MAIOR que o catálogo que substitui - e um guia que cresceu não pode ser
|
|
403
|
+
* anunciado como dieta de contexto. MEDIDO em 01/09: no `codelevel`, com 62 componentes, o corte é
|
|
404
|
+
* de 82% (114 431 -> 20 425 bytes); numa fixture de 3, o ponteiro custaria 7% a mais.
|
|
405
|
+
*
|
|
406
|
+
* Comparar os dois textos responde sozinho, em todo tamanho de sistema, sem limiar escrito à mão
|
|
407
|
+
* que envelheceria no dia em que o ponteiro mudasse de tamanho. Há 4 sistemas de 2 componentes em
|
|
408
|
+
* produção, e para eles copiar continua sendo o mais barato.
|
|
409
|
+
*/
|
|
410
|
+
const catalogueText = componentLines.join("\n\n");
|
|
411
|
+
const pointerText = toolsPointer(componentLines.length);
|
|
412
|
+
const pointerBeatsCatalogue = pointerText.length < catalogueText.length;
|
|
363
413
|
// Motion vocabulary: the system's keyframes, named and usable. Intent is
|
|
364
414
|
// classified from the frames (same logic that compiled the animate-*
|
|
365
415
|
// utilities into theme.css), so what this section PROMISES about a utility's
|
|
@@ -742,7 +792,7 @@ Each recipe becomes a \`.ds-<name>\` class (inside the \`[data-ds="${slug}"]\` s
|
|
|
742
792
|
\`data-<axis>="<option>"\` attributes; states (hover/focus/active/disabled) ship in the CSS;
|
|
743
793
|
multi-part components expose \`.ds-<name>-<part>\` classes (listed under each).
|
|
744
794
|
|
|
745
|
-
${
|
|
795
|
+
${toolsReachable && pointerBeatsCatalogue ? pointerText : catalogueText}
|
|
746
796
|
${blockEntries.length
|
|
747
797
|
? `
|
|
748
798
|
---
|
|
@@ -760,6 +810,9 @@ ${blockLines.join("\n\n")}
|
|
|
760
810
|
: ""}
|
|
761
811
|
---
|
|
762
812
|
|
|
763
|
-
_Full canonical source of truth (including values and keyframes) in \`design-system.json\`._
|
|
813
|
+
_Full canonical source of truth (including values and keyframes) in \`design-system.json\`._${toolsReachable && pointerBeatsCatalogue
|
|
814
|
+
? `
|
|
815
|
+
_The component catalogue is served by the tools rather than copied here - see the section above._`
|
|
816
|
+
: ""}
|
|
764
817
|
`;
|
|
765
818
|
}
|
package/dist/install-marks.js
CHANGED
|
@@ -152,7 +152,7 @@
|
|
|
152
152
|
* comentou vinha sendo escrito na pasta dele como componente de verdade. Medido em 14 arquivos
|
|
153
153
|
* das duas populações o nó fantasma era a RAIZ - o elemento cujas classes viram a `base`.
|
|
154
154
|
*/
|
|
155
|
-
export const MATERIALISER_SINCE = "0.16.
|
|
155
|
+
export const MATERIALISER_SINCE = "0.16.349";
|
|
156
156
|
/**
|
|
157
157
|
* A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
|
|
158
158
|
*
|
|
@@ -532,7 +532,42 @@ export const CHECKER_SINCE = "0.16.308";
|
|
|
532
532
|
* si, o campo fica ausente, e ausente ali continua sendo a resposta certa - o `codelevel-ui` dá
|
|
533
533
|
* 0 de 0 contra 145 arquivos de página do `frontend-hub`.
|
|
534
534
|
*/
|
|
535
|
-
|
|
535
|
+
/**
|
|
536
|
+
* 0.16.349 -> 0.16.350 em 01/09 (`INV-COLETA-12`, a metade do MONOREPO): as páginas do app dele
|
|
537
|
+
* passam a chegar ao censo mesmo quando a leitura foi apontada para a biblioteca.
|
|
538
|
+
*
|
|
539
|
+
* O QUE ELE GANHA COM O `sync`: a linha *"N of your components hold up M pages"* - quais peças
|
|
540
|
+
* dele são a espinha das telas dele. Quem roda `--scope packages/ui --usage apps/web` recebia
|
|
541
|
+
* ZERO página e o relatório calava as duas seções que falam delas.
|
|
542
|
+
*
|
|
543
|
+
* A CORREÇÃO DE UMA FRASE QUE ESTAVA AQUI: "ausente continua sendo a resposta certa para quem
|
|
544
|
+
* apontou a leitura para uma biblioteca" só valia para quem NÃO passou `--usage`. As páginas
|
|
545
|
+
* moram no app, e a varredura da evidência lia cada um desses arquivos para contagem e lei e
|
|
546
|
+
* passava direto pela composição. Medido em 01/09, com o mesmo `--scope`/`--usage` que o
|
|
547
|
+
* `import` e o `sync` passam:
|
|
548
|
+
*
|
|
549
|
+
* codelevel 0 páginas guardadas · 9 existem (apps/web 3, apps/landing 6)
|
|
550
|
+
* frontend-hub 0 páginas guardadas · 156 existem (apps/web-dashboard)
|
|
551
|
+
*
|
|
552
|
+
* QUEM NÃO É AFETADO: quem mede um app inteiro sem escopo - esse caminho já carregava as páginas
|
|
553
|
+
* desde 0.16.348. E quem aponta para uma biblioteca sem `--usage`: ela não compõe telas dentro de
|
|
554
|
+
* si, o campo segue ausente, e ali a ausência é a resposta certa de verdade.
|
|
555
|
+
*/
|
|
556
|
+
/**
|
|
557
|
+
* 0.16.350 -> 0.16.351 em 02/09 (`INV-COLETA-16`): um frame de uma animação que o censo CAPTUROU
|
|
558
|
+
* deixa de ser contado como declaração sem leitor.
|
|
559
|
+
*
|
|
560
|
+
* O QUE ELE GANHA COM O `sync`: o número de cobertura para de contar como perdido o que a
|
|
561
|
+
* plataforma leu inteiro. `0% { opacity: 0 }` nunca vai ter leitor de RECEITA - um frame não é a
|
|
562
|
+
* decisão de um componente -, mas a animação viaja em `census.keyframes` e o compilador a emite.
|
|
563
|
+
*
|
|
564
|
+
* codelevel 35 keyframes capturados · shape-not-read 130 -> 18 · CSS lido 35% -> 88%
|
|
565
|
+
* frontend-hub 1 keyframe capturado · shape-not-read 6 -> 6 · nada muda
|
|
566
|
+
*
|
|
567
|
+
* QUEM NÃO É AFETADO: quem não escreve `@keyframes`. E quem escreve um que a captura NÃO alcançou -
|
|
568
|
+
* ali o frame continua sendo lacuna, porque ali a perda é real.
|
|
569
|
+
*/
|
|
570
|
+
export const READER_SINCE = "0.16.351";
|
|
536
571
|
/**
|
|
537
572
|
* O QUE ESTÁ INSTALADO AQUI FICOU PARA TRÁS - e as DUAS condições que fazem isso ser verdade.
|
|
538
573
|
*
|
package/dist/merge-census.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mergePages } from "./census-pages.js";
|
|
1
2
|
import { gapReason } from "./doctor/architecture.js";
|
|
2
3
|
const asRecord = (v) => (v ?? {});
|
|
3
4
|
/** Nome do escopo para as mensagens - `.` quando a medição não escopou nada. */
|
|
@@ -296,6 +297,25 @@ export function mergeCensus(list) {
|
|
|
296
297
|
out.conventions = conventions;
|
|
297
298
|
if (architectures && architectures.length > 0)
|
|
298
299
|
out.architectures = architectures;
|
|
300
|
+
/**
|
|
301
|
+
* AS PÁGINAS DOS ESCOPOS SOMAM - ver `mergePages` em `census-pages.ts`.
|
|
302
|
+
*
|
|
303
|
+
* `out` nasce de um spread do PRIMEIRO censo e depois é montado campo a campo, então `pages`
|
|
304
|
+
* do primeiro escopo viajava de carona e a dos SEGUINTES caía inteira, sem uma linha dizendo
|
|
305
|
+
* isso. Com um escopo só o defeito não aparece (a fusão de uma lista devolve o próprio censo),
|
|
306
|
+
* e é por isso que ele sobreviveu: o caminho que o expõe é `--scope A --scope B`, que existe
|
|
307
|
+
* justamente para o sistema que mora em dois pacotes.
|
|
308
|
+
*
|
|
309
|
+
* `pagesTotal` também precisava ser recontado: herdado do primeiro, ele descrevia uma medição
|
|
310
|
+
* e era lido como o total do projeto.
|
|
311
|
+
*/
|
|
312
|
+
const merged = mergePages(list);
|
|
313
|
+
delete out.pages;
|
|
314
|
+
delete out.pagesTotal;
|
|
315
|
+
if (merged.pages)
|
|
316
|
+
out.pages = merged.pages;
|
|
317
|
+
if (merged.pagesTotal)
|
|
318
|
+
out.pagesTotal = merged.pagesTotal;
|
|
299
319
|
if (gaps.length > 0)
|
|
300
320
|
out.architectureGaps = gaps;
|
|
301
321
|
if (newComponentHome)
|
package/dist/tracked.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
const run = promisify(execFile);
|
|
4
|
+
/**
|
|
5
|
+
* ESTES ARQUIVOS ESTÃO NO CONTROLE DE VERSÃO DELE? - e a resposta muda o que o `add` diz no fim.
|
|
6
|
+
*
|
|
7
|
+
* O QUE O CLIENTE PERCEBE SEM ISTO. O `.gitignore` que NÓS geramos dentro de `_synthesisui/` promete,
|
|
8
|
+
* em comentário: *"a identidade e o CSS são commitados para que um clone fresco seja governado"*. Ele
|
|
9
|
+
* lista o que NÃO versionar, e daí em diante todo mundo assume que o resto está versionado. Ninguém
|
|
10
|
+
* nunca fez o `git add`, e nenhum comando checou.
|
|
11
|
+
*
|
|
12
|
+
* MEDIDO em 01/09 no `codelevel`, depois de um `add` bem-sucedido: `git ls-files _synthesisui/`
|
|
13
|
+
* devolve **0 arquivos**, e o status mostra `?? _synthesisui/`. A promessa não se cumpria em lugar
|
|
14
|
+
* nenhum, e o custo apareceu no mesmo dia - o dono apagou a pasta, ela não voltou do `git checkout`,
|
|
15
|
+
* e ele precisou de um `add` para recuperá-la. Um colega clonando o repositório teria um build sem
|
|
16
|
+
* tokens.
|
|
17
|
+
*
|
|
18
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
19
|
+
* LEITURA, NUNCA ESCRITA - e isso não é cautela, é o contrato.
|
|
20
|
+
*
|
|
21
|
+
* Rodar `git add` por conta própria mexeria no index dele: alguém no meio de um commit parcial
|
|
22
|
+
* encontraria arquivos nossos na área de stage sem ter pedido. O produto DIZ o que falta e por quê;
|
|
23
|
+
* quem versiona o repositório é ele.
|
|
24
|
+
*
|
|
25
|
+
* E ela CALA quando não sabe: fora de um repositório git, ou sem o binário, a pergunta não tem
|
|
26
|
+
* resposta - e um aviso sobre versionamento num diretório que não é versionado seria ruído puro.
|
|
27
|
+
*/
|
|
28
|
+
export async function tracksAnyOf(root, path) {
|
|
29
|
+
try {
|
|
30
|
+
const { stdout } = await run("git", ["ls-files", "--", path], {
|
|
31
|
+
cwd: root,
|
|
32
|
+
timeout: 5000,
|
|
33
|
+
});
|
|
34
|
+
return stdout.trim().length > 0;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
/** Não é repositório git, ou o git não está aqui: sem resposta, sem aviso. */
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
package/package.json
CHANGED