synthesisui 0.16.501 → 0.16.503

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.
package/dist/claude-md.js CHANGED
@@ -10,7 +10,7 @@ import { readResponsiveForm, responsiveLine } from "./responsive-form.js";
10
10
  * A LEITURA - o caminho do sistema desde a fase 1 (18/10). Escrita uma vez e usada nos dois contratos: o de quem já tem
11
11
  * sistema sem `rules.json` e o de quem chegou pela porta e ainda não tem sistema nenhum (fase 2).
12
12
  */
13
- const READ_THE_PAGE = "**Once the first screen is done, offer to read it - once.** If their next message does not take the offer up, do not offer it again; they can still ask. When the person asks you to read their page - or right after the creation of the first screen ends (with impeccable, once it has written `DESIGN.md`; never while it still asks for another round) - run `npx synthesisui read` with the dev server up, tell them in a few lines the system it found, then ask the questions in `_synthesisui/reading.json` ONE AT A TIME with your question tool (the first option is the default). Save their answers as `{ \"<question id>\": <option index> }` and run `npx synthesisui read --confirm <that file>`: their answers become the roles and breaks this project keeps.";
13
+ const READ_THE_PAGE = "**Once the first screen is done, offer to read it - once.** If their next message does not take the offer up, do not offer it again; they can still ask. When the person asks you to read their page - or right after the creation of the first screen ends (with impeccable, once it has written `DESIGN.md`; never while it still asks for another round) - run `npx synthesisui read` with the dev server up, tell them in a few lines the system it found, then ask ALL the questions in `_synthesisui/reading.json` TOGETHER, in one call of your question tool (up to 4 per call; more go in a second call), in plain words and quoting the text on the page each one is about (the first option is the default). Save their answers as `{ \"<question id>\": <option index> }` and run `npx synthesisui read --confirm <that file>`: their answers become the roles and breaks this project keeps.";
14
14
  /** A frase do revisor depois da criação - ver o comentário em `renderRegion`, junto dos papéis. */
15
15
  const REVIEWER_AFTER_CREATION = "\n\n**How a change closes here.** The visual world is decided - the roles and breaks above. From here on SynthesisUI's check closes a change (`check_file`, and the end-of-turn check). Impeccable's reviewer is for creating: spawn `impeccable-finish-reviewer` or run `impeccable detect` only when the person asks for a new visual direction or a large new surface with its own look - never as the routine close of a change, and never its degraded self-review in its place.";
16
16
  const START = "<!-- synthesisui:start -->";
@@ -8,7 +8,7 @@ import { join, relative } from "node:path";
8
8
  import { classify, pixelRatio } from "../reading/classify.js";
9
9
  import { PAGE_SCRIPT } from "../reading/page-script.js";
10
10
  import { decodePng } from "../reading/png.js";
11
- import { moduleClassName, pxOf } from "../role-rules.js";
11
+ import { globalClassOf, globalStyles, moduleClassName, pxOf } from "../role-rules.js";
12
12
  import { agentsToMaintain } from "../agents-chosen.js";
13
13
  import { syncClaudeMd } from "../claude-md.js";
14
14
  import { projectRootFrom } from "./hook.js";
@@ -134,15 +134,25 @@ function moduleOf(cls, files) {
134
134
  }
135
135
  return null;
136
136
  }
137
+ let stylesOf = null;
137
138
  function homeOf(root, files, t) {
138
- const mod = sizeClassOf(t.cls) ? null : moduleOf(t.cls, files);
139
- const cls = sizeClassOf(t.cls) ?? mod?.name ?? null;
139
+ const sized = sizeClassOf(t.cls);
140
+ /* a classe global (o juízo dele, 19/10): `className="section-title"` com o tamanho no globals.css - a do elemento
141
+ primeiro, depois a do pai (o `<h3>` sem classe dentro de `.route`, medido em `.route h3`) */
142
+ if (!sized && stylesOf?.root !== root)
143
+ stylesOf = { root, styles: globalStyles(root) };
144
+ const glob = sized || moduleOf(t.cls, files) ? null
145
+ : globalClassOf(t.cls.split(" | ").flatMap((part) => part.split(/\s+/)), stylesOf?.styles ?? []);
146
+ const mod = sized ? null : (moduleOf(t.cls, files) ?? (glob ? { name: glob.name, local: glob.local } : null));
147
+ const cls = sized ?? mod?.name ?? null;
140
148
  const lineOf = (f) => {
141
149
  if (!cls)
142
150
  return null;
143
151
  const lines = readFileSync(f, "utf8").split("\n");
144
152
  const i = mod
145
- ? lines.findIndex((l) => new RegExp(`\\b\\w+(?:\\.${mod.local}\\b|\\[["']${mod.local}["']\\])`).test(l))
153
+ ? lines.findIndex((l) => glob
154
+ ? new RegExp(`className=[^>]*?(?<![\\w-])${mod.local.replace(/-/g, "\\-")}(?![\\w-])`).test(l)
155
+ : new RegExp(`\\b\\w+(?:\\.${mod.local}\\b|\\[["']${mod.local}["']\\])`).test(l))
146
156
  : lines.findIndex((l) => l.includes(cls));
147
157
  return i >= 0 ? i + 1 : null;
148
158
  };
@@ -164,6 +174,17 @@ function homeOf(root, files, t) {
164
174
  * "5-8 perguntas por projeto, não 300"). As quebras vêm primeiro: dizer sim à própria assinatura é o caminho feliz. A
165
175
  * primeira opção é sempre a padrão, e é o que vale quando a pessoa não responde.
166
176
  */
177
+ /**
178
+ * O TEXTO QUE A PESSOA VÊ NA PÁGINA - o juízo dele (19/10): "O título de capa passa bem acima da escala (180px contra
179
+ * 70px)" pede que ela ache sozinha qual título é. Com o texto ("TRILHO 07") ela reconhece na hora. `seen` vem como
180
+ * "rota: texto"; até 40 caracteres.
181
+ */
182
+ function quoted(seen) {
183
+ const t = seen?.[0]?.replace(/^[^:]*:\s*/, "").trim();
184
+ if (!t)
185
+ return "this";
186
+ return `"${t.length > 40 ? `${t.slice(0, 39)}…` : t}"`;
187
+ }
167
188
  /** O nome que a pessoa entende - nunca o rótulo interno da classificação ("h3 de cartão", "número"). */
168
189
  function plainRole(role) {
169
190
  if (role === "número")
@@ -174,10 +195,10 @@ function plainRole(role) {
174
195
  return "block";
175
196
  return nameOf(role);
176
197
  }
177
- function questionsFor(breaks, drift) {
198
+ export function questionsFor(breaks, drift) {
178
199
  const qs = breaks.map((b) => ({
179
200
  id: `break:${b.rule}:${b.role}:${b.px}`,
180
- text: `Is this on purpose? The ${plainRole(b.role)} ${b.rule === "Q2" ? "is tilted off the grid" : b.rule === "Q3" ? "is a big motif, outside the headings" : "goes above the heading scale"} (${b.what}).`,
201
+ text: `Is ${quoted(b.seen)} on purpose? The ${plainRole(b.role)} ${b.rule === "Q2" ? "is tilted off the grid" : b.rule === "Q3" ? "is a big motif, outside the headings" : "goes above the heading scale"} (${b.what}).`,
181
202
  options: ["Yes, it is part of my design", "No, make it follow the scale"],
182
203
  }));
183
204
  const groups = new Map();
@@ -260,6 +281,10 @@ export async function confirm(opts) {
260
281
  writeFileSync(join(dsDir, slug, "rules.json"), `${JSON.stringify(rules, null, 2)}\n`);
261
282
  const contract = await syncClaudeMd(root, await agentsToMaintain(root));
262
283
  say(`Your system: ${roles.length} roles and ${breaks.length} breaks, written to _synthesisui/ds/${slug}/rules.json.`);
284
+ /* NUNCA CALADO (o juízo dele, 19/10): um papel lido sem a classe que o identifica no código fica fora - e isso se diz */
285
+ const unplaced = R.roles.filter((r) => !r.home?.cls);
286
+ if (unplaced.length > 0)
287
+ say(`Left out: ${unplaced.map((r) => `${r.role.split(" ")[0]} ${r.px}px`).join(", ")} - the page shows ${unplaced.length === 1 ? "it" : "them"}, but no class in the code says where ${unplaced.length === 1 ? "it is" : "they are"}, so the check could not hold ${unplaced.length === 1 ? "it" : "them"}.`);
263
288
  say(contract ? "The contract your agent reads now lists them - every heading is checked against its role at the end of each turn." : "Run `synthesisui connect` so your agent reads and checks it.");
264
289
  }
265
290
  export async function read(opts) {
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
2
  import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
3
3
  import { basename, dirname, extname, join } from "node:path";
4
4
  export async function loadRoleRules(root) {
@@ -76,6 +76,11 @@ function moduleClasses(attrs, imports) {
76
76
  }
77
77
  return out;
78
78
  }
79
+ /** As classes escritas como texto em `className="a b"` (ou num template sem expressão no meio do nome). */
80
+ function plainClasses(attrs) {
81
+ const lit = attrs.match(/className=\{?\s*["'`]([^"'`]*)["'`]/)?.[1] ?? "";
82
+ return lit.split(/\s+/).filter((t) => /^[A-Za-z_][\w-]*$/.test(t));
83
+ }
79
84
  /** O `font-size` de uma classe dentro do `.module.css` - o maior de um `clamp()` (o desktop), `rem` a 16px. */
80
85
  export function modulePx(css, local) {
81
86
  let best = null;
@@ -96,6 +101,60 @@ export function modulePx(css, local) {
96
101
  }
97
102
  return best;
98
103
  }
104
+ /**
105
+ * A CLASSE GLOBAL - achado no juízo dele (19/10, a landing da Naiker, `synthesisui-tests/juizo-porta`): o impeccable
106
+ * escreveu os títulos com classes comuns, `className="section-title"`, e o tamanho no `globals.css`. A leitura achou os
107
+ * papéis (h1 180, h2 70,4, h3 28,8) e o `--confirm` gravou ZERO, calado - o mesmo defeito do CSS Modules (porta-2) numa
108
+ * terceira forma de escrever estilo.
109
+ *
110
+ * A identidade é a mesma forma do módulo: o arquivo e a classe, `globals.css .section-title`. Vale a classe que tem um
111
+ * `font-size` numa folha de estilo comum do projeto (não `.module.css`), na ordem em que vem - a do próprio elemento
112
+ * primeiro.
113
+ */
114
+ export function globalStyles(root) {
115
+ const out = [];
116
+ const walk = (d) => {
117
+ let names = [];
118
+ try {
119
+ names = readdirSync(d);
120
+ }
121
+ catch {
122
+ return;
123
+ }
124
+ for (const n of names) {
125
+ if (n === "node_modules" || n.startsWith(".") || n === "_synthesisui" || n === "public")
126
+ continue;
127
+ const p = join(d, n);
128
+ let dir = false;
129
+ try {
130
+ dir = statSync(p).isDirectory();
131
+ }
132
+ catch {
133
+ continue;
134
+ }
135
+ if (dir)
136
+ walk(p);
137
+ else if (/\.s?css$/.test(n) && !/\.module\.s?css$/.test(n))
138
+ out.push({ file: n, css: readFileSync(p, "utf8") });
139
+ }
140
+ };
141
+ walk(existsSync(join(root, "src")) ? join(root, "src") : root);
142
+ if (existsSync(join(root, "app")) && existsSync(join(root, "src")))
143
+ walk(join(root, "app"));
144
+ return out;
145
+ }
146
+ export function globalClassOf(tokens, styles) {
147
+ for (const tok of tokens) {
148
+ if (!/^[A-Za-z_][\w-]*$/.test(tok))
149
+ continue;
150
+ for (const st of styles) {
151
+ const px = modulePx(st.css, tok);
152
+ if (px !== null)
153
+ return { name: moduleClassName(st.file, tok), local: tok, px };
154
+ }
155
+ }
156
+ return null;
157
+ }
99
158
  /** Dentro de um `.map(` ainda aberto = o título se repete = é de cartão. A mesma pergunta que a régua faz ao DOM. */
100
159
  function insideMap(src, at) {
101
160
  const before = src.slice(0, at);
@@ -121,6 +180,9 @@ export function checkRoles(rel, src, rules, root) {
121
180
  cssOf.set(file, existsSync(file) ? readFileSync(file, "utf8") : "");
122
181
  return modulePx(cssOf.get(file), local);
123
182
  };
183
+ /** As folhas de estilo comuns do projeto, lidas uma vez e só se algum título usar classe sem tamanho do Tailwind. */
184
+ let globals = null;
185
+ const styles = () => (globals ??= root ? globalStyles(root) : []);
124
186
  const component = basename(rel, extname(rel));
125
187
  const known = new Set((rules.knownDrift ?? []).filter((d) => d.at.startsWith(rel)).map((d) => d.class));
126
188
  const section = rules.roles.find((r) => r.element === "h2" && r.context === "section");
@@ -128,7 +190,8 @@ export function checkRoles(rel, src, rules, root) {
128
190
  for (const m of src.matchAll(/<(h1|h2|h3)\b([^>]*)>/g)) {
129
191
  const el = m[1];
130
192
  const mod = sizeClass(m[2]) ? null : (moduleClasses(m[2], imports)[0] ?? null);
131
- const cls = sizeClass(m[2]) ?? mod?.name ?? null;
193
+ const glob = sizeClass(m[2]) || mod ? null : globalClassOf(plainClasses(m[2]), styles());
194
+ const cls = sizeClass(m[2]) ?? mod?.name ?? glob?.name ?? null;
132
195
  if (!cls)
133
196
  continue;
134
197
  const line = src.slice(0, m.index).split("\n").length;
@@ -141,7 +204,7 @@ export function checkRoles(rel, src, rules, root) {
141
204
  continue;
142
205
  if (known.has(bare))
143
206
  continue;
144
- const px = mod ? pxOfModule(mod.path, mod.local) : pxOf(bare);
207
+ const px = mod ? pxOfModule(mod.path, mod.local) : glob ? glob.px : pxOf(bare);
145
208
  const role = mine.find((r) => r.context === context) ?? mine[0];
146
209
  const brk = rules.breaks.find((b) => b.class === bare);
147
210
  if (brk) {
@@ -184,7 +247,8 @@ export function checkRoles(rel, src, rules, root) {
184
247
  continue;
185
248
  const cls = m[2].match(/className=\{?\s*["'`]([^"'`]*)["'`]/)?.[1] ?? "";
186
249
  const mods = moduleClasses(m[2], imports).map((c) => c.name);
187
- if (!` ${cls} `.includes(` ${b.class} `) && !mods.includes(b.class))
250
+ const globs = b.class.includes(" .") && !b.class.includes(".module.") ? cls.split(/\s+/).filter(Boolean).map((t) => globalClassOf([t], styles())?.name) : [];
251
+ if (!` ${cls} `.includes(` ${b.class} `) && !mods.includes(b.class) && !globs.includes(b.class))
188
252
  continue;
189
253
  const line = src.slice(0, m.index).split("\n").length;
190
254
  if (said.has(`${line} ${b.class}`))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.501",
3
+ "version": "0.16.503",
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": {