synthesisui 0.11.3 → 0.12.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.
@@ -252,6 +252,14 @@ export async function doctor(opts) {
252
252
  r.findings = r.findings.filter((f) => !explained.has(`${r.file}:${f.line}:${f.literal.toLowerCase()}`));
253
253
  }
254
254
  const d = diagnose(reports);
255
+ // Nine releases in one evening added a section each, every one justified on
256
+ // its own, and nobody read the whole. The result was 151 lines carrying about
257
+ // eight lines of meaning. Detail is now something you ask for.
258
+ const verbose = opts.verbose === true;
259
+ const say = (line) => {
260
+ if (verbose)
261
+ console.log(line);
262
+ };
255
263
  console.log(section("Doctor"));
256
264
  console.log(body(hasSystem
257
265
  ? `${table.name ?? table.slug} v${table.version ?? "?"} - ${table.byName.size} tokens, ${d.scanned} files read`
@@ -270,8 +278,14 @@ export async function doctor(opts) {
270
278
  aside.set(a.reason, (aside.get(a.reason) ?? 0) + a.count);
271
279
  }
272
280
  }
273
- for (const [reason, count] of aside) {
274
- console.log(body(`set aside: ${count} value(s) in ${reason}`));
281
+ const asideTotal = [...aside.values()].reduce((n, v) => n + v, 0);
282
+ if (verbose) {
283
+ for (const [reason, count] of aside) {
284
+ console.log(body(`set aside: ${count} value(s) in ${reason}`));
285
+ }
286
+ }
287
+ else if (asideTotal > 0) {
288
+ console.log(body(`set aside ${asideTotal} value(s) a token could never hold (--verbose for why)`));
275
289
  }
276
290
  // 0 of 0 is not a perfect score, it is an empty measurement - printing a
277
291
  // full bar there would be the report's first lie.
@@ -282,18 +296,18 @@ export async function doctor(opts) {
282
296
  console.log(body(` ${d.tokenUses} from the system, ${d.findings.length} by hand`));
283
297
  }
284
298
  if (d.findings.length > 0) {
285
- console.log(section("Drift"));
299
+ say(section("Drift"));
286
300
  const order = ["color", "radius", "spacing", "font"].filter((k) => d.counts[k] > 0);
287
301
  for (const kind of order) {
288
- console.log(body(`${d.counts[kind]} ${KIND_LABEL[kind]}`));
302
+ say(body(`${d.counts[kind]} ${KIND_LABEL[kind]}`));
289
303
  }
290
304
  // What a person actually acts on first: the value repeated everywhere.
291
305
  // One decision here retires dozens of sites, and a list sorted by file
292
306
  // never tells you that.
293
307
  const repeats = d.repeats.slice(0, 5);
294
308
  if (repeats.length > 0) {
295
- console.log("");
296
- console.log(body("Most repeated"));
309
+ say("");
310
+ say(body("Most repeated"));
297
311
  const w = Math.max(...repeats.map((r) => r.literal.length));
298
312
  for (const r of repeats) {
299
313
  const where = `${r.count}\u00d7 in ${r.files} file${r.files === 1 ? "" : "s"}`;
@@ -303,17 +317,17 @@ export async function doctor(opts) {
303
317
  : near
304
318
  ? ` → nearest is ${near.name} (${near.value})`
305
319
  : "";
306
- console.log(` ${r.literal.padEnd(w)} ${where}${named}`);
320
+ say(` ${r.literal.padEnd(w)} ${where}${named}`);
307
321
  }
308
322
  }
309
323
  // Loudest files first: drift concentrates, and the fix is usually one
310
324
  // shared component rather than two hundred call sites.
311
325
  const files = [...d.files].sort((a, b) => b.findings.length - a.findings.length);
312
- const shownFiles = opts.all ? files : files.slice(0, 8);
313
- console.log("");
326
+ const shownFiles = verbose ? files : files.slice(0, 8);
327
+ say("");
314
328
  for (const f of shownFiles) {
315
- console.log(body(`${f.file}`));
316
- const shown = opts.all ? f.findings : f.findings.slice(0, 3);
329
+ say(body(`${f.file}`));
330
+ const shown = verbose ? f.findings : f.findings.slice(0, 3);
317
331
  for (const x of shown) {
318
332
  // A dead end with a neighbour is not a dead end. Only for lengths -
319
333
  // "nearly the same blue" is the guess this tool must never make.
@@ -323,15 +337,15 @@ export async function doctor(opts) {
323
337
  : near
324
338
  ? `→ nearest is ${near.name} (${near.value})`
325
339
  : "→ no token holds this value yet";
326
- console.log(` ${String(x.line).padStart(4)} ${x.literal} ${named}`);
340
+ say(` ${String(x.line).padStart(4)} ${x.literal} ${named}`);
327
341
  }
328
342
  if (f.findings.length > shown.length) {
329
- console.log(` +${f.findings.length - shown.length} more`);
343
+ say(` +${f.findings.length - shown.length} more`);
330
344
  }
331
- console.log("");
345
+ say("");
332
346
  }
333
347
  if (files.length > shownFiles.length) {
334
- console.log(body(`+${files.length - shownFiles.length} more files. Run with --all to see everything.`));
348
+ say(body(`+${files.length - shownFiles.length} more files. Run with --all to see everything.`));
335
349
  }
336
350
  }
337
351
  // Every other section asks whether the code obeys the system. This one asks
@@ -341,19 +355,19 @@ export async function doctor(opts) {
341
355
  const conflicts = documents.flatMap((doc) => findSelfConflicts(doc));
342
356
  const conflictsInUse = conflicts.filter((c) => used.has(c.component));
343
357
  if (conflictsInUse.length > 0) {
344
- console.log(section("Your system contradicts itself"));
345
- console.log(body(conflictsInUse.length === 1
358
+ say(section("Your system contradicts itself"));
359
+ say(body(conflictsInUse.length === 1
346
360
  ? "One component says one thing in prose and another in its recipe."
347
361
  : `${conflictsInUse.length} components say one thing in prose and another in their recipe.`));
348
- console.log("");
362
+ say("");
349
363
  for (const c of conflictsInUse) {
350
- console.log(body(`ds-${c.component}`));
351
- console.log(` "${c.law}"`);
352
- console.log(` but ${c.where} binds ${c.value}`);
353
- console.log("");
364
+ say(body(`ds-${c.component}`));
365
+ say(` "${c.law}"`);
366
+ say(` but ${c.where} binds ${c.value}`);
367
+ say("");
354
368
  }
355
- console.log(body("Whoever wrote the law and whoever wrote the recipe disagree."));
356
- console.log(body("Until they do not, no code here can be correct."));
369
+ say(body("Whoever wrote the law and whoever wrote the recipe disagree."));
370
+ say(body("Until they do not, no code here can be correct."));
357
371
  }
358
372
  // The other way a system fails itself: a recipe that names a SHELF where the
359
373
  // system has a ROLE. The value is legitimate, the reference resolves, the CSS
@@ -362,22 +376,24 @@ export async function doctor(opts) {
362
376
  .flatMap((doc) => findFrozenBindings(doc))
363
377
  .filter((f) => used.has(f.component));
364
378
  if (frozen.length > 0) {
365
- console.log(section("These will not follow your other scheme"));
366
- console.log(body(frozen.length === 1
379
+ say(section("These will not follow your other scheme"));
380
+ say(body(frozen.length === 1
367
381
  ? "One recipe names a primitive where a role holds the same value."
368
382
  : `${frozen.length} recipes name a primitive where a role holds the same value.`));
369
- console.log("");
383
+ say("");
370
384
  for (const f of frozen) {
371
- console.log(body(`ds-${f.component} · ${f.where}`));
372
- console.log(` binds ${f.wrote}`);
373
- console.log(` {color.semantic.${f.role}} holds that, and becomes ${f.becomes}`);
374
- console.log("");
385
+ say(body(`ds-${f.component} · ${f.where}`));
386
+ say(` binds ${f.wrote}`);
387
+ say(` {color.semantic.${f.role}} holds that, and becomes ${f.becomes}`);
388
+ say("");
375
389
  }
376
- console.log(body("The value resolves and the CSS compiles, so nothing"));
377
- console.log(body("complains - the surface just stays put when the scheme"));
378
- console.log(body("moves around it."));
390
+ say(body("The value resolves and the CSS compiles, so nothing"));
391
+ say(body("complains - the surface just stays put when the scheme"));
392
+ say(body("moves around it."));
379
393
  }
380
394
  const frozenAt = new Set(frozen.map((f) => `${f.component}:${f.where.split(" · ").pop()}`));
395
+ let offSystemCount = 0;
396
+ let lawKeepingCount = 0;
381
397
  if (overrides.length > 0) {
382
398
  // An override on a property the component's own law FORBIDS, written as a
383
399
  // reset, is not drift - it is the author keeping a promise the recipe
@@ -406,6 +422,7 @@ export async function doctor(opts) {
406
422
  };
407
423
  const drifting = overrides.filter((o) => lawKept(o) === null);
408
424
  const correcting = overrides.length - drifting.length;
425
+ lawKeepingCount = correcting;
409
426
  /**
410
427
  * Two very different acts wearing one label. Choosing a DIFFERENT token
411
428
  * from the system is a decision inside the vocabulary - the recipe says
@@ -431,18 +448,19 @@ export async function doctor(opts) {
431
448
  // REMOVES what the recipe sets, and no token can hold "no transform".
432
449
  // Marking it as a raw value asked for something that cannot exist.
433
450
  const offSystem = drifting.filter((o) => !onSystem(o) && !isReset(o.wrote)).length;
434
- console.log(section("Overruled"));
435
- console.log(body(`${drifting.length} place${drifting.length === 1 ? "" : "s"} where the code takes a component`));
436
- console.log(body("the system defines, and then overrules it locally."));
437
- console.log("");
438
- console.log(body(`${offSystem} of them leave the system entirely; the rest pick a different token.`));
451
+ offSystemCount = offSystem;
452
+ say(section("Overruled"));
453
+ say(body(`${drifting.length} place${drifting.length === 1 ? "" : "s"} where the code takes a component`));
454
+ say(body("the system defines, and then overrules it locally."));
455
+ say("");
456
+ say(body(`${offSystem} of them leave the system entirely; the rest pick a different token.`));
439
457
  if (correcting > 0) {
440
- console.log("");
441
- console.log(body(correcting === 1
458
+ say("");
459
+ say(body(correcting === 1
442
460
  ? "One more overrules it to KEEP a law the recipe breaks - marked below."
443
461
  : `${correcting} more overrule it to KEEP a law the recipe breaks - marked below.`));
444
462
  }
445
- console.log("");
463
+ say("");
446
464
  const byFile = new Map();
447
465
  for (const o of overrides) {
448
466
  const list = byFile.get(o.file);
@@ -451,20 +469,20 @@ export async function doctor(opts) {
451
469
  else
452
470
  byFile.set(o.file, [o]);
453
471
  }
454
- const shown = opts.all ? [...byFile] : [...byFile].slice(0, 6);
472
+ const shown = verbose ? [...byFile] : [...byFile].slice(0, 6);
455
473
  for (const [file, list] of shown) {
456
- console.log(body(file));
457
- for (const o of opts.all ? list : list.slice(0, 3)) {
458
- console.log(` ${String(o.line).padStart(4)} ds-${o.component} · ${o.prop}: ${o.wrote}`);
474
+ say(body(file));
475
+ for (const o of verbose ? list : list.slice(0, 3)) {
476
+ say(` ${String(o.line).padStart(4)} ds-${o.component} · ${o.prop}: ${o.wrote}`);
459
477
  // Always say what is being overruled. A finding that cannot name it is
460
478
  // indistinguishable from a bug in the reader's eyes, and one of these
461
479
  // (`.ds-card:hover { transform: none }`) was a real, deliberate call.
462
480
  if (o.recipe)
463
- console.log(` the recipe binds ${o.recipe}`);
481
+ say(` the recipe binds ${o.recipe}`);
464
482
  else if (o.where)
465
- console.log(` the recipe binds it ${o.where}`);
483
+ say(` the recipe binds it ${o.where}`);
466
484
  if (lawKept(o) === null && !onSystem(o)) {
467
- console.log(isReset(o.wrote)
485
+ say(isReset(o.wrote)
468
486
  ? " ↑ removes it rather than replacing it"
469
487
  : " ↑ a raw value, not a token");
470
488
  }
@@ -473,21 +491,21 @@ export async function doctor(opts) {
473
491
  // that as an infraction is the same unfairness as the law case above.
474
492
  if (frozenAt.has(`${o.component}:${o.prop}`) &&
475
493
  /var\(\s*--ds-color-semantic-/.test(o.wrote)) {
476
- console.log(" ✓ the recipe is frozen here; yours names a role");
494
+ say(" ✓ the recipe is frozen here; yours names a role");
477
495
  }
478
496
  const kept = lawKept(o);
479
497
  if (kept) {
480
- console.log(` ✓ but the law says: "${kept}"`);
481
- console.log(" this override keeps it - the recipe does not");
498
+ say(` ✓ but the law says: "${kept}"`);
499
+ say(" this override keeps it - the recipe does not");
482
500
  }
483
501
  }
484
- if (!opts.all && list.length > 3) {
485
- console.log(` +${list.length - 3} more`);
502
+ if (!verbose && list.length > 3) {
503
+ say(` +${list.length - 3} more`);
486
504
  }
487
- console.log("");
505
+ say("");
488
506
  }
489
507
  if (byFile.size > shown.length) {
490
- console.log(body(`+${byFile.size - shown.length} more files. Run with --all.`));
508
+ say(body(`+${byFile.size - shown.length} more files. Run with --all.`));
491
509
  }
492
510
  }
493
511
  // The system's own words, for the components this project actually uses.
@@ -498,28 +516,114 @@ export async function doctor(opts) {
498
516
  .filter(([name]) => (recipes.get(name)?.usage.length ?? 0) > 0)
499
517
  .sort((a, b) => b[1] - a[1]);
500
518
  if (inUse.length > 0) {
501
- console.log(section("What your system says about what you use"));
502
- const shownComps = opts.laws || opts.all ? inUse : inUse.slice(0, 4);
519
+ say(section("What your system says about what you use"));
520
+ const shownComps = opts.laws || verbose ? inUse : inUse.slice(0, 4);
503
521
  for (const [name, count] of shownComps) {
504
522
  const laws = recipes.get(name)?.usage ?? [];
505
- const shown = opts.laws || opts.all ? laws : laws.slice(0, 2);
506
- console.log(body(`ds-${name} · ${count} place${count === 1 ? "" : "s"}`));
523
+ const shown = opts.laws || verbose ? laws : laws.slice(0, 2);
524
+ say(body(`ds-${name} · ${count} place${count === 1 ? "" : "s"}`));
507
525
  for (const law of shown)
508
- console.log(` ${law}`);
526
+ say(` ${law}`);
509
527
  if (laws.length > shown.length) {
510
- console.log(` +${laws.length - shown.length} more`);
528
+ say(` +${laws.length - shown.length} more`);
511
529
  }
512
- console.log("");
530
+ say("");
513
531
  }
514
532
  if (inUse.length > shownComps.length) {
515
- console.log(body(`+${inUse.length - shownComps.length} more components carry laws. Run with --laws.`));
516
- console.log("");
533
+ say(body(`+${inUse.length - shownComps.length} more components carry laws. Run with --laws.`));
534
+ say("");
517
535
  }
518
536
  }
519
- console.log(section("What this means"));
537
+ say(section("What this means"));
520
538
  for (const line of verdict(d, hasSystem, overrides.length, conflictsInUse.length))
521
- console.log(line);
522
- console.log("");
539
+ say(line);
540
+ say("");
541
+ const plan = [];
542
+ for (const c of conflictsInUse) {
543
+ plan.push({
544
+ rank: 0,
545
+ what: `the ds-${c.component} contradiction`,
546
+ size: `${used.get(c.component) ?? 0} places`,
547
+ cheap: false,
548
+ });
549
+ }
550
+ for (const f of frozen) {
551
+ plan.push({
552
+ rank: 1,
553
+ what: `the frozen ds-${f.component} ${f.where.split(" · ").pop()}`,
554
+ size: `${used.get(f.component) ?? 0} places`,
555
+ cheap: true,
556
+ });
557
+ }
558
+ if (offSystemCount > 0) {
559
+ plan.push({
560
+ rank: 2,
561
+ what: `the ${offSystemCount} override${offSystemCount === 1 ? "" : "s"} that left the system`,
562
+ size: `${offSystemCount} place${offSystemCount === 1 ? "" : "s"}`,
563
+ cheap: false,
564
+ });
565
+ }
566
+ for (const r of d.repeats.filter((x) => x.token).slice(0, 3)) {
567
+ plan.push({
568
+ rank: 3,
569
+ what: `${r.literal} → ${r.token}`,
570
+ size: `${r.files} file${r.files === 1 ? "" : "s"}`,
571
+ cheap: true,
572
+ });
573
+ }
574
+ for (const r of d.repeats.filter((x) => !x.token).slice(0, 2)) {
575
+ const near = nearestToken(table, r.literal);
576
+ plan.push({
577
+ rank: 4,
578
+ what: near
579
+ ? `${r.literal} - name it, or snap to ${near.name}`
580
+ : `${r.literal} - the system has no name for it`,
581
+ size: `${r.files} file${r.files === 1 ? "" : "s"}`,
582
+ cheap: false,
583
+ });
584
+ }
585
+ plan.sort((a, b) => a.rank - b.rank);
586
+ if (!verbose && !hasSystem) {
587
+ // The cold run is the whole pitch: someone with no system, one command, a
588
+ // number they did not have. The summary must not swallow it - it is the
589
+ // only path here that has to persuade rather than inform.
590
+ console.log("");
591
+ for (const line of verdict(d, false, 0, 0))
592
+ console.log(line);
593
+ console.log("");
594
+ }
595
+ else if (!verbose) {
596
+ if (conflictsInUse.length + frozen.length > 0) {
597
+ console.log("");
598
+ console.log(body(`${conflictsInUse.length + frozen.length} problem(s) in the SYSTEM - not fixable from this repo`));
599
+ for (const c of conflictsInUse) {
600
+ console.log(` ds-${c.component} the law forbids ${c.forbids}, the recipe binds it`);
601
+ }
602
+ for (const f of frozen) {
603
+ console.log(` ds-${f.component} ${f.where.split(" · ").pop()} frozen at a primitive, will not follow the other scheme`);
604
+ }
605
+ }
606
+ if (d.findings.length > 0 || overrides.length > 0) {
607
+ console.log("");
608
+ console.log(body(`${d.findings.length} value(s) by hand · ${d.named} already have a name`));
609
+ console.log(body(`${overrides.length - lawKeepingCount} override(s) · ${offSystemCount} left the system` +
610
+ (lawKeepingCount > 0
611
+ ? ` · ${lawKeepingCount} more kept a law`
612
+ : "")));
613
+ }
614
+ if (plan.length > 0) {
615
+ console.log("");
616
+ console.log(body("Where to start"));
617
+ const w = Math.min(52, Math.max(...plan.slice(0, 5).map((j) => j.what.length)));
618
+ plan.slice(0, 5).forEach((j, i) => {
619
+ const what = j.what.length > w ? `${j.what.slice(0, w - 1)}…` : j.what.padEnd(w);
620
+ console.log(` ${i + 1}. ${what} ${j.size}${j.cheap ? " (cheap)" : ""}`);
621
+ });
622
+ }
623
+ console.log("");
624
+ console.log(body("synthesisui doctor --verbose every finding, file by file"));
625
+ console.log("");
626
+ }
523
627
  if (opts.strict) {
524
628
  const mine = d.findings.length > 0 || overrides.length > 0;
525
629
  // The two system lenses are deliberately NOT in here by default. A team
package/dist/index.js CHANGED
@@ -26,7 +26,7 @@ Usage - deterministic, FREE:
26
26
  synthesisui upgrade <slug> update an installed DS + regenerate your components + migration brief
27
27
  synthesisui use <slug> "<intent>" print a ready-to-paste agent prompt to build/modify on-system
28
28
  synthesisui clean [--force] strip create-next-app boilerplate (dry run without --force)
29
- synthesisui doctor [paths…] [--all] audit for DRIFT: every design value written by
29
+ synthesisui doctor [paths…] [--verbose] audit for DRIFT: every design value written by
30
30
  hand, the token your system already has for it, and the
31
31
  laws your system carries for what you use
32
32
 
@@ -54,7 +54,7 @@ Options:
54
54
  --force clean: apply the changes (without it, dry run)
55
55
  --strict doctor: exit 1 when drift is found in THIS repo (for CI)
56
56
  --strict-system doctor: also exit 1 when the system itself is inconsistent
57
- --all doctor: list every finding, not just the loudest files
57
+ --verbose doctor: every finding, file by file (default is a summary)
58
58
  --laws doctor: show every usage law, not just the busiest components
59
59
  --out <path> output path for the generated template (default: <pagesDir>/<file>)
60
60
  -h, --help this help
@@ -129,7 +129,9 @@ async function main() {
129
129
  scopes: args,
130
130
  strict: flags.strict === true || flags["strict-system"] === true,
131
131
  strictSystem: flags["strict-system"] === true,
132
- all: flags.all === true,
132
+ // --all is the old spelling; it keeps working silently so a script
133
+ // written last week does not break.
134
+ verbose: flags.verbose === true || flags.all === true,
133
135
  laws: flags.laws === true,
134
136
  });
135
137
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.11.3",
3
+ "version": "0.12.0",
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": {