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.
- package/dist/commands/doctor.js +172 -68
- package/dist/index.js +5 -3
- package/package.json +1 -1
package/dist/commands/doctor.js
CHANGED
|
@@ -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
|
-
|
|
274
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
296
|
-
|
|
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
|
-
|
|
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 =
|
|
313
|
-
|
|
326
|
+
const shownFiles = verbose ? files : files.slice(0, 8);
|
|
327
|
+
say("");
|
|
314
328
|
for (const f of shownFiles) {
|
|
315
|
-
|
|
316
|
-
const shown =
|
|
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
|
-
|
|
340
|
+
say(` ${String(x.line).padStart(4)} ${x.literal} ${named}`);
|
|
327
341
|
}
|
|
328
342
|
if (f.findings.length > shown.length) {
|
|
329
|
-
|
|
343
|
+
say(` +${f.findings.length - shown.length} more`);
|
|
330
344
|
}
|
|
331
|
-
|
|
345
|
+
say("");
|
|
332
346
|
}
|
|
333
347
|
if (files.length > shownFiles.length) {
|
|
334
|
-
|
|
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
|
-
|
|
345
|
-
|
|
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
|
-
|
|
362
|
+
say("");
|
|
349
363
|
for (const c of conflictsInUse) {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
364
|
+
say(body(`ds-${c.component}`));
|
|
365
|
+
say(` "${c.law}"`);
|
|
366
|
+
say(` but ${c.where} binds ${c.value}`);
|
|
367
|
+
say("");
|
|
354
368
|
}
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
366
|
-
|
|
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
|
-
|
|
383
|
+
say("");
|
|
370
384
|
for (const f of frozen) {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
441
|
-
|
|
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
|
-
|
|
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 =
|
|
472
|
+
const shown = verbose ? [...byFile] : [...byFile].slice(0, 6);
|
|
455
473
|
for (const [file, list] of shown) {
|
|
456
|
-
|
|
457
|
-
for (const o of
|
|
458
|
-
|
|
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
|
-
|
|
481
|
+
say(` the recipe binds ${o.recipe}`);
|
|
464
482
|
else if (o.where)
|
|
465
|
-
|
|
483
|
+
say(` the recipe binds it ${o.where}`);
|
|
466
484
|
if (lawKept(o) === null && !onSystem(o)) {
|
|
467
|
-
|
|
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
|
-
|
|
494
|
+
say(" ✓ the recipe is frozen here; yours names a role");
|
|
477
495
|
}
|
|
478
496
|
const kept = lawKept(o);
|
|
479
497
|
if (kept) {
|
|
480
|
-
|
|
481
|
-
|
|
498
|
+
say(` ✓ but the law says: "${kept}"`);
|
|
499
|
+
say(" this override keeps it - the recipe does not");
|
|
482
500
|
}
|
|
483
501
|
}
|
|
484
|
-
if (!
|
|
485
|
-
|
|
502
|
+
if (!verbose && list.length > 3) {
|
|
503
|
+
say(` +${list.length - 3} more`);
|
|
486
504
|
}
|
|
487
|
-
|
|
505
|
+
say("");
|
|
488
506
|
}
|
|
489
507
|
if (byFile.size > shown.length) {
|
|
490
|
-
|
|
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
|
-
|
|
502
|
-
const shownComps = opts.laws ||
|
|
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 ||
|
|
506
|
-
|
|
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
|
-
|
|
526
|
+
say(` ${law}`);
|
|
509
527
|
if (laws.length > shown.length) {
|
|
510
|
-
|
|
528
|
+
say(` +${laws.length - shown.length} more`);
|
|
511
529
|
}
|
|
512
|
-
|
|
530
|
+
say("");
|
|
513
531
|
}
|
|
514
532
|
if (inUse.length > shownComps.length) {
|
|
515
|
-
|
|
516
|
-
|
|
533
|
+
say(body(`+${inUse.length - shownComps.length} more components carry laws. Run with --laws.`));
|
|
534
|
+
say("");
|
|
517
535
|
}
|
|
518
536
|
}
|
|
519
|
-
|
|
537
|
+
say(section("What this means"));
|
|
520
538
|
for (const line of verdict(d, hasSystem, overrides.length, conflictsInUse.length))
|
|
521
|
-
|
|
522
|
-
|
|
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…] [--
|
|
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
|
-
--
|
|
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
|
-
|
|
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