synthesisui 0.9.1 → 0.10.1

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.
@@ -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
  ];
@@ -362,9 +380,36 @@ export async function doctor(opts) {
362
380
  };
363
381
  const drifting = overrides.filter((o) => lawKept(o) === null);
364
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
+ // Three acts, not two. `transform: none` does not leave the system - it
405
+ // REMOVES what the recipe sets, and no token can hold "no transform".
406
+ // Marking it as a raw value asked for something that cannot exist.
407
+ const offSystem = drifting.filter((o) => !onSystem(o) && !isReset(o.wrote)).length;
365
408
  console.log(section("Overruled"));
366
409
  console.log(body(`${drifting.length} place${drifting.length === 1 ? "" : "s"} where the code takes a component`));
367
410
  console.log(body("the system defines, and then overrules it locally."));
411
+ console.log("");
412
+ console.log(body(`${offSystem} of them leave the system entirely; the rest pick a different token.`));
368
413
  if (correcting > 0) {
369
414
  console.log("");
370
415
  console.log(body(correcting === 1
@@ -392,6 +437,11 @@ export async function doctor(opts) {
392
437
  console.log(` the recipe binds ${o.recipe}`);
393
438
  else if (o.where)
394
439
  console.log(` the recipe binds it ${o.where}`);
440
+ if (lawKept(o) === null && !onSystem(o)) {
441
+ console.log(isReset(o.wrote)
442
+ ? " ↑ removes it rather than replacing it"
443
+ : " ↑ a raw value, not a token");
444
+ }
395
445
  const kept = lawKept(o);
396
446
  if (kept) {
397
447
  console.log(` ✓ but the law says: "${kept}"`);
@@ -434,7 +484,7 @@ export async function doctor(opts) {
434
484
  }
435
485
  }
436
486
  console.log(section("What this means"));
437
- for (const line of verdict(d, hasSystem, overrides.length))
487
+ for (const line of verdict(d, hasSystem, overrides.length, conflictsInUse.length))
438
488
  console.log(line);
439
489
  console.log("");
440
490
  if (opts.strict && (d.findings.length > 0 || overrides.length > 0))
@@ -40,6 +40,21 @@ const SPACING = /(?:\b[pmg](?:[trblxy])?-\[|gap-\[|(?:padding|margin|gap)\s*:\s*
40
40
  const FONT = /font-family\s*:\s*([^;}\n]+)/g;
41
41
  /** Uses of the system. Coverage is meaningless without them. */
42
42
  const TOKEN_USE = /var\(\s*--ds-[a-z0-9-]+/gi;
43
+ /**
44
+ * `var(--ds-color-semantic-primary, #5266eb)` - the literal is the TOKEN'S OWN
45
+ * fallback, written for safety, and reporting it as drift told an author to
46
+ * tokenize something they had already tokenized (investidorez, 25/07). The
47
+ * spans below are the fallback arguments on a line.
48
+ */
49
+ const VAR_FALLBACK = /var\(\s*--ds-[a-z0-9-]+\s*,([^()]*)\)/gi;
50
+ function fallbackSpans(line) {
51
+ const out = [];
52
+ for (const m of line.matchAll(VAR_FALLBACK)) {
53
+ const start = (m.index ?? 0) + m[0].indexOf(",") + 1;
54
+ out.push([start, start + m[1].length]);
55
+ }
56
+ return out;
57
+ }
43
58
  /** Under this, a radius or spacing value is idiom rather than a decision. */
44
59
  const IDIOM = new Set(["0", "0px", "1px", "9999px", "100%", "50%"]);
45
60
  export function scanSource(file, source, table) {
@@ -77,10 +92,18 @@ export function scanSource(file, source, table) {
77
92
  // written by hand - flagging it would be telling someone to tokenize a
78
93
  // variable. And the same literal twice in one declaration (a two-stop
79
94
  // shadow) is one decision, so it is reported once.
95
+ const spans = fallbackSpans(line);
96
+ const inFallback = (at) => spans.some(([a, b]) => at >= a && at <= b);
80
97
  const seen = new Set();
81
- const push = (kind, literal) => {
98
+ // NOT named `at`: that is the line number in this scope, and shadowing it
99
+ // put column positions into the report as line numbers.
100
+ const push = (kind, literal, col = -1) => {
82
101
  if (literal.includes("$") || literal.includes("{"))
83
102
  return;
103
+ if (col >= 0 && inFallback(col)) {
104
+ aside++;
105
+ return;
106
+ }
84
107
  const key = `${kind}:${literal}`;
85
108
  if (seen.has(key))
86
109
  return;
@@ -103,17 +126,17 @@ export function scanSource(file, source, table) {
103
126
  aside++;
104
127
  continue;
105
128
  }
106
- push("color", m[0]);
129
+ push("color", m[0], m.index ?? -1);
107
130
  }
108
131
  for (const m of line.matchAll(RADIUS)) {
109
132
  const value = `${m[1]}${m[2]}`;
110
133
  if (!IDIOM.has(value))
111
- push("radius", value);
134
+ push("radius", value, m.index ?? -1);
112
135
  }
113
136
  for (const m of line.matchAll(SPACING)) {
114
137
  const value = `${m[1]}${m[2]}`;
115
138
  if (!IDIOM.has(value))
116
- push("spacing", value);
139
+ push("spacing", value, m.index ?? -1);
117
140
  }
118
141
  for (const m of line.matchAll(FONT)) {
119
142
  const stack = m[1].trim();
@@ -128,7 +151,12 @@ export function scanSource(file, source, table) {
128
151
  findings,
129
152
  tokenUses,
130
153
  ...(aside > 0
131
- ? { setAside: { reason: "SVG artwork", count: aside } }
154
+ ? {
155
+ setAside: {
156
+ reason: "SVG artwork or a token's own fallback",
157
+ count: aside,
158
+ },
159
+ }
132
160
  : null),
133
161
  };
134
162
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.9.1",
3
+ "version": "0.10.1",
4
4
  "description": "Bring SynthesisUI design systems into any project - tokens, typed components, whole pages and an agent-ready CLAUDE.md manifest.",
5
5
  "type": "module",
6
6
  "bin": {