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.
@@ -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);
@@ -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
- const PRIMITIVE = /^\{color\.([a-z0-9]+)\.([a-z0-9]+)\}$/i;
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
- // The dark half has its own home in the transcription and no slot inside a
83
- // layer. Saying so beats writing the light value into a dark rule.
84
- out.lost = true;
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
- return `${v}|${c.state ?? ""}|${c.within ?? ""}|${c.at ?? ""}`;
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(`\\b[A-Za-z_$][\\w$]*(?:\\[[^\\]]*\\])?\\.${field}\\b`)
692
+ ? new RegExp(destructured
693
+ ? `\\b${field}\\b`
694
+ : `\\b[A-Za-z_$][\\w$]*(?:\\[[^\\]]*\\])?\\.${field}\\b`)
643
695
  : new RegExp(`\\b${symbol}\\s*\\[`);
644
- for (const m of source.matchAll(/className=\{\s*(?:[A-Za-z_$][\w$]*\.)?(?:cn|clsx|classNames|cx|twMerge|twJoin|classcat)\(/g)) {
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 `className` claims is a
791
- * decision we cannot place, and placing it on the component would paint
792
- * the wrong element - which is the whole bug this reads around.
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
- unattributed?.push(`\`${symbol}.${field.key}\` is a field of a per-element table that no \`className\` call consumes, so there is no element to put it on`);
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
- unslotted.push(named.length === 0
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
+ }