synthesisui 0.16.277 → 0.16.278

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.
@@ -8,6 +8,7 @@ import { hasInteractiveTemplate, interactiveTemplate, } from "../interactive-tem
8
8
  import { body, section, snippet } from "../output.js";
9
9
  import { findCollision, reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
10
10
  import { fetchComponent, RegistryError } from "../registry.js";
11
+ import { readCensus, unreadComment, unreadForComponent, } from "../unread-for-component.js";
11
12
  /**
12
13
  * Writes the shared `cn.ts` next to the components, built from THIS project's
13
14
  * installed theme.
@@ -165,8 +166,21 @@ export async function component(slug, name, opts) {
165
166
  * construction. Falling back to disk keeps an older registry working.
166
167
  */
167
168
  res.classNames ?? (await readInstalledConvention(root, slug)), res.name, await readInstalledScheme(root, slug));
169
+ /**
170
+ * AS DECLARAÇÕES DO ARQUIVO DELE QUE O INTERPRETADOR NÃO LEU - ver `unread-for-component.ts`.
171
+ *
172
+ * Sem isto ele recebe o componente sem elas e nada diz que faltou algo: ele descobre
173
+ * comparando o resultado com o código original. Elas saem em comentário no `.tsx`, com
174
+ * caminho e linha, porque a plataforma não sabe a qual parte do componente elas se aplicam -
175
+ * escrevê-las como estilo seria supor o destino e entregar um componente errado em vez de um
176
+ * incompleto.
177
+ *
178
+ * Vazio não vira comentário, e o censo ausente não é erro: um projeto que só instalou o
179
+ * sistema não tem censo nenhum, e ele recebe o que sempre recebeu.
180
+ */
181
+ const nota = unreadComment(unreadForComponent(await readCensus(root), res.name));
168
182
  for (const file of files) {
169
- await writeFile(join(compDir, file.filename), file.code, "utf8");
183
+ await writeFile(join(compDir, file.filename), file.filename.endsWith(".tsx") ? `${nota}${file.code}` : file.code, "utf8");
170
184
  }
171
185
  if (config.styles === "tailwind")
172
186
  await writeCn(root, compDir, slug);
@@ -151,7 +151,7 @@ const TOOLS = [
151
151
  },
152
152
  {
153
153
  name: "list_components",
154
- description: "The components this design system defines, with what each is for. Look here BEFORE writing any UI element from scratch - if something covers the purpose, materialize it with add_component instead.",
154
+ description: "The components this design system defines, with what each is for. Look here BEFORE writing any UI element from scratch - if something covers the purpose, materialize it with add_blueprint instead.",
155
155
  inputSchema: { type: "object", properties: {} },
156
156
  },
157
157
  {
@@ -169,7 +169,7 @@ const TOOLS = [
169
169
  },
170
170
  },
171
171
  {
172
- name: "add_component",
172
+ name: "add_blueprint",
173
173
  description: "Materialize a component from the design system as real typed code in this project, ready to import and extend. Use it yourself - the person who asked for a feature should never have to know component names.",
174
174
  inputSchema: {
175
175
  type: "object",
@@ -570,7 +570,7 @@ async function listComponents(root) {
570
570
  if (rows.length === 0)
571
571
  return "This system defines no components yet - write what you need with its tokens.";
572
572
  return [
573
- `${rows.length} components. Materialize with add_component before writing one from scratch:`,
573
+ `${rows.length} components. Materialize with add_blueprint before writing one from scratch:`,
574
574
  "",
575
575
  ...rows.sort(),
576
576
  ].join("\n");
@@ -1311,7 +1311,7 @@ async function describeComponent(root, name) {
1311
1311
  if (states.length > 0) {
1312
1312
  out.push(` states the recipe already carries: ${states.join(", ")} - they are CSS, so do not write them again in JS.`);
1313
1313
  }
1314
- out.push(` Or call add_component { "name": "${name}" } and get it as typed code, one prop per axis.`);
1314
+ out.push(` Or call add_blueprint { "name": "${name}" } and get it as typed code, one prop per axis.`);
1315
1315
  }
1316
1316
  /**
1317
1317
  * AS INSTRUÇÕES DE RENDER QUE A LEITURA MEDIU - e até aqui elas morriam no
@@ -1387,6 +1387,13 @@ async function addComponent(root, name) {
1387
1387
  */
1388
1388
  const BY_COMPONENT = {
1389
1389
  describe_component: "describe",
1390
+ /**
1391
+ * OS DOIS NOMES GRAVAM `add`, e é isso que impede a métrica de se dividir em duas.
1392
+ *
1393
+ * Sem esta linha o uso da ferramenta cairia pela metade no dia do apelido sem nada ter caído - e
1394
+ * uma métrica que despenca por causa de um rename é uma métrica que ninguém confia depois.
1395
+ */
1396
+ add_blueprint: "add",
1390
1397
  add_component: "add",
1391
1398
  };
1392
1399
  /**
@@ -1419,6 +1426,7 @@ const BY_COMPONENT = {
1419
1426
  */
1420
1427
  const SUBJECT_ARG = {
1421
1428
  describe_component: "name",
1429
+ add_blueprint: "name",
1422
1430
  add_component: "name",
1423
1431
  /** A família pedida - o sinal que o A1 precisa e que não estava sendo guardado. */
1424
1432
  compose_context: "family",
@@ -1520,6 +1528,18 @@ cli) {
1520
1528
  return fromContract(await listComponents(root));
1521
1529
  case "describe_component":
1522
1530
  return describeComponent(root, String(args.name ?? ""));
1531
+ /**
1532
+ * OS DOIS NOMES CHAMAM A MESMA FUNÇÃO, e o antigo sai numa versão futura.
1533
+ *
1534
+ * `blueprint` é o nome público do que a plataforma produz, e `add_component` era o único lugar
1535
+ * da superfície pública que ainda usava o nome antigo. Renomear SUBSTITUINDO quebraria o agente
1536
+ * que já carregou a lista de ferramentas na sessão em curso: ele chamaria um nome que não
1537
+ * existe mais e a sessão dele falharia sem explicação.
1538
+ *
1539
+ * A remoção do apelido fica para quando a telemetria mostrar que ninguém mais o chama - e aí
1540
+ * ela é medida, não estimada.
1541
+ */
1542
+ case "add_blueprint":
1523
1543
  case "add_component":
1524
1544
  return addComponent(root, String(args.name ?? ""));
1525
1545
  case "playbook": {
@@ -9,6 +9,7 @@ import { installedBehind, MATERIALISER_SINCE } from "../install-marks.js";
9
9
  import { body, section, snippet } from "../output.js";
10
10
  import { reactMajorOf, readInstalledConvention, readInstalledScheme, } from "../project-facts.js";
11
11
  import { fetchChangelog, fetchComponent, fetchDesignSystem, RegistryError, } from "../registry.js";
12
+ import { readCensus, unreadComment, unreadForComponent, } from "../unread-for-component.js";
12
13
  import { add } from "./add.js";
13
14
  import { reportWhatIsLeft } from "./align.js";
14
15
  import { doctor } from "./doctor.js";
@@ -301,8 +302,18 @@ export async function upgrade(asked, opts) {
301
302
  // components somebody already has: without the convention it would have
302
303
  // taken a working component and stripped its styles.
303
304
  await reactMajorOf(root), res.classNames ?? (await readInstalledConvention(root, slug)), res.name, await readInstalledScheme(root, slug));
305
+ /**
306
+ * A NOTA DAS DECLARAÇÕES NÃO INTERPRETADAS SOBREVIVE AO UPGRADE.
307
+ *
308
+ * Este comando REESCREVE componentes que a pessoa já tem. Sem esta linha, a nota apareceria
309
+ * no `component`/`add_blueprint` e sumiria no próximo `upgrade` - o cliente veria a
310
+ * informação uma vez e a perderia sem nada dizer que ela existiu.
311
+ *
312
+ * Ver `unread-for-component.ts`: censo ausente responde vazio, e vazio não vira comentário.
313
+ */
314
+ const nota = unreadComment(unreadForComponent(await readCensus(root), res.name));
304
315
  for (const file of files) {
305
- await writeFile(join(componentsRoot, entry, file.filename), file.code, "utf8");
316
+ await writeFile(join(componentsRoot, entry, file.filename), file.filename.endsWith(".tsx") ? `${nota}${file.code}` : file.code, "utf8");
306
317
  }
307
318
  regenerated.push(entry);
308
319
  }
package/dist/index.js CHANGED
@@ -44,6 +44,7 @@ Usage - deterministic, FREE:
44
44
  synthesisui list [options] list the published design systems
45
45
  synthesisui list --mine your own systems, with their group
46
46
  synthesisui add <slug> [options] materialize a DS into _synthesisui/ds/<slug>/
47
+ synthesisui bp <slug> [options] the same command, short for blueprint
47
48
  synthesisui component <slug> <name> bring one EXISTING component in as YOUR <Pascal>.tsx
48
49
  synthesisui template <slug> <name> materialize a whole page from a DS template
49
50
  (--as landing-home names the output - multi-page safe)
@@ -348,6 +349,15 @@ async function main() {
348
349
  */
349
350
  await list({ registry, mine: flags.mine === true });
350
351
  break;
352
+ /**
353
+ * `bp` É O MESMO COMANDO QUE `add`, e o apelido existe porque `blueprint` é o nome público do
354
+ * que a plataforma produz.
355
+ *
356
+ * Apelido e não substituição: `add` está escrito em toda mensagem que o CLI já imprimiu, em
357
+ * todo GUIDE.md já materializado e na cabeça de quem usa. Trocá-lo faria um comando que a pessoa
358
+ * digitou ontem parar de existir hoje.
359
+ */
360
+ case "bp":
351
361
  case "add": {
352
362
  const slug = args[0];
353
363
  if (!slug) {
@@ -128,7 +128,7 @@
128
128
  * é sempre o bump deste PR - nunca o número que o `package.json` já carrega, porque alguém pode
129
129
  * publicar no meio.
130
130
  */
131
- export const MATERIALISER_SINCE = "0.16.276";
131
+ export const MATERIALISER_SINCE = "0.16.278";
132
132
  /**
133
133
  * A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
134
134
  *
@@ -0,0 +1,127 @@
1
+ /**
2
+ * AS DECLARAÇÕES QUE O INTERPRETADOR NÃO LEU, entregues junto com o componente.
3
+ *
4
+ * O QUE O CLIENTE GANHA. Ele recebe o componente com as declarações que a plataforma interpretou,
5
+ * mais as que ela não interpretou, identificadas com arquivo e linha. Hoje ele recebe só as
6
+ * primeiras e nada diz que faltou algo - ele descobre comparando o resultado com o código original.
7
+ *
8
+ * O QUE MUDA NO CÓDIGO. O gerador passa a ler `ledger.unread[].unreadable` do censo e a casar o
9
+ * arquivo de cada declaração com o arquivo do componente. As que casam saem em comentário no arquivo
10
+ * gerado.
11
+ *
12
+ * O QUE O CLIENTE PERCEBE. O mesmo componente que ele recebe hoje, mais um bloco dizendo quais
13
+ * declarações do arquivo dele não estão governadas por token ainda - com o caminho e a linha, para
14
+ * ele decidir o que fazer. Ele não para de codificar por causa de uma lacuna nossa.
15
+ *
16
+ * POR QUE COMENTÁRIO E NÃO CÓDIGO. Uma declaração não interpretada não tem lugar conhecido na
17
+ * recipe: a plataforma não sabe a qual parte do componente ela se aplica. Escrevê-la como estilo
18
+ * seria supor onde ela vai, e supor aqui produz um componente errado em vez de um incompleto. O
19
+ * comentário entrega a informação sem inventar o destino.
20
+ *
21
+ * O CAMINHO PRECISA DO PREFIXO DO ESCOPO. `ledger.unread[].unreadable[].file` é relativo ao escopo
22
+ * medido (`src/Card/Card.tsx`) e `looks[X].api.file` é relativo à raiz do repositório
23
+ * (`packages/ui/src/Card/Card.tsx`). Comparar os dois direto não casa nenhum - medido no censo do
24
+ * cliente: 0 casam sem o prefixo, 4 casam com ele.
25
+ *
26
+ * E A MAIOR PARTE NÃO É DE COMPONENTE NENHUM. Das 194 declarações não interpretadas daquele censo,
27
+ * 190 estão no `globals.css` - reset, `@theme`, `@utility` - que não veste componente. Elas têm
28
+ * destino próprio (a nota do sistema) e não podem entrar aqui: 190 linhas de reset dentro de um
29
+ * `Card` é ruído que faz alguém parar de ler o bloco inteiro.
30
+ */
31
+ import { readFile } from "node:fs/promises";
32
+ import { join } from "node:path";
33
+ /**
34
+ * AS DECLARAÇÕES NÃO INTERPRETADAS DO ARQUIVO DAQUELE COMPONENTE.
35
+ *
36
+ * `name` é o nome do componente COMO O CENSO O CONHECE - a chave de `looks`. O chamador tem o slug
37
+ * da recipe (`metric-card`), então ele precisa resolver o nome antes; a resolução não mora aqui
38
+ * porque ela é do crosswalk, e duas cópias dela discordariam.
39
+ */
40
+ export function unreadForComponent(census, name) {
41
+ const file = census.looks?.[name]?.api?.file ?? fileForSlug(census, name);
42
+ if (!file)
43
+ return [];
44
+ const scope = census.scope ? `${census.scope}/` : "";
45
+ const out = [];
46
+ for (const group of census.ledger?.unread ?? []) {
47
+ for (const one of group.unreadable ?? []) {
48
+ if (!one.file || !one.text)
49
+ continue;
50
+ if (`${scope}${one.file}` !== file)
51
+ continue;
52
+ out.push({
53
+ file: one.file,
54
+ ...(typeof one.line === "number" ? { line: one.line } : {}),
55
+ text: one.text,
56
+ reason: group.reason ?? "unread",
57
+ });
58
+ }
59
+ }
60
+ return out;
61
+ }
62
+ /**
63
+ * O CENSO CONHECE O COMPONENTE PELO NOME DELE; QUEM CHAMA TEM O SLUG DA RECIPE.
64
+ *
65
+ * `MetricCard` no censo é `metric-card` na recipe, e o comando `add` recebe o segundo. A conversão
66
+ * é a mesma que `exclusive-contract.ts` faz do outro lado - se as duas divergirem, o componente
67
+ * certo perde as declarações dele e nenhum erro aparece.
68
+ *
69
+ * Busca reversa e não um mapa novo: o censo já tem os nomes, e guardar a correspondência seria uma
70
+ * segunda fonte para uma pergunta que a primeira já responde.
71
+ */
72
+ function fileForSlug(census, slug) {
73
+ const kebab = (v) => v
74
+ .split(".")
75
+ .join("-")
76
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
77
+ .toLowerCase()
78
+ .replace(/[^a-z0-9-]/g, "-")
79
+ .replace(/-+/g, "-")
80
+ .replace(/^-|-$/g, "");
81
+ for (const [name, look] of Object.entries(census.looks ?? {}))
82
+ if (kebab(name) === slug && look?.api?.file)
83
+ return look.api.file;
84
+ return undefined;
85
+ }
86
+ /**
87
+ * O BLOCO DE COMENTÁRIO, ou string vazia quando não há nada a dizer.
88
+ *
89
+ * Vazio não vira comentário: um bloco que aparece em todo arquivo dizendo "nada aqui" ensina a
90
+ * pular o bloco, e aí ele não é lido no arquivo onde ele importa.
91
+ *
92
+ * A frase diz as três coisas que decidem o que a pessoa faz: que aquilo NÃO está governado por
93
+ * token, ONDE está no código dela, e que a plataforma está resolvendo do lado dela. Sem a terceira,
94
+ * a leitura é "a plataforma não deu conta"; com ela, é "isto chega governado depois".
95
+ */
96
+ export function unreadComment(lines) {
97
+ if (lines.length === 0)
98
+ return "";
99
+ const rows = lines.map((l) => ` * ${l.file}${l.line ? `:${l.line}` : ""} ${l.text}`);
100
+ const n = lines.length;
101
+ return `/**
102
+ * ${n} declaration${n === 1 ? "" : "s"} from your own file ${n === 1 ? "was" : "were"} not interpreted, so ${n === 1 ? "it is" : "they are"} not
103
+ * governed by a token here yet. ${n === 1 ? "It" : "They"} still work${n === 1 ? "s" : ""} in your project - this is a gap in
104
+ * what this pipeline can read, not in your code, and it is being closed on our side.
105
+ *
106
+ ${rows.join("\n")}
107
+ */
108
+ `;
109
+ }
110
+ /**
111
+ * O CENSO DO PROJETO, ou vazio quando não existe.
112
+ *
113
+ * Um projeto que só instalou o sistema nunca rodou `import` e não tem censo. Isso não é erro: é o
114
+ * estado normal de quem CONSOME o design system sem alimentá-lo. Ele recebe o componente como
115
+ * sempre recebeu, sem a nota - e um erro aqui transformaria uma ausência normal em falha do comando.
116
+ */
117
+ export async function readCensus(root) {
118
+ const raw = await readFile(join(root, "_synthesisui", "census.json"), "utf8").catch(() => null);
119
+ if (!raw)
120
+ return {};
121
+ try {
122
+ return JSON.parse(raw);
123
+ }
124
+ catch {
125
+ return {};
126
+ }
127
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.277",
3
+ "version": "0.16.278",
4
4
  "description": "Bring SynthesisUI design systems into any project - tokens, typed components, whole pages and an agent-ready CLAUDE.md manifest.",
5
5
  "type": "module",
6
6
  "bin": {