synthesisui 0.16.172 → 0.16.176

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.
@@ -130,16 +130,39 @@ export async function add(slug, opts) {
130
130
  const measured = await censusScope(projectRoot);
131
131
  const scope = measured.system ?? prev?.scope ?? null;
132
132
  const usage = measured.usage.length > 0 ? measured.usage : (prev?.usage ?? []);
133
- const lock = {
133
+ /**
134
+ * `fetchedAt` SÓ ANDA QUANDO ALGO ATERROU - e a alternativa sujava o git de um time inteiro.
135
+ *
136
+ * O `.lock` é commitado, e este era o único campo não determinístico dele: tudo mais é função da
137
+ * versão, do compilador e das regras. Então cada `connect` de cada pessoa produzia um diff mesmo
138
+ * quando nada tinha mudado, e um comando que sempre gera commit é um comando que as pessoas param
139
+ * de rodar (dono, 07/08).
140
+ *
141
+ * O campo continua significando o que ele diz - quando esta instalação chegou -, e `repo-state` o
142
+ * manda para a plataforma como `installedAt`. Se nada chegou, ela não chegou de novo.
143
+ */
144
+ const identity = {
134
145
  slug: payload.slug,
135
146
  name: payload.name,
136
147
  version: payload.version,
137
148
  registry: base,
138
- fetchedAt: new Date().toISOString(),
139
149
  ...(opts.cli ? { cli: opts.cli } : {}),
150
+ /** Ver `RegistryPayload.compiler`: é o que faz um conserto de CSS chegar a um install. */
151
+ ...(payload.compiler != null ? { compiler: payload.compiler } : {}),
152
+ ...(payload.rulesStamp ? { rules: payload.rulesStamp } : {}),
140
153
  ...(scope ? { scope } : {}),
141
154
  ...(usage.length > 0 ? { usage } : {}),
142
155
  };
156
+ const landed = (() => {
157
+ if (!prev?.fetchedAt)
158
+ return true;
159
+ const { fetchedAt: _was, ...before } = prev;
160
+ return JSON.stringify(before) !== JSON.stringify(identity);
161
+ })();
162
+ const lock = {
163
+ ...identity,
164
+ fetchedAt: landed || !prev?.fetchedAt ? new Date().toISOString() : prev.fetchedAt,
165
+ };
143
166
  await writeFile(rootLockPath, `${JSON.stringify(lock, null, 2)}\n`, "utf8");
144
167
  await writeGovernanceIgnore(projectRoot);
145
168
  const retired = await retireMaterializedDoctrine(slugDir);
@@ -4,6 +4,25 @@ import { join, resolve } from "node:path";
4
4
  import { readToken, resolveRegistry } from "../config.js";
5
5
  import { readEvents } from "../doctor/ledger.js";
6
6
  import { measuredScope } from "../measured-scope.js";
7
+ import { READER } from "../reader-version.js";
8
+ /**
9
+ * Qual leitor mediu o censo em disco, quando ele o registra.
10
+ *
11
+ * `null` para censo anterior a esta marca: um censo que não diz quem o leu não prova nada, e supor
12
+ * que ele é velho faria toda pessoa que ainda não re-mediu ver o aviso para sempre.
13
+ */
14
+ async function censusReader(root) {
15
+ const raw = await readFile(join(root, "_synthesisui", "census.json"), "utf8").catch(() => null);
16
+ if (!raw)
17
+ return null;
18
+ try {
19
+ const c = JSON.parse(raw);
20
+ return typeof c.ledger?.reader === "number" ? c.ledger.reader : null;
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ }
7
26
  async function locksIn(root) {
8
27
  const dsDir = join(root, "_synthesisui", "ds");
9
28
  const entries = await readdir(dsDir, { withFileTypes: true }).catch(() => []);
@@ -69,6 +88,23 @@ opts = {}) {
69
88
  says: `Your session is for ${creds.registry} and this system came from ${lock.registry}. That answers 401, which reads as an expired session on a host that never issued one.`,
70
89
  run: `npx synthesisui login --registry ${lock.registry}`,
71
90
  });
91
+ /**
92
+ * A MEDIÇÃO LIDA POR UM LEITOR ANTERIOR - e este era o quarto lado, sem cobertura nenhuma.
93
+ *
94
+ * Metade da esteira roda na máquina de quem tem o código, e o servidor não tem os arquivos: quando
95
+ * `transcribe`, `sketch` ou a derivação de anatomia melhoram, o censo guardado continua sendo a
96
+ * leitura de antes e NENHUMA re-interpretação nossa alcança isso. Só uma medição nova alcança, e
97
+ * ela é um comando que ninguém tinha motivo para rodar.
98
+ *
99
+ * Compara `READER`, não a string do CLI: em 07/08 o CLI subiu quatro vezes e nenhuma delas mudou um
100
+ * leitor - pedir re-medição em cada bump faria a pessoa parar de ler estas linhas.
101
+ */
102
+ const reader = await censusReader(root);
103
+ if (reader != null && reader !== READER)
104
+ out.push({
105
+ says: `the measurement stored in this repo was read by an older reader, so what the platform knows about your components is what that reader could see. A re-measure is the only thing that reaches it - the fix lives on this machine, not on the server.`,
106
+ run: "npx synthesisui sync",
107
+ });
72
108
  /**
73
109
  * O ESCOPO, que é o desalinho mais caro e o mais silencioso: sem ele o `sync` mede o repo inteiro
74
110
  * e manda os componentes de todos os apps para um sistema que é uma biblioteca (06/08, 338 num
@@ -138,16 +174,52 @@ export async function versionBehind(root, opts = {}) {
138
174
  const token = await readToken();
139
175
  if (!token)
140
176
  return null;
141
- const res = await fetch(`${lock.registry ?? resolveRegistry()}/api/registry/ds/${lock.slug}`, { headers: { Authorization: `Bearer ${token}` } }).catch(() => null);
177
+ /**
178
+ * `?meta=1` - a versão e o compilador, e nada mais.
179
+ *
180
+ * Isto baixava o payload INTEIRO para ler um número: o documento, três CSS compilados e o GUIDE,
181
+ * a cada abertura de sessão e a cada entrada na pasta pelo terminal.
182
+ */
183
+ const res = await fetch(`${lock.registry ?? resolveRegistry()}/api/registry/ds/${lock.slug}?meta=1`, { headers: { Authorization: `Bearer ${token}` } }).catch(() => null);
142
184
  if (!res?.ok)
143
185
  return null;
144
186
  const body = (await res.json().catch(() => null));
145
- if (!body?.version || body.version <= lock.version)
187
+ if (!body?.version)
146
188
  return null;
147
- return {
148
- says: `v${body.version} of "${lock.slug}" is published and this repo is on v${lock.version}. The CSS here and the rules your agent reads are both v${lock.version} - they move together, which is why this is worth saying rather than applying.`,
149
- run: `npx synthesisui upgrade ${lock.slug}`,
150
- };
189
+ if (body.version > lock.version)
190
+ return {
191
+ says: `v${body.version} of "${lock.slug}" is published and this repo is on v${lock.version}. The CSS here and the rules your agent reads are both v${lock.version} - they move together, which is why this is worth saying rather than applying.`,
192
+ run: `npx synthesisui upgrade ${lock.slug}`,
193
+ };
194
+ /**
195
+ * A MESMA VERSÃO, COMPILADA DIFERENTE - o caso que não tinha como ser dito nem consertado.
196
+ *
197
+ * O CSS não é congelado na publicação: ele é compilado do documento a cada busca. Então um conserto
198
+ * nosso vale para uma versão JÁ publicada, e nenhum comando o buscava - `upgrade` age por versão e
199
+ * `connect` agia por CLI. Em 07/08 o bloco de esquema alternativo passou a emitir os dois
200
+ * ancestrais e não havia caminho até o disco de quem já tinha instalado.
201
+ */
202
+ if (typeof body.compiler === "number" &&
203
+ body.compiler !== (lock.compiler ?? null))
204
+ return {
205
+ says: `the css for "${lock.slug}" v${lock.version} is compiled differently now - same version, same document, a fix on our side. The files in this repo were written before it.`,
206
+ run: "npx synthesisui connect",
207
+ };
208
+ /**
209
+ * AS REGRAS MUDARAM - e nem a versão nem o CLI diziam isso.
210
+ *
211
+ * A governança de um sistema não é versionada com o documento: `listDsRuleSetByDsId` lê por
212
+ * SISTEMA, então uma lei escrita na plataforma vale no instante seguinte. O `doctrine.json` em
213
+ * disco, que é o que o agente lê com `system_doctrine`, só é reescrito por um `add`. Então quem
214
+ * cuida do sistema escrevia uma lei e o agente de quem o consome continuava obedecendo as de
215
+ * ontem, sem sinal nenhum (medido em 07/08).
216
+ */
217
+ if (body.rulesStamp && body.rulesStamp !== (lock.rules ?? null))
218
+ return {
219
+ says: `the rules that govern "${lock.slug}" changed since this repo materialized them - your agent reads the copy on disk, so it is still following the previous set.`,
220
+ run: "npx synthesisui connect",
221
+ };
222
+ return null;
151
223
  }
152
224
  /**
153
225
  * A linha que a sessão vê.
@@ -2,6 +2,7 @@ import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
3
  import { wireAgent } from "../agent-wiring.js";
4
4
  import { blockHomes, syncClaudeMd } from "../claude-md.js";
5
+ import { resolveRegistry } from "../config.js";
5
6
  import { body, paint, section, snippet } from "../output.js";
6
7
  import { hasHook, rcPathFor, shellFrom, shellSnippet, withHook, } from "../shell-hook.js";
7
8
  import { IMPORT_SKILL, IMPORT_SKILL_PATH } from "../skill-import.js";
@@ -48,6 +49,13 @@ const LEGACY_SKILLS = ["import-design-system"];
48
49
  * SILENCIOSO QUANDO JÁ ESTÁ EM DIA, e sem rede não faz nada - um `connect` que falha por estar num
49
50
  * avião seria pior que a defasagem que ele conserta.
50
51
  */
52
+ /** O compilador e as regras que serviriam esta versão HOJE - vazio quando não dá para saber. */
53
+ async function fetchMeta(base, slug, version) {
54
+ const res = await fetch(`${base}/api/registry/ds/${slug}?version=${version}&meta=1`).catch(() => null);
55
+ if (!res?.ok)
56
+ return {};
57
+ return ((await res.json().catch(() => null)) ?? {});
58
+ }
51
59
  async function refreshInstall(root, cli, registry) {
52
60
  const dsDir = join(root, "_synthesisui", "ds");
53
61
  const names = await readdir(dsDir, { withFileTypes: true }).catch(() => []);
@@ -67,7 +75,24 @@ async function refreshInstall(root, cli, registry) {
67
75
  /** Um DS adotado é descrito aqui e pertencido em outro lugar - não há o que rematerializar. */
68
76
  if (!lock.slug || lock.adopted || typeof lock.version !== "number")
69
77
  continue;
70
- if (lock.cli === cli)
78
+ /**
79
+ * O COMPILADOR TAMBÉM DECIDE, e sem ele um conserto de CSS não alcançava ninguém.
80
+ *
81
+ * `tokens.css` é compilado no servidor a cada busca, então ele melhora sem que uma linha do CLI
82
+ * mude e sem que a versão do sistema ande. Aí `upgrade` (que age por versão) e este bloco (que
83
+ * agia só por CLI) passavam batido, e o conserto ficava disponível para sempre sem caminho até o
84
+ * disco de quem instalou - foi o que aconteceu em 07/08 com o seletor do esquema alternativo.
85
+ *
86
+ * `?meta=1` custa algumas centenas de bytes; falhar nele não pode custar o `connect`, então um
87
+ * erro de rede simplesmente não aciona a rematerialização.
88
+ */
89
+ /** O host que emitiu ESTE install manda - um token pertence ao host que o emitiu. */
90
+ const base = resolveRegistry(registry ?? lock.registry);
91
+ const remote = await fetchMeta(base, lock.slug, lock.version);
92
+ const compilerMoved = remote.compiler != null && remote.compiler !== (lock.compiler ?? null);
93
+ /** Regras valem no instante em que são escritas - ver `align`, e `rulesStamp` na plataforma. */
94
+ const rulesMoved = remote.rulesStamp != null && remote.rulesStamp !== (lock.rules ?? null);
95
+ if (lock.cli === cli && !compilerMoved && !rulesMoved)
71
96
  continue;
72
97
  const done = await add(lock.slug, {
73
98
  ...(registry ? { registry } : {}),
@@ -283,12 +308,30 @@ export async function connect(opts) {
283
308
  console.log(paint.blue(snippet(["npx synthesisui@latest ci"])));
284
309
  }
285
310
  await offerShellHook(opts.shell === true);
286
- // The step that cost a round trip the first time this was tried by hand,
287
- // and would cost every single person one.
288
- console.log("");
289
- console.log(body("Reopen your editor session - all of them are read at startup."));
290
- if (want.mcp) {
291
- console.log(body('A project MCP server needs approving once; say yes when it asks. Then "/mcp" lists synthesisui.'));
311
+ /**
312
+ * O REINÍCIO QUANDO ELE É NECESSÁRIO - e pedi-lo sempre é o que fez o dono achar que reiniciar
313
+ * o editor fazia parte do fluxo (07/08).
314
+ *
315
+ * Hooks e servidores MCP são lidos UMA vez, quando a sessão abre - se um deles mudou, a sessão em
316
+ * curso está rodando o anterior e não como avisá-la. Mas `connect` também roda quando só o CSS
317
+ * foi re-materializado, e aí não há nada em memória para atualizar: os arquivos são lidos pelo
318
+ * build, não pelo editor.
319
+ *
320
+ * Um pedido que aparece sempre é um pedido que a pessoa passa a ignorar - e aí ele não serve para
321
+ * a vez em que era mesmo obrigatório.
322
+ */
323
+ const moved = (s) => s === "added" || s === "updated";
324
+ const needsRestart = moved(wired.hook) ||
325
+ moved(wired.session) ||
326
+ (Array.isArray(wired.mcp) && wired.mcp.some((m) => moved(m.status)));
327
+ if (needsRestart) {
328
+ console.log("");
329
+ console.log(body("Reopen your editor session - all of them are read at startup."));
330
+ if (want.mcp &&
331
+ Array.isArray(wired.mcp) &&
332
+ wired.mcp.some((m) => moved(m.status))) {
333
+ console.log(body('A project MCP server needs approving once; say yes when it asks. Then "/mcp" lists synthesisui.'));
334
+ }
292
335
  }
293
336
  if (wired.command.startsWith("npx synthesisui@")) {
294
337
  console.log("");
@@ -32,7 +32,20 @@ function idOf(kind, name, at) {
32
32
  }
33
33
  export async function fileRequest(root, req) {
34
34
  const at = req.at ?? new Date().toISOString();
35
- const full = { ...req, at, id: idOf(req.kind, req.name, at) };
35
+ /**
36
+ * O CARIMBO SÓ PODE SER FEITO AGORA: depois que alguém edita o sistema, não há como saber se o nome
37
+ * existia quando o pedido nasceu - e é essa a diferença entre "apareceu" e "sempre esteve lá".
38
+ */
39
+ const name = checkableName({ ...req, at, id: "" });
40
+ const existed = name
41
+ ? declaresName(await readInstalledCss(root), name)
42
+ : false;
43
+ const full = {
44
+ ...req,
45
+ at,
46
+ id: idOf(req.kind, req.name, at),
47
+ ...(existed ? { existed: true } : {}),
48
+ };
36
49
  try {
37
50
  await appendFile(path(root), `${JSON.stringify(full)}\n`, "utf8");
38
51
  }
@@ -94,24 +107,80 @@ export function checkableName(req) {
94
107
  const m = /[a-z][a-z0-9]*(?:-[a-z0-9]+)+/.exec(req.name.trim());
95
108
  return m ? m[0] : null;
96
109
  }
97
- /** Requests whose ask the installed css now demonstrably delivers. */
110
+ /**
111
+ * Cada `--custom-property: valor` que o css instalado declara. A ÚLTIMA vence, que é o que a cascata
112
+ * faz para declarações do mesmo peso - ler a primeira responderia sobre um bloco que o navegador
113
+ * descarta.
114
+ */
115
+ function declaredValues(css) {
116
+ const out = new Map();
117
+ const re = /(--[a-z0-9-]+)\s*:\s*([^;}]+)/gi;
118
+ let m = re.exec(css);
119
+ while (m) {
120
+ out.set(m[1].toLowerCase(), m[2].trim());
121
+ m = re.exec(css);
122
+ }
123
+ return out;
124
+ }
125
+ /**
126
+ * O valor final de uma declaração, seguindo `var()` enquanto der. Um design system nomeia um valor
127
+ * apontando um papel para uma primitiva, então o valor pedido quase nunca está escrito na linha do
128
+ * papel - parar no primeiro `var()` reprovaria justamente o caso comum.
129
+ */
130
+ function resolved(map, value, hops = 5) {
131
+ let v = value.trim();
132
+ for (let i = 0; i < hops; i += 1) {
133
+ const m = /^var\(\s*(--[a-z0-9-]+)/i.exec(v);
134
+ if (!m)
135
+ break;
136
+ const next = map.get(m[1].toLowerCase());
137
+ if (next == null)
138
+ break;
139
+ v = next.trim();
140
+ }
141
+ return v.toLowerCase().replace(/\s+/g, " ");
142
+ }
143
+ const candidatesFor = (name) => name.startsWith("animate-") ? [`--${name}`] : [`--ds-${name}`, `--${name}`];
144
+ /** Os nomes que o css instalado JÁ declara - ver `GapRequest.existed`. */
145
+ export function declaresName(installedCss, name) {
146
+ const declared = declaredValues(installedCss);
147
+ return candidatesFor(name).some((c) => declared.has(c.toLowerCase()));
148
+ }
149
+ /**
150
+ * Requests whose ask the installed css now demonstrably delivers.
151
+ *
152
+ * APARECER É PROVA; EXISTIR NÃO É - e a diferença fechou um bug real como entregue.
153
+ *
154
+ * Um pedido é "isto não tem nome". Quando o nome de fato não existia, ele passar a existir é prova
155
+ * suficiente e é o caso comum - `animate-rise` não tem um valor comparável, o pedido é pelo NOME.
156
+ *
157
+ * Mas um pedido feito SOBRE um token que já existe é sobre o VALOR dele, e aí existir responde sim
158
+ * no instante em que o pedido nasce. Em 07/08 um agente reportou `color-semantic-canvas` apontando
159
+ * para a mesma primitiva que o `foreground` - 1.00:1, texto invisível - e o `sync` seguinte imprimiu
160
+ * *"delivered. Nothing for anyone to do"* com o defeito intacto no disco do dono.
161
+ *
162
+ * Então `existed` é carimbado quando o pedido é feito, e um pedido assim só fecha quando o css
163
+ * ENTREGA o valor pedido. Sem valor comparável ele fica aberto e quem decide é a pessoa, no cartão -
164
+ * um pedido aberto custa uma linha numa tela, um fechamento falso custa o bug.
165
+ */
98
166
  export function satisfiedRequests(requests, installedCss) {
167
+ const declared = declaredValues(installedCss);
99
168
  return requests.filter((r) => {
100
169
  const name = checkableName(r);
101
170
  if (!name)
102
171
  return false;
103
- const candidates = name.startsWith("animate-")
104
- ? [`--${name}`]
105
- : [`--ds-${name}`, `--${name}`];
106
- return candidates.some((c) => installedCss.includes(`${c}:`));
172
+ const present = candidatesFor(name).filter((c) => declared.has(c.toLowerCase()));
173
+ if (present.length === 0)
174
+ return false;
175
+ if (!r.existed)
176
+ return true;
177
+ const want = r.value?.trim();
178
+ if (!want)
179
+ return false;
180
+ const asked = resolved(declared, want);
181
+ return present.some((c) => resolved(declared, declared.get(c.toLowerCase()) ?? "") === asked);
107
182
  });
108
183
  }
109
- /**
110
- * Everything the pinned install actually ships, as one string - tokens.css
111
- * AND theme.css of the locked version, for every installed system. This is
112
- * the ground verification stands on: not the doc, not the GUIDE's promises,
113
- * the css a build would really read.
114
- */
115
184
  export async function readInstalledCss(root) {
116
185
  const dsDir = join(root, "_synthesisui", "ds");
117
186
  let css = "";
@@ -26,6 +26,7 @@
26
26
  * interpretação como um número que se move. Dizer 100% de cobertura existindo uma classe
27
27
  * calculada em runtime seria exatamente a mentira que esta esteira existe para não contar.
28
28
  */
29
+ import { READER } from "../reader-version.js";
29
30
  const SHAPES = [
30
31
  "class",
31
32
  "template",
@@ -97,6 +98,7 @@ export function buildLedger(cli, seen) {
97
98
  }
98
99
  return {
99
100
  cli,
101
+ reader: READER,
100
102
  counted,
101
103
  interpreted,
102
104
  unread: [...groups.values()]
@@ -0,0 +1,24 @@
1
+ /**
2
+ * A VERSÃO DO LEITOR - quem diz que uma medição já guardada ficou velha.
3
+ *
4
+ * Metade da esteira roda AQUI, na máquina de quem tem o código: `transcribe`, `sketch`,
5
+ * `anatomy-from-sketch`, os leitores de CSS Modules e de variantes. Quando um deles melhora, o censo
6
+ * gravado continua sendo a leitura antiga - e nenhuma re-interpretação no servidor alcança isso,
7
+ * porque o servidor não tem os arquivos. Só uma nova medição alcança, e ela é um comando: `sync`.
8
+ *
9
+ * O problema era saber QUANDO pedir. O censo grava a string do CLI que mediu, e o CLI sobe várias
10
+ * vezes por dia por motivos que não têm nada a ver com leitura - em 07/08 foram quatro versões, e
11
+ * nenhuma delas mudou um leitor. Comparar a string mandaria re-medir em todas, e um aviso que aparece
12
+ * à toa é um aviso que a pessoa aprende a não ler.
13
+ *
14
+ * Então este número existe e sobe SOZINHO, pelo mesmo critério do `COMPILER` na plataforma: quando os
15
+ * leitores passam a produzir um censo diferente para os MESMOS arquivos. Nunca por refactor, nunca por
16
+ * publicação, nunca por conserto que não toca leitura.
17
+ *
18
+ * As três marcas, e o que cada uma alcança:
19
+ *
20
+ * READER a medição na máquina dela -> npx synthesisui sync
21
+ * COMPILER o CSS de uma versão já instalada -> npx synthesisui connect
22
+ * INTERPRETATION a nossa metade, sobre censos já guardados -> roda no servidor, sem comando
23
+ */
24
+ export const READER = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.172",
3
+ "version": "0.16.176",
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": {