synthesisui 0.16.153 → 0.16.155
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 +154 -0
- package/dist/commands/absorb.js +211 -0
- package/dist/commands/doctor.js +117 -14
- package/dist/commands/import.js +34 -0
- package/dist/commands/init.js +14 -0
- package/dist/commands/summary.js +79 -0
- package/dist/config.js +54 -0
- package/dist/doctor/frozen.js +13 -1
- package/dist/doctor/variant-read.js +86 -12
- package/dist/index.js +71 -0
- package/dist/measured-scope.js +63 -0
- package/dist/skill-init.js +92 -33
- package/package.json +1 -1
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { measuredScope } from "../measured-scope.js";
|
|
4
|
+
import { body, paint, section, snippet } from "../output.js";
|
|
5
|
+
/**
|
|
6
|
+
* `synthesisui summary <slug>` - AS QUATRO LINHAS DO PRIMEIRO MINUTO, impressas por um programa.
|
|
7
|
+
*
|
|
8
|
+
* A skill `sui-init` já mandava terminar em quatro linhas, e listava o que nunca deveria aparecer
|
|
9
|
+
* nelas: quantos arquivos foram varridos, quanto tempo levou, quais leitores rodaram, o que o ledger
|
|
10
|
+
* diz. O fechamento que o dono recebeu em 06/08 tinha ~400 palavras e violava as duas regras - com
|
|
11
|
+
* três dos quatro parágrafos falando do AGENTE e não do sistema dele:
|
|
12
|
+
*
|
|
13
|
+
* "my notes from 2026-08-05 carry two confirmed defects" a memoria do agente
|
|
14
|
+
* "the generator had been deleted, so I rebuilt it" o retrabalho do agente
|
|
15
|
+
* "a census regression I stripped by hand" o conserto do agente
|
|
16
|
+
*
|
|
17
|
+
* E duas daquelas advertências estavam DESATUALIZADAS: os dois defeitos do `--fix` foram consertados
|
|
18
|
+
* na 0.16.153, então a skill assustou o usuário com um bug que não existe mais.
|
|
19
|
+
*
|
|
20
|
+
* Mais prosa proibindo não conserta isso. O que o agente tem em mãos no fim é justamente o próprio
|
|
21
|
+
* trabalho dele, e a atração de reportá-lo vence uma linha de proibição a sessenta linhas de distância.
|
|
22
|
+
* Então o fechamento sai das mãos dele: este comando sabe as contagens (o censo está no disco), sabe a
|
|
23
|
+
* versão instalada (o `.lock`), e recebe o resto por flag. Um agente que executa isto não tem o que
|
|
24
|
+
* compor - e uma anotação velha dele não tem por onde entrar.
|
|
25
|
+
*
|
|
26
|
+
* LOCAL, E NUNCA FALHA. Sem censo ele ainda diz o essencial; sem rede ele não perde nada, porque nada
|
|
27
|
+
* aqui é remoto.
|
|
28
|
+
*/
|
|
29
|
+
export async function summary(slug, opts) {
|
|
30
|
+
const root = resolve(opts.dir ?? process.cwd());
|
|
31
|
+
const census = await readFile(join(root, "_synthesisui", "census.json"), "utf8")
|
|
32
|
+
.then((raw) => JSON.parse(raw))
|
|
33
|
+
.catch(() => null);
|
|
34
|
+
const lock = await readFile(join(root, "_synthesisui", "ds", slug, ".lock"), "utf8")
|
|
35
|
+
.then((raw) => JSON.parse(raw))
|
|
36
|
+
.catch(() => null);
|
|
37
|
+
const measured = await measuredScope(root);
|
|
38
|
+
const name = lock?.name ?? slug;
|
|
39
|
+
/**
|
|
40
|
+
* `looks`, NUNCA `components`.
|
|
41
|
+
*
|
|
42
|
+
* `components` é a lista de candidatos que o portão avaliou - 128 no repo do dono, contra os 36 que
|
|
43
|
+
* o sistema realmente tem. Dizer 128 no primeiro minuto é prometer quatro vezes o que foi entregue,
|
|
44
|
+
* e o número certo já estava no mesmo arquivo.
|
|
45
|
+
*/
|
|
46
|
+
const components = census?.looks ? Object.keys(census.looks).length : null;
|
|
47
|
+
const tokens = census?.declared ? Object.keys(census.declared).length : null;
|
|
48
|
+
console.log(section(`${name} is yours`));
|
|
49
|
+
/**
|
|
50
|
+
* LINHA 1 - O QUE ELE TEM, nas contagens dele. Nunca "36 de 128 candidatos", nunca "2787 arquivos":
|
|
51
|
+
* o que importa no primeiro minuto é o tamanho do que ele ganhou e de onde saiu.
|
|
52
|
+
*/
|
|
53
|
+
const from = measured.system ? `, read out of ${measured.system}` : "";
|
|
54
|
+
console.log(body(components != null && tokens != null
|
|
55
|
+
? `${components} components and ${tokens} of your own tokens${from}.`
|
|
56
|
+
: `Installed${lock?.version ? ` at v${lock.version}` : ""}${from}.`));
|
|
57
|
+
/**
|
|
58
|
+
* LINHA 2 - AS DUAS VERSÕES E O CAMINHO DE VOLTA, com os números que a PLATAFORMA deu.
|
|
59
|
+
*
|
|
60
|
+
* Escrever "v1" e "v2" aqui por conta própria já esteve errado uma vez: enquanto o nascimento
|
|
61
|
+
* publicava uma baseline, o import caía na v2 e o congelamento produzia a v3. Então os números
|
|
62
|
+
* chegam por flag, e sem eles esta linha simplesmente não é dita.
|
|
63
|
+
*/
|
|
64
|
+
if (opts.base != null && opts.draft != null) {
|
|
65
|
+
console.log(body(`v${opts.base} is your code as it is today${opts.fixes ? ` · v${opts.draft} adds ${opts.fixes} deterministic ${opts.fixes === 1 ? "fix" : "fixes"}` : ` · v${opts.draft} is the draft`}.`));
|
|
66
|
+
console.log(body(paint.dim(`Back to v${opts.base} is one click: /dashboard/mine/${slug}/publish`)));
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* LINHA 3 - O QUE FAZER AGORA, e é uma coisa só.
|
|
70
|
+
*
|
|
71
|
+
* As duas linhas de fiação são o portão de tudo o mais: sem elas nenhum token chega ao navegador e
|
|
72
|
+
* todo número seguinte é vazio. O `doctor` diz isso melhor e com o estado real, então o próximo passo
|
|
73
|
+
* é ele - não uma explicação nossa aqui.
|
|
74
|
+
*/
|
|
75
|
+
console.log("");
|
|
76
|
+
console.log(body("Two lines wire it up, and `init` printed both:"));
|
|
77
|
+
console.log(body(paint.dim(` @import "./_synthesisui/ds/${slug}/tokens.css"; and data-ds="${slug}" on <html>`)));
|
|
78
|
+
console.log(paint.blue(snippet(["npx synthesisui@latest doctor"])));
|
|
79
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -98,12 +98,66 @@ export async function readProjectConfig(root) {
|
|
|
98
98
|
? parsed.componentsDir
|
|
99
99
|
: DEFAULT_CONFIG.componentsDir,
|
|
100
100
|
styles: parsed.styles === "tailwind" ? "tailwind" : "css",
|
|
101
|
+
/**
|
|
102
|
+
* A INTENÇÃO NÃO TEM DEFAULT AQUI, de propósito.
|
|
103
|
+
*
|
|
104
|
+
* Um default silencioso é uma decisão nossa vestida de decisão deles: quem não respondeu não
|
|
105
|
+
* escolheu, e o doctor tem que poder dizer "ninguém declarou" em vez de ordenar por um palpite.
|
|
106
|
+
* Quem consome isto é `intentOf`, que separa os três casos.
|
|
107
|
+
*/
|
|
108
|
+
...(parsed.intent === "migrate" || parsed.intent === "adopt"
|
|
109
|
+
? { intent: parsed.intent }
|
|
110
|
+
: {}),
|
|
111
|
+
...(typeof parsed.intentAt === "string" && parsed.intentAt
|
|
112
|
+
? { intentAt: parsed.intentAt }
|
|
113
|
+
: {}),
|
|
114
|
+
/** Sem valor gravado o comando usa `system`, que é o default do produto e não uma suposição. */
|
|
115
|
+
...(parsed.absorb === "code" || parsed.absorb === "system"
|
|
116
|
+
? { absorb: parsed.absorb }
|
|
117
|
+
: {}),
|
|
101
118
|
};
|
|
102
119
|
}
|
|
103
120
|
catch {
|
|
104
121
|
return DEFAULT_CONFIG;
|
|
105
122
|
}
|
|
106
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* A PRECEDÊNCIA, num lugar só: flag > config > default.
|
|
126
|
+
*
|
|
127
|
+
* E ela é IMPRESSA por quem chama, sempre. Estado persistido que não se anuncia é a etiqueta que
|
|
128
|
+
* envelhece em silêncio - o mesmo defeito que a tela de causas acabou de perder (dono, 06/08). Uma
|
|
129
|
+
* linha dizendo "migrate, definido em 06/08, --adopt inverte" transforma estado velho em fato.
|
|
130
|
+
*
|
|
131
|
+
* O default é `adopt` porque é o que uma pessoa com um sistema instalado quer na maioria dos dias, e
|
|
132
|
+
* porque é o único dos dois cuja lista tem conserto automático (`--fix`). Migrar é uma fase, e uma
|
|
133
|
+
* fase se declara.
|
|
134
|
+
*/
|
|
135
|
+
export function intentOf(config, flag) {
|
|
136
|
+
if (flag === "migrate" || flag === "adopt")
|
|
137
|
+
return { intent: flag, from: "flag" };
|
|
138
|
+
if (config.intent)
|
|
139
|
+
return {
|
|
140
|
+
intent: config.intent,
|
|
141
|
+
from: "config",
|
|
142
|
+
...(config.intentAt ? { at: config.intentAt } : {}),
|
|
143
|
+
};
|
|
144
|
+
return { intent: "adopt", from: "default" };
|
|
145
|
+
}
|
|
146
|
+
/** A frase que acompanha o relatório - ver `intentOf` para por que ela é obrigatória. */
|
|
147
|
+
export function describeIntent(source) {
|
|
148
|
+
const what = source.intent === "migrate"
|
|
149
|
+
? "reading as a MIGRATION - values your system does not name yet come first"
|
|
150
|
+
: "reading as ADOPTION - values your system already names come first";
|
|
151
|
+
const where = source.from === "flag"
|
|
152
|
+
? "this run only"
|
|
153
|
+
: source.from === "config"
|
|
154
|
+
? `set in _synthesisui/config.json${source.at ? ` on ${source.at.slice(0, 10)}` : ""}`
|
|
155
|
+
: "nobody declared one, so this is the default";
|
|
156
|
+
const flip = source.intent === "migrate"
|
|
157
|
+
? "`doctor --adopt` flips it"
|
|
158
|
+
: "`doctor --migrate` flips it";
|
|
159
|
+
return `${what} · ${where} · ${flip}`;
|
|
160
|
+
}
|
|
107
161
|
/** Writes the project config (committable, plain JSON). */
|
|
108
162
|
export async function writeProjectConfig(root, config) {
|
|
109
163
|
const path = projectConfigPath(root);
|
package/dist/doctor/frozen.js
CHANGED
|
@@ -19,7 +19,19 @@
|
|
|
19
19
|
* actually differs between the two schemes; a role that is the same in both
|
|
20
20
|
* costs nothing to spell either way, and this stays quiet about it.
|
|
21
21
|
*/
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* A GRAMÁTICA DE UMA REF DE PRIMITIVA, e ela recusava hífen nos DOIS segmentos.
|
|
24
|
+
*
|
|
25
|
+
* Contada em 06/08: a mesma pergunta era feita por quatro regexes e as quatro discordavam - duas
|
|
26
|
+
* aceitavam hífen só na família, uma aceitava nos dois, e esta em nenhum. Consequência medida no repo do
|
|
27
|
+
* dono, que tem as famílias `royal-blue`, `vivid-pink` e `electric-indigo`: toda ref para elas parseava
|
|
28
|
+
* como nada aqui, e esta checagem passava calada por cima delas - um achado que não existe é
|
|
29
|
+
* indistinguível de um achado que não há.
|
|
30
|
+
*
|
|
31
|
+
* Gêmea de `PRIMITIVE_REF_PATTERN` em `libs/ds-contracts/src/token-ref.ts`, copiada e não importada
|
|
32
|
+
* porque o CLI é publicado standalone. O spec ao lado assere a fonte da regex nos dois lados.
|
|
33
|
+
*/
|
|
34
|
+
const PRIMITIVE = /^\{color\.([a-z0-9-]+)\.([a-z0-9-]+)\}$/i;
|
|
23
35
|
const BACKGROUND = /^background(-color)?$/;
|
|
24
36
|
const kebab = (s) => s.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
25
37
|
function* colorBindings(node, where) {
|
|
@@ -79,9 +79,16 @@ export function conditionOf(modifiers) {
|
|
|
79
79
|
const out = {};
|
|
80
80
|
for (const raw of modifiers) {
|
|
81
81
|
if (raw === "dark") {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
/**
|
|
83
|
+
* O ESQUEMA É UMA CONDIÇÃO, e o layer tem slot para ele.
|
|
84
|
+
*
|
|
85
|
+
* O comentário anterior dizia "the dark half has its own home in the transcription and no slot
|
|
86
|
+
* inside a layer" - verdade quando foi escrito, e falso desde que o layer passou a carregar
|
|
87
|
+
* `scheme`. A "casa na transcrição" só existe para a classe INCONDICIONAL: uma `dark:` dentro de
|
|
88
|
+
* um record por variante passava por aqui e morria, e é assim que 9 utilities do Button e do
|
|
89
|
+
* Toggle sumiram com uma linha de aviso e outras 66 sem nenhuma (dono, 06/08).
|
|
90
|
+
*/
|
|
91
|
+
out.scheme = "dark";
|
|
85
92
|
continue;
|
|
86
93
|
}
|
|
87
94
|
const relational = RELATIONAL.exec(raw);
|
|
@@ -121,7 +128,8 @@ function keyOf(c, variant) {
|
|
|
121
128
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
122
129
|
.map(([k, o]) => `${k}=${o}`)
|
|
123
130
|
.join("&");
|
|
124
|
-
|
|
131
|
+
/** O ESQUEMA ENTRA NA CHAVE: sem ele, `bg-white` e `dark:bg-black` caem no mesmo layer e um vence. */
|
|
132
|
+
return `${v}|${c.state ?? ""}|${c.within ?? ""}|${c.at ?? ""}|${c.scheme ?? ""}`;
|
|
125
133
|
}
|
|
126
134
|
/**
|
|
127
135
|
* Split a class list into the base and the conditional layers it contains.
|
|
@@ -157,12 +165,13 @@ export function splitByCondition(classes, variant) {
|
|
|
157
165
|
const key = keyOf(cond, variant);
|
|
158
166
|
const layer = layers.get(key) ??
|
|
159
167
|
{
|
|
160
|
-
...(variant || cond.state || cond.within
|
|
168
|
+
...(variant || cond.state || cond.within || cond.scheme
|
|
161
169
|
? {
|
|
162
170
|
when: {
|
|
163
171
|
...(variant ? { variant } : {}),
|
|
164
172
|
...(cond.state ? { state: cond.state } : {}),
|
|
165
173
|
...(cond.within ? { within: cond.within } : {}),
|
|
174
|
+
...(cond.scheme ? { scheme: cond.scheme } : {}),
|
|
166
175
|
},
|
|
167
176
|
}
|
|
168
177
|
: {}),
|
|
@@ -623,6 +632,31 @@ function objectFields(value) {
|
|
|
623
632
|
return [];
|
|
624
633
|
return topLevelEntries(trimmed.slice(1, end - 1));
|
|
625
634
|
}
|
|
635
|
+
/**
|
|
636
|
+
* TEM A FORMA DE UMA LISTA DE CLASSES?
|
|
637
|
+
*
|
|
638
|
+
* Usada só no resíduo: quando nenhuma prop de classe consome o campo, esta é a última pergunta antes de
|
|
639
|
+
* afirmar que ele é estilo. Ela é deliberadamente TÍMIDA - dois tokens no mínimo, tudo minúsculo, nada
|
|
640
|
+
* começando com `#`:
|
|
641
|
+
*
|
|
642
|
+
* "px-2 py-2 gap-0.5" 3 tokens, minusculo -> lista de classes
|
|
643
|
+
* "overline" 1 token -> silencio (e o `variant` de um <Text>)
|
|
644
|
+
* "body-md" 1 token -> silencio, e indistinguivel de `text-xs`
|
|
645
|
+
* "#1394DC" comeca com # -> silencio (cor de grafico)
|
|
646
|
+
* "Waiting" maiuscula -> silencio (conteudo)
|
|
647
|
+
*
|
|
648
|
+
* Ela erra CALANDO sobre uma classe única e não-consumida, e esse é o lado certo de errar: uma linha
|
|
649
|
+
* inventada num relatório custa a confiança na linha seguinte, que estava certa.
|
|
650
|
+
*/
|
|
651
|
+
function looksLikeClassList(value) {
|
|
652
|
+
const text = value.trim().replace(/^[`'"]|[`'"]$/g, "");
|
|
653
|
+
if (!text || text.startsWith("#"))
|
|
654
|
+
return false;
|
|
655
|
+
const tokens = text.split(/\s+/).filter(Boolean);
|
|
656
|
+
if (tokens.length < 2)
|
|
657
|
+
return false;
|
|
658
|
+
return tokens.every((t) => /^[a-z0-9[\]()/:._!-]+$/.test(t));
|
|
659
|
+
}
|
|
626
660
|
/**
|
|
627
661
|
* THE STATIC CLASSES OF THE `className` CALL THAT CONSUMES A LOOKUP.
|
|
628
662
|
*
|
|
@@ -636,12 +670,34 @@ function objectFields(value) {
|
|
|
636
670
|
* local first (`const config = statusConfig[status]`); a record whose value is a plain
|
|
637
671
|
* string has no field, and there the indexing expression is what is written at the call
|
|
638
672
|
* site (`variantColors[variant]`), so the SYMBOL is the reliable half.
|
|
673
|
+
*
|
|
674
|
+
* E DUAS COISAS QUE FALTAVAM, medidas em 8 de 8 casos de um repo real (dono, 06/08) - nenhum deles
|
|
675
|
+
* escrevia `simbolo.campo` no ponto de consumo:
|
|
676
|
+
*
|
|
677
|
+
* 1. A PROP NÃO É SÓ `className`. O MetricCard deles passa o campo em
|
|
678
|
+
* `contentClassName={cn("p-0 flex flex-col", currentSize.container)}` - é o Card deles que recebe
|
|
679
|
+
* a classe por outro nome, e exigir `className` literal descartava a linha inteira.
|
|
680
|
+
* 2. O CAMPO PODE VIR DESESTRUTURADO. O CircularProgress escreve
|
|
681
|
+
* `const { diameter, strokeWidth, fontSize } = sizeConfig[size]` e depois usa `fontSize` NU dentro
|
|
682
|
+
* do `cn()` - sem ponto nenhum, então a forma `<algo>.campo` não casa.
|
|
683
|
+
*
|
|
684
|
+
* As duas juntas são a diferença entre colocar o look no elemento certo e reportá-lo como "não há
|
|
685
|
+
* elemento onde pôr".
|
|
639
686
|
*/
|
|
640
687
|
function callSiteClasses(source, symbol, field) {
|
|
688
|
+
/** Desestruturado da própria tabela: `const { fontSize } = sizeConfig[size]`. */
|
|
689
|
+
const destructured = field != null &&
|
|
690
|
+
new RegExp(`\\{[^}]*\\b${field}\\b[^}]*\\}\\s*=\\s*${symbol}\\s*\\[`).test(source);
|
|
641
691
|
const use = field
|
|
642
|
-
? new RegExp(
|
|
692
|
+
? new RegExp(destructured
|
|
693
|
+
? `\\b${field}\\b`
|
|
694
|
+
: `\\b[A-Za-z_$][\\w$]*(?:\\[[^\\]]*\\])?\\.${field}\\b`)
|
|
643
695
|
: new RegExp(`\\b${symbol}\\s*\\[`);
|
|
644
|
-
|
|
696
|
+
/**
|
|
697
|
+
* `className` E QUALQUER `*ClassName`, que é como um componente deles recebe a classe de um filho.
|
|
698
|
+
* A borda `(?:^|[^A-Za-z])` existe para não casar no meio de um identificador maior.
|
|
699
|
+
*/
|
|
700
|
+
for (const m of source.matchAll(/(?:^|[^A-Za-z])[A-Za-z]*[Cc]lassName=\{\s*(?:[A-Za-z_$][\w$]*\.)?(?:cn|clsx|classNames|cx|twMerge|twJoin|classcat)\(/g)) {
|
|
645
701
|
const open = (m.index ?? 0) + m[0].length - 1;
|
|
646
702
|
const end = endOf(source, open);
|
|
647
703
|
if (end === -1)
|
|
@@ -787,11 +843,23 @@ unattributed) {
|
|
|
787
843
|
const on = callSiteClasses(clean, symbol, field.key);
|
|
788
844
|
if (!on) {
|
|
789
845
|
/**
|
|
790
|
-
* NOWHERE TO PUT IT IS NOT THE ROOT. A field no
|
|
791
|
-
*
|
|
792
|
-
*
|
|
846
|
+
* NOWHERE TO PUT IT IS NOT THE ROOT. A field no class prop claims is a decision we cannot
|
|
847
|
+
* place, and placing it on the component would paint the wrong element - which is the whole
|
|
848
|
+
* bug this reads around.
|
|
849
|
+
*
|
|
850
|
+
* MAS SÓ SE FOR UMA LISTA DE CLASSES. Medido em 8 de 8 casos de um repo real (dono, 06/08),
|
|
851
|
+
* SEIS dos campos não eram estilo nenhum: `labelVariant: "overline"` e `valueVariant:
|
|
852
|
+
* "body-md"` são o `variant` de um `<Text>`, `text: "Waiting"` é conteúdo, e
|
|
853
|
+
* `start: "#1394DC"` é a cor de um gráfico. Chamá-los de "look que não pôde ser colocado" é
|
|
854
|
+
* inventar um problema, e foi o que fez uma causa de 33 ocorrências ter 6 falsos positivos.
|
|
855
|
+
*
|
|
856
|
+
* A regra é o que sobra depois do consumo: se NENHUMA prop de classe consome o campo E o
|
|
857
|
+
* valor não tem a forma de uma lista de classes, a gente não sabe que é estilo - e o que não
|
|
858
|
+
* se sabe não se afirma. Um valor de um token só fica em silêncio de propósito: `overline` e
|
|
859
|
+
* `text-xs` são indistinguíveis pela forma, e o consumo era a única prova.
|
|
793
860
|
*/
|
|
794
|
-
|
|
861
|
+
if (looksLikeClassList(field.value))
|
|
862
|
+
unattributed?.push(`\`${symbol}.${field.key}\` is a field of a per-element table that no class prop consumes, so there is no element to put it on`);
|
|
795
863
|
continue;
|
|
796
864
|
}
|
|
797
865
|
const per = splitByCondition(classes, { [axis]: option.key });
|
|
@@ -856,6 +924,7 @@ export function transcribeVariants(source, declared) {
|
|
|
856
924
|
const reads = readVariants(source);
|
|
857
925
|
const fromRecords = {};
|
|
858
926
|
const unslotted = [];
|
|
927
|
+
const notes = [];
|
|
859
928
|
const inline = [
|
|
860
929
|
...readInlineConditions(source),
|
|
861
930
|
// The two shapes the enumeration found to be five times more common than `cva`.
|
|
@@ -925,7 +994,11 @@ export function transcribeVariants(source, declared) {
|
|
|
925
994
|
continue;
|
|
926
995
|
}
|
|
927
996
|
const attr = axis.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
928
|
-
|
|
997
|
+
/**
|
|
998
|
+
* NOTA, NÃO PERDA. O layer está na receita; o que falta é um atributo no elemento para ele aplicar.
|
|
999
|
+
* Enquanto isto viajava em `unslotted`, a tela contava 16 destes como pendência de conserto.
|
|
1000
|
+
*/
|
|
1001
|
+
notes.push(named.length === 0
|
|
929
1002
|
? `\`${axis}\` gates a look and is a boolean rather than an axis - the layer is kept, and \`data-${attr}\` has to be set for it to apply`
|
|
930
1003
|
: `\`${axis}\` gates a look on one option (\`${named[0]}\`) - one option is not an axis, so the layer is kept and \`data-${attr}="${named[0]}"\` has to be set for it to apply`);
|
|
931
1004
|
}
|
|
@@ -957,5 +1030,6 @@ export function transcribeVariants(source, declared) {
|
|
|
957
1030
|
layers,
|
|
958
1031
|
base: transcribe(baseClasses, declared),
|
|
959
1032
|
unslotted,
|
|
1033
|
+
notes,
|
|
960
1034
|
};
|
|
961
1035
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
|
+
import { absorb } from "./commands/absorb.js";
|
|
3
4
|
import { add } from "./commands/add.js";
|
|
4
5
|
import { adopt } from "./commands/adopt.js";
|
|
5
6
|
import { advise } from "./commands/advise.js";
|
|
@@ -19,6 +20,7 @@ import { mcp } from "./commands/mcp.js";
|
|
|
19
20
|
import { refit } from "./commands/refit.js";
|
|
20
21
|
import { request } from "./commands/request.js";
|
|
21
22
|
import { status } from "./commands/status.js";
|
|
23
|
+
import { summary } from "./commands/summary.js";
|
|
22
24
|
import { sync } from "./commands/sync.js";
|
|
23
25
|
import { template } from "./commands/template.js";
|
|
24
26
|
import { upgrade } from "./commands/upgrade.js";
|
|
@@ -99,6 +101,10 @@ Options:
|
|
|
99
101
|
--strict doctor: exit 1 when drift is found in THIS repo (for CI)
|
|
100
102
|
--strict-system doctor: also exit 1 when the system itself is inconsistent
|
|
101
103
|
--verbose doctor: every finding, file by file (default is a summary)
|
|
104
|
+
--send absorb: send the batch you reviewed (default writes the file)
|
|
105
|
+
--migrate doctor: lead with what your system does NOT name yet - the
|
|
106
|
+
order for moving design INTO a shared package
|
|
107
|
+
--adopt doctor: lead with what your system already names (default)
|
|
102
108
|
--laws doctor: show every usage law, not just the busiest components
|
|
103
109
|
--out <path> output path for the generated template (default: <pagesDir>/<file>)
|
|
104
110
|
-h, --help this help
|
|
@@ -279,9 +285,57 @@ async function main() {
|
|
|
279
285
|
*/
|
|
280
286
|
...(flags.write === true ? { write: true } : {}),
|
|
281
287
|
...(flags["write-baseline"] === true ? { writeBaseline: true } : {}),
|
|
288
|
+
/**
|
|
289
|
+
* A INTENÇÃO DESTA RODADA - atalho para `config.json`, ver `intentOf`.
|
|
290
|
+
*
|
|
291
|
+
* Duas flags e não um valor (`--intent=migrate`), porque o que se digita no meio de uma tarefa
|
|
292
|
+
* é uma palavra: `--migrate` ou `--adopt`.
|
|
293
|
+
*/
|
|
294
|
+
...(flags.migrate === true
|
|
295
|
+
? { intent: "migrate" }
|
|
296
|
+
: flags.adopt === true
|
|
297
|
+
? { intent: "adopt" }
|
|
298
|
+
: {}),
|
|
282
299
|
cli: CLI_VERSION,
|
|
283
300
|
});
|
|
284
301
|
break;
|
|
302
|
+
case "absorb":
|
|
303
|
+
/**
|
|
304
|
+
* O OUTRO SENTIDO: o sistema aprende o design que o código já tem - ver `commands/absorb.ts`.
|
|
305
|
+
*
|
|
306
|
+
* Sem flag ESCREVE uma proposta e não fala com a rede; `--send` manda o lote. A fase do meio é
|
|
307
|
+
* humana de propósito: nomear uma cor é decisão de design.
|
|
308
|
+
*/
|
|
309
|
+
await absorb({
|
|
310
|
+
dir,
|
|
311
|
+
registry,
|
|
312
|
+
...(flags.send === true ? { send: true } : {}),
|
|
313
|
+
});
|
|
314
|
+
break;
|
|
315
|
+
case "summary": {
|
|
316
|
+
/**
|
|
317
|
+
* AS QUATRO LINHAS DO PRIMEIRO MINUTO - ver `commands/summary.ts`.
|
|
318
|
+
*
|
|
319
|
+
* Existe para tirar o fechamento do `sui-init` das mãos do agente: ele terminou com ~400 palavras
|
|
320
|
+
* sobre o próprio trabalho, incluindo duas advertências já desatualizadas (dono, 06/08).
|
|
321
|
+
*/
|
|
322
|
+
/** `args` já exclui o comando - o slug é o primeiro posicional, como em `template <slug>`. */
|
|
323
|
+
const target = args[0];
|
|
324
|
+
if (!target) {
|
|
325
|
+
console.log("usage: synthesisui summary <slug> [--base N --draft N --fixes N]");
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
const num = (v) => typeof v === "string" && /^\d+$/.test(v)
|
|
329
|
+
? Number.parseInt(v, 10)
|
|
330
|
+
: undefined;
|
|
331
|
+
await summary(target, {
|
|
332
|
+
dir,
|
|
333
|
+
...(num(flags.base) != null ? { base: num(flags.base) } : {}),
|
|
334
|
+
...(num(flags.draft) != null ? { draft: num(flags.draft) } : {}),
|
|
335
|
+
...(num(flags.fixes) != null ? { fixes: num(flags.fixes) } : {}),
|
|
336
|
+
});
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
285
339
|
case "status":
|
|
286
340
|
/** A resposta do dia a dia no terminal - ver `commands/status.ts`. Local primeiro. */
|
|
287
341
|
await status({ dir, registry, cli: CLI_VERSION });
|
|
@@ -338,11 +392,28 @@ async function main() {
|
|
|
338
392
|
: undefined;
|
|
339
393
|
const ds = typeof flags.ds === "string" ? flags.ds : undefined;
|
|
340
394
|
const styles = typeof flags.styles === "string" ? flags.styles : undefined;
|
|
395
|
+
/**
|
|
396
|
+
* A INTENÇÃO, gravada com a data - ver `ProjectConfig.intent`.
|
|
397
|
+
*
|
|
398
|
+
* Aceita as duas formas porque as duas são naturais: `--intent migrate` quando um agente monta o
|
|
399
|
+
* comando, `--migrate` quando uma pessoa digita.
|
|
400
|
+
*/
|
|
401
|
+
const intent = typeof flags.intent === "string"
|
|
402
|
+
? flags.intent
|
|
403
|
+
: flags.migrate === true
|
|
404
|
+
? "migrate"
|
|
405
|
+
: flags.adopt === true
|
|
406
|
+
? "adopt"
|
|
407
|
+
: undefined;
|
|
341
408
|
await init({
|
|
342
409
|
dir,
|
|
343
410
|
registry,
|
|
344
411
|
target,
|
|
345
412
|
pagesDir,
|
|
413
|
+
...(flags.absorb === "code" || flags.absorb === "system"
|
|
414
|
+
? { absorb: flags.absorb }
|
|
415
|
+
: {}),
|
|
416
|
+
...(intent ? { intent } : {}),
|
|
346
417
|
componentsDir,
|
|
347
418
|
styles,
|
|
348
419
|
ds,
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
const EMPTY = { system: null, usage: [], from: "none" };
|
|
4
|
+
/** Existe como diretório? Um escopo gravado e depois renomeado não pode derrubar o comando. */
|
|
5
|
+
async function present(root, rel) {
|
|
6
|
+
return stat(resolve(root, rel)).then((s) => s.isDirectory(), () => false);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Lê `_synthesisui/census.json` e devolve o que ele diz sobre ONDE.
|
|
10
|
+
*
|
|
11
|
+
* Só caminhos que ainda existem: o censo é uma foto de quando o import rodou, e uma pasta renomeada
|
|
12
|
+
* desde então não pode fazer o doctor estourar - ela simplesmente sai da lista, e a lista vazia cai no
|
|
13
|
+
* comportamento antigo (a raiz).
|
|
14
|
+
*/
|
|
15
|
+
export async function measuredScope(root) {
|
|
16
|
+
const raw = await readFile(join(root, "_synthesisui", "census.json"), "utf8").catch(() => null);
|
|
17
|
+
if (!raw)
|
|
18
|
+
return EMPTY;
|
|
19
|
+
let parsed;
|
|
20
|
+
try {
|
|
21
|
+
parsed = JSON.parse(raw);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return EMPTY;
|
|
25
|
+
}
|
|
26
|
+
const scope = typeof parsed.scope === "string" && parsed.scope.trim()
|
|
27
|
+
? parsed.scope.trim()
|
|
28
|
+
: null;
|
|
29
|
+
const usage = Array.isArray(parsed.usage)
|
|
30
|
+
? parsed.usage.filter((u) => typeof u === "string" && !!u.trim())
|
|
31
|
+
: [];
|
|
32
|
+
const system = scope && (await present(root, scope)) ? scope : null;
|
|
33
|
+
const kept = [];
|
|
34
|
+
for (const u of usage)
|
|
35
|
+
if (await present(root, u))
|
|
36
|
+
kept.push(u);
|
|
37
|
+
if (!system && kept.length === 0)
|
|
38
|
+
return EMPTY;
|
|
39
|
+
return { system, usage: kept, from: "census" };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* As pastas a LER, na ordem em que importam - o sistema primeiro.
|
|
43
|
+
*
|
|
44
|
+
* A ordem não é cosmética: ela decide qual arquivo entra primeiro no relatório e, com teto de
|
|
45
|
+
* anotações no CI, decide o que chega ao PR.
|
|
46
|
+
*/
|
|
47
|
+
export function scopePaths(m) {
|
|
48
|
+
return [...(m.system ? [m.system] : []), ...m.usage];
|
|
49
|
+
}
|
|
50
|
+
/** Como dizer o que foi lido. Uma leitura que não declara o universo é um número sem denominador. */
|
|
51
|
+
export function describeScope(m) {
|
|
52
|
+
if (m.from === "none")
|
|
53
|
+
return null;
|
|
54
|
+
const parts = [
|
|
55
|
+
...(m.system ? [`${m.system} (the system)`] : []),
|
|
56
|
+
...(m.usage.length > 0
|
|
57
|
+
? [
|
|
58
|
+
`${m.usage.join(", ")} (${m.usage.length === 1 ? "consumer" : "consumers"})`,
|
|
59
|
+
]
|
|
60
|
+
: []),
|
|
61
|
+
];
|
|
62
|
+
return `read from your census: ${parts.join(" · ")}`;
|
|
63
|
+
}
|