synthesisui 0.16.18 → 0.16.19
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 +27 -0
- package/dist/doctor/coherence.js +117 -0
- package/package.json +1 -1
package/dist/commands/doctor.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readdir, readFile } from "node:fs/promises";
|
|
2
2
|
import { join, relative, resolve } from "node:path";
|
|
3
|
+
import { findDivergences } from "../doctor/coherence.js";
|
|
3
4
|
import { findFrozenBindings } from "../doctor/frozen.js";
|
|
4
5
|
import { bindingsFromDocument, countComponents, findOverrides, } from "../doctor/overrides.js";
|
|
5
6
|
import { diagnose, scanSource, siblingTokens, } from "../doctor/scan.js";
|
|
@@ -633,6 +634,32 @@ export async function doctor(opts) {
|
|
|
633
634
|
say(body("Whoever wrote the law and whoever wrote the recipe disagree."));
|
|
634
635
|
say(body("Until they do not, no code here can be correct."));
|
|
635
636
|
}
|
|
637
|
+
/**
|
|
638
|
+
* THE FAILURE THAT SURVIVES A PERFECT SCORE.
|
|
639
|
+
*
|
|
640
|
+
* Coverage asks whether the vocabulary was used. It cannot ask whether the
|
|
641
|
+
* system says ONE thing, because every component here can reference the right
|
|
642
|
+
* token and still answer the same question differently. Measured on 28/07 in
|
|
643
|
+
* a system this tool had just graded 100%: the input shows focus by moving its
|
|
644
|
+
* border, the button by drawing an outline.
|
|
645
|
+
*
|
|
646
|
+
* Reported quietly and only against a clear house style. Hover is legitimately
|
|
647
|
+
* different per component, and a checker that flagged variety would be
|
|
648
|
+
* uninstalled the same day it shipped.
|
|
649
|
+
*/
|
|
650
|
+
const divergences = findDivergences(documents);
|
|
651
|
+
if (divergences.length > 0) {
|
|
652
|
+
say(section("The system answers the same question two ways"));
|
|
653
|
+
for (const d of divergences) {
|
|
654
|
+
say(body(`${d.concern}: ${d.houseCount} components use ${d.house.join(" + ")}.`));
|
|
655
|
+
for (const o of d.outliers) {
|
|
656
|
+
say(` ${o.component} uses ${o.props.join(" + ")} instead`);
|
|
657
|
+
}
|
|
658
|
+
say("");
|
|
659
|
+
}
|
|
660
|
+
say(body("Every one of these uses your tokens, so coverage says nothing"));
|
|
661
|
+
say(body("about them. A person meets the difference on screen."));
|
|
662
|
+
}
|
|
636
663
|
// The other way a system fails itself: a recipe that names a SHELF where the
|
|
637
664
|
// system has a ROLE. The value is legitimate, the reference resolves, the CSS
|
|
638
665
|
// compiles - and the binding sits still while everything around it flips.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOES THE SYSTEM AGREE WITH ITSELF?
|
|
3
|
+
*
|
|
4
|
+
* Token coverage answers a different question than everybody assumes. It asks
|
|
5
|
+
* whether the vocabulary was used. It cannot ask whether the system says one
|
|
6
|
+
* thing, because every component can reference the correct token and still
|
|
7
|
+
* express the same idea in a different way.
|
|
8
|
+
*
|
|
9
|
+
* Measured on 28/07, in a system this tool had just graded at 100%: the input
|
|
10
|
+
* shows focus by moving its border, the button by drawing an outline. Same
|
|
11
|
+
* token, same toolbar, two answers to one question. Every deterministic
|
|
12
|
+
* assertion we had passed, and a person looking at the screen sees a system
|
|
13
|
+
* that has not decided what focus looks like.
|
|
14
|
+
*
|
|
15
|
+
* WHY IT LIVES IN CODE AND NOT IN THE DESIGN TOOL. A designer in the Design
|
|
16
|
+
* Systems Slack built a lint plugin that does this inside Figma. Asked whether
|
|
17
|
+
* it can compare across components rather than one at a time: "it can check one
|
|
18
|
+
* at a time or all, but this crashes figma most of the time". A plugin walks a
|
|
19
|
+
* document graph inside a sandbox; this reads recipes off disk, and the whole
|
|
20
|
+
* catalogue costs a few milliseconds. The check is feasible on this side and not
|
|
21
|
+
* on that one.
|
|
22
|
+
*
|
|
23
|
+
* WHAT MAKES IT HONEST RATHER THAN NOISY. Variety is not drift. Hover is
|
|
24
|
+
* legitimately different per component - a row tints, a link recolours, a chip
|
|
25
|
+
* moves its border - and a checker that flagged all of it would be uninstalled
|
|
26
|
+
* the same day. So it reports a MINORITY against a DOMINANT majority and says
|
|
27
|
+
* nothing at all when the catalogue has no majority to disagree with.
|
|
28
|
+
*
|
|
29
|
+
* Measured against the shipped catalogue, that rule reports the two real ones
|
|
30
|
+
* and neither of the false ones:
|
|
31
|
+
*
|
|
32
|
+
* focus 9 use outline, 3 use border-color → reported (75% dominant)
|
|
33
|
+
* disabled 12 dim and change the cursor, 1 only dims → reported (92%)
|
|
34
|
+
* hover 5 shapes across 11, none dominant → silent
|
|
35
|
+
* active 1 and 1 → silent
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* State names that answer the SAME question, folded together.
|
|
39
|
+
*
|
|
40
|
+
* Without this the check finds nothing here, because each name is unanimous on
|
|
41
|
+
* its own: `focusVisible` is nine-for-nine outline, `focus` is three-for-three
|
|
42
|
+
* border. The disagreement only exists once you know they are the same concern,
|
|
43
|
+
* which is exactly why it survived every per-name check anyone could write.
|
|
44
|
+
*/
|
|
45
|
+
const CONCERNS = {
|
|
46
|
+
focus: "focus",
|
|
47
|
+
focusvisible: "focus",
|
|
48
|
+
focuswithin: "focus",
|
|
49
|
+
active: "pressed",
|
|
50
|
+
pressed: "pressed",
|
|
51
|
+
checked: "selected",
|
|
52
|
+
selected: "selected",
|
|
53
|
+
disabled: "disabled",
|
|
54
|
+
hover: "hover",
|
|
55
|
+
};
|
|
56
|
+
/** Below this share, the catalogue has no house style to disagree with, and
|
|
57
|
+
* every "outlier" is just the variety the concern is supposed to have. */
|
|
58
|
+
const DOMINANT = 0.7;
|
|
59
|
+
function collect(documents) {
|
|
60
|
+
const byConcern = new Map();
|
|
61
|
+
const visit = (label, node) => {
|
|
62
|
+
for (const [state, block] of Object.entries(node.states ?? {})) {
|
|
63
|
+
const concern = CONCERNS[state.toLowerCase()];
|
|
64
|
+
// An unrecognised state is not folded into anything. Guessing that two
|
|
65
|
+
// names mean the same thing is how a checker starts inventing findings.
|
|
66
|
+
if (!concern)
|
|
67
|
+
continue;
|
|
68
|
+
const props = Object.keys(block ?? {}).sort();
|
|
69
|
+
if (props.length === 0)
|
|
70
|
+
continue;
|
|
71
|
+
const list = byConcern.get(concern) ?? [];
|
|
72
|
+
list.push({ component: label, props });
|
|
73
|
+
byConcern.set(concern, list);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
for (const raw of documents) {
|
|
77
|
+
const doc = raw;
|
|
78
|
+
for (const [name, recipe] of Object.entries(doc.components ?? {})) {
|
|
79
|
+
visit(name, recipe);
|
|
80
|
+
for (const [part, block] of Object.entries(recipe.parts ?? {})) {
|
|
81
|
+
visit(`${name}.${part}`, block);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return byConcern;
|
|
86
|
+
}
|
|
87
|
+
/** Every concern the system answers more than one way, with a clear majority. */
|
|
88
|
+
export function findDivergences(documents) {
|
|
89
|
+
const out = [];
|
|
90
|
+
for (const [concern, entries] of collect(documents)) {
|
|
91
|
+
// One component cannot disagree with itself, and two cannot form a house
|
|
92
|
+
// style - the smallest catalogue this can speak about is three.
|
|
93
|
+
if (entries.length < 3)
|
|
94
|
+
continue;
|
|
95
|
+
const shapes = new Map();
|
|
96
|
+
for (const e of entries) {
|
|
97
|
+
const key = e.props.join("+");
|
|
98
|
+
shapes.set(key, [...(shapes.get(key) ?? []), e]);
|
|
99
|
+
}
|
|
100
|
+
if (shapes.size < 2)
|
|
101
|
+
continue;
|
|
102
|
+
const ranked = [...shapes.entries()].sort((a, b) => b[1].length - a[1].length);
|
|
103
|
+
const [houseKey, houseList] = ranked[0];
|
|
104
|
+
if (houseList.length / entries.length < DOMINANT)
|
|
105
|
+
continue;
|
|
106
|
+
out.push({
|
|
107
|
+
concern,
|
|
108
|
+
house: houseKey.split("+"),
|
|
109
|
+
houseCount: houseList.length,
|
|
110
|
+
outliers: ranked
|
|
111
|
+
.slice(1)
|
|
112
|
+
.flatMap(([, list]) => list)
|
|
113
|
+
.sort((a, b) => a.component.localeCompare(b.component)),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return out.sort((a, b) => b.outliers.length - a.outliers.length);
|
|
117
|
+
}
|
package/package.json
CHANGED