synthesisui 0.16.325 → 0.16.327

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.
@@ -369,9 +369,17 @@ export async function versionBehind(root, opts = {}) {
369
369
  const lock = locks[0];
370
370
  if (!lock?.slug || lock.version == null)
371
371
  return null;
372
- if (!(await credentials(opts.home ?? homedir())))
372
+ /**
373
+ * UM `home` SÓ PARA AS DUAS LEITURAS, e não um por linha.
374
+ *
375
+ * Até 27/08 a primeira respeitava o `home` recebido e a segunda lia o `homedir()` da máquina, o que
376
+ * fazia os dois testes que esperam resultado passarem só onde havia credencial de produção salva.
377
+ * Uma variável dividida não deixa a segunda esquecer.
378
+ */
379
+ const home = opts.home ?? homedir();
380
+ if (!(await credentials(home)))
373
381
  return null;
374
- const token = await readToken();
382
+ const token = await readToken(home);
375
383
  if (!token)
376
384
  return null;
377
385
  /**
@@ -8,7 +8,7 @@ import { emptyTally, internalSpecifiers, scanComponentsInto, tallyToInventory, }
8
8
  import { checkContracts } from "../doctor/contract-check.js";
9
9
  import { describeMissing, missingDependencies, summarizeMissing, } from "../doctor/dependencies.js";
10
10
  import { findFrozenBindings } from "../doctor/frozen.js";
11
- import { appendEvent, COVERAGE_RULE, readEvents, suggestionsFrom, summarize, } from "../doctor/ledger.js";
11
+ import { appendEvent, COVERAGE_RULE, readEvents, suggestionsFrom, unrepresentedFrom, summarize, } from "../doctor/ledger.js";
12
12
  import { bindingsFromDocument, countComponents, findOverrides, } from "../doctor/overrides.js";
13
13
  import { checkableName, readRequests, verifyAndCloseRequests, } from "../doctor/requests.js";
14
14
  import { DEFAULT_ROOT_PX, rootSizeOf, saidOfRoot, } from "../doctor/root-size.js";
@@ -938,6 +938,8 @@ export async function doctor(opts) {
938
938
  ...(d.repeats.some((r) => nameToWrite(r))
939
939
  ? { matched: suggestionsFrom(d.repeats) }
940
940
  : {}),
941
+ /** SEMPRE, inclusive vazio - ver `CheckEvent.unrepresentedMatches`: o vazio é a medição. */
942
+ unrepresentedMatches: unrepresentedFrom(d.findings, suggestionsFrom(d.repeats)),
941
943
  }).catch(() => { });
942
944
  console.log("");
943
945
  if (opts.fix) {
@@ -1149,6 +1151,15 @@ export async function doctor(opts) {
1149
1151
  ...(d.repeats.some((r) => nameToWrite(r))
1150
1152
  ? { matched: suggestionsFrom(d.repeats) }
1151
1153
  : {}),
1154
+ /**
1155
+ * E AS OCORRÊNCIAS QUE OS PARES NÃO CARREGAM - ver `CheckEvent.unrepresentedMatches`.
1156
+ *
1157
+ * ESCRITO SEMPRE, inclusive como lista vazia, e é a única forma de o campo significar alguma
1158
+ * coisa: ausente quer dizer "um CLI que não media isto", e `[]` quer dizer "mediu e não há".
1159
+ * Escrevê-lo condicionalmente faria os dois estados virarem um, e um cliente com ledger antigo
1160
+ * passaria a parecer um cliente medido sem achados.
1161
+ */
1162
+ unrepresentedMatches: unrepresentedFrom(d.findings, suggestionsFrom(d.repeats)),
1152
1163
  });
1153
1164
  }
1154
1165
  if (hasSystem && fullRun) {
@@ -21,6 +21,6 @@ export async function logout() {
21
21
  return;
22
22
  }
23
23
  console.log(`✓ Signed out${existing?.registry ? ` of ${existing.registry}` : ""} on this machine.`);
24
- console.log(` Removed ${credentialsPath}`);
24
+ console.log(` Removed ${credentialsPath()}`);
25
25
  console.log(" Run `synthesisui login` to sign in as someone else.");
26
26
  }
package/dist/config.js CHANGED
@@ -11,8 +11,24 @@ export function resolveRegistry(flag) {
11
11
  const base = flag || process.env.SYNTHESISUI_REGISTRY_URL || DEFAULT_REGISTRY;
12
12
  return base.replace(/\/+$/, ""); // no trailing slash
13
13
  }
14
- /** Where the device-flow token lives - per machine, in the home dir. */
15
- export const credentialsPath = join(homedir(), ".synthesisui", "credentials.json");
14
+ /**
15
+ * Where the device-flow token lives - per machine, in the home dir.
16
+ *
17
+ * `home` É INJETÁVEL PELO MESMO MOTIVO QUE EM `align`: uma verificação que lê o home da máquina que
18
+ * roda o teste não é verificável - ela passa ou falha conforme quem está logado ali.
19
+ *
20
+ * O QUE ISTO CONSERTA, medido em 27/08: `versionBehind` recebia `home` e o respeitava em
21
+ * `credentials(...)`, e uma linha depois chamava `readToken()`, que lia uma CONSTANTE de módulo
22
+ * resolvida com o `homedir()` real. Os dois testes de `decided-waiting` que esperam resultado só
23
+ * passavam porque encontravam a credencial de produção de quem rodava a suíte; num runner limpo eles
24
+ * caíam. Era uma porta declarada aberta e fechada pela metade - e o CLAUDE.md pede fechar a porta em
25
+ * vez de vigiá-la, então ela deixou de ser constante.
26
+ *
27
+ * O comportamento do produto é o mesmo: sem argumento, `homedir()`, exatamente como antes.
28
+ */
29
+ export function credentialsPath(home = homedir()) {
30
+ return join(home, ".synthesisui", "credentials.json");
31
+ }
16
32
  /**
17
33
  * Reads the saved token, if any. Optional for now (open gate); the device-flow
18
34
  * is what writes it. Sent as a Bearer header when present.
@@ -25,9 +41,9 @@ export const credentialsPath = join(homedir(), ".synthesisui", "credentials.json
25
41
  * refused it with a 401 that read as "your session expired" (dono, 31/07). A
26
42
  * session cannot expire on a host that never issued it.
27
43
  */
28
- export async function readCredentials() {
44
+ export async function readCredentials(home) {
29
45
  try {
30
- const raw = await readFile(credentialsPath, "utf8");
46
+ const raw = await readFile(credentialsPath(home), "utf8");
31
47
  const parsed = JSON.parse(raw);
32
48
  if (!parsed.token)
33
49
  return null;
@@ -44,7 +60,7 @@ export function sameRegistry(a, b) {
44
60
  const norm = (u) => u.trim().replace(/\/+$/, "").toLowerCase();
45
61
  return norm(a) === norm(b);
46
62
  }
47
- export async function readToken() {
63
+ export async function readToken(home) {
48
64
  /**
49
65
  * O AMBIENTE GANHA DO ARQUIVO, e é o que permite CI sem navegador.
50
66
  *
@@ -60,7 +76,7 @@ export async function readToken() {
60
76
  if (fromEnv)
61
77
  return fromEnv;
62
78
  try {
63
- const raw = await readFile(credentialsPath, "utf8");
79
+ const raw = await readFile(credentialsPath(home), "utf8");
64
80
  const parsed = JSON.parse(raw);
65
81
  return parsed.token ?? null;
66
82
  }
@@ -69,10 +85,11 @@ export async function readToken() {
69
85
  }
70
86
  }
71
87
  /** Persists the device-flow token (chmod 600, user-only dir). */
72
- export async function writeToken(token, registry) {
73
- await mkdir(dirname(credentialsPath), { recursive: true, mode: 0o700 });
88
+ export async function writeToken(token, registry, home) {
89
+ const path = credentialsPath(home);
90
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
74
91
  const payload = { token, registry, savedAt: new Date().toISOString() };
75
- await writeFile(credentialsPath, `${JSON.stringify(payload, null, 2)}\n`, {
92
+ await writeFile(path, `${JSON.stringify(payload, null, 2)}\n`, {
76
93
  mode: 0o600,
77
94
  });
78
95
  }
@@ -90,10 +107,10 @@ export async function writeToken(token, registry) {
90
107
  * Devolve `false` quando não havia nada - a diferença entre "saí" e "você já
91
108
  * estava fora" é a única coisa que o comando tem para dizer.
92
109
  */
93
- export async function clearCredentials() {
94
- const existing = await readCredentials();
110
+ export async function clearCredentials(home) {
111
+ const existing = await readCredentials(home);
95
112
  try {
96
- await rm(credentialsPath, { force: true });
113
+ await rm(credentialsPath(home), { force: true });
97
114
  }
98
115
  catch {
99
116
  // já não existe, ou o disco recusou - o estado final é o mesmo
@@ -36,6 +36,17 @@ export const LEDGER_FILE = "ledger.jsonl";
36
36
  * inteiro o que decide alguma coisa.
37
37
  */
38
38
  const MATCHED_CAP = 300;
39
+ /**
40
+ * O MESMO TETO PARA AS OCORRÊNCIAS, e aqui ele corta MAIS, porque cada entrada é uma ocorrência e
41
+ * não um grupo. Medido antes de escolher o número: 1 no escopo que o repositório real mede, 2 no
42
+ * repositório inteiro - o teto está três ordens de grandeza acima do caso real, e existe para que
43
+ * um projeto grande não escreva um evento de centenas de KB no ledger dele.
44
+ *
45
+ * O corte é SILENCIOSO neste arquivo, e isso está dito em vez de calado: sem um campo de contagem
46
+ * não há onde escrever "e mais N". Se algum dia um projeto real encostar no teto, a resposta é um
47
+ * campo que declare o que ficou de fora - nunca subir o número e seguir.
48
+ */
49
+ const UNREPRESENTED_CAP = 300;
39
50
  /**
40
51
  * Os pares a mandar, do agregado que o doctor já monta.
41
52
  *
@@ -64,6 +75,49 @@ export function suggestionsFrom(repeats) {
64
75
  count: r.count,
65
76
  }));
66
77
  }
78
+ /**
79
+ * A IDENTIDADE ESTRUTURAL DE UMA OCORRÊNCIA - a MESMA chave que agrupa os repeats em `scan.ts`.
80
+ *
81
+ * Comparar por texto de apresentação (`shown`) seria julgar o nome que a pessoa leu em vez do fato
82
+ * medido, que é o acoplamento que o #1174 removeu. A família faz parte da identidade: o mesmo
83
+ * literal em duas famílias são dois fatos diferentes, e só um deles pode ser coincidência.
84
+ */
85
+ const identityOf = (f) => `${f.kind}:${f.literal.toLowerCase()}`;
86
+ /**
87
+ * AS OCORRÊNCIAS QUE `matched` NÃO TRANSPORTOU - ver `CheckEvent.unrepresentedMatches`.
88
+ *
89
+ * A regra é ESTRUTURAL e nunca semântica: entra a ocorrência que casou com um token nosso e cuja
90
+ * identidade não está entre as entradas que `matched` de fato carregou. Nada aqui olha
91
+ * `finding.crossFamily` - filtrar por aquele veredito congelaria a régua do dia da coleta e
92
+ * impediria uma régua futura de reavaliar o que este CLI decidiu não mandar.
93
+ *
94
+ * `matched` entra como ARGUMENTO, e não é detalhe: o que importa é o que foi de fato transportado,
95
+ * depois do filtro e depois do teto. Um repeat que existiu e foi cortado sai daqui como ocorrência,
96
+ * porque do ponto de vista de quem lê ele não chegou - e é isso que o campo promete.
97
+ */
98
+ export function unrepresentedFrom(findings, matched) {
99
+ const carried = new Set(matched.map(identityOf));
100
+ const out = [];
101
+ for (const f of findings) {
102
+ /** Sem token nosso não houve match nosso, e não há o que a régua de família possa julgar. */
103
+ if (!f.token)
104
+ continue;
105
+ if (carried.has(identityOf(f)))
106
+ continue;
107
+ if (out.length >= UNREPRESENTED_CAP)
108
+ break;
109
+ out.push({
110
+ literal: f.literal,
111
+ kind: f.kind,
112
+ token: f.token,
113
+ file: f.file,
114
+ line: f.line,
115
+ ...(f.theirToken ? { theirToken: f.theirToken } : {}),
116
+ ...(f.fontRelative ? { fontRelative: true } : {}),
117
+ });
118
+ }
119
+ return out;
120
+ }
67
121
  const ledgerPath = (root) => join(root, "_synthesisui", LEDGER_FILE);
68
122
  /** Keep the file from growing forever: past ~1MB, keep the newest half. A
69
123
  * trend needs recent history, not an archive. */
@@ -39,6 +39,60 @@ export const FRONTEND_HUB_CENSUS = join(POPULATIONS, "frontend-hub.census.json")
39
39
  * que a jornada é conferida na tela.
40
40
  */
41
41
  export const CODELEVEL_CENSUS = join(POPULATIONS, "codelevel-ui.census.json");
42
+ /**
43
+ * O HISTÓRICO DO DOCTOR - a única entrada destas provas que NÃO se deriva de um censo.
44
+ *
45
+ * O que ele carrega e o censo não: os PARES que o doctor casou (`matched`) e o placar que ele
46
+ * fechou (`crossFamily`). O censo é a leitura do CÓDIGO num instante; o ledger é a história das
47
+ * EXECUÇÕES - nenhum dos dois se produz a partir do outro.
48
+ *
49
+ * Congelado em 28/08 porque só existia na máquina do dono, e porque as provas que dependem dele
50
+ * ficariam órfãs no dia em que aquela pasta sumisse - foi exatamente assim que oito provas do
51
+ * `signalui` viraram provas apagadas. Ver `fixtures/populations/README.md`.
52
+ */
53
+ export const CODELEVEL_LEDGER = join(POPULATIONS, "codelevel-ui.ledger.jsonl");
54
+ /**
55
+ * OS DOIS DOCUMENTOS HISTÓRICOS - a exceção autorizada à regra do derivável (dono, 28/08).
56
+ *
57
+ * A regra desta pasta continua valendo para artefato ATUAL: um `design-system.json` de hoje se
58
+ * deriva do censo, e congelá-lo criaria uma segunda fonte de verdade que envelhece calada.
59
+ *
60
+ * Um documento PUBLICADO no passado é outra coisa. A cadeia só sabe produzir o presente: derivar
61
+ * hoje o que foi publicado em duas versões diferentes devolve o mesmo documento duas vezes, e a
62
+ * propriedade que o `change-preview` protege - que cada lado do par desenhe com o CSS DA SUA
63
+ * versão - deixaria de ter o que comparar. O passado não é derivável nem hoje nem nunca.
64
+ *
65
+ * PROVENIÊNCIA. `~/personal/codelevel-monorepo/_synthesisui/ds/codelevel/{v2,v3}/design-system.json`,
66
+ * escritos em 26/08/2026 pelo próprio produto - `v2` por `import`, `v3` por `sync` (`source.by` em
67
+ * todas as 62 receitas). Copiados byte a byte pelo VALOR: só a formatação foi compactada, e um
68
+ * spec assere que o objeto lido é idêntico ao original. Nenhum valor foi alterado para caber num
69
+ * teste, e nenhuma versão foi gerada artificialmente.
70
+ *
71
+ * O PAR FOI ESCOLHIDO PELA PROPRIEDADE, e a escolha foi medida nos três pares possíveis:
72
+ *
73
+ * v1 -> v2 0 componentes com declaração movida, 0 que mudaram sem pintar - não serve
74
+ * v2 -> v3 20 componentes com declaração movida, 42 que mudaram sem pintar
75
+ * v1 -> v3 idêntico ao v2 -> v3
76
+ *
77
+ * INSPECIONADOS ANTES DE VERSIONAR, e nada precisou ser mascarado - o que importa, porque mascarar
78
+ * mudaria o documento:
79
+ *
80
+ * credenciais / bearer / api-key / password nenhuma
81
+ * caminhos absolutos ou de máquina nenhum
82
+ * e-mails ou identificadores pessoais nenhum
83
+ * URLs uma, o namespace de SVG do W3C
84
+ *
85
+ * O que eles CARREGAM, e está dito em vez de calado: a narrativa e o tagline do produto (texto de
86
+ * marketing do próprio dono), os caminhos RELATIVOS dos arquivos de origem dentro do repositório
87
+ * (`packages/ui/src/...`, os mesmos que o censo congelado ao lado já carrega) e, no `v3`, um
88
+ * carimbo `meta.repairs` com data.
89
+ *
90
+ * E SÃO EVIDÊNCIA HISTÓRICA, NUNCA FONTE NORMATIVA. Nenhuma regra do SynthesisUI pode ler daqui o
91
+ * que é certo - nem um nome de componente, nem um valor, nem uma convenção. Eles existem para que
92
+ * uma prova tenha DOIS instantes do mesmo sistema para comparar, e é só isso.
93
+ */
94
+ export const CODELEVEL_DOCUMENT_OLDER = join(POPULATIONS, "codelevel-ui.v2.document.json");
95
+ export const CODELEVEL_DOCUMENT_NEWER = join(POPULATIONS, "codelevel-ui.v3.document.json");
42
96
  /**
43
97
  * OS REPOSITÓRIOS VIVOS - e estes NÃO estão congelados, de propósito.
44
98
  *
@@ -25,7 +25,7 @@
25
25
  */
26
26
  import { readFile } from "node:fs/promises";
27
27
  import { join } from "node:path";
28
- import { readEvents } from "./doctor/ledger.js";
28
+ import { readEvents, } from "./doctor/ledger.js";
29
29
  /**
30
30
  * O estado de um slug neste projeto, ou nada quando não há `.lock`.
31
31
  *
@@ -83,6 +83,15 @@ export async function repoStateOf(projectRoot, slug, cli) {
83
83
  ...(doctor?.matched && doctor.matched.length > 0
84
84
  ? { matched: doctor.matched }
85
85
  : {}),
86
+ /**
87
+ * `Array.isArray` E NUNCA `length > 0` - a linha acima é o contraexemplo, e ela está certa para
88
+ * `matched`, que não distingue "não mediu" de "mediu zero". Este campo distingue, e é a razão de
89
+ * ele existir: `[]` é uma medição, e transformá-la em ausência faria um cliente com CLI novo
90
+ * parecer um cliente com ledger antigo.
91
+ */
92
+ ...(Array.isArray(doctor?.unrepresentedMatches)
93
+ ? { unrepresentedMatches: doctor.unrepresentedMatches }
94
+ : {}),
86
95
  ...(runs.length > 0 ? { runs } : {}),
87
96
  ...(typeof lock?.fetchedAt === "string"
88
97
  ? { installedAt: lock.fetchedAt }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.325",
3
+ "version": "0.16.327",
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": {