synthesisui 0.16.368 → 0.16.370

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.
@@ -554,6 +554,16 @@ export async function add(slug, opts) {
554
554
  if (aligned.dropped.length > 0) {
555
555
  console.log(line(` ${aligned.kept} Tailwind utilit${aligned.kept === 1 ? "y" : "ies"} now point at your own decisions; ${aligned.dropped.length} were left alone because the value would be ours (${aligned.dropped.slice(0, 3).join(", ")}${aligned.dropped.length > 3 ? ", …" : ""}) - your classes keep meaning what they mean today.`));
556
556
  }
557
+ /**
558
+ * O CICLO RECUSADO É DITO, e não engolido - senão o cliente lê um número menor sem saber por quê.
559
+ *
560
+ * A folha traz a ponte (`--ease-in-out: var(--ds-motion-easings-in-out)`) para que a utility dele
561
+ * resolva pelo sistema. Apontar de volta fecharia um ciclo, e um ciclo de custom property não deixa
562
+ * o valor errado: invalida a propriedade no elemento e em todos os descendentes. Recusar é o certo,
563
+ * e a linha diz que nada foi perdido - a utility dele continua resolvendo pelo sistema.
564
+ */
565
+ if (theirVars.cycles > 0)
566
+ console.log(` ${theirVars.cycles} ${theirVars.cycles === 1 ? "value" : "values"} already had the bridge pointing the other way, so ${theirVars.cycles === 1 ? "it was" : "they were"} left alone - your own utility still resolves through this system, and pointing back would have cancelled both.`);
557
567
  if (theirVars.pointed > 0)
558
568
  console.log(` ${theirVars.pointed} value${theirVars.pointed === 1 ? "" : "s"} in tokens.css now point at the name YOUR code already gives ${theirVars.pointed === 1 ? "it" : "them"} - change yours and the system follows${theirVars.pruned > 0 ? `; ${theirVars.pruned} matched but your build does not emit ${theirVars.pruned === 1 ? "that name" : "those names"}, so ${theirVars.pruned === 1 ? "it keeps" : "they keep"} the value` : ""}`);
559
569
  /**
@@ -201,6 +201,24 @@ const TOOLS = [
201
201
  required: ["skill"],
202
202
  },
203
203
  },
204
+ {
205
+ name: "apply_upgrade",
206
+ description: "The optimisation this system can write on its OWN, and the one question to ask before writing it. Call it with { slug } to GET the question - it comes back composed, with their numbers in it, and you deliver it as it is rather than rephrasing it. Only after they say yes, call again with { slug, confirmed: true }. What it writes never adds a word they did not write and never moves a value they chose: a literal only becomes a reference when it resolves to the same value. Everything that would need a NAME from them stays out and comes back counted, so you can say what is still waiting. Do NOT call this with confirmed:true off your own judgement - the whole point of the question is that the answer is theirs. AND THIS RUN ENDS WHEN IT IS APPLIED: report what changed in one short block and stop. If you found something else worth doing - a wiring gap, a defect, a fix waiting - name it in ONE line and offer it as a next step, never as a second investigation in the same breath. A run that answers `optimise my design system` with a nine-row impact table and a second three-option decision has buried the result it was asked for: whatever you measured is worth its own turn, where they can arrive with attention instead of spending what is left of it.",
207
+ inputSchema: {
208
+ type: "object",
209
+ properties: {
210
+ slug: {
211
+ type: "string",
212
+ description: "The system's slug, as the import printed it.",
213
+ },
214
+ confirmed: {
215
+ type: "boolean",
216
+ description: "false or omitted: returns the question and the numbers, writes nothing. true: applies it - only after they answered yes in their own words.",
217
+ },
218
+ },
219
+ required: ["slug"],
220
+ },
221
+ },
204
222
  {
205
223
  name: "recipe_vocabulary",
206
224
  description: "What a recipe CAN hold: every state that compiles, every preview form, the floor per kind, and the rules a media region needs. Call this BEFORE writing a recipe, not after - the reader that skipped it sent a `dark:` inside a variant and a state the compiler cannot spell, and both were dropped in silence. Served from the catalogue, so it grows as the contract grows.",
@@ -603,6 +621,54 @@ async function listComponents(root) {
603
621
  * sumário; com, UM capítulo. O corpo inteiro nunca viaja - a dieta de contexto
604
622
  * é servida por construção.
605
623
  */
624
+ /**
625
+ * A OTIMIZAÇÃO QUE UMA RESPOSTA "SIM" ESCREVE - e a pergunta que a precede, composta na plataforma.
626
+ *
627
+ * O QUE O CLIENTE GANHA: ele acaba de importar e o agente lhe faz UMA pergunta ali mesmo, em vez de
628
+ * lhe entregar um link para uma tela com quarenta e seis decisões enfileiradas. Ele responde, e o
629
+ * sistema é otimizado na mesma conversa.
630
+ *
631
+ * A PERGUNTA NÃO É COMPOSTA AQUI NEM PELO AGENTE, e essa é a decisão que faz o texto sobreviver: o
632
+ * agente escreve bem e escreve DIFERENTE a cada execução, e uma pergunta que muda de forma a cada
633
+ * vez é uma paráfrase e não uma pergunta do produto. Foi assim que dois fechamentos consecutivos
634
+ * afirmaram que 198 declarações não chegaram a receita nenhuma quando 22 utilities dele já estavam
635
+ * no sistema - o resumo dropou exatamente a metade que tinha acabado de entrar. Então o texto vem
636
+ * pronto de `askToUpgrade`, com os números da medição dentro dele.
637
+ *
638
+ * DOIS PASSOS DE PROPÓSITO. Sem `confirmed` nada é escrito: é o que impede a jornada de pular a
639
+ * pergunta, que é a única coisa nela que pertence à pessoa.
640
+ */
641
+ async function applyUpgrade(slug, confirmed) {
642
+ const answer = await askPlatform(`/api/registry/ds/${encodeURIComponent(slug)}/upgrade`, confirmed ? "POST" : "GET");
643
+ if (!answer.ok)
644
+ return answer.because;
645
+ const body = answer.body;
646
+ if (body.error)
647
+ return `${body.error}${body.message ? `: ${body.message}` : ""}`;
648
+ if (!confirmed) {
649
+ if (!body.ask)
650
+ return "Nothing here is safe to apply on its own - ask them nothing, and say the system is already on its own names.";
651
+ return [
652
+ "ASK THEM THIS, and deliver it as it is - it is composed here so the numbers cannot drift:",
653
+ "",
654
+ body.ask,
655
+ "",
656
+ `Then, and only then: call apply_upgrade again with { slug: "${slug}", confirmed: true }. If they say no, say what they are choosing - the system stays exactly as their code has it, and the optimisation waits on a button.`,
657
+ ].join("\n");
658
+ }
659
+ if (body.applied === false)
660
+ return body.message ?? "Nothing was applied.";
661
+ return [
662
+ `Done - v${body.version} published.`,
663
+ `${body.repointed ?? 0} literal${body.repointed === 1 ? "" : "s"} now follow their own tokens, and ${body.named ?? 0} value${body.named === 1 ? "" : "s"} became a named token.`,
664
+ body.waiting
665
+ ? `${body.waiting} decision${body.waiting === 1 ? "" : "s"} still need a word only they have - say the number, and that those wait on their own screen.`
666
+ : "",
667
+ "The version they had before is published and frozen, so going back is one click.",
668
+ ]
669
+ .filter(Boolean)
670
+ .join(" ");
671
+ }
606
672
  async function playbook(skill, section) {
607
673
  const answer = await askCatalogue("playbook", undefined, section ? { skill, section } : { skill });
608
674
  if (!answer.ok)
@@ -993,6 +1059,45 @@ cli) {
993
1059
  }
994
1060
  })();
995
1061
  }
1062
+ /**
1063
+ * UMA ROTA DA PLATAFORMA QUE NÃO É O CATÁLOGO, com a mesma sessão do device-login.
1064
+ *
1065
+ * `askCatalogue` monta `/api/catalogue?want=…` na própria assinatura, o que é certo para o que É
1066
+ * catálogo - e a otimização não é: ela ESCREVE no sistema dele, e uma escrita atrás de um `?want=`
1067
+ * leria como leitura. Então a única coisa compartilhada é o que deve ser: o token e o host.
1068
+ *
1069
+ * O MODO DE FALHA É DITO EM VOZ ALTA, nas três formas em que ele acontece - sem sessão, resposta de
1070
+ * erro, sem rede. Um `catch` que devolve silêncio faria o agente concluir que não havia nada a
1071
+ * fazer, e o cliente perderia a otimização sem nunca saber que ela existia.
1072
+ */
1073
+ async function askPlatform(path, method) {
1074
+ const token = await readToken();
1075
+ if (!token)
1076
+ return {
1077
+ ok: false,
1078
+ because: "Not signed in, so the platform could not answer. Run `synthesisui login` in the terminal and call this again.",
1079
+ };
1080
+ try {
1081
+ const res = await fetch(`${resolveRegistry()}${path}`, {
1082
+ method,
1083
+ headers: { Authorization: `Bearer ${token}` },
1084
+ });
1085
+ if (!res.ok)
1086
+ return {
1087
+ ok: false,
1088
+ because: res.status === 404
1089
+ ? "No system by that slug on this account - check the slug the import printed."
1090
+ : `The platform answered ${res.status}. ${res.status === 401 ? "Run `synthesisui login` and try again." : "Nothing was written."}`,
1091
+ };
1092
+ return { ok: true, body: await res.json() };
1093
+ }
1094
+ catch {
1095
+ return {
1096
+ ok: false,
1097
+ because: "No network, so the platform could not answer. Nothing was written - say that, and offer to try again rather than describing what would have happened.",
1098
+ };
1099
+ }
1100
+ }
996
1101
  async function askCatalogue(want, post,
997
1102
  /** Parâmetros extras do GET (ex.: o playbook e o capítulo pedidos). */
998
1103
  query) {
@@ -1547,6 +1652,9 @@ cli) {
1547
1652
  const section = args?.section ? String(args.section) : undefined;
1548
1653
  return fromContract(await playbook(skill, section));
1549
1654
  }
1655
+ case "apply_upgrade": {
1656
+ return fromContract(await applyUpgrade(String(args?.slug ?? ""), args?.confirmed === true));
1657
+ }
1550
1658
  case "recipe_vocabulary":
1551
1659
  return fromContract(await recipeVocabulary());
1552
1660
  case "validate_recipe": {
@@ -170,7 +170,7 @@
170
170
  * O que o cliente ganha ao rodar `upgrade`: o agente dele no Codex passa a poder PERGUNTAR ao
171
171
  * sistema, em vez de só receber as regras e adivinhar o resto.
172
172
  */
173
- export const MATERIALISER_SINCE = "0.16.362";
173
+ export const MATERIALISER_SINCE = "0.16.370";
174
174
  /**
175
175
  * A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
176
176
  *
@@ -101,11 +101,22 @@ export function pointAtTheirNames(css, theirs, resolvable) {
101
101
  * buildou não tem como provar o que resolve, e um par afirmado sem prova é pior que par nenhum.
102
102
  */
103
103
  if (!resolvable || theirs.byName.size === 0)
104
- return { css, pointed: 0, pruned: 0, pairs: [] };
104
+ return { css, pointed: 0, pruned: 0, cycles: 0, pairs: [] };
105
105
  const ours = buildTable({ css, source: "installed" });
106
106
  const alias = theirNames(ours, theirs);
107
+ /**
108
+ * A PONTE QUE JÁ ESTÁ NA FOLHA: o nome DELE -> o nosso token para o qual ele já aponta.
109
+ *
110
+ * Lido da própria folha e não de uma lista, porque quem decide o que é bridgeado é o compilador e a
111
+ * lista dele muda: hoje são famílias, pesos, easings, escala de tipo e gradientes, e amanhã pode ser
112
+ * outra. Uma cópia da regra aqui divergiria no dia em que uma família entrasse lá.
113
+ */
114
+ const bridged = new Map();
115
+ for (const [, theirName, ourName] of css.matchAll(/^\s*(--[a-zA-Z0-9_-]+)\s*:\s*var\((--ds-[a-zA-Z0-9_-]+)\)\s*;/gm))
116
+ bridged.set(theirName, ourName);
107
117
  let pointed = 0;
108
118
  let pruned = 0;
119
+ let cycles = 0;
109
120
  /** Ver `PointedAt.pairs`: o mapa sai do mesmo casamento que reescreve a linha. */
110
121
  const pairs = [];
111
122
  const out = css.replace(/^(\s*)(--ds-[a-zA-Z0-9_-]+)(\s*:\s*)([^;]+);/gm, (line, indent, name, sep, value) => {
@@ -118,6 +129,15 @@ export function pointAtTheirNames(css, theirs, resolvable) {
118
129
  const theirName = alias.get(`${kind}:${normalizeValue(value.trim(), ours.rootPx)}`)?.name;
119
130
  if (!theirName || theirName === name)
120
131
  return line;
132
+ /**
133
+ * A PONTE JÁ APONTA PARA CÁ - então apontar de volta fecha um ciclo, e um ciclo não deixa o
134
+ * valor errado: deixa a propriedade INVÁLIDA. A utility dele já resolve pelo nosso token, que é
135
+ * o que a ponte existe para fazer, então esta linha não tem nada a ganhar e tudo a perder.
136
+ */
137
+ if (bridged.get(theirName) === name) {
138
+ cycles += 1;
139
+ return line;
140
+ }
121
141
  if (!resolvable.has(theirName)) {
122
142
  pruned += 1;
123
143
  return line;
@@ -126,7 +146,7 @@ export function pointAtTheirNames(css, theirs, resolvable) {
126
146
  pairs.push({ ours: name, theirs: theirName, value: value.trim() });
127
147
  return `${indent}${name}${sep}var(${theirName});`;
128
148
  });
129
- return { css: out, pointed, pruned, pairs };
149
+ return { css: out, pointed, pruned, cycles, pairs };
130
150
  }
131
151
  /**
132
152
  * O PREFIXO DA NOSSA VARIÁVEL -> A FAMÍLIA, e a chave que `theirNames` devolve é `<família>:<valor>`.
@@ -151,7 +171,7 @@ const FAMILY_KIND = {
151
171
  export async function pointTokensAtTheirNames(root, payload) {
152
172
  const css = payload.artifacts["tokens.css"] ?? "";
153
173
  if (!css)
154
- return { css, pointed: 0, pruned: 0, pairs: [] };
174
+ return { css, pointed: 0, pruned: 0, cycles: 0, pairs: [] };
155
175
  const [theirs, resolvable] = await Promise.all([
156
176
  harvestTheirCss(root),
157
177
  resolvableVars(root),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.368",
3
+ "version": "0.16.370",
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": {