synthesisui 0.9.0 → 0.10.0
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/commands/doctor.js +108 -25
- package/dist/doctor/self-conflict.js +37 -0
- package/package.json +1 -1
package/dist/commands/doctor.js
CHANGED
|
@@ -2,7 +2,7 @@ import { readdir, readFile } from "node:fs/promises";
|
|
|
2
2
|
import { join, relative, resolve } from "node:path";
|
|
3
3
|
import { bindingsFromDocument, countComponents, findOverrides, } from "../doctor/overrides.js";
|
|
4
4
|
import { diagnose, scanSource, } from "../doctor/scan.js";
|
|
5
|
-
import { findSelfConflicts } from "../doctor/self-conflict.js";
|
|
5
|
+
import { findSelfConflicts, forbiddenProps, isReset, propMatchesLabel, } from "../doctor/self-conflict.js";
|
|
6
6
|
import { buildTable, EMPTY_TABLE, nearestToken, } from "../doctor/tokens.js";
|
|
7
7
|
import { body, section, snippet } from "../output.js";
|
|
8
8
|
/**
|
|
@@ -147,11 +147,25 @@ function meter(pct, width = 24) {
|
|
|
147
147
|
const filled = Math.round((pct / 100) * width);
|
|
148
148
|
return `${"█".repeat(filled)}${"░".repeat(width - filled)}`;
|
|
149
149
|
}
|
|
150
|
-
function verdict(d, hasSystem, overruled) {
|
|
150
|
+
function verdict(d, hasSystem, overruled, conflicts) {
|
|
151
|
+
// A system at war with itself outranks every other number here. The closing
|
|
152
|
+
// line is the one people quote, and it used to end on "6 of 14 have a name"
|
|
153
|
+
// while the page above it said the recipe contradicts its own law.
|
|
154
|
+
const head = conflicts > 0
|
|
155
|
+
? [
|
|
156
|
+
body(conflicts === 1
|
|
157
|
+
? "First, above everything: your system contradicts itself in one place."
|
|
158
|
+
: `First, above everything: your system contradicts itself in ${conflicts} places.`),
|
|
159
|
+
body("A law says one thing and the recipe under it does another, and"),
|
|
160
|
+
body("no code can be correct against a rule that disagrees with itself."),
|
|
161
|
+
"",
|
|
162
|
+
]
|
|
163
|
+
: [];
|
|
151
164
|
// Nothing found and nothing installed: a utility package, a config folder,
|
|
152
165
|
// the wrong directory. Selling a design system here would be noise.
|
|
153
166
|
if (!hasSystem && d.findings.length === 0) {
|
|
154
167
|
return [
|
|
168
|
+
...head,
|
|
155
169
|
body("No design values are written by hand here, and no system is"),
|
|
156
170
|
body("installed. Nothing to fix, and nothing to compare against."),
|
|
157
171
|
];
|
|
@@ -159,6 +173,7 @@ function verdict(d, hasSystem, overruled) {
|
|
|
159
173
|
if (!hasSystem) {
|
|
160
174
|
const distinct = new Set(d.findings.map((f) => f.literal.toLowerCase()));
|
|
161
175
|
return [
|
|
176
|
+
...head,
|
|
162
177
|
body(`${distinct.size} distinct design values are written by hand here.`),
|
|
163
178
|
body("No design system is installed, so none of them has a name yet."),
|
|
164
179
|
"",
|
|
@@ -169,6 +184,7 @@ function verdict(d, hasSystem, overruled) {
|
|
|
169
184
|
}
|
|
170
185
|
if (d.findings.length === 0 && overruled === 0) {
|
|
171
186
|
return [
|
|
187
|
+
...head,
|
|
172
188
|
body("No drift. Every design value in this project comes from the"),
|
|
173
189
|
body("system. That is a rarer sentence than it sounds."),
|
|
174
190
|
];
|
|
@@ -177,6 +193,7 @@ function verdict(d, hasSystem, overruled) {
|
|
|
177
193
|
// "no drift" is a report nobody believes twice.
|
|
178
194
|
if (d.findings.length === 0) {
|
|
179
195
|
return [
|
|
196
|
+
...head,
|
|
180
197
|
body("No loose values - everything is a token."),
|
|
181
198
|
body(overruled === 1
|
|
182
199
|
? "But one place takes a component the system defines and"
|
|
@@ -188,6 +205,7 @@ function verdict(d, hasSystem, overruled) {
|
|
|
188
205
|
];
|
|
189
206
|
}
|
|
190
207
|
const lines = [
|
|
208
|
+
...head,
|
|
191
209
|
body(`${d.named} of ${d.findings.length} already have a name in your system.`),
|
|
192
210
|
body("Those are the cheap ones: swap the literal for the token."),
|
|
193
211
|
];
|
|
@@ -313,11 +331,89 @@ export async function doctor(opts) {
|
|
|
313
331
|
console.log(body(`+${files.length - shownFiles.length} more files. Run with --all to see everything.`));
|
|
314
332
|
}
|
|
315
333
|
}
|
|
334
|
+
// Every other section asks whether the code obeys the system. This one asks
|
|
335
|
+
// whether the system obeys itself, and it is only here because the tool once
|
|
336
|
+
// reported a consumer for overruling a recipe while they were obeying the
|
|
337
|
+
// law written on that same component.
|
|
338
|
+
const conflicts = documents.flatMap((doc) => findSelfConflicts(doc));
|
|
339
|
+
const conflictsInUse = conflicts.filter((c) => used.has(c.component));
|
|
340
|
+
if (conflictsInUse.length > 0) {
|
|
341
|
+
console.log(section("Your system contradicts itself"));
|
|
342
|
+
console.log(body(conflictsInUse.length === 1
|
|
343
|
+
? "One component says one thing in prose and another in its recipe."
|
|
344
|
+
: `${conflictsInUse.length} components say one thing in prose and another in their recipe.`));
|
|
345
|
+
console.log("");
|
|
346
|
+
for (const c of conflictsInUse) {
|
|
347
|
+
console.log(body(`ds-${c.component}`));
|
|
348
|
+
console.log(` "${c.law}"`);
|
|
349
|
+
console.log(` but ${c.where} binds ${c.value}`);
|
|
350
|
+
console.log("");
|
|
351
|
+
}
|
|
352
|
+
console.log(body("Whoever wrote the law and whoever wrote the recipe disagree."));
|
|
353
|
+
console.log(body("Until they do not, no code here can be correct."));
|
|
354
|
+
}
|
|
316
355
|
if (overrides.length > 0) {
|
|
356
|
+
// An override on a property the component's own law FORBIDS, written as a
|
|
357
|
+
// reset, is not drift - it is the author keeping a promise the recipe
|
|
358
|
+
// broke. Counting it against them was the report being unfair to the only
|
|
359
|
+
// person in the loop who read the law (investidorez, 25/07).
|
|
360
|
+
const forbidden = new Map();
|
|
361
|
+
for (const doc of documents) {
|
|
362
|
+
for (const [comp, props] of forbiddenProps(doc)) {
|
|
363
|
+
const merged = forbidden.get(comp) ?? new Map();
|
|
364
|
+
for (const [label, law] of props)
|
|
365
|
+
merged.set(label, law);
|
|
366
|
+
forbidden.set(comp, merged);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const lawKept = (o) => {
|
|
370
|
+
if (!isReset(o.wrote))
|
|
371
|
+
return null;
|
|
372
|
+
const props = forbidden.get(o.component);
|
|
373
|
+
if (!props)
|
|
374
|
+
return null;
|
|
375
|
+
for (const [label, law] of props) {
|
|
376
|
+
if (propMatchesLabel(o.prop, label))
|
|
377
|
+
return law;
|
|
378
|
+
}
|
|
379
|
+
return null;
|
|
380
|
+
};
|
|
381
|
+
const drifting = overrides.filter((o) => lawKept(o) === null);
|
|
382
|
+
const correcting = overrides.length - drifting.length;
|
|
383
|
+
/**
|
|
384
|
+
* Two very different acts wearing one label. Choosing a DIFFERENT token
|
|
385
|
+
* from the system is a decision inside the vocabulary - the recipe says
|
|
386
|
+
* surface, the author wanted raised, and both are the system's words.
|
|
387
|
+
* Writing `#fff` leaves the system entirely. Reporting them identically
|
|
388
|
+
* buried the ones that matter (investidorez, 25/07).
|
|
389
|
+
*/
|
|
390
|
+
const onSystem = (o) => {
|
|
391
|
+
if (o.wrote.includes("var(--ds-"))
|
|
392
|
+
return true;
|
|
393
|
+
// a utility whose step exists in the scale - `gap-2xs` when the project
|
|
394
|
+
// has --ds-spacing-2xs - is the scale being used, just at another step
|
|
395
|
+
const step = /-([a-z0-9]+)$/.exec(o.wrote);
|
|
396
|
+
if (!step || o.wrote.includes("["))
|
|
397
|
+
return false;
|
|
398
|
+
for (const name of table.byName.keys()) {
|
|
399
|
+
if (name.endsWith(`-${step[1]}`))
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
return false;
|
|
403
|
+
};
|
|
404
|
+
const offSystem = drifting.filter((o) => !onSystem(o)).length;
|
|
317
405
|
console.log(section("Overruled"));
|
|
318
|
-
console.log(body(`${
|
|
406
|
+
console.log(body(`${drifting.length} place${drifting.length === 1 ? "" : "s"} where the code takes a component`));
|
|
319
407
|
console.log(body("the system defines, and then overrules it locally."));
|
|
320
408
|
console.log("");
|
|
409
|
+
console.log(body(`${offSystem} of them leave the system entirely; the rest pick a different token.`));
|
|
410
|
+
if (correcting > 0) {
|
|
411
|
+
console.log("");
|
|
412
|
+
console.log(body(correcting === 1
|
|
413
|
+
? "One more overrules it to KEEP a law the recipe breaks - marked below."
|
|
414
|
+
: `${correcting} more overrule it to KEEP a law the recipe breaks - marked below.`));
|
|
415
|
+
}
|
|
416
|
+
console.log("");
|
|
321
417
|
const byFile = new Map();
|
|
322
418
|
for (const o of overrides) {
|
|
323
419
|
const list = byFile.get(o.file);
|
|
@@ -338,6 +434,14 @@ export async function doctor(opts) {
|
|
|
338
434
|
console.log(` the recipe binds ${o.recipe}`);
|
|
339
435
|
else if (o.where)
|
|
340
436
|
console.log(` the recipe binds it ${o.where}`);
|
|
437
|
+
if (!onSystem(o) && lawKept(o) === null) {
|
|
438
|
+
console.log(" ↑ a raw value, not a token");
|
|
439
|
+
}
|
|
440
|
+
const kept = lawKept(o);
|
|
441
|
+
if (kept) {
|
|
442
|
+
console.log(` ✓ but the law says: "${kept}"`);
|
|
443
|
+
console.log(" this override keeps it - the recipe does not");
|
|
444
|
+
}
|
|
341
445
|
}
|
|
342
446
|
if (!opts.all && list.length > 3) {
|
|
343
447
|
console.log(` +${list.length - 3} more`);
|
|
@@ -351,27 +455,6 @@ export async function doctor(opts) {
|
|
|
351
455
|
// The system's own words, for the components this project actually uses.
|
|
352
456
|
// Prose, so nothing verifies it - the value is putting it in front of
|
|
353
457
|
// whoever is touching the component, which no other tool is positioned to
|
|
354
|
-
// Every other section asks whether the code obeys the system. This one asks
|
|
355
|
-
// whether the system obeys itself, and it is only here because the tool once
|
|
356
|
-
// reported a consumer for overruling a recipe while they were obeying the
|
|
357
|
-
// law written on that same component.
|
|
358
|
-
const conflicts = documents.flatMap((doc) => findSelfConflicts(doc));
|
|
359
|
-
const conflictsInUse = conflicts.filter((c) => used.has(c.component));
|
|
360
|
-
if (conflictsInUse.length > 0) {
|
|
361
|
-
console.log(section("Your system contradicts itself"));
|
|
362
|
-
console.log(body(conflictsInUse.length === 1
|
|
363
|
-
? "One component says one thing in prose and another in its recipe."
|
|
364
|
-
: `${conflictsInUse.length} components say one thing in prose and another in their recipe.`));
|
|
365
|
-
console.log("");
|
|
366
|
-
for (const c of conflictsInUse) {
|
|
367
|
-
console.log(body(`ds-${c.component}`));
|
|
368
|
-
console.log(` "${c.law}"`);
|
|
369
|
-
console.log(` but ${c.where} binds ${c.value}`);
|
|
370
|
-
console.log("");
|
|
371
|
-
}
|
|
372
|
-
console.log(body("Whoever wrote the law and whoever wrote the recipe disagree."));
|
|
373
|
-
console.log(body("Until they do not, no code here can be correct."));
|
|
374
|
-
}
|
|
375
458
|
// do because no other tool knows these laws exist.
|
|
376
459
|
const inUse = [...used.entries()]
|
|
377
460
|
.filter(([name]) => (recipes.get(name)?.usage.length ?? 0) > 0)
|
|
@@ -396,7 +479,7 @@ export async function doctor(opts) {
|
|
|
396
479
|
}
|
|
397
480
|
}
|
|
398
481
|
console.log(section("What this means"));
|
|
399
|
-
for (const line of verdict(d, hasSystem, overrides.length))
|
|
482
|
+
for (const line of verdict(d, hasSystem, overrides.length, conflictsInUse.length))
|
|
400
483
|
console.log(line);
|
|
401
484
|
console.log("");
|
|
402
485
|
if (opts.strict && (d.findings.length > 0 || overrides.length > 0))
|
|
@@ -94,6 +94,43 @@ function* declarations(node, where) {
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Per component, the properties its own laws forbid and the law that does it.
|
|
99
|
+
*
|
|
100
|
+
* The report needs this separately from the conflicts: an override on a
|
|
101
|
+
* forbidden property is not drift, it is the author keeping a promise the
|
|
102
|
+
* recipe broke. Listing them together was accusing the person who was right.
|
|
103
|
+
*/
|
|
104
|
+
export function forbiddenProps(document) {
|
|
105
|
+
const out = new Map();
|
|
106
|
+
const doc = (document ?? {});
|
|
107
|
+
const components = (doc.components ?? {});
|
|
108
|
+
for (const [name, raw] of Object.entries(components)) {
|
|
109
|
+
const recipe = (raw ?? {});
|
|
110
|
+
const laws = Array.isArray(recipe.usage)
|
|
111
|
+
? recipe.usage.filter((x) => typeof x === "string")
|
|
112
|
+
: [];
|
|
113
|
+
for (const law of laws) {
|
|
114
|
+
for (const rule of FORBIDDABLE) {
|
|
115
|
+
if (!forbids(law, rule.words))
|
|
116
|
+
continue;
|
|
117
|
+
const byProp = out.get(name) ?? new Map();
|
|
118
|
+
byProp.set(rule.label, law);
|
|
119
|
+
out.set(name, byProp);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
/** Is this property one the law names, given how the rule matches? */
|
|
126
|
+
export function propMatchesLabel(prop, label) {
|
|
127
|
+
const rule = FORBIDDABLE.find((r) => r.label === label);
|
|
128
|
+
return rule ? rule.prop.test(prop) : false;
|
|
129
|
+
}
|
|
130
|
+
/** A value that removes rather than sets - the law being kept. */
|
|
131
|
+
export function isReset(value) {
|
|
132
|
+
return !isSet(value);
|
|
133
|
+
}
|
|
97
134
|
/**
|
|
98
135
|
* Every place a component's recipe does what its own law forbids.
|
|
99
136
|
*/
|
package/package.json
CHANGED