synthesisui 0.16.400 → 0.16.401

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.
@@ -1,5 +1,6 @@
1
1
  import { deltaE, JND } from "./doctor/color-distance.js";
2
2
  import { familySays, nearestToken, normalizeValue, } from "./doctor/tokens.js";
3
+ import { proposeFromTheirScale, tooCloseToPropose } from "./proposed-name.js";
3
4
  /** Onde cada tipo de valor mora na fundação. `color` é o único com dois segmentos. */
4
5
  const HOME = {
5
6
  color: "color",
@@ -180,7 +181,26 @@ export function absorbPlan(d, theirs, have, cap = 40) {
180
181
  const value = normalizeValue(r.literal, theirs.rootPx);
181
182
  const theirName = nameOf(r.kind, theirs.byValue.get(value)) ??
182
183
  nameOf(r.kind, theirs.compiledAway.get(value)?.map((c) => c.name));
183
- const path = pathFor(r.kind, theirName);
184
+ /**
185
+ * A LISTA NÃO CHEGA EM BRANCO - e é o que faz o vocabulário dele CRESCER.
186
+ *
187
+ * Especificado em 07/09 por entrevista (`steps/02-jornada-projeto-novo.md`): a plataforma
188
+ * PROPÕE, ele aprova ou edita, e nada entra sem o aval dele. Sem proposta, cada valor novo custa
189
+ * um batismo manual - e num repositório com 553 valores distintos ninguém batiza 553 vezes. O
190
+ * vocabulário para de crescer e o próximo componente nasce com outro literal, que é o oposto do
191
+ * que a plataforma existe para fazer.
192
+ *
193
+ * A ORDEM É A DE SEMPRE: o nome que ele JÁ deu vence. Só onde ninguém nomeia é que a derivação
194
+ * entra - e ela devolve `null` na dúvida (ver `proposed-name.ts`).
195
+ */
196
+ /**
197
+ * UMA FONTE POR ENQUANTO - a escala dele. A segunda está especificada e NÃO entrou, e o motivo
198
+ * está medido: ver `proposed-name.ts`, no fim.
199
+ */
200
+ const proposal = theirName || tooCloseToPropose(r.literal, theirs)
201
+ ? null
202
+ : proposeFromTheirScale(r.literal, theirs);
203
+ const path = pathFor(r.kind, theirName) || proposal?.path || "";
184
204
  /** "≈ perto de um token seu" só interessa a quem não tem um EXATO - com o nome na mão, a dica
185
205
  * vira ruído, e no arquivo de proposta ela vira uma segunda opção que não é opção. */
186
206
  const near = theirName ? undefined : nearestOwn(theirs, r.literal, r.kind);
@@ -194,6 +214,8 @@ export function absorbPlan(d, theirs, have, cap = 40) {
194
214
  count: r.count,
195
215
  path,
196
216
  ...(theirName ? { theirName: clean(theirName) } : {}),
217
+ /** A PROCEDÊNCIA VIAJA COM A PROPOSTA - ele julga pela origem, nunca pela palavra. */
218
+ ...(proposal ? { proposed: proposal.because } : {}),
197
219
  ...(near ? { near } : {}),
198
220
  });
199
221
  }
@@ -213,7 +235,17 @@ export const needingName = (plan) => plan.entries.filter((e) => !e.path);
213
235
  * nada.
214
236
  */
215
237
  export function describeAbsorb(plan) {
216
- const ready = plan.entries.filter((e) => e.path);
238
+ /**
239
+ * O QUE ELE JÁ NOMEIA E O QUE A PLATAFORMA PROPÔS SÃO DUAS LISTAS - e juntá-las é dizer que ele
240
+ * nomeou o que ele não nomeou.
241
+ *
242
+ * A primeira versão da proposta caiu direto em `ready`, e a linha de cima passou a dizer
243
+ * *"1 of 1 values already have a name in YOUR vocabulary"* sobre um nome que a PLATAFORMA acabou
244
+ * de derivar. É a mesma família de defeito que o dia inteiro de 07/09 consertou: uma frase que
245
+ * afirma sobre o repositório dele algo que veio da nossa dedução.
246
+ */
247
+ const ready = plan.entries.filter((e) => e.path && !e.proposed);
248
+ const proposed = plan.entries.filter((e) => e.proposed);
217
249
  const pending = needingName(plan);
218
250
  const lines = [];
219
251
  /**
@@ -245,6 +277,15 @@ export function describeAbsorb(plan) {
245
277
  lines.push(` ${e.value.padEnd(24)} ${e.path.padEnd(28)} ${e.files} file${e.files === 1 ? "" : "s"}${e.theirName ? ` · your ${e.theirName}` : ""}`);
246
278
  if (ready.length > 8)
247
279
  lines.push(` (${ready.length - 8} more)`);
280
+ if (proposed.length > 0) {
281
+ if (ready.length > 0)
282
+ lines.push("");
283
+ lines.push(`${proposed.length} ${proposed.length === 1 ? "value has" : "values have"} a name PROPOSED from your own scale - approve, edit, or leave ${proposed.length === 1 ? "it" : "them"} hard-coded:`);
284
+ for (const e of proposed.slice(0, 8))
285
+ lines.push(` ${e.value.padEnd(24)} ${e.path.padEnd(28)} ${e.proposed}`);
286
+ if (proposed.length > 8)
287
+ lines.push(` (${proposed.length - 8} more)`);
288
+ }
248
289
  if (pending.length > 0) {
249
290
  const close = pending.filter((e) => e.near);
250
291
  if (ready.length > 0)
@@ -262,7 +303,12 @@ export function describeAbsorb(plan) {
262
303
  ? `${pending.length} nobody names yet. Naming is a design decision, so those wait for a word from you:`
263
304
  : `${pending.length} ${everyOneRepeats ? "repeated values" : "values written by hand"}, and none of them has a name yet. Naming is a design decision, so each waits for a word from you:`);
264
305
  for (const e of pending.slice(0, 8))
265
- lines.push(` ${e.value.padEnd(24)} ${`<${e.kind}, ${e.files} file${e.files === 1 ? "" : "s"}>`.padEnd(28)} ${e.near ? `≈ your ${e.near.name} (${e.near.away})` : 'path: ""'}`);
306
+ lines.push(` ${e.value.padEnd(24)} ${`<${e.kind}, ${e.files} file${e.files === 1 ? "" : "s"}>`.padEnd(28)} ${e.near
307
+ ? `≈ your ${e.near.name} (${e.near.away})`
308
+ : /** A proposta chega COM a procedência: ele julga pela origem, não pela palavra. */
309
+ e.proposed
310
+ ? `${e.path} · ${e.proposed}`
311
+ : 'path: ""'}`);
266
312
  if (pending.length > 8)
267
313
  lines.push(` (${pending.length - 8} more)`);
268
314
  /**
@@ -0,0 +1,121 @@
1
+ import { chroma, deltaE, lightness } from "./doctor/color-distance.js";
2
+ import { normalizeValue } from "./doctor/tokens.js";
3
+ function stepsOf(theirs) {
4
+ const out = [];
5
+ for (const [name, value] of theirs.byName) {
6
+ const m = /^--(.*[a-z0-9])-(\d{2,4})$/.exec(name.toLowerCase());
7
+ if (!m)
8
+ continue;
9
+ out.push({ family: m[1], step: Number(m[2]), value });
10
+ }
11
+ return out;
12
+ }
13
+ /**
14
+ * O DEGRAU QUE FALTA NA ESCALA DELE.
15
+ *
16
+ * A proposta só existe quando o valor cai ENTRE dois degraus vizinhos da MESMA família dele, e o
17
+ * degrau proposto é o ponto médio entre os dois. A palavra é inteiramente dele; só a posição é
18
+ * calculada.
19
+ *
20
+ * AS TRÊS PORTAS, e cada uma existe para o lado do "propor de menos":
21
+ *
22
+ * mesma família dois degraus de famílias diferentes não formam escala - `ink-700` e
23
+ * `brand-900` não têm um meio
24
+ * entre eles um valor fora do intervalo não tem degrau intermediário; extrapolar seria
25
+ * inventar a continuação da escala dele
26
+ * lugar livre se o degrau do meio já existe, o valor não é um degrau novo - é outra
27
+ * decisão que por acaso caiu perto, e propor sobrescreveria a dele
28
+ */
29
+ export function proposeFromTheirScale(literal, theirs) {
30
+ const l = lightness(normalizeValue(literal, theirs.rootPx));
31
+ if (l === null)
32
+ return null;
33
+ /**
34
+ * O CROMA ENTRA JUNTO COM A LUMINOSIDADE, e sem ele a derivação INVENTA.
35
+ *
36
+ * MEDIDO NA PRIMEIRA VERSÃO DISTO, em 07/09: um verde (`#0d5c2a`) recebeu `color.ink.800`, porque
37
+ * a luminosidade dele cai entre dois cinzas dele. Um degrau que falta numa escala está perto dos
38
+ * vizinhos NA COR, não só no claro-escuro - propor o contrário é dizer que o verde dele é um tom
39
+ * de cinza.
40
+ *
41
+ * O INTERVALO É O DOS PRÓPRIOS VIZINHOS, nunca um limiar escolhido: um cinza tem croma perto de
42
+ * zero e um verde não, então a porta se fecha sozinha. Numa escala de azul os dois vizinhos têm
43
+ * croma alto e o degrau novo passa. Sem folga, porque o lado seguro do erro é hard-coded.
44
+ */
45
+ const c = chroma(normalizeValue(literal, theirs.rootPx));
46
+ if (c === null)
47
+ return null;
48
+ const steps = stepsOf(theirs)
49
+ .map((s) => {
50
+ const v = normalizeValue(s.value, theirs.rootPx);
51
+ return { ...s, l: lightness(v), c: chroma(v) };
52
+ })
53
+ .filter((s) => s.l !== null && s.c !== null);
54
+ if (steps.length < 2)
55
+ return null;
56
+ let best = null;
57
+ for (const a of steps) {
58
+ for (const b of steps) {
59
+ if (a.family !== b.family || a.step >= b.step)
60
+ continue;
61
+ /** O valor tem de cair ENTRE os dois - extrapolar seria inventar a continuação da escala. */
62
+ const lo = Math.min(a.l, b.l);
63
+ const hi = Math.max(a.l, b.l);
64
+ if (l <= lo || l >= hi)
65
+ continue;
66
+ /** E na COR também: fora do croma dos dois vizinhos, não é um degrau desta escala. */
67
+ const cLo = Math.min(a.c, b.c);
68
+ const cHi = Math.max(a.c, b.c);
69
+ if (c < cLo || c > cHi)
70
+ continue;
71
+ const mid = Math.round((a.step + b.step) / 2 / 50) * 50;
72
+ if (mid === a.step || mid === b.step)
73
+ continue;
74
+ /** O degrau já existe: então este valor não é um degrau que falta, é outra decisão. */
75
+ if (steps.some((s) => s.family === a.family && s.step === mid))
76
+ continue;
77
+ const gap = b.step - a.step;
78
+ if (!best || gap < best.gap)
79
+ best = { family: a.family, step: mid, gap };
80
+ }
81
+ }
82
+ if (!best)
83
+ return null;
84
+ return {
85
+ path: `${best.family.replace(/-/g, ".")}.${best.step}`,
86
+ because: `between your ${best.family}-${best.step - Math.round(best.gap / 2)} and ${best.family}-${best.step + Math.round(best.gap / 2)}`,
87
+ };
88
+ }
89
+ /**
90
+ * PERTO DEMAIS DE UM TOKEN DELE NÃO É UM DEGRAU NOVO - é a mesma decisão escrita duas vezes.
91
+ *
92
+ * `nearestOwn` já diz isso na proposta do `absorb` (o `≈` que aparece na lista), e propor um nome
93
+ * ali empurraria para BATIZAR o que a leitura já diz para NORMALIZAR. As duas linhas se
94
+ * contradiriam na mesma tela.
95
+ */
96
+ export function tooCloseToPropose(literal, theirs, jnd = 2) {
97
+ const v = normalizeValue(literal, theirs.rootPx);
98
+ for (const value of theirs.byName.values()) {
99
+ const d = deltaE(v, normalizeValue(value, theirs.rootPx));
100
+ if (d !== null && d < jnd)
101
+ return true;
102
+ }
103
+ return false;
104
+ }
105
+ /**
106
+ * A SEGUNDA FONTE - ESPECIFICADA, TENTADA, E NÃO ENTREGUE. O motivo é medido, e ele é o valor deste
107
+ * comentário.
108
+ *
109
+ * A especificação da PARTE 2 (07/09, por entrevista) admite derivar *"da função observada no código
110
+ * dele - o valor aparece 12× em `background` e 0× em texto, então é proposto como uma superfície"*.
111
+ *
112
+ * O QUE A TENTATIVA ENCONTROU, no mesmo dia: `scanSource` reporta UMA ocorrência por literal POR
113
+ * LINHA. Numa linha com `background: "#7a1f1f"` e `borderColor: "#7a1f1f"`, ele devolve um achado -
114
+ * e a contagem por propriedade opera sobre uma amostra que não é a contagem real. A proposta então
115
+ * afirmaria *"todos os seus 2 usos são texto"* sobre um valor que também é borda.
116
+ *
117
+ * Uma afirmação falsa sobre o repositório DELE é exatamente o que esta parte existe para impedir, e
118
+ * é a razão de a fonte 2 não estar aqui. O pré-requisito está nomeado: o scan precisa reportar cada
119
+ * ocorrência, e não a primeira de cada literal por linha - o que muda a granularidade de tudo que
120
+ * consome `findings`, e por isso é uma frente própria, não uma linha a mais neste arquivo.
121
+ */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.400",
3
+ "version": "0.16.401",
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": {