synthesisui 0.16.238 → 0.16.240
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/anatomy-read.js +15 -0
- package/dist/claude-md.js +7 -6
- package/dist/commands/adopt.js +9 -1
- package/dist/commands/doctor.js +20 -12
- package/dist/commands/gaps.js +2 -2
- package/dist/commands/import.js +21 -10
- package/dist/commands/mcp.js +22 -5
- package/dist/commands/sync.js +4 -4
- package/dist/commands/use.js +31 -2
- package/dist/doctor/ci-format.js +14 -5
- package/dist/doctor/tokens.js +14 -2
- package/dist/install-marks.js +17 -2
- package/dist/skill-import.js +1 -1
- package/dist/stack.js +79 -17
- package/package.json +1 -1
package/dist/anatomy-read.js
CHANGED
|
@@ -417,6 +417,7 @@ sxSpacing) {
|
|
|
417
417
|
(kind === "headless" || kind === "motion" || kind === "icon")) {
|
|
418
418
|
node_.from = measured.from;
|
|
419
419
|
node_.refName = measured.tag;
|
|
420
|
+
// a versão vem do apanhador geral abaixo - um lugar só, depois de todos os ramos
|
|
420
421
|
}
|
|
421
422
|
}
|
|
422
423
|
/**
|
|
@@ -431,6 +432,20 @@ sxSpacing) {
|
|
|
431
432
|
if (measured?.motionProps)
|
|
432
433
|
node_.motionProps = measured.motionProps;
|
|
433
434
|
}
|
|
435
|
+
/**
|
|
436
|
+
* TODO NÓ COM PACOTE LEVA A VERSÃO - o apanhador geral, depois de todos os ramos.
|
|
437
|
+
*
|
|
438
|
+
* A costura já falhou QUATRO vezes ramo a ramo (frontier-kind, import 09/08, o ramo de
|
|
439
|
+
* fronteira, o ramo icon/motion), porque cada caminho novo que carimba `from` esquecia de
|
|
440
|
+
* carimbar a versão. Medido no caminho DERIVADO em 16/08: base-ui com 1 de 23 nós
|
|
441
|
+
* versionados e lucide com 11 de 45 - os nós nasciam no `anatomyFromSketch` com `from` e
|
|
442
|
+
* sem versão, e nenhum ramo daqui os alcançava. Um lugar só, depois de todos.
|
|
443
|
+
*/
|
|
444
|
+
if (node_.from && !node_.version) {
|
|
445
|
+
const v = deps?.[packageRoot(String(node_.from))] ?? deps?.[String(node_.from)];
|
|
446
|
+
if (v)
|
|
447
|
+
node_.version = v;
|
|
448
|
+
}
|
|
434
449
|
// The NAME is what carries the styles, so it is claimed even when nothing
|
|
435
450
|
// resolved: the tree triggers on presence, and structure is most of the
|
|
436
451
|
// value. A node with no name is pure structure and that is legitimate.
|
package/dist/claude-md.js
CHANGED
|
@@ -186,9 +186,9 @@ session. It costs nothing when nothing changed: the tool holds both versions and
|
|
|
186
186
|
refuses politely when there is nothing to refresh, so calling it is cheaper than
|
|
187
187
|
deciding whether to.
|
|
188
188
|
|
|
189
|
-
It re-measures this repo with the current reader, reuses
|
|
190
|
-
|
|
191
|
-
it runs, this project's system is missing them.
|
|
189
|
+
It re-measures this repo with the current reader, reuses what was already written
|
|
190
|
+
about this app, and sends it - a newer reader sees styles the old one could not, and
|
|
191
|
+
until it runs, this project's system is missing them.
|
|
192
192
|
|
|
193
193
|
It never asks anyone anything, and it never overwrites a recipe a person wrote on
|
|
194
194
|
the screen: when a send would rewrite one, it refuses and names them. If it says
|
|
@@ -196,8 +196,8 @@ nothing changed, nothing changed - carry on.
|
|
|
196
196
|
|
|
197
197
|
**And when you describe this system, open with the age of the measurement:** the
|
|
198
198
|
\`ledger.cli\` and the date in \`_synthesisui/census.json\`. A long, correct report
|
|
199
|
-
about a
|
|
200
|
-
in it may have been filled since, and the reader who trusts it has no way to know.
|
|
199
|
+
about a measurement taken by an older reader is the worst way to be right - every
|
|
200
|
+
absence in it may have been filled since, and the reader who trusts it has no way to know.
|
|
201
201
|
|
|
202
202
|
**And if it says the design system needs updating, do these three things in order:**
|
|
203
203
|
say the one line, ask, and run it.
|
|
@@ -334,7 +334,8 @@ extend that. The person who asked you for a feature should never have to know th
|
|
|
334
334
|
names or type them; finding the right component is your job, not theirs.
|
|
335
335
|
|
|
336
336
|
Overriding a style a component already sets just works - write
|
|
337
|
-
\`className="text
|
|
337
|
+
\`className="text-<role>"\` with one of this system's own colour roles (the GUIDE
|
|
338
|
+
lists them), with no \`!\`. The generated components resolve your class
|
|
338
339
|
against their own, so the last one written wins, which is what the call site reads like.
|
|
339
340
|
If an override is ignored, that component predates the resolver: regenerate it with
|
|
340
341
|
\`npx synthesisui@latest component <slug> <name>\` rather than reaching for \`!\`.
|
package/dist/commands/adopt.js
CHANGED
|
@@ -134,6 +134,14 @@ async function findTokens(root, only) {
|
|
|
134
134
|
/** The contract. Names THEIR tokens - there is no translation layer. */
|
|
135
135
|
function guide(name, slug, tokens) {
|
|
136
136
|
const lines = [...tokens.entries()].map(([k, v]) => ` ${k}: ${v};`);
|
|
137
|
+
/**
|
|
138
|
+
* O EXEMPLO É UM TOKEN REAL DELE. A promessa três linhas abaixo é "YOUR tokens, under YOUR
|
|
139
|
+
* names" - e o exemplo inventava `--<slug>-color-primary`, um prefixo derivado por nós + um papel
|
|
140
|
+
* nosso, que pode não existir no CSS de ninguém. O primeiro token de cor DELE cumpre a promessa.
|
|
141
|
+
*/
|
|
142
|
+
const example = [...tokens.entries()].find(([, v]) => /^#|^(rgb|hsl|oklch|color)\(/i.test(v.trim()))?.[0] ??
|
|
143
|
+
[...tokens.keys()][0] ??
|
|
144
|
+
`--${slug}-color-primary`;
|
|
137
145
|
return `# ${name} - the contract your agent follows
|
|
138
146
|
|
|
139
147
|
Adopted from this repository by \`synthesisui adopt\`. These are YOUR tokens,
|
|
@@ -145,7 +153,7 @@ When writing or editing UI in this project, use these custom properties.
|
|
|
145
153
|
Do not write raw colours, spacings or radii that a token already covers.
|
|
146
154
|
|
|
147
155
|
✗ background: #3b82f6
|
|
148
|
-
✓ background: var(
|
|
156
|
+
✓ background: var(${example})
|
|
149
157
|
|
|
150
158
|
If a value has no token, say so instead of inventing one silently - a new
|
|
151
159
|
token is a decision for a person to make.
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1179,17 +1179,21 @@ export async function doctor(opts) {
|
|
|
1179
1179
|
const w = Math.max(...repeats.map((r) => r.literal.length));
|
|
1180
1180
|
for (const r of repeats) {
|
|
1181
1181
|
const where = `${r.count}\u00d7 in ${r.files} file${r.files === 1 ? "" : "s"}`;
|
|
1182
|
-
const near = nameToWrite(r)
|
|
1182
|
+
const near = nameToWrite(r)
|
|
1183
|
+
? null
|
|
1184
|
+
: nearestToken(table, r.literal, r.kind);
|
|
1183
1185
|
/**
|
|
1184
1186
|
* COINCIDÊNCIA NÃO É RESPOSTA - ver `crossFamily` em `scan.ts`. Para aquele `kind` o
|
|
1185
1187
|
* sistema não tem nome; o valor mora noutra família, e dizer só a seta afirma o contrário.
|
|
1188
|
+
* MAS quando o REPO DELE nomeia o valor (`theirToken`), esse nome vale - `apply-fix.ts:140`
|
|
1189
|
+
* e a classificação de sugestão já tratam assim; só a impressão tinha ficado para trás.
|
|
1186
1190
|
*/
|
|
1187
|
-
const named = r.crossFamily
|
|
1191
|
+
const named = r.crossFamily && !r.theirToken
|
|
1188
1192
|
? ` → no ${r.kind} named for it · the value lives as ${r.token}`
|
|
1189
1193
|
: nameToWrite(r)
|
|
1190
1194
|
? ` → ${nameToWrite(r)}`
|
|
1191
1195
|
: near
|
|
1192
|
-
? ` → nearest is ${near.name} (${near.value})`
|
|
1196
|
+
? ` → nearest is ${near.theirs ?? near.name} (${near.value})`
|
|
1193
1197
|
: "";
|
|
1194
1198
|
say(` ${r.literal.padEnd(w)} ${where}${named}`);
|
|
1195
1199
|
}
|
|
@@ -1205,13 +1209,15 @@ export async function doctor(opts) {
|
|
|
1205
1209
|
for (const x of shown) {
|
|
1206
1210
|
// A dead end with a neighbour is not a dead end. Only for lengths -
|
|
1207
1211
|
// "nearly the same blue" is the guess this tool must never make.
|
|
1208
|
-
const near = nameToWrite(x)
|
|
1209
|
-
|
|
1212
|
+
const near = nameToWrite(x)
|
|
1213
|
+
? null
|
|
1214
|
+
: nearestToken(table, x.literal, x.kind);
|
|
1215
|
+
const named = x.crossFamily && !x.theirToken
|
|
1210
1216
|
? `→ no ${x.kind} named for it · the value lives as ${x.token}`
|
|
1211
1217
|
: nameToWrite(x)
|
|
1212
1218
|
? `→ ${nameToWrite(x)}`
|
|
1213
1219
|
: near
|
|
1214
|
-
? `→ nearest is ${near.name} (${near.value})`
|
|
1220
|
+
? `→ nearest is ${near.theirs ?? near.name} (${near.value})`
|
|
1215
1221
|
: "→ no token holds this value yet";
|
|
1216
1222
|
say(` ${String(x.line).padStart(4)} ${x.literal} ${named}`);
|
|
1217
1223
|
}
|
|
@@ -1308,13 +1314,14 @@ export async function doctor(opts) {
|
|
|
1308
1314
|
if (frozen.length > 0) {
|
|
1309
1315
|
say(section("These will not follow your other scheme"));
|
|
1310
1316
|
say(body(frozen.length === 1
|
|
1311
|
-
? "One recipe
|
|
1312
|
-
: `${frozen.length} recipes
|
|
1317
|
+
? "One recipe pins a fixed shade where a surface colour of this system holds the same value."
|
|
1318
|
+
: `${frozen.length} recipes pin a fixed shade where a surface colour of this system holds the same value.`));
|
|
1313
1319
|
say("");
|
|
1314
1320
|
for (const f of frozen) {
|
|
1315
1321
|
say(body(`ds-${f.component} · ${f.where}`));
|
|
1316
1322
|
say(` binds ${f.wrote}`);
|
|
1317
|
-
|
|
1323
|
+
/** A grafia que EXISTE no repo dele - o CSS compilado carrega esta variável; `{color.semantic.*}` é formato nosso e não sai. */
|
|
1324
|
+
say(` var(--ds-color-semantic-${f.role}) holds that, and becomes ${f.becomes}`);
|
|
1318
1325
|
say("");
|
|
1319
1326
|
}
|
|
1320
1327
|
say(body("The value resolves and the CSS compiles, so nothing"));
|
|
@@ -1557,7 +1564,7 @@ export async function doctor(opts) {
|
|
|
1557
1564
|
});
|
|
1558
1565
|
}
|
|
1559
1566
|
for (const r of unnamed.slice(0, migrating ? 3 : 2)) {
|
|
1560
|
-
const near = nearestToken(table, r.literal);
|
|
1567
|
+
const near = nearestToken(table, r.literal, r.kind);
|
|
1561
1568
|
plan.push({
|
|
1562
1569
|
rank: migrating ? 3 : 4,
|
|
1563
1570
|
/**
|
|
@@ -1567,7 +1574,7 @@ export async function doctor(opts) {
|
|
|
1567
1574
|
* fila da anterior: aquela tem nome esperando, esta precisa de um.
|
|
1568
1575
|
*/
|
|
1569
1576
|
what: near
|
|
1570
|
-
? `${r.literal} - name it, or snap to ${near.name}`
|
|
1577
|
+
? `${r.literal} - name it, or snap to ${near.theirs ?? near.name}`
|
|
1571
1578
|
: r.crossFamily
|
|
1572
1579
|
? `${r.literal} - no ${r.kind} named for it, and the value lives as ${r.token} in another family`
|
|
1573
1580
|
: `${r.literal} - no name for it, here or in your CSS`,
|
|
@@ -1753,7 +1760,8 @@ export async function doctor(opts) {
|
|
|
1753
1760
|
const verdict = compareToBaseline(d, base, {
|
|
1754
1761
|
...(relScopes.length ? { scope: relScopes.join(",") } : {}),
|
|
1755
1762
|
});
|
|
1756
|
-
|
|
1763
|
+
/** "Drift" é a nossa palavra e não sai (ver a tradução da seção principal); aqui idem. */
|
|
1764
|
+
console.log(section(verdict.worse ? "Hand-written values went up" : "Holding the line"));
|
|
1757
1765
|
for (const line of describeVerdict(verdict))
|
|
1758
1766
|
console.log(body(line));
|
|
1759
1767
|
if (verdict.worse)
|
package/dist/commands/gaps.js
CHANGED
|
@@ -22,7 +22,7 @@ export async function gaps(opts) {
|
|
|
22
22
|
const raw = await readFile(path, "utf8").catch(() => null);
|
|
23
23
|
console.log(section("What this pipeline did not read"));
|
|
24
24
|
if (!raw) {
|
|
25
|
-
console.log(body(`No
|
|
25
|
+
console.log(body(`No measurement at ${path}. Run \`synthesisui import --dry\` first - that is what measures your files, and it writes the result there.`));
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
28
|
let ledger;
|
|
@@ -39,7 +39,7 @@ export async function gaps(opts) {
|
|
|
39
39
|
* contar.
|
|
40
40
|
*/
|
|
41
41
|
if (!ledger) {
|
|
42
|
-
console.log(body(`This
|
|
42
|
+
console.log(body(`This measurement was written by a tool version that did not count style fragments yet, so the numbers do not exist rather than being zero. Run \`synthesisui import --dry\` again with ${opts.cli} to measure.`));
|
|
43
43
|
return;
|
|
44
44
|
}
|
|
45
45
|
for (const line of describeTriage(triageLedger(ledger, opts.cli)))
|
package/dist/commands/import.js
CHANGED
|
@@ -36,7 +36,7 @@ import { frontierKind, packageRoot } from "../frontier-kind.js";
|
|
|
36
36
|
import { withLibraryStructure } from "../library-structure.js";
|
|
37
37
|
import { body, paint, section } from "../output.js";
|
|
38
38
|
import { phase, startProgress } from "../progress.js";
|
|
39
|
-
import { detectStack, resolveDeps } from "../stack.js";
|
|
39
|
+
import { detectStack, resolveDeps, stackVersions } from "../stack.js";
|
|
40
40
|
import { walk, walkAll } from "./doctor.js";
|
|
41
41
|
/**
|
|
42
42
|
* How many distinct values travel, PER KIND.
|
|
@@ -1739,18 +1739,23 @@ export async function takeCensus(root, opts) {
|
|
|
1739
1739
|
}));
|
|
1740
1740
|
const pkgRaw = await readFile(join(root, "package.json"), "utf8").catch(() => null);
|
|
1741
1741
|
let name = null;
|
|
1742
|
-
/** Their pinned ranges, so a library signal cites the version rather than going stale. */
|
|
1743
|
-
let versions = {};
|
|
1744
1742
|
if (pkgRaw) {
|
|
1745
1743
|
try {
|
|
1746
|
-
|
|
1747
|
-
name = pkg.name ?? null;
|
|
1748
|
-
versions = { ...pkg.devDependencies, ...pkg.dependencies };
|
|
1744
|
+
name = JSON.parse(pkgRaw).name ?? null;
|
|
1749
1745
|
}
|
|
1750
1746
|
catch {
|
|
1751
1747
|
// an unreadable package.json costs the name, not the run
|
|
1752
1748
|
}
|
|
1753
1749
|
}
|
|
1750
|
+
/**
|
|
1751
|
+
* Their pinned ranges, so a library signal cites the version rather than going stale.
|
|
1752
|
+
*
|
|
1753
|
+
* PELO RESOLVEDOR, nunca pela leitura crua de um nível: com `--scope packages/ui` este `root` é a
|
|
1754
|
+
* pasta do escopo, cujo manifesto tem ZERO deps - e 15 de 15 bibliotecas do censo vivo saíam sem
|
|
1755
|
+
* versão (medido 16/08). `resolveDeps` sobe até a raiz do workspace e anda para os irmãos que os
|
|
1756
|
+
* `workspaces` declaram, que é onde recharts/chakra/mui realmente moram.
|
|
1757
|
+
*/
|
|
1758
|
+
const versions = await resolveDeps(root).catch(() => ({}));
|
|
1754
1759
|
finishSignals(signals, importTally, versions);
|
|
1755
1760
|
// Every name their CSS defines, from the same harvest the token table came
|
|
1756
1761
|
// from - so a reference is judged against what actually exists, not against
|
|
@@ -1862,7 +1867,13 @@ export async function takeCensus(root, opts) {
|
|
|
1862
1867
|
const classStyle = detectClassStyle(sources);
|
|
1863
1868
|
return {
|
|
1864
1869
|
census: 1,
|
|
1865
|
-
project: {
|
|
1870
|
+
project: {
|
|
1871
|
+
name,
|
|
1872
|
+
stack: await detectStack(root),
|
|
1873
|
+
...(Object.keys(stackVersions(versions)).length > 0
|
|
1874
|
+
? { versions: stackVersions(versions) }
|
|
1875
|
+
: {}),
|
|
1876
|
+
},
|
|
1866
1877
|
declared: Object.fromEntries(table.byName),
|
|
1867
1878
|
...(Object.keys(keyframes).length > 0 ? { keyframes } : {}),
|
|
1868
1879
|
...(animations.size > 0 ? { animations: [...animations].sort() } : {}),
|
|
@@ -3113,7 +3124,7 @@ export async function runImport(opts) {
|
|
|
3113
3124
|
if (creds && !sameRegistry(creds.registry, base)) {
|
|
3114
3125
|
console.log("");
|
|
3115
3126
|
console.log(body(`You are logged in to ${paint.strong(creds.registry ?? "another registry")}, and this would go to ${paint.strong(base)}.`));
|
|
3116
|
-
console.log(body(
|
|
3127
|
+
console.log(body(`Your measurement is saved at ${out}. Either name the one you meant:`));
|
|
3117
3128
|
console.log("");
|
|
3118
3129
|
console.log(body(` synthesisui import --census ${out} --registry ${creds.registry}`));
|
|
3119
3130
|
console.log(body("or log in to this one:"));
|
|
@@ -3143,7 +3154,7 @@ export async function runImport(opts) {
|
|
|
3143
3154
|
// which of the two it was (dono, 31/07). A stale token gets past the
|
|
3144
3155
|
// `readToken` guard above and only fails here.
|
|
3145
3156
|
if (res?.status === 401) {
|
|
3146
|
-
console.log(body(
|
|
3157
|
+
console.log(body(`Your session has expired. Your measurement is saved at ${out}.`));
|
|
3147
3158
|
console.log("");
|
|
3148
3159
|
console.log(body(` ${paint.strong("synthesisui login")}`));
|
|
3149
3160
|
console.log(body(` synthesisui import --census ${out}${chosen ? ` --name "${chosen}"` : ""}`));
|
|
@@ -3152,7 +3163,7 @@ export async function runImport(opts) {
|
|
|
3152
3163
|
}
|
|
3153
3164
|
console.log(body(res
|
|
3154
3165
|
? `The registry refused it (HTTP ${res.status})${detail ? `: ${detail}` : "."}`
|
|
3155
|
-
:
|
|
3166
|
+
: `Could not reach the registry. Your measurement is saved at ${out} - try again later.`));
|
|
3156
3167
|
console.log("");
|
|
3157
3168
|
return;
|
|
3158
3169
|
}
|
package/dist/commands/mcp.js
CHANGED
|
@@ -6,7 +6,7 @@ import { readToken, resolveRegistry } from "../config.js";
|
|
|
6
6
|
import { readEvents } from "../doctor/ledger.js";
|
|
7
7
|
import { fileRequest } from "../doctor/requests.js";
|
|
8
8
|
import { diagnose, nameToWrite, scanSource } from "../doctor/scan.js";
|
|
9
|
-
import { nearestToken, tokenFor } from "../doctor/tokens.js";
|
|
9
|
+
import { nearestToken, normalizeValue, tokenFor } from "../doctor/tokens.js";
|
|
10
10
|
import { repoStateOf } from "../repo-state.js";
|
|
11
11
|
import { component } from "./component.js";
|
|
12
12
|
import { loadSystem, walkAll } from "./doctor.js";
|
|
@@ -290,12 +290,29 @@ async function findToken(root, value) {
|
|
|
290
290
|
const { table } = await loadSystem(root);
|
|
291
291
|
if (table.byName.size === 0)
|
|
292
292
|
return "No design system installed here.";
|
|
293
|
+
/**
|
|
294
|
+
* O NOME DELE PRIMEIRO, como no doctor (`nameToWrite`): `loadSystem` já devolve a tabela COM os
|
|
295
|
+
* aliases dele (`withTheirNames`) e este era o único leitor que nunca os consultava. Sem o `kind`
|
|
296
|
+
* do contexto, a busca varre as famílias - um alias em qualquer uma delas é o nome que o repo
|
|
297
|
+
* DELE dá àquele valor.
|
|
298
|
+
*/
|
|
299
|
+
const theirNameFor = (v) => {
|
|
300
|
+
const norm = normalizeValue(v, table.rootPx);
|
|
301
|
+
for (const [key, name] of table.aliases)
|
|
302
|
+
if (key.endsWith(`:${norm}`))
|
|
303
|
+
return name;
|
|
304
|
+
return null;
|
|
305
|
+
};
|
|
293
306
|
const exact = tokenFor(table, value);
|
|
294
|
-
if (exact)
|
|
295
|
-
|
|
307
|
+
if (exact) {
|
|
308
|
+
const shown = theirNameFor(value) ?? exact;
|
|
309
|
+
return `${value} is ${shown} in this project. Use var(${shown}).`;
|
|
310
|
+
}
|
|
296
311
|
const near = nearestToken(table, value);
|
|
297
|
-
if (near)
|
|
298
|
-
|
|
312
|
+
if (near) {
|
|
313
|
+
const shown = theirNameFor(near.value) ?? near.name;
|
|
314
|
+
return `No token holds ${value}. The closest is ${shown} at ${near.value}. If that is what you meant, use it - if it genuinely is not, say so rather than inventing a token.`;
|
|
315
|
+
}
|
|
299
316
|
// Same words the managed block uses. A refusal is only useful if it is the
|
|
300
317
|
// same refusal every time.
|
|
301
318
|
return `No token in this system holds ${value}, and nothing is close. Do NOT invent one. Say which value you need and what you would call it, and let a person decide.`;
|
package/dist/commands/sync.js
CHANGED
|
@@ -269,9 +269,9 @@ export async function remeasure(args) {
|
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
271
|
console.log(section("Measuring your repository again"));
|
|
272
|
-
console.log(body(paint.faint(`${scope ? `
|
|
273
|
-
? " · read from
|
|
274
|
-
: ""} ·
|
|
272
|
+
console.log(body(paint.faint(`${scope ? `reading ${scope}` : "the whole repo"}${local.system && local.system !== stored.scope
|
|
273
|
+
? " · read from _synthesisui/census.json, where the last measurement recorded it"
|
|
274
|
+
: ""} · what you already wrote about your app travels unchanged`)));
|
|
275
275
|
const census = await takeCensus(scope ? join(root, scope) : root, {
|
|
276
276
|
...(scope ? { scopeLabel: scope } : {}),
|
|
277
277
|
...(args.cli ? { cli: args.cli } : {}),
|
|
@@ -333,7 +333,7 @@ export async function remeasure(args) {
|
|
|
333
333
|
* repositório inteiro e o rascunho curado de 37 vira 339).
|
|
334
334
|
*/
|
|
335
335
|
if (!fresh.scope && local.system)
|
|
336
|
-
console.log(body(`⚠ the
|
|
336
|
+
console.log(body(`⚠ the last measurement was saved without the folder it read, while "${local.system}" is the one recorded - the next one would read the whole repo. Please report this: it is a state we have not reproduced.`));
|
|
337
337
|
if (usage.length > 0)
|
|
338
338
|
fresh.usage = usage;
|
|
339
339
|
for (const key of ["scheme", "reading"])
|
package/dist/commands/use.js
CHANGED
|
@@ -57,14 +57,43 @@ export async function use(slug, intent, opts) {
|
|
|
57
57
|
: "",
|
|
58
58
|
`- ${base}/v${version}/GUIDE.md - how to apply the system, the recipes and the token vocabulary`,
|
|
59
59
|
].filter(Boolean);
|
|
60
|
+
/**
|
|
61
|
+
* OS EXEMPLOS SÃO DO SISTEMA DELE, não uma lista nossa de cor. `bg-primary`/`p-md` cravados
|
|
62
|
+
* ensinavam classes que um sistema importado pode nem compilar - os nomes reais moram no
|
|
63
|
+
* `design-system.json` instalado, e é dele que os exemplos saem. Ilegível, ficam os genéricos.
|
|
64
|
+
*/
|
|
65
|
+
let utilities = "`bg-primary`, `text-foreground`, `p-md`, `rounded-lg`, `font-display`";
|
|
66
|
+
try {
|
|
67
|
+
const doc = JSON.parse(await readFile(join(slugDir, `v${version}`, "design-system.json"), "utf8"));
|
|
68
|
+
const roles = Object.keys(doc.foundations?.color?.semantic ?? {});
|
|
69
|
+
const spacing = Object.keys(doc.foundations?.spacing ?? {});
|
|
70
|
+
const radius = Object.keys(doc.foundations?.radius ?? {});
|
|
71
|
+
const parts = [
|
|
72
|
+
roles.length > 0
|
|
73
|
+
? `\`bg-${roles.includes("primary") ? "primary" : roles[0]}\``
|
|
74
|
+
: null,
|
|
75
|
+
roles.includes("foreground")
|
|
76
|
+
? "`text-foreground`"
|
|
77
|
+
: roles[1]
|
|
78
|
+
? `\`text-${roles[1]}\``
|
|
79
|
+
: null,
|
|
80
|
+
spacing[0] ? `\`p-${spacing[0]}\`` : null,
|
|
81
|
+
radius[0] ? `\`rounded-${radius[0]}\`` : null,
|
|
82
|
+
"`font-display`",
|
|
83
|
+
].filter(Boolean);
|
|
84
|
+
if (parts.length >= 3)
|
|
85
|
+
utilities = parts.join(", ");
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// sem manifesto legível, os exemplos genéricos ficam - pior seria nenhum
|
|
89
|
+
}
|
|
60
90
|
// The styling contract differs by target: Next projects in this product use
|
|
61
91
|
// Tailwind v4 backed by the DS; the "general" target is framework-agnostic CSS.
|
|
62
92
|
const stylingRule = config.target === "general"
|
|
63
93
|
? `- Style with the design system only: reuse the \`.ds-*\` recipe classes and the ` +
|
|
64
94
|
`\`var(--ds-*)\` custom properties. Never use raw hex/px outside the system's scale.`
|
|
65
95
|
: `- Style with the design system only: reuse the \`.ds-*\` recipe classes and the ` +
|
|
66
|
-
`DS-backed Tailwind utilities (
|
|
67
|
-
`\`font-display\`…). Never use raw hex/px outside the system's scale.`;
|
|
96
|
+
`DS-backed Tailwind utilities (${utilities}…). Never use raw hex/px outside the system's scale.`;
|
|
68
97
|
const task = intent.trim() || "build the UI I describe next";
|
|
69
98
|
const prompt = [
|
|
70
99
|
`Use the "${name}" design system (slug: ${slug}, v${version}) to: ${task}`,
|
package/dist/doctor/ci-format.js
CHANGED
|
@@ -79,12 +79,20 @@ export function compareToBaseline(d, base, now) {
|
|
|
79
79
|
: {}),
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
|
-
/**
|
|
82
|
+
/**
|
|
83
|
+
* O que a mensagem de uma anotação diz, e é a mesma frase nos dois formatos.
|
|
84
|
+
*
|
|
85
|
+
* O NOME DELE PRIMEIRO, como em todo o resto do doctor (`nameToWrite`): este era o único caminho
|
|
86
|
+
* que ignorava `their-names` por completo - e é o que aparece anotado na linha do PR de todo o
|
|
87
|
+
* time dele. Um `theirToken` também desarma o caso `crossFamily`: o repo DELE nomeia o valor, então
|
|
88
|
+
* a troca existe (mesma regra de `apply-fix.ts`).
|
|
89
|
+
*/
|
|
83
90
|
function messageFor(f) {
|
|
84
|
-
|
|
85
|
-
|
|
91
|
+
const shown = f.theirToken ?? f.token;
|
|
92
|
+
return shown
|
|
93
|
+
? f.crossFamily && !f.theirToken
|
|
86
94
|
? `${f.literal} is written by hand, and this project's system has no ${f.kind} named for it. The value does exist elsewhere in the system - as \`${f.token}\`, in another family - so this is a decision to make, not a swap: name a ${f.kind}, or leave it.`
|
|
87
|
-
: `${f.literal} is written by hand, and this project
|
|
95
|
+
: `${f.literal} is written by hand, and this project already names it: var(${shown}). Use the name.`
|
|
88
96
|
: `${f.literal} is a ${f.kind} value written by hand, and the system has no name for it yet. Name it, or file a token request: npx synthesisui request token`;
|
|
89
97
|
}
|
|
90
98
|
/**
|
|
@@ -192,7 +200,8 @@ export function describeVerdict(v) {
|
|
|
192
200
|
lines.push(`Baseline not comparable: ${v.incomparable}.`);
|
|
193
201
|
return lines;
|
|
194
202
|
}
|
|
195
|
-
|
|
203
|
+
/** "Drift"/"phantom" são palavras nossas - a linha fala do que ELE vê: valores à mão e nomes que não resolvem. */
|
|
204
|
+
lines.push(`Hand-written values ${v.drift.was} → ${v.drift.now}${v.phantoms.was || v.phantoms.now ? ` · names that resolve to nothing ${v.phantoms.was} → ${v.phantoms.now}` : ""}.`);
|
|
196
205
|
if (v.improved > 0)
|
|
197
206
|
lines.push(`${v.improved} file${v.improved === 1 ? "" : "s"} improved since the baseline.`);
|
|
198
207
|
if (v.regressed.length === 0) {
|
package/dist/doctor/tokens.js
CHANGED
|
@@ -467,7 +467,9 @@ export function buildTable(input) {
|
|
|
467
467
|
* Deliberately narrow: same unit, within 25% or 8px, and never for a colour -
|
|
468
468
|
* "nearly the same blue" is exactly the guess this tool must not make.
|
|
469
469
|
*/
|
|
470
|
-
export function nearestToken(table, literal
|
|
470
|
+
export function nearestToken(table, literal,
|
|
471
|
+
/** A família do achado - com ela, o vizinho também é oferecido pelo NOME DELE quando o repo o nomeia. */
|
|
472
|
+
kind) {
|
|
471
473
|
const m = /^(-?\d*\.?\d+)(px|rem)$/.exec(literal.trim().toLowerCase());
|
|
472
474
|
if (!m)
|
|
473
475
|
return null;
|
|
@@ -490,7 +492,17 @@ export function nearestToken(table, literal) {
|
|
|
490
492
|
if (!best)
|
|
491
493
|
return null;
|
|
492
494
|
const limit = unit === "rem" ? Math.max(n * 0.25, 0.5) : Math.max(n * 0.25, 8);
|
|
493
|
-
|
|
495
|
+
if (best.delta > limit)
|
|
496
|
+
return null;
|
|
497
|
+
/**
|
|
498
|
+
* O VIZINHO PELO NOME DELE - a mesma chave que `theirNames` construiu (`kind:valor`), porque o
|
|
499
|
+
* valor sozinho não decide a família. Só a tabela nossa era consultada aqui, então a oferta saía
|
|
500
|
+
* sempre no nosso vocabulário mesmo quando o repo dele nomeia o mesmo valor.
|
|
501
|
+
*/
|
|
502
|
+
const theirs = kind
|
|
503
|
+
? table.aliases.get(`${kind}:${normalizeValue(best.value, table.rootPx)}`)
|
|
504
|
+
: undefined;
|
|
505
|
+
return theirs ? { ...best, theirs } : best;
|
|
494
506
|
}
|
|
495
507
|
/**
|
|
496
508
|
* The family a token belongs to, from the drift it was found in.
|
package/dist/install-marks.js
CHANGED
|
@@ -80,7 +80,16 @@
|
|
|
80
80
|
* Zero ocorrências no sistema real medido (todos os headings dele estão aninhados, e o `children > 0`
|
|
81
81
|
* já os pegava), então para ele o `upgrade` é no-op. A marca é sobre o caso geral.
|
|
82
82
|
*/
|
|
83
|
-
|
|
83
|
+
/**
|
|
84
|
+
* 0.16.237 -> 0.16.239 em 16/08: os arquivos que o agente dele LÊ mudam de conselho, não só de
|
|
85
|
+
* bytes. O bloco gerenciado do CLAUDE.md ensinava `text-info` cravado - uma classe que um sistema
|
|
86
|
+
* importado pode nem compilar - e passa a apontar para os papéis que o GUIDE do sistema DELE lista;
|
|
87
|
+
* o GUIDE do sistema adotado exemplificava com `--<slug>-color-primary`, um nome que nós derivamos,
|
|
88
|
+
* e passa a usar o primeiro token de cor REAL dele (a promessa três linhas acima do exemplo é
|
|
89
|
+
* "YOUR tokens, under YOUR names"). É a fatia de CLI da decisão de 16/08: toda superfície fala a
|
|
90
|
+
* língua do cliente.
|
|
91
|
+
*/
|
|
92
|
+
export const MATERIALISER_SINCE = "0.16.239";
|
|
84
93
|
/**
|
|
85
94
|
* A ÚLTIMA VERSÃO EM QUE O QUE O HOOK RODA MUDOU.
|
|
86
95
|
*
|
|
@@ -175,8 +184,14 @@ export const CHECKER_SINCE = "0.16.232";
|
|
|
175
184
|
* comparação com UM literal, e a divisão do motivo as tirou dele em silêncio
|
|
176
185
|
*
|
|
177
186
|
* Um censo medido antes disto carrega as duas leituras erradas.
|
|
187
|
+
*
|
|
188
|
+
* 0.16.240 a VERSÃO passa a viajar (fase B1, 16/08): `project.versions` (frameworks pinados),
|
|
189
|
+
* 15 de 15 `signals.libraries` com versão (o leitor de manifesto lia o package.json
|
|
190
|
+
* do ESCOPO, que tinha zero deps), e todo nó de fronteira com pacote carimba o range
|
|
191
|
+
* (era 53% no melhor caminho; o derivado dava 38%). Um censo medido antes disto não
|
|
192
|
+
* tem versão nenhuma - e o mapa de blueprints por versão não tem o que consultar.
|
|
178
193
|
*/
|
|
179
|
-
export const READER_SINCE = "0.16.
|
|
194
|
+
export const READER_SINCE = "0.16.240";
|
|
180
195
|
/**
|
|
181
196
|
* O QUE ESTÁ INSTALADO AQUI FICOU PARA TRÁS - e as DUAS condições que fazem isso ser verdade.
|
|
182
197
|
*
|
package/dist/skill-import.js
CHANGED
|
@@ -12,4 +12,4 @@
|
|
|
12
12
|
* invocada, e ela não é segredo - a esteira é.
|
|
13
13
|
*/
|
|
14
14
|
export const IMPORT_SKILL_PATH = ".claude/skills/sui-import-ds/SKILL.md";
|
|
15
|
-
export const IMPORT_SKILL = '---\nname: sui-import-ds\ndescription: Turn a codebase the user ALREADY has into a SynthesisUI design system. Use when someone points at an existing repo, app or component library and asks to import it, adopt it, bring it in, or "make a design system from this" (e.g. "/sui-import-ds", "importa o meu packages/ui", "turn this app into a design system"). Drives the full pipeline -
|
|
15
|
+
export const IMPORT_SKILL = '---\nname: sui-import-ds\ndescription: Turn a codebase the user ALREADY has into a SynthesisUI design system. Use when someone points at an existing repo, app or component library and asks to import it, adopt it, bring it in, or "make a design system from this" (e.g. "/sui-import-ds", "importa o meu packages/ui", "turn this app into a design system"). Drives the full pipeline - measurement \u2192 your reading of the app \u2192 import \u2192 upgrade proposal - and answers the questions arithmetic cannot.\n---\n\n# Import Design System - served live\n\nThis playbook is served from the platform, not shipped in this file - it is\nalways current, and your context only carries the step you are on.\n\n1. Call the `playbook` tool on the `synthesisui` MCP server with\n { "skill": "import" } - you get the framing and a table of contents.\n2. Fetch ONLY the chapter for the step you are on:\n { "skill": "import", "section": "<id from the toc>" }. Never fetch more\n than the current step needs.\n3. Follow it exactly. When the step is done, fetch the next chapter.\n\nIf the tool answers that you are not signed in, run `npx synthesisui login`\nin the terminal and call it again. If the `synthesisui` MCP server is not\navailable at all, run `npx synthesisui connect`, restart the session, and\ninvoke this skill again.\n';
|
package/dist/stack.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* One implementation, because two detectors that disagreed would put a rule in a
|
|
10
10
|
* project the census said was something else.
|
|
11
11
|
*/
|
|
12
|
-
import { readFile } from "node:fs/promises";
|
|
12
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
/**
|
|
15
15
|
* Dependencies as the project actually resolves them - which means reading
|
|
@@ -19,36 +19,98 @@ import { join } from "node:path";
|
|
|
19
19
|
* Next + React + Tailwind app, because a workspace hoists those to the root and
|
|
20
20
|
* the leaf package.json lists only its own icons. Three levels up covers every
|
|
21
21
|
* pnpm/npm workspace layout without wandering into someone's home directory.
|
|
22
|
+
*
|
|
23
|
+
* E PARA OS LADOS TAMBÉM, pelos `workspaces` do manifesto que os declara. Subir
|
|
24
|
+
* sozinho deixou 15 de 15 bibliotecas do censo vivo sem versão (16/08): o escopo
|
|
25
|
+
* era `packages/ui` (zero deps) e recharts/chakra/mui moram em `apps/*`, um
|
|
26
|
+
* IRMÃO - invisível para uma caminhada que só sobe. O componente medido vem do
|
|
27
|
+
* app que usa o sistema, então o manifesto daquele app é evidência tanto quanto
|
|
28
|
+
* o da raiz.
|
|
22
29
|
*/
|
|
23
30
|
export async function resolveDeps(root) {
|
|
24
31
|
const deps = {};
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
const workspaceDirs = [];
|
|
33
|
+
const readManifest = async (dir) => {
|
|
27
34
|
const raw = await readFile(join(dir, "package.json"), "utf8").catch(() => null);
|
|
28
|
-
if (raw)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
if (!raw)
|
|
36
|
+
return;
|
|
37
|
+
try {
|
|
38
|
+
const p = JSON.parse(raw);
|
|
39
|
+
// The nearest package.json wins on a version clash; we only ever ask
|
|
40
|
+
// whether a name is present, so first-seen is enough. The up-chain is
|
|
41
|
+
// read before any sibling, so siblings only ever FILL, never override.
|
|
42
|
+
for (const [k, v] of Object.entries({
|
|
43
|
+
...p.dependencies,
|
|
44
|
+
...p.devDependencies,
|
|
45
|
+
})) {
|
|
46
|
+
if (deps[k] == null)
|
|
47
|
+
deps[k] = String(v);
|
|
40
48
|
}
|
|
41
|
-
|
|
42
|
-
|
|
49
|
+
const globs = Array.isArray(p.workspaces)
|
|
50
|
+
? p.workspaces
|
|
51
|
+
: Array.isArray(p.workspaces?.packages)
|
|
52
|
+
? p.workspaces.packages
|
|
53
|
+
: [];
|
|
54
|
+
for (const g of globs) {
|
|
55
|
+
if (typeof g !== "string")
|
|
56
|
+
continue;
|
|
57
|
+
// Only the two shapes real manifests use: `packages/*` and a literal
|
|
58
|
+
// path. Anything fancier is skipped rather than guessed at.
|
|
59
|
+
if (g.endsWith("/*")) {
|
|
60
|
+
const parent = join(dir, g.slice(0, -2));
|
|
61
|
+
const children = await readdir(parent, {
|
|
62
|
+
withFileTypes: true,
|
|
63
|
+
}).catch(() => []);
|
|
64
|
+
for (const c of children)
|
|
65
|
+
if (c.isDirectory())
|
|
66
|
+
workspaceDirs.push(join(parent, c.name));
|
|
67
|
+
}
|
|
68
|
+
else if (!g.includes("*")) {
|
|
69
|
+
workspaceDirs.push(join(dir, g));
|
|
70
|
+
}
|
|
43
71
|
}
|
|
44
72
|
}
|
|
73
|
+
catch {
|
|
74
|
+
// unreadable manifest costs the detection, not the run
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
let dir = root;
|
|
78
|
+
for (let up = 0; up < 4; up++) {
|
|
79
|
+
await readManifest(dir);
|
|
45
80
|
const parent = join(dir, "..");
|
|
46
81
|
if (parent === dir)
|
|
47
82
|
break;
|
|
48
83
|
dir = parent;
|
|
49
84
|
}
|
|
85
|
+
for (const w of workspaceDirs)
|
|
86
|
+
await readManifest(w);
|
|
50
87
|
return deps;
|
|
51
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* OS FRAMEWORKS DO PROJETO, COM A VERSÃO PINADA - o que `Census.project.versions` carrega.
|
|
91
|
+
*
|
|
92
|
+
* `detectStack` colapsa `next@16.0.8` na string `"next"`, e nada no censo guardava versão de
|
|
93
|
+
* framework nenhuma (medido 16/08: zero em todos os campos). Curado de propósito: framework aqui,
|
|
94
|
+
* biblioteca por biblioteca em `signals.libraries` - duas listas com dois papéis, nunca o
|
|
95
|
+
* package.json inteiro dentro do censo.
|
|
96
|
+
*/
|
|
97
|
+
const STACK_PACKAGES = [
|
|
98
|
+
"next",
|
|
99
|
+
"react",
|
|
100
|
+
"react-dom",
|
|
101
|
+
"vue",
|
|
102
|
+
"svelte",
|
|
103
|
+
"vite",
|
|
104
|
+
"tailwindcss",
|
|
105
|
+
"typescript",
|
|
106
|
+
];
|
|
107
|
+
export function stackVersions(deps) {
|
|
108
|
+
const out = {};
|
|
109
|
+
for (const name of STACK_PACKAGES)
|
|
110
|
+
if (deps[name])
|
|
111
|
+
out[name] = deps[name];
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
52
114
|
export async function detectStack(root) {
|
|
53
115
|
const stack = [];
|
|
54
116
|
const has = async (f) => (await readFile(join(root, f), "utf8").catch(() => null)) !== null;
|
package/package.json
CHANGED