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 +1 -1
- package/dist/commands/read.js +31 -6
- package/dist/role-rules.js +68 -4
- package/package.json +1 -1
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`
|
|
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 -->";
|
package/dist/commands/read.js
CHANGED
|
@@ -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
|
|
139
|
-
|
|
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) =>
|
|
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
|
|
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) {
|
package/dist/role-rules.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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