synthesisui 0.16.499 → 0.16.501
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 +27 -5
- package/dist/commands/add.js +2 -0
- package/dist/commands/connect.js +7 -3
- package/dist/commands/hook.js +197 -5
- package/dist/commands/init.js +18 -5
- package/dist/commands/read.js +355 -0
- package/dist/door.js +176 -0
- package/dist/index.js +25 -2
- package/dist/install-marks.js +25 -2
- package/dist/reading/classify.js +181 -0
- package/dist/reading/page-script.js +64 -0
- package/dist/reading/png.js +66 -0
- package/dist/role-rules.js +237 -0
- package/package.json +1 -1
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* O QUE A PÁGINA JÁ DECIDIU - a classificação da leitura, pura (jornada "a leitura no agente", E02, 18/10).
|
|
3
|
+
*
|
|
4
|
+
* Porta, regra por regra, a régua do conjunto (`synthesisui-tests/regua/conjunto/analisar.py`), que provou em 7 lugares -
|
|
5
|
+
* impeccable, Claude livre, SynthesisUI livre, dois sites públicos - que a mesma conta separa, sem ajuste por projeto:
|
|
6
|
+
*
|
|
7
|
+
* REGRA o que se repete: os passos de tipo (3+ lugares), o papel de cada título
|
|
8
|
+
* QUEBRA uma regra nomeada quebrada num papel só, de propósito - o vocabulário das peças de Rule breakers:
|
|
9
|
+
* Q1 passa da escala dos títulos · Q2 sai do eixo (inclinado) · Q3 motivo grande fora dos títulos
|
|
10
|
+
* DERIVA o mesmo papel com tamanhos diferentes na mesma faixa
|
|
11
|
+
*
|
|
12
|
+
* Determinístico: os mesmos textos medidos dão a mesma classificação. E TUDO É CANDIDATO - a pessoa confirma (E03). Este
|
|
13
|
+
* módulo não lê a página nem o disco: recebe o que o navegador mediu e devolve a proposta.
|
|
14
|
+
*/
|
|
15
|
+
const VIZ = 0.05;
|
|
16
|
+
const FAIXA = 1.4;
|
|
17
|
+
const pagina = (rota) => (rota === "/" ? "landing" : rota);
|
|
18
|
+
const superficie = (rota) => (/^\/(dashboard|admin|team)\b/.test(rota) ? "painel" : "site");
|
|
19
|
+
const r1 = (v) => Math.round(v * 10) / 10;
|
|
20
|
+
/** O papel de um texto: o título que ele é (ou está dentro), de cartão ou do texto corrido; decoração; número; texto. */
|
|
21
|
+
export function roleOf(t) {
|
|
22
|
+
const h = /^h[1-3]$/.test(t.tag) ? t.tag : t.papel && /^h[1-3]$/.test(t.papel) ? t.papel : null;
|
|
23
|
+
if (h)
|
|
24
|
+
return h + (h !== "h1" && t.cartao ? " de cartão" : h !== "h1" && t.corrido ? " do texto corrido" : "");
|
|
25
|
+
if (t.decorativo)
|
|
26
|
+
return "decoração";
|
|
27
|
+
if (/^[\d.,%$“”"#]+$/.test(t.texto))
|
|
28
|
+
return "número";
|
|
29
|
+
return "texto";
|
|
30
|
+
}
|
|
31
|
+
function lum(c) {
|
|
32
|
+
const f = (v) => (v / 255 <= 0.03928 ? v / 255 / 12.92 : ((v / 255 + 0.055) / 1.055) ** 2.4);
|
|
33
|
+
return 0.2126 * f(c.r) + 0.7152 * f(c.g) + 0.0722 * f(c.b);
|
|
34
|
+
}
|
|
35
|
+
export const mix = (t, b) => ({ r: t.r * t.a + b.r * (1 - t.a), g: t.g * t.a + b.g * (1 - t.a), b: t.b * t.a + b.b * (1 - t.a), a: 1 });
|
|
36
|
+
export function ratio(c, f) {
|
|
37
|
+
const [a, b] = [lum(mix(c, f)), lum(f)].sort((x, y) => y - x);
|
|
38
|
+
return (a + 0.05) / (b + 0.05);
|
|
39
|
+
}
|
|
40
|
+
export const hex = (c) => `#${[c.r, c.g, c.b].map((v) => Math.round(v).toString(16).padStart(2, "0")).join("")}`;
|
|
41
|
+
export function classify(seen) {
|
|
42
|
+
const all = seen.map((t) => ({ ...t, px: r1(t.tam), pp: roleOf(t), sup: superficie(t.rota), pg: pagina(t.rota) }));
|
|
43
|
+
const desk = all.filter((t) => t.disp === "desktop");
|
|
44
|
+
const lugar = (xs) => new Set(xs.map((x) => `${x.pg}|${x.secao}`)).size;
|
|
45
|
+
const label = (x) => `${x.pg}: ${x.texto.slice(0, 44)}`;
|
|
46
|
+
const uniq = (xs) => [...new Set(xs)].sort();
|
|
47
|
+
// A ESCALA: tamanhos a menos de 5% um do outro são o mesmo passo; passo = usado em 3+ lugares
|
|
48
|
+
const tams = [...new Set(desk.map((t) => t.px))].sort((a, b) => a - b);
|
|
49
|
+
const grupos = [];
|
|
50
|
+
for (const v of tams) {
|
|
51
|
+
const g = grupos[grupos.length - 1];
|
|
52
|
+
if (g && v <= g[0] * (1 + VIZ))
|
|
53
|
+
g.push(v);
|
|
54
|
+
else
|
|
55
|
+
grupos.push([v]);
|
|
56
|
+
}
|
|
57
|
+
const passos = [];
|
|
58
|
+
for (const g of grupos) {
|
|
59
|
+
const xs = desk.filter((t) => g.includes(t.px));
|
|
60
|
+
if (lugar(xs) < 3)
|
|
61
|
+
continue;
|
|
62
|
+
const anc = [...g].sort((a, b) => lugar(xs.filter((t) => t.px === b)) - lugar(xs.filter((t) => t.px === a)) || xs.filter((t) => t.px === b).length - xs.filter((t) => t.px === a).length)[0];
|
|
63
|
+
passos.push(anc);
|
|
64
|
+
}
|
|
65
|
+
// OS PAPÉIS E A DERIVA: por papel e superfície; dentro de uma faixa (vizinhos até 1,4x), o tamanho com mais lugares manda
|
|
66
|
+
const por = new Map();
|
|
67
|
+
for (const t of desk)
|
|
68
|
+
if (t.pp.startsWith("h"))
|
|
69
|
+
por.set(`${t.sup}|${t.pp}`, [...(por.get(`${t.sup}|${t.pp}`) ?? []), t]);
|
|
70
|
+
const best = (vals, xs) => [...vals].sort((a, b) => lugar(xs.filter((x) => x.px === b)) - lugar(xs.filter((x) => x.px === a)) || b - a)[0];
|
|
71
|
+
const papeis = [];
|
|
72
|
+
const deriva = [];
|
|
73
|
+
for (const xs of por.values()) {
|
|
74
|
+
const { sup, pp } = xs[0];
|
|
75
|
+
const principal = best([...new Set(xs.map((x) => x.px))], xs);
|
|
76
|
+
const ps = xs.filter((x) => x.px === principal);
|
|
77
|
+
papeis.push({ papel: pp, superficie: sup, px: principal, lugares: lugar(ps), onde: uniq(ps.map(label)).slice(0, 3), sample: ps[0] });
|
|
78
|
+
const vs = [...new Set(xs.map((x) => x.px))].sort((a, b) => a - b);
|
|
79
|
+
const faixas = [];
|
|
80
|
+
for (const v of vs) {
|
|
81
|
+
const f = faixas[faixas.length - 1];
|
|
82
|
+
if (f && v <= f[f.length - 1] * FAIXA)
|
|
83
|
+
f.push(v);
|
|
84
|
+
else
|
|
85
|
+
faixas.push([v]);
|
|
86
|
+
}
|
|
87
|
+
for (const fx of faixas) {
|
|
88
|
+
if (fx.length < 2)
|
|
89
|
+
continue;
|
|
90
|
+
const grupo = xs.filter((x) => fx.includes(x.px));
|
|
91
|
+
const regra = best(fx, grupo);
|
|
92
|
+
const rs = grupo.filter((x) => x.px === regra);
|
|
93
|
+
for (const v of fx) {
|
|
94
|
+
if (v === regra)
|
|
95
|
+
continue;
|
|
96
|
+
const ys = grupo.filter((x) => x.px === v);
|
|
97
|
+
deriva.push({ papel: pp, superficie: sup, px: v, regraPx: regra, onde: uniq(ys.map(label)), sample: ys[0], ruleSample: rs[0] });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// O TÍTULO DE SEÇÃO = a maior faixa de h2 do site com 2+ textos (os próprios <h2>, não os <span> dentro deles)
|
|
102
|
+
const h2own = desk.filter((x) => x.sup === "site" && x.pp === "h2" && x.tag === "h2");
|
|
103
|
+
const h2 = h2own.length ? h2own : desk.filter((x) => x.sup === "site" && x.pp === "h2");
|
|
104
|
+
const near = (a, b) => Math.abs(a - b) <= Math.max(a, b) * VIZ;
|
|
105
|
+
const cands = [...new Set(h2.map((x) => x.px))].sort((a, b) => b - a);
|
|
106
|
+
const tituloSecao = cands.find((v) => new Set(h2.filter((x) => near(x.px, v)).map((x) => x.texto)).size >= 2) ?? passos[passos.length - 1] ?? 0;
|
|
107
|
+
// AS QUEBRAS
|
|
108
|
+
const derivados = new Set(deriva.map((d) => `${d.superficie}|${d.papel}|${d.px}`));
|
|
109
|
+
const quebras = new Map();
|
|
110
|
+
for (const t of desk) {
|
|
111
|
+
if (t.sup !== "site")
|
|
112
|
+
continue;
|
|
113
|
+
let q = null;
|
|
114
|
+
if (t.px > tituloSecao * 1.04 && !derivados.has(`${t.sup}|${t.pp}|${t.px}`))
|
|
115
|
+
q = { regra: "Q1", nome: "above the heading scale", papel: t.pp, px: t.px, medida: `${t.px}px against the ${tituloSecao}px section title` };
|
|
116
|
+
else if ((t.pp === "número" || t.pp === "decoração") && t.px >= 40)
|
|
117
|
+
q = { regra: "Q3", nome: "big motif outside the headings", papel: t.pp, px: t.px, medida: `${t.px}px, ${t.decorativo ? "hidden from screen readers" : "read as content"}` };
|
|
118
|
+
if (!q || quebras.has(`${q.regra}|${q.papel}|${q.px}`))
|
|
119
|
+
continue;
|
|
120
|
+
const xs = desk.filter((x) => x.sup === "site" && x.pp === t.pp && x.px === t.px);
|
|
121
|
+
quebras.set(`${q.regra}|${q.papel}|${q.px}`, { ...q, vezes: xs.length, onde: uniq(xs.map(label)).slice(0, 5), sample: t });
|
|
122
|
+
}
|
|
123
|
+
const giros = new Map();
|
|
124
|
+
for (const t of desk)
|
|
125
|
+
if (t.giro)
|
|
126
|
+
giros.set(`${t.pg}|${t.giro}`, [...(giros.get(`${t.pg}|${t.giro}`) ?? []), t]);
|
|
127
|
+
for (const xs of giros.values()) {
|
|
128
|
+
const alvo = xs.find((x) => x.pp === "h1") ?? xs[0];
|
|
129
|
+
const k = `Q2|bloco|${alvo.px}`;
|
|
130
|
+
if (!quebras.has(k))
|
|
131
|
+
quebras.set(k, { regra: "Q2", nome: "off the axis (tilted)", papel: "bloco", px: alvo.px, medida: `${alvo.giro}° - the whole block, ${xs.length} texts inside`, vezes: 1, onde: uniq(xs.map(label)).slice(0, 4), sample: alvo });
|
|
132
|
+
}
|
|
133
|
+
// O CONTRASTE - candidatos pelo fundo do DOM; quem chama confere cada um pelos pixels (`pixelRatio`)
|
|
134
|
+
const falhas = new Map();
|
|
135
|
+
let sobreImagem = 0;
|
|
136
|
+
for (const t of all) {
|
|
137
|
+
if (t.decorativo || !t.cor)
|
|
138
|
+
continue;
|
|
139
|
+
if (t.sobreImagem) {
|
|
140
|
+
sobreImagem++;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const r = ratio(t.cor, t.fundo);
|
|
144
|
+
const alvo = t.tam >= 24 || (t.tam >= 18.66 && t.peso >= 700) ? 3 : 4.5;
|
|
145
|
+
if (r >= alvo)
|
|
146
|
+
continue;
|
|
147
|
+
const k = `${hex(mix(t.cor, t.fundo))}|${hex(t.fundo)}|${Math.round(t.tam)}`;
|
|
148
|
+
const f = falhas.get(k) ?? { razao: Math.round(r * 100) / 100, alvo, texto: hex(mix(t.cor, t.fundo)), fundo: hex(t.fundo), px: t.tam, onde: [], sample: t };
|
|
149
|
+
f.onde = uniq([...f.onde, label(t)]).slice(0, 5);
|
|
150
|
+
falhas.set(k, f);
|
|
151
|
+
}
|
|
152
|
+
return { passos, tituloSecao, papeis, deriva, quebras: [...quebras.values()], contraste: [...falhas.values()].sort((a, b) => a.razao - b.razao), sobreImagem };
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* A TINTA DE VERDADE, PELOS PIXELS - na régua, a conta pelo fundo do DOM acusou 1,0:1 num "$" visível (o fundo vinha de
|
|
156
|
+
* outra camada), e o linear.app teve 7 textos que nem estavam pintados. Recebe os pixels RGBA da caixa do texto: o fundo é
|
|
157
|
+
* a cor mais presente; a tinta, entre as que ocupam 3%+ da caixa, a que mais contrasta com ele. Sem tinta (< 1,2:1) é
|
|
158
|
+
* texto não pintado ali, nunca uma falha.
|
|
159
|
+
*/
|
|
160
|
+
export function pixelRatio(rgba) {
|
|
161
|
+
const n = Math.floor(rgba.length / 4);
|
|
162
|
+
if (n < 4)
|
|
163
|
+
return null;
|
|
164
|
+
const buckets = new Map();
|
|
165
|
+
for (let i = 0; i < n; i++) {
|
|
166
|
+
const r = rgba[i * 4], g = rgba[i * 4 + 1], b = rgba[i * 4 + 2];
|
|
167
|
+
const k = ((r >> 4) << 8) | ((g >> 4) << 4) | (b >> 4);
|
|
168
|
+
const e = buckets.get(k) ?? { c: 0, r: 0, g: 0, b: 0 };
|
|
169
|
+
e.c++;
|
|
170
|
+
e.r += r;
|
|
171
|
+
e.g += g;
|
|
172
|
+
e.b += b;
|
|
173
|
+
buckets.set(k, e);
|
|
174
|
+
}
|
|
175
|
+
const cores = [...buckets.values()].sort((a, b) => b.c - a.c).map((e) => ({ c: e.c, cor: { r: e.r / e.c, g: e.g / e.c, b: e.b / e.c, a: 1 } }));
|
|
176
|
+
const fundo = cores[0].cor;
|
|
177
|
+
const tintas = cores.slice(1).filter((x) => x.c / n >= 0.03);
|
|
178
|
+
if (tintas.length === 0)
|
|
179
|
+
return cores.length > 1 ? ratio(cores[1].cor, fundo) : 1;
|
|
180
|
+
return Math.max(...tintas.map((x) => ratio(x.cor, fundo)));
|
|
181
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* O QUE RODA DENTRO DA PÁGINA - a medida de cada texto como o visitante o vê (jornada "a leitura no agente", E02, 18/10).
|
|
3
|
+
*
|
|
4
|
+
* O mesmo leitor da régua do conjunto (`synthesisui-tests/regua/conjunto/ler.mjs`), reduzido ao que a classificação usa:
|
|
5
|
+
* tamanho, família, peso, cor, o fundo real atrás do texto (subindo pelos ancestrais e misturando transparência), se está
|
|
6
|
+
* sobre imagem, inclinação, ênfase, cartão (estrutura repetida 3+), texto corrido, o COMPONENTE (pela fibra do React, no
|
|
7
|
+
* `next dev` - a ligação ao código que independe da forma de estilo) e a classe. É uma string porque roda no navegador,
|
|
8
|
+
* não no Node: `page.evaluate(PAGE_SCRIPT)`.
|
|
9
|
+
*/
|
|
10
|
+
export const PAGE_SCRIPT = String.raw `(() => {
|
|
11
|
+
const parse = (s) => { const m = s.match(/rgba?\(([^)]+)\)/); if (!m) return null; const v = m[1].split(/[ ,/]+/).filter(Boolean).map(Number); return { r: v[0], g: v[1], b: v[2], a: v.length > 3 ? v[3] : 1 }; };
|
|
12
|
+
const mix = (top, bot) => ({ r: top.r * top.a + bot.r * (1 - top.a), g: top.g * top.a + bot.g * (1 - top.a), b: top.b * top.a + bot.b * (1 - top.a), a: 1 });
|
|
13
|
+
const fundo = (el) => {
|
|
14
|
+
const camadas = []; let img = false;
|
|
15
|
+
for (let e = el; e; e = e.parentElement) {
|
|
16
|
+
const cs = getComputedStyle(e);
|
|
17
|
+
if (cs.backgroundImage && cs.backgroundImage !== "none") img = img || /url\(|gradient/.test(cs.backgroundImage);
|
|
18
|
+
const bg = parse(cs.backgroundColor); if (bg && bg.a > 0) { camadas.push(bg); if (bg.a >= 1) break; }
|
|
19
|
+
if (e.parentElement && [...e.parentElement.children].some((s) => s !== e && /^(VIDEO|IMG)$/.test(s.tagName) && getComputedStyle(s).position === "absolute")) img = true;
|
|
20
|
+
}
|
|
21
|
+
let cor = { r: 255, g: 255, b: 255, a: 1 };
|
|
22
|
+
for (let i = camadas.length - 1; i >= 0; i--) cor = mix(camadas[i], cor);
|
|
23
|
+
return { cor, img };
|
|
24
|
+
};
|
|
25
|
+
const secao = (el) => { for (let e = el; e; e = e.parentElement) { if (/^(SECTION|HEADER|FOOTER|NAV|MAIN|ASIDE|ARTICLE|DIALOG)$/.test(e.tagName) || e.id) return e.tagName.toLowerCase() + (e.id ? "#" + e.id : ""); } return "body"; };
|
|
26
|
+
const textos = [];
|
|
27
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
|
28
|
+
const vistos = new Set();
|
|
29
|
+
while (walker.nextNode()) {
|
|
30
|
+
const t = walker.currentNode; const el = t.parentElement; if (!el || vistos.has(el)) continue;
|
|
31
|
+
const txt = t.textContent.trim(); if (!txt) continue;
|
|
32
|
+
const cs = getComputedStyle(el);
|
|
33
|
+
if (cs.visibility === "hidden" || cs.display === "none" || Number(cs.opacity) === 0) continue;
|
|
34
|
+
const r = el.getBoundingClientRect(); if (r.width < 1 || r.height < 1) continue;
|
|
35
|
+
vistos.add(el);
|
|
36
|
+
let giro = 0; for (let e = el; e && e !== document.body; e = e.parentElement) { const tf = getComputedStyle(e).transform; if (tf && tf !== "none") { const m = tf.match(/matrix\(([^)]+)\)/); if (m) { const [a, bb] = m[1].split(",").map(Number); const ang = Math.round(Math.atan2(bb, a) * 1800 / Math.PI) / 10; if (ang) { giro = ang; break; } } } }
|
|
37
|
+
const tit = el.closest("h1,h2,h3"); const enfase = !!(tit && tit !== el && getComputedStyle(tit).color !== cs.color);
|
|
38
|
+
let cartao = false;
|
|
39
|
+
for (let e = el, i = 0; e && e.parentElement && i < 6 && !cartao; e = e.parentElement, i++) {
|
|
40
|
+
const irmaos = [...e.parentElement.children].filter((x) => x.tagName === e.tagName && (x.getAttribute("class") || "") === (e.getAttribute("class") || ""));
|
|
41
|
+
if (irmaos.length >= 3 && e !== el) cartao = true;
|
|
42
|
+
}
|
|
43
|
+
const corrido = !!(tit || el).closest("article") || (((tit || el).parentElement && (tit || el).parentElement.querySelectorAll(":scope > p").length) || 0) >= 3;
|
|
44
|
+
let comp = null;
|
|
45
|
+
const fk = Object.keys(el).find((k) => k.startsWith("__reactFiber$"));
|
|
46
|
+
for (let f = fk ? el[fk] : null, i = 0; f && i < 40 && !comp; f = f.return, i++) {
|
|
47
|
+
const st = f._debugStack && String(f._debugStack.stack || "");
|
|
48
|
+
if (st) for (const ln of st.split("\n").slice(1)) { const mm = ln.match(/at ([A-Z][A-Za-z0-9_]*) \(/); if (mm && !/^(Object|Array|Error|Promise)$/.test(mm[1])) { comp = mm[1]; break; } }
|
|
49
|
+
}
|
|
50
|
+
let op = 1; for (let e = el; e; e = e.parentElement) op *= Number(getComputedStyle(e).opacity);
|
|
51
|
+
const cor = parse(cs.color); if (cor) cor.a *= op;
|
|
52
|
+
const f = fundo(el);
|
|
53
|
+
textos.push({
|
|
54
|
+
texto: txt.slice(0, 60), tag: el.tagName.toLowerCase(),
|
|
55
|
+
papel: (el.closest("h1,h2,h3,h4,h5,h6,button,a,label,th,figcaption,blockquote,input") || { tagName: "" }).tagName.toLowerCase() || null,
|
|
56
|
+
tam: parseFloat(cs.fontSize), familia: cs.fontFamily.split(",")[0].replace(/["']/g, "").trim(), peso: Number(cs.fontWeight),
|
|
57
|
+
cor, fundo: f.cor, sobreImagem: f.img, decorativo: !!el.closest("[aria-hidden=true]"),
|
|
58
|
+
cartao, corrido, giro, enfase, comp,
|
|
59
|
+
cls: [el, ...(tit && tit !== el ? [tit] : []), el.parentElement].filter(Boolean).map((x) => x.getAttribute("class") || "").join(" | ").slice(0, 600),
|
|
60
|
+
secao: secao(el), x: r.left + scrollX, y: r.top + scrollY, w: r.width, h: r.height,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return textos;
|
|
64
|
+
})()`;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { inflateSync } from "node:zlib";
|
|
2
|
+
/**
|
|
3
|
+
* O PNG DE UMA CAIXA DE TEXTO, EM PIXELS - sem dependência (o CLI não tem nenhuma, e não é a conferência do contraste que vai
|
|
4
|
+
* inaugurar a primeira). Lê o que o navegador devolve num `screenshot`: 8 bits, RGB ou RGBA, sem entrelaçamento. Qualquer
|
|
5
|
+
* outra forma devolve `null`, e quem chama trata como "não conferido" - nunca como falha.
|
|
6
|
+
*/
|
|
7
|
+
export function decodePng(buf) {
|
|
8
|
+
if (buf.length < 8 || buf.readUInt32BE(0) !== 0x89504e47)
|
|
9
|
+
return null;
|
|
10
|
+
let off = 8, w = 0, h = 0, type = 0, depth = 0, interlace = 0;
|
|
11
|
+
const idat = [];
|
|
12
|
+
while (off + 8 <= buf.length) {
|
|
13
|
+
const len = buf.readUInt32BE(off);
|
|
14
|
+
const kind = buf.toString("ascii", off + 4, off + 8);
|
|
15
|
+
const data = buf.subarray(off + 8, off + 8 + len);
|
|
16
|
+
if (kind === "IHDR") {
|
|
17
|
+
w = data.readUInt32BE(0);
|
|
18
|
+
h = data.readUInt32BE(4);
|
|
19
|
+
depth = data[8];
|
|
20
|
+
type = data[9];
|
|
21
|
+
interlace = data[12];
|
|
22
|
+
}
|
|
23
|
+
else if (kind === "IDAT")
|
|
24
|
+
idat.push(data);
|
|
25
|
+
else if (kind === "IEND")
|
|
26
|
+
break;
|
|
27
|
+
off += 12 + len;
|
|
28
|
+
}
|
|
29
|
+
if (depth !== 8 || interlace !== 0 || (type !== 2 && type !== 6) || !w || !h)
|
|
30
|
+
return null;
|
|
31
|
+
const bpp = type === 6 ? 4 : 3;
|
|
32
|
+
const raw = inflateSync(Buffer.concat(idat));
|
|
33
|
+
const stride = w * bpp;
|
|
34
|
+
const out = new Uint8Array(w * h * 4);
|
|
35
|
+
const prev = new Uint8Array(stride);
|
|
36
|
+
const cur = new Uint8Array(stride);
|
|
37
|
+
for (let y = 0; y < h; y++) {
|
|
38
|
+
const filter = raw[y * (stride + 1)];
|
|
39
|
+
for (let x = 0; x < stride; x++) {
|
|
40
|
+
const v = raw[y * (stride + 1) + 1 + x];
|
|
41
|
+
const a = x >= bpp ? cur[x - bpp] : 0;
|
|
42
|
+
const b = prev[x];
|
|
43
|
+
const c = x >= bpp ? prev[x - bpp] : 0;
|
|
44
|
+
let p = 0;
|
|
45
|
+
if (filter === 1)
|
|
46
|
+
p = a;
|
|
47
|
+
else if (filter === 2)
|
|
48
|
+
p = b;
|
|
49
|
+
else if (filter === 3)
|
|
50
|
+
p = (a + b) >> 1;
|
|
51
|
+
else if (filter === 4) {
|
|
52
|
+
const pa = Math.abs(b - c), pb = Math.abs(a - c), pc = Math.abs(a + b - 2 * c);
|
|
53
|
+
p = pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
|
|
54
|
+
}
|
|
55
|
+
cur[x] = (v + p) & 255;
|
|
56
|
+
}
|
|
57
|
+
for (let x = 0; x < w; x++) {
|
|
58
|
+
out[(y * w + x) * 4] = cur[x * bpp];
|
|
59
|
+
out[(y * w + x) * 4 + 1] = cur[x * bpp + 1];
|
|
60
|
+
out[(y * w + x) * 4 + 2] = cur[x * bpp + 2];
|
|
61
|
+
out[(y * w + x) * 4 + 3] = bpp === 4 ? cur[x * bpp + 3] : 255;
|
|
62
|
+
}
|
|
63
|
+
prev.set(cur);
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, extname, join } from "node:path";
|
|
4
|
+
export async function loadRoleRules(root) {
|
|
5
|
+
const ds = join(root, "_synthesisui", "ds");
|
|
6
|
+
const slugs = await readdir(ds).catch(() => []);
|
|
7
|
+
for (const slug of slugs) {
|
|
8
|
+
const raw = await readFile(join(ds, slug, "rules.json"), "utf8").catch(() => null);
|
|
9
|
+
if (!raw)
|
|
10
|
+
continue;
|
|
11
|
+
try {
|
|
12
|
+
const r = JSON.parse(raw);
|
|
13
|
+
if (Array.isArray(r.roles))
|
|
14
|
+
return { ...r, breaks: r.breaks ?? [] };
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
/* um rules.json quebrado não derruba a checagem: ela só não fala de papéis */
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const NAMED = { xs: 12, sm: 14, base: 16, lg: 18, xl: 20, "2xl": 24, "3xl": 30, "4xl": 36, "5xl": 48, "6xl": 60, "7xl": 72, "8xl": 96, "9xl": 128 };
|
|
23
|
+
/** Só classe de TAMANHO: `text-[#1b1406]` é cor e não entra (achado na leitura do synthesisui.com, 18/10). */
|
|
24
|
+
const SIZE = /(?:^|\s)((?:[a-z0-9]+:)?text-(?:\[(?:clamp\([^\]\s]+\)|[\d.]+(?:rem|px|em))\]|xs|sm|base|lg|[2-9]?xl))(?=\s|$)/g;
|
|
25
|
+
/** O tamanho final (desktop) de uma classe: o teto do `clamp`, o valor do colchete, ou a escala do Tailwind. */
|
|
26
|
+
export function pxOf(cls) {
|
|
27
|
+
const bare = cls.replace(/^(?:[a-z0-9]+:)+/, "");
|
|
28
|
+
const named = bare.match(/^text-(xs|sm|base|lg|[2-9]?xl)$/);
|
|
29
|
+
if (named)
|
|
30
|
+
return NAMED[named[1]] ?? null;
|
|
31
|
+
const inner = bare.match(/^text-\[(.+)\]$/)?.[1];
|
|
32
|
+
if (!inner)
|
|
33
|
+
return null;
|
|
34
|
+
const last = inner.startsWith("clamp(") ? inner.slice(6, -1).split(",").pop()?.trim() : inner;
|
|
35
|
+
const m = last?.match(/^([\d.]+)(rem|px)$/);
|
|
36
|
+
if (!m)
|
|
37
|
+
return null;
|
|
38
|
+
return m[2] === "rem" ? Number(m[1]) * 16 : Number(m[1]);
|
|
39
|
+
}
|
|
40
|
+
/** O maior tamanho entre as classes de um elemento - o que vale no desktop (a régua mede em 1440). */
|
|
41
|
+
function sizeClass(attrs) {
|
|
42
|
+
const cls = attrs.match(/className=\{?\s*["'`]([^"'`]*)["'`]/)?.[1] ?? attrs.match(/className=\{cn\(([^)]*)\)/)?.[1] ?? "";
|
|
43
|
+
const found = [...` ${cls} `.matchAll(SIZE)].map((m) => m[1]);
|
|
44
|
+
if (found.length === 0)
|
|
45
|
+
return null;
|
|
46
|
+
return found.sort((a, b) => (pxOf(b) ?? 0) - (pxOf(a) ?? 0))[0] ?? null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A CLASSE DE UM CSS MODULE - achado na rodada real da porta (18/10, `regua/rodadas/porta-2`): o impeccable escreveu a
|
|
50
|
+
* landing com `className={styles.h2}` (117 usos, 4 `.module.css`), a leitura achou 5 papéis e o `--confirm` gravou ZERO,
|
|
51
|
+
* porque a identidade de um papel era só a classe de tamanho do Tailwind. Uma de três criações medidas foi assim.
|
|
52
|
+
*
|
|
53
|
+
* A identidade passa a ser o ARQUIVO e o nome local - `page.module.css .h2` -, que é o que a pessoa acha no código e o
|
|
54
|
+
* que o hash do build não muda. O apelido do import (`styles`, `s`) não entra: dois arquivos podem chamar o mesmo módulo
|
|
55
|
+
* de jeitos diferentes.
|
|
56
|
+
*/
|
|
57
|
+
export function moduleClassName(file, local) {
|
|
58
|
+
return `${file} .${local}`;
|
|
59
|
+
}
|
|
60
|
+
/** Os imports de CSS Module de um arquivo: apelido -> o caminho como foi escrito. */
|
|
61
|
+
function moduleImports(src) {
|
|
62
|
+
const out = new Map();
|
|
63
|
+
for (const m of src.matchAll(/import\s+(\w+)\s+from\s+["']([^"']+\.module\.s?css)["']/g))
|
|
64
|
+
out.set(m[1], m[2]);
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
/** As classes de módulo dentro de um `className={...}`, na ordem em que aparecem. */
|
|
68
|
+
function moduleClasses(attrs, imports) {
|
|
69
|
+
const expr = attrs.match(/className=\{([^}]*(?:\}[^}]*)?)\}/)?.[1] ?? "";
|
|
70
|
+
const out = [];
|
|
71
|
+
for (const m of expr.matchAll(/\b(\w+)(?:\.([A-Za-z_][\w-]*)|\[["']([\w-]+)["']\])/g)) {
|
|
72
|
+
const path = imports.get(m[1]);
|
|
73
|
+
const local = (m[2] ?? m[3]);
|
|
74
|
+
if (path && local)
|
|
75
|
+
out.push({ name: moduleClassName(basename(path), local), path, local });
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
/** O `font-size` de uma classe dentro do `.module.css` - o maior de um `clamp()` (o desktop), `rem` a 16px. */
|
|
80
|
+
export function modulePx(css, local) {
|
|
81
|
+
let best = null;
|
|
82
|
+
const esc = local.replace(/[-]/g, "\\-");
|
|
83
|
+
for (const m of css.matchAll(new RegExp(`([^{}]*\\.${esc}(?![\\w-])[^{}]*)\\{([^}]*)\\}`, "g"))) {
|
|
84
|
+
const v = m[2].match(/font-size\s*:\s*([^;]+)/)?.[1]?.trim();
|
|
85
|
+
if (!v)
|
|
86
|
+
continue;
|
|
87
|
+
const parts = v.startsWith("clamp(") ? v.slice(6, -1).split(",") : [v];
|
|
88
|
+
for (const p of parts) {
|
|
89
|
+
const n = p.trim().match(/^([\d.]+)(px|rem)$/);
|
|
90
|
+
if (!n)
|
|
91
|
+
continue;
|
|
92
|
+
const px = Number(n[1]) * (n[2] === "rem" ? 16 : 1);
|
|
93
|
+
if (best === null || px > best)
|
|
94
|
+
best = px;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return best;
|
|
98
|
+
}
|
|
99
|
+
/** 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
|
+
function insideMap(src, at) {
|
|
101
|
+
const before = src.slice(0, at);
|
|
102
|
+
const open = before.lastIndexOf(".map(");
|
|
103
|
+
if (open < 0)
|
|
104
|
+
return false;
|
|
105
|
+
let depth = 0;
|
|
106
|
+
for (const ch of before.slice(open + 4))
|
|
107
|
+
depth += ch === "(" ? 1 : ch === ")" ? -1 : 0;
|
|
108
|
+
return depth > 0;
|
|
109
|
+
}
|
|
110
|
+
const near = (a, b) => Math.abs(a - b) / Math.max(a, b) < 0.03;
|
|
111
|
+
export function checkRoles(rel, src, rules, root) {
|
|
112
|
+
if (rules.enforce === false || !/\.(tsx|jsx)$/.test(rel))
|
|
113
|
+
return [];
|
|
114
|
+
const imports = moduleImports(src);
|
|
115
|
+
const cssOf = new Map();
|
|
116
|
+
const pxOfModule = (path, local) => {
|
|
117
|
+
if (!root)
|
|
118
|
+
return null;
|
|
119
|
+
const file = join(root, dirname(rel), path);
|
|
120
|
+
if (!cssOf.has(file))
|
|
121
|
+
cssOf.set(file, existsSync(file) ? readFileSync(file, "utf8") : "");
|
|
122
|
+
return modulePx(cssOf.get(file), local);
|
|
123
|
+
};
|
|
124
|
+
const component = basename(rel, extname(rel));
|
|
125
|
+
const known = new Set((rules.knownDrift ?? []).filter((d) => d.at.startsWith(rel)).map((d) => d.class));
|
|
126
|
+
const section = rules.roles.find((r) => r.element === "h2" && r.context === "section");
|
|
127
|
+
const out = [];
|
|
128
|
+
for (const m of src.matchAll(/<(h1|h2|h3)\b([^>]*)>/g)) {
|
|
129
|
+
const el = m[1];
|
|
130
|
+
const mod = sizeClass(m[2]) ? null : (moduleClasses(m[2], imports)[0] ?? null);
|
|
131
|
+
const cls = sizeClass(m[2]) ?? mod?.name ?? null;
|
|
132
|
+
if (!cls)
|
|
133
|
+
continue;
|
|
134
|
+
const line = src.slice(0, m.index).split("\n").length;
|
|
135
|
+
const context = insideMap(src, m.index ?? 0) ? "card" : "section";
|
|
136
|
+
const bare = cls.replace(/^(?:[a-z0-9]+:)+/, "");
|
|
137
|
+
const mine = rules.roles.filter((r) => r.element === el);
|
|
138
|
+
if (mine.some((r) => r.class === bare))
|
|
139
|
+
continue;
|
|
140
|
+
if (rules.breaks.some((b) => b.class === bare && b.allowedIn === component))
|
|
141
|
+
continue;
|
|
142
|
+
if (known.has(bare))
|
|
143
|
+
continue;
|
|
144
|
+
const px = mod ? pxOfModule(mod.path, mod.local) : pxOf(bare);
|
|
145
|
+
const role = mine.find((r) => r.context === context) ?? mine[0];
|
|
146
|
+
const brk = rules.breaks.find((b) => b.class === bare);
|
|
147
|
+
if (brk) {
|
|
148
|
+
out.push(` line ${line} <${el}> ${bare} is the break "${brk.rule}", which the person chose for ${brk.allowedIn} only (${brk.home}).${role ? ` Here, use the ${role.name}: ${role.class} (${role.home}).` : ""}`);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (role && px && near(px, role.px)) {
|
|
152
|
+
out.push(` line ${line} <${el}> ${bare} renders like the ${role.name} but is written differently - use ${role.class} (${role.home}), so the two stay one rule.`);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (section && px && px > section.px * 1.04 && el !== "h1") {
|
|
156
|
+
out.push(` line ${line} <${el}> ${bare} (~${Math.round(px)}px) goes above this project's section titles (${Math.round(section.px)}px). Only the breaks the person chose go there. Use the ${role?.name ?? "section title"} (${role?.class ?? section.class}), or ask whether this is a new break.`);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (!role || !px)
|
|
160
|
+
continue;
|
|
161
|
+
/**
|
|
162
|
+
* PERTO DO PAPEL É DERIVA; LONGE DELE É PROVAVELMENTE UM PAPEL NOVO - e papel novo é decisão da pessoa. Medido na
|
|
163
|
+
* primeira versão, contra o corte: o título DENTRO do artigo (36px) ouviu "use o título de seção (86px)", e seguir
|
|
164
|
+
* isso estragaria a página. A mesma faixa da régua (1,4x) separa os dois casos.
|
|
165
|
+
*/
|
|
166
|
+
if (px >= role.px / 1.4 && px <= role.px * 1.4)
|
|
167
|
+
out.push(` line ${line} <${el}> ${bare} (~${Math.round(px)}px) - the ${role.name} role in this project is ${role.class} (${Math.round(role.px)}px, ${role.home}). Use it, so the role stays one size.`);
|
|
168
|
+
else
|
|
169
|
+
out.push(` line ${line} <${el}> ${bare} (~${Math.round(px)}px) is a heading size this project has no role for yet (its ${role.name} is ${Math.round(role.px)}px). If it is a new role - a heading inside an article, a card - keep it and name the role to the person in one line; if it is a ${role.name}, use ${role.class}.`);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* A QUEBRA FORA DO LUGAR, EM QUALQUER ELEMENTO - medido na linha B (18/10): sem checagem, a página do cachorro copiou os
|
|
173
|
+
* números grandes da landing. Os títulos já estão acima; aqui, todo elemento que não é título e usa a classe de uma
|
|
174
|
+
* quebra fora do componente dela. A cópia com OUTRO tamanho (41,6 no lugar de 57,6) não aparece no código - é a régua
|
|
175
|
+
* da página renderizada que vê ("o mesmo motivo"), no fecho do turno.
|
|
176
|
+
*/
|
|
177
|
+
/* duas quebras podem morar na mesma classe (a capa da porta-2: "Batch" e "Nº 01" em `.batch span`) - uma linha basta */
|
|
178
|
+
const said = new Set();
|
|
179
|
+
for (const b of rules.breaks) {
|
|
180
|
+
if (!b.class || b.allowedIn === component)
|
|
181
|
+
continue;
|
|
182
|
+
for (const m of src.matchAll(/<([a-z][a-z0-9]*)\b([^>]*)>/g)) {
|
|
183
|
+
if (/^h[1-3]$/.test(m[1]))
|
|
184
|
+
continue;
|
|
185
|
+
const cls = m[2].match(/className=\{?\s*["'`]([^"'`]*)["'`]/)?.[1] ?? "";
|
|
186
|
+
const mods = moduleClasses(m[2], imports).map((c) => c.name);
|
|
187
|
+
if (!` ${cls} `.includes(` ${b.class} `) && !mods.includes(b.class))
|
|
188
|
+
continue;
|
|
189
|
+
const line = src.slice(0, m.index).split("\n").length;
|
|
190
|
+
if (said.has(`${line} ${b.class}`))
|
|
191
|
+
continue;
|
|
192
|
+
said.add(`${line} ${b.class}`);
|
|
193
|
+
out.push(` line ${line} <${m[1]}> ${b.class} is the break "${b.rule}" the person chose for ${b.allowedIn} (${b.home}). Reuse that component here instead of copying the motif, or ask whether this page gets its own.`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
198
|
+
/** O bloco do contrato: os papéis e as quebras, para o agente saber ANTES de escrever (a prevenção). */
|
|
199
|
+
export function rolesContract(rules) {
|
|
200
|
+
const roles = rules.roles
|
|
201
|
+
.map((r) => `- ${r.name} (\`<${r.element}>\`${r.context === "card" ? " in a repeated card" : ""}): \`${r.class}\` - ${r.home}`)
|
|
202
|
+
.join("\n");
|
|
203
|
+
const breaks = rules.breaks
|
|
204
|
+
.filter((b) => b.class)
|
|
205
|
+
.map((b) => `- ${b.rule}: \`${b.class}\` - only in ${b.allowedIn} (${b.home})`)
|
|
206
|
+
.join("\n");
|
|
207
|
+
return `\n\n**The type roles this project already decided** (read from its own pages). A new heading takes the class of its role - never a new size for an existing role:\n${roles}${breaks ? `\n\n**The breaks the person chose** - they stay where they live, and nowhere else:\n${breaks}` : ""}`;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* O PAPEL NOVO É DITO UMA VEZ - medido na linha A (18/10): o aviso "a heading size this project has no role for yet" voltou
|
|
211
|
+
* em TODO fim de turno nos mesmos arquivos do blog, e cada repetição custou leitura e resposta do agente (a rodada saiu
|
|
212
|
+
* ~US$ 1 mais cara por turno que o corte). Papel novo é decisão da pessoa; depois de dito, repetir é ruído. A deriva (o
|
|
213
|
+
* título perto do papel e escrito diferente) continua sendo dita sempre - ela é conserto, não decisão.
|
|
214
|
+
*/
|
|
215
|
+
const SAID = (root) => join(root, "_synthesisui", ".roles-said.json");
|
|
216
|
+
export async function onlyOnce(root, rel, lines) {
|
|
217
|
+
const said = new Set(JSON.parse((await readFile(SAID(root), "utf8").catch(() => "[]")) || "[]"));
|
|
218
|
+
const out = [];
|
|
219
|
+
let changed = false;
|
|
220
|
+
for (const l of lines) {
|
|
221
|
+
if (!l.includes("has no role for yet")) {
|
|
222
|
+
out.push(l);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const key = `${rel}|${l.match(/> (\S+)/)?.[1] ?? l}`;
|
|
226
|
+
if (said.has(key))
|
|
227
|
+
continue;
|
|
228
|
+
said.add(key);
|
|
229
|
+
changed = true;
|
|
230
|
+
out.push(l);
|
|
231
|
+
}
|
|
232
|
+
if (changed) {
|
|
233
|
+
await mkdir(join(root, "_synthesisui"), { recursive: true }).catch(() => undefined);
|
|
234
|
+
await writeFile(SAID(root), JSON.stringify([...said], null, 1)).catch(() => undefined);
|
|
235
|
+
}
|
|
236
|
+
return out;
|
|
237
|
+
}
|
package/package.json
CHANGED