synthesisui 0.16.235 → 0.16.239
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 +7 -6
- package/dist/commands/add.js +8 -10
- package/dist/commands/adopt.js +9 -1
- package/dist/commands/component.js +13 -6
- package/dist/commands/doctor.js +20 -12
- package/dist/commands/gaps.js +2 -2
- package/dist/commands/generate.js +9 -5
- package/dist/commands/import.js +3 -3
- package/dist/commands/mcp.js +114 -11
- package/dist/commands/refit.js +8 -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/guide.js +4 -3
- package/dist/install-marks.js +10 -1
- package/dist/skill-adapt.js +10 -342
- package/dist/skill-import.js +10 -1311
- package/dist/skill-init.js +10 -320
- package/package.json +1 -1
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/add.js
CHANGED
|
@@ -250,15 +250,13 @@ export async function add(slug, opts) {
|
|
|
250
250
|
await writeFile(join(slugDir, "requires.json"), `${JSON.stringify(required, null, 2)}\n`, "utf8");
|
|
251
251
|
}
|
|
252
252
|
/** A filosofia continua sendo CONTADA na saída, e ela vive no documento - ver abaixo. */
|
|
253
|
-
const
|
|
254
|
-
const sections = philosophy?.sections ?? [];
|
|
253
|
+
const voice = payload.voice ?? { hasContext: false, sections: 0 };
|
|
255
254
|
/**
|
|
256
|
-
* 5c. A
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
* um deles começa a discordar do outro. O `system_doctrine` lê do documento.
|
|
255
|
+
* 5c. A VOZ NÃO É MATERIALIZADA - nem no `.md` (aposentado acima) nem no
|
|
256
|
+
* `design-system.json` (R2, 16/08: o payload viaja sem `philosophy`). Ela
|
|
257
|
+
* mora na plataforma e o `system_doctrine` a busca servida - sempre a
|
|
258
|
+
* versão atual, nada em texto plano no repo. As REGRAS continuam locais
|
|
259
|
+
* (`doctrine.json`): o doctor e o hook precisam delas offline.
|
|
262
260
|
*/
|
|
263
261
|
// 6. discovery by the agent
|
|
264
262
|
const claudeMd = await syncClaudeMd(projectRoot);
|
|
@@ -288,8 +286,8 @@ export async function add(slug, opts) {
|
|
|
288
286
|
*/
|
|
289
287
|
const doctrine = [
|
|
290
288
|
...(rules.length > 0 ? [`${rules.length} rule(s)`] : []),
|
|
291
|
-
...(sections
|
|
292
|
-
? [
|
|
289
|
+
...(voice.sections > 0 || voice.hasContext
|
|
290
|
+
? [`the voice (${voice.sections} section(s), served from the platform)`]
|
|
293
291
|
: []),
|
|
294
292
|
];
|
|
295
293
|
if (doctrine.length > 0)
|
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.
|
|
@@ -59,14 +59,21 @@ export async function component(slug, name, opts) {
|
|
|
59
59
|
if (!SAFE_NAME.test(res.name)) {
|
|
60
60
|
throw new RegistryError(`Registry returned an unsafe component name.`);
|
|
61
61
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
// R3 (dono, 16/08): as cópias .json/.css em _synthesisui só nascem quando
|
|
63
|
+
// são o PRODUTO do comando - `--artifacts-only` e targets não-Next. Quando
|
|
64
|
+
// o .tsx é materializado logo abaixo, ninguém as lê depois (medido na
|
|
65
|
+
// auditoria de 16/08), e cada arquivo a mais no repo dele é superfície.
|
|
66
|
+
const config = await readProjectConfig(root);
|
|
67
|
+
const artifactsAreTheProduct = opts.artifactsOnly === true || config.target !== "next";
|
|
68
|
+
if (artifactsAreTheProduct) {
|
|
69
|
+
const dir = join(root, "_synthesisui", "ds", slug, "components");
|
|
70
|
+
await mkdir(dir, { recursive: true });
|
|
71
|
+
await writeFile(join(dir, `${res.name}.json`), `${JSON.stringify(res.recipe, null, 2)}\n`, "utf8");
|
|
72
|
+
await writeFile(join(dir, `${res.name}.css`), `${res.css}\n`, "utf8");
|
|
73
|
+
console.log(`✓ ${res.name} → _synthesisui/ds/${slug}/components/${res.name}.{json,css} (${slug} v${res.version})`);
|
|
74
|
+
}
|
|
67
75
|
// 2. YOUR component - a real, importable `export function <Pascal>()` in the
|
|
68
76
|
// project's flavor (config: styles css|tailwind), under componentsDir.
|
|
69
|
-
const config = await readProjectConfig(root);
|
|
70
77
|
const wantInteractive = opts.interactive && hasInteractiveTemplate(res.name);
|
|
71
78
|
if (!opts.artifactsOnly && config.target === "next") {
|
|
72
79
|
/**
|
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)))
|
|
@@ -55,17 +55,21 @@ export async function generate(description, opts) {
|
|
|
55
55
|
}
|
|
56
56
|
console.log(`→ generating a component for "${slug}" at ${base} …`);
|
|
57
57
|
const res = await postGenerate(base, { slug, description, name: opts.name });
|
|
58
|
-
const dir = join(root, "_synthesisui", "ds", slug, "generated");
|
|
59
|
-
await mkdir(dir, { recursive: true });
|
|
60
|
-
await writeFile(join(dir, `${res.name}.json`), `${JSON.stringify(res.recipe, null, 2)}\n`, "utf8");
|
|
61
|
-
await writeFile(join(dir, `${res.name}.css`), `${res.css}\n`, "utf8");
|
|
62
58
|
const tries = `${res.tries} ${res.tries === 1 ? "try" : "tries"}`;
|
|
63
59
|
console.log(`✓ ${res.name} generated (${res.model}, ${tries})`);
|
|
64
|
-
console.log(` → _synthesisui/ds/${slug}/generated/${res.name}.{json,css}`);
|
|
65
60
|
// Materialize YOUR component (.tsx) too - the SAME codegen `component` uses -
|
|
66
61
|
// so a generated component is as usable as a brought-in one, not just a
|
|
67
62
|
// recipe you have to wire by hand.
|
|
68
63
|
const config = await readProjectConfig(root);
|
|
64
|
+
// R3 (dono, 16/08): as cópias .json/.css só quando são o produto - num
|
|
65
|
+
// target não-Next o .tsx abaixo não nasce, e aí elas são a entrega.
|
|
66
|
+
if (config.target !== "next") {
|
|
67
|
+
const dir = join(root, "_synthesisui", "ds", slug, "generated");
|
|
68
|
+
await mkdir(dir, { recursive: true });
|
|
69
|
+
await writeFile(join(dir, `${res.name}.json`), `${JSON.stringify(res.recipe, null, 2)}\n`, "utf8");
|
|
70
|
+
await writeFile(join(dir, `${res.name}.css`), `${res.css}\n`, "utf8");
|
|
71
|
+
console.log(` → _synthesisui/ds/${slug}/generated/${res.name}.{json,css}`);
|
|
72
|
+
}
|
|
69
73
|
let materialized = false;
|
|
70
74
|
if (config.target === "next") {
|
|
71
75
|
const version = await readActiveVersion(root, slug);
|
package/dist/commands/import.js
CHANGED
|
@@ -3113,7 +3113,7 @@ export async function runImport(opts) {
|
|
|
3113
3113
|
if (creds && !sameRegistry(creds.registry, base)) {
|
|
3114
3114
|
console.log("");
|
|
3115
3115
|
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(
|
|
3116
|
+
console.log(body(`Your measurement is saved at ${out}. Either name the one you meant:`));
|
|
3117
3117
|
console.log("");
|
|
3118
3118
|
console.log(body(` synthesisui import --census ${out} --registry ${creds.registry}`));
|
|
3119
3119
|
console.log(body("or log in to this one:"));
|
|
@@ -3143,7 +3143,7 @@ export async function runImport(opts) {
|
|
|
3143
3143
|
// which of the two it was (dono, 31/07). A stale token gets past the
|
|
3144
3144
|
// `readToken` guard above and only fails here.
|
|
3145
3145
|
if (res?.status === 401) {
|
|
3146
|
-
console.log(body(
|
|
3146
|
+
console.log(body(`Your session has expired. Your measurement is saved at ${out}.`));
|
|
3147
3147
|
console.log("");
|
|
3148
3148
|
console.log(body(` ${paint.strong("synthesisui login")}`));
|
|
3149
3149
|
console.log(body(` synthesisui import --census ${out}${chosen ? ` --name "${chosen}"` : ""}`));
|
|
@@ -3152,7 +3152,7 @@ export async function runImport(opts) {
|
|
|
3152
3152
|
}
|
|
3153
3153
|
console.log(body(res
|
|
3154
3154
|
? `The registry refused it (HTTP ${res.status})${detail ? `: ${detail}` : "."}`
|
|
3155
|
-
:
|
|
3155
|
+
: `Could not reach the registry. Your measurement is saved at ${out} - try again later.`));
|
|
3156
3156
|
console.log("");
|
|
3157
3157
|
return;
|
|
3158
3158
|
}
|
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";
|
|
@@ -109,6 +109,25 @@ const TOOLS = [
|
|
|
109
109
|
required: ["name"],
|
|
110
110
|
},
|
|
111
111
|
},
|
|
112
|
+
{
|
|
113
|
+
name: "playbook",
|
|
114
|
+
description: "The skill playbooks (init, import, adapt) - SERVED, not shipped, so they are always current and your context only carries the step you are on. Call with { skill } to get the framing and a table of contents; then fetch ONLY the chapter for your current step with { skill, section }. Never fetch more than the step needs.",
|
|
115
|
+
inputSchema: {
|
|
116
|
+
type: "object",
|
|
117
|
+
properties: {
|
|
118
|
+
skill: {
|
|
119
|
+
type: "string",
|
|
120
|
+
enum: ["init", "import", "adapt"],
|
|
121
|
+
description: "Which playbook.",
|
|
122
|
+
},
|
|
123
|
+
section: {
|
|
124
|
+
type: "string",
|
|
125
|
+
description: "A chapter id from the toc. Omit to get the framing + toc.",
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
required: ["skill"],
|
|
129
|
+
},
|
|
130
|
+
},
|
|
112
131
|
{
|
|
113
132
|
name: "recipe_vocabulary",
|
|
114
133
|
description: "What a recipe CAN hold: every state that compiles, every preview form, the floor per kind, and the rules a media region needs. Call this BEFORE writing a recipe, not after - the reader that skipped it sent a `dark:` inside a variant and a state the compiler cannot spell, and both were dropped in silence. Served from the catalogue, so it grows as the contract grows.",
|
|
@@ -271,12 +290,29 @@ async function findToken(root, value) {
|
|
|
271
290
|
const { table } = await loadSystem(root);
|
|
272
291
|
if (table.byName.size === 0)
|
|
273
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
|
+
};
|
|
274
306
|
const exact = tokenFor(table, value);
|
|
275
|
-
if (exact)
|
|
276
|
-
|
|
307
|
+
if (exact) {
|
|
308
|
+
const shown = theirNameFor(value) ?? exact;
|
|
309
|
+
return `${value} is ${shown} in this project. Use var(${shown}).`;
|
|
310
|
+
}
|
|
277
311
|
const near = nearestToken(table, value);
|
|
278
|
-
if (near)
|
|
279
|
-
|
|
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
|
+
}
|
|
280
316
|
// Same words the managed block uses. A refusal is only useful if it is the
|
|
281
317
|
// same refusal every time.
|
|
282
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.`;
|
|
@@ -297,18 +333,55 @@ async function findToken(root, value) {
|
|
|
297
333
|
* palavra que a pessoa já tem. Sem rede, a doutrina responde igual e a checagem é que se cala: o
|
|
298
334
|
* dado é local justamente para não depender de servidor nenhum.
|
|
299
335
|
*/
|
|
336
|
+
/** A voz do sistema, servida da plataforma - ver o comentário no chamador. */
|
|
337
|
+
async function fetchVoice(root) {
|
|
338
|
+
const slug = await installedSlug(root);
|
|
339
|
+
const token = await readToken();
|
|
340
|
+
if (!slug || !token)
|
|
341
|
+
return { kind: "unreachable" };
|
|
342
|
+
const res = await fetch(`${resolveRegistry()}/api/registry/ds/${slug}?voice=1`, { headers: { Authorization: `Bearer ${token}` } }).catch(() => null);
|
|
343
|
+
if (!res?.ok)
|
|
344
|
+
return { kind: "unreachable" };
|
|
345
|
+
const body = (await res.json().catch(() => null));
|
|
346
|
+
if (!body)
|
|
347
|
+
return { kind: "unreachable" };
|
|
348
|
+
return {
|
|
349
|
+
kind: "ok",
|
|
350
|
+
voice: {
|
|
351
|
+
context: body.context ?? undefined,
|
|
352
|
+
sections: body.sections ?? [],
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
}
|
|
300
356
|
async function systemDoctrine(root) {
|
|
301
357
|
const { documents, doctrines } = await loadSystem(root);
|
|
302
358
|
const rules = doctrines.flatMap((d) => d.rules);
|
|
303
359
|
const parts = [];
|
|
304
360
|
const pinned = doctrines[0]?.version;
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
361
|
+
/**
|
|
362
|
+
* A VOZ É SERVIDA, NÃO LIDA DO DISCO (R2, dono 16/08). As REGRAS continuam
|
|
363
|
+
* locais - o doctor e o hook precisam delas offline, e essa decisão está
|
|
364
|
+
* declarada três vezes. A voz nunca foi lida por nenhum loop local: só esta
|
|
365
|
+
* ferramenta a renderiza, e ela agora pergunta à plataforma - sempre a
|
|
366
|
+
* versão atual, e nada dela em texto plano no repo. Sem rede, as regras
|
|
367
|
+
* respondem igual e a voz se declara indisponível em vez de sumir calada.
|
|
368
|
+
*
|
|
369
|
+
* Installs antigos (design-system.json com `philosophy` dentro) continuam
|
|
370
|
+
* funcionando: o disco é o fallback quando a rede não responde.
|
|
371
|
+
*/
|
|
372
|
+
const localVoice = documents.map((doc) => doc.philosophy ?? {});
|
|
373
|
+
const served = await fetchVoice(root);
|
|
374
|
+
const voices = served.kind === "ok"
|
|
375
|
+
? [served.voice]
|
|
376
|
+
: localVoice.filter((p) => p.context || (p.sections?.length ?? 0) > 0);
|
|
377
|
+
for (const p of voices) {
|
|
378
|
+
if (p.context)
|
|
308
379
|
parts.push(`## What this product is\n\n${p.context}`);
|
|
309
|
-
for (const sec of p
|
|
380
|
+
for (const sec of p.sections ?? [])
|
|
310
381
|
parts.push(`## ${sec.title}\n\n${sec.body}`);
|
|
311
382
|
}
|
|
383
|
+
if (served.kind === "unreachable" && voices.length === 0)
|
|
384
|
+
parts.push("## Voice\n\nThe voice is served from the platform and could not be reached right now. The rules above still apply in full - build with them, and fetch the voice again before writing user-facing copy.");
|
|
312
385
|
if (rules.length === 0 && parts.length === 0)
|
|
313
386
|
return "This system carries no rules and no philosophy yet. Nothing here overrides your judgement - build with its tokens and its components, and say what you needed and could not find.";
|
|
314
387
|
const head = [
|
|
@@ -408,6 +481,28 @@ async function listComponents(root) {
|
|
|
408
481
|
* properties need to be dynamic - the more we map, the more precision". A list written
|
|
409
482
|
* into a published package is a list that stops matching the contract.
|
|
410
483
|
*/
|
|
484
|
+
/**
|
|
485
|
+
* O PLAYBOOK, na fatia pedida (R1, dono 16/08): sem `section`, a moldura + o
|
|
486
|
+
* sumário; com, UM capítulo. O corpo inteiro nunca viaja - a dieta de contexto
|
|
487
|
+
* é servida por construção.
|
|
488
|
+
*/
|
|
489
|
+
async function playbook(skill, section) {
|
|
490
|
+
const answer = await askCatalogue("playbook", undefined, section ? { skill, section } : { skill });
|
|
491
|
+
if (!answer.ok)
|
|
492
|
+
return answer.because;
|
|
493
|
+
const body = answer.body;
|
|
494
|
+
if ("error" in body)
|
|
495
|
+
return [body.error, body.hint].filter(Boolean).join("\n");
|
|
496
|
+
if ("sections" in body) {
|
|
497
|
+
return [
|
|
498
|
+
body.intro,
|
|
499
|
+
"",
|
|
500
|
+
"## Chapters - fetch ONLY the one for your current step",
|
|
501
|
+
...body.sections.map((c) => `- ${c.id} - ${c.title}`),
|
|
502
|
+
].join("\n");
|
|
503
|
+
}
|
|
504
|
+
return body.body;
|
|
505
|
+
}
|
|
411
506
|
async function recipeVocabulary() {
|
|
412
507
|
const answer = await askCatalogue("vocabulary");
|
|
413
508
|
if (!answer.ok)
|
|
@@ -769,7 +864,9 @@ cli) {
|
|
|
769
864
|
}
|
|
770
865
|
})();
|
|
771
866
|
}
|
|
772
|
-
async function askCatalogue(want, post
|
|
867
|
+
async function askCatalogue(want, post,
|
|
868
|
+
/** Parâmetros extras do GET (ex.: o playbook e o capítulo pedidos). */
|
|
869
|
+
query) {
|
|
773
870
|
const token = await readToken();
|
|
774
871
|
if (!token) {
|
|
775
872
|
return {
|
|
@@ -779,7 +876,8 @@ async function askCatalogue(want, post) {
|
|
|
779
876
|
}
|
|
780
877
|
const base = resolveRegistry();
|
|
781
878
|
try {
|
|
782
|
-
const
|
|
879
|
+
const extra = query ? `&${new URLSearchParams(query).toString()}` : "";
|
|
880
|
+
const res = await fetch(`${base}/api/catalogue${post ? "" : `?want=${want}${extra}`}`, post
|
|
783
881
|
? {
|
|
784
882
|
method: "POST",
|
|
785
883
|
headers: {
|
|
@@ -966,6 +1064,11 @@ cli) {
|
|
|
966
1064
|
case "add_component":
|
|
967
1065
|
countAgentRead(root, String(args.name ?? ""), "add", cli);
|
|
968
1066
|
return text(await addComponent(root, String(args.name ?? "")));
|
|
1067
|
+
case "playbook": {
|
|
1068
|
+
const skill = String(args?.skill ?? "");
|
|
1069
|
+
const section = args?.section ? String(args.section) : undefined;
|
|
1070
|
+
return text(await playbook(skill, section));
|
|
1071
|
+
}
|
|
969
1072
|
case "recipe_vocabulary":
|
|
970
1073
|
return text(await recipeVocabulary());
|
|
971
1074
|
case "validate_recipe": {
|
package/dist/commands/refit.js
CHANGED
|
@@ -123,12 +123,15 @@ export async function refit(file, opts) {
|
|
|
123
123
|
recipe: res.recipe,
|
|
124
124
|
});
|
|
125
125
|
console.log(`✓ saved into "${slug}" (draft v${saved.version} - ships with your next publish)`);
|
|
126
|
-
// 5. materialize back into the project:
|
|
127
|
-
|
|
128
|
-
await mkdir(artifactsDir, { recursive: true });
|
|
129
|
-
await writeFile(join(artifactsDir, `${res.name}.json`), `${JSON.stringify(res.recipe, null, 2)}\n`, "utf8");
|
|
130
|
-
await writeFile(join(artifactsDir, `${res.name}.css`), `${res.css}\n`, "utf8");
|
|
126
|
+
// 5. materialize back into the project: YOUR typed component - e as cópias
|
|
127
|
+
// .json/.css só quando são o produto (target não-Next; R3, dono 16/08).
|
|
131
128
|
const config = await readProjectConfig(root);
|
|
129
|
+
if (config.target !== "next") {
|
|
130
|
+
const artifactsDir = join(root, "_synthesisui", "ds", slug, "components");
|
|
131
|
+
await mkdir(artifactsDir, { recursive: true });
|
|
132
|
+
await writeFile(join(artifactsDir, `${res.name}.json`), `${JSON.stringify(res.recipe, null, 2)}\n`, "utf8");
|
|
133
|
+
await writeFile(join(artifactsDir, `${res.name}.css`), `${res.css}\n`, "utf8");
|
|
134
|
+
}
|
|
132
135
|
let materialized = false;
|
|
133
136
|
if (config.target === "next") {
|
|
134
137
|
const compDir = join(root, config.componentsDir, res.name);
|
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.
|