synthesisui 0.16.51 → 0.16.53

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.
@@ -4,6 +4,7 @@ import { readToken, resolveRegistry } from "../config.js";
4
4
  import { isNearDuplicate } from "../doctor/color-distance.js";
5
5
  import { emptyTally, internalSpecifiers, scanComponentsInto, tallyToInventory, } from "../doctor/components-scan.js";
6
6
  import { crosswalk, observedRules } from "../doctor/crosswalk.js";
7
+ import { reconcile, scanDefinitions, } from "../doctor/definitions-scan.js";
7
8
  import { diagnose, scanSource } from "../doctor/scan.js";
8
9
  import { parseSchemeBlocks } from "../doctor/scheme-blocks.js";
9
10
  import { buildTable } from "../doctor/tokens.js";
@@ -271,6 +272,7 @@ export async function takeCensus(root) {
271
272
  const reports = [];
272
273
  const tally = emptyTally();
273
274
  const internal = await internalSpecifiers(root);
275
+ const defined = [];
274
276
  for await (const file of walk(root)) {
275
277
  const src = await readFile(file, "utf8").catch(() => "");
276
278
  if (!src)
@@ -282,14 +284,44 @@ export async function takeCensus(root) {
282
284
  // they were set aside.
283
285
  if (!/(\.(spec|test|stories)\.[a-z]+$|__tests__\/|(^|\/)\.storybook\/)/.test(rel)) {
284
286
  scanComponentsInto(tally, rel, src, internal);
287
+ // The other half: what this file EXPORTS, with the axes its types
288
+ // declare. A library composes almost nothing and exports everything.
289
+ defined.push(...scanDefinitions(rel, src));
285
290
  }
286
291
  }
287
292
  const d = diagnose(reports);
288
293
  const inventory = tallyToInventory(tally);
294
+ /**
295
+ * THE TWO READINGS BECOME ONE LIST before anything is judged.
296
+ *
297
+ * The crosswalk saw only what a project COMPOSES, so a library's report said
298
+ * "only yours (4)" three lines above "37 components exported here" - the two
299
+ * sections talking past each other (dono, 31/07). Worse, the contracts that
300
+ * travel are picked from that list, so a library would have sent four.
301
+ *
302
+ * A component that is defined and never composed here is still a component:
303
+ * in a library that is the entire point. Its axes come from the TYPE, which
304
+ * is the better source anyway - usage shows what somebody picked, the
305
+ * declaration shows what the author designed.
306
+ */
307
+ const composed = new Set(inventory.map((c) => c.name));
308
+ const fromTypes = defined
309
+ .filter((d) => !composed.has(d.name))
310
+ .map((d) => ({
311
+ name: d.name,
312
+ count: 0,
313
+ files: 0,
314
+ props: {
315
+ ...Object.fromEntries(Object.entries(d.axes)),
316
+ ...Object.fromEntries(d.flags.map((f) => [f, ["true"]])),
317
+ },
318
+ propFiles: {},
319
+ }));
320
+ const merged = [...inventory, ...fromTypes];
289
321
  // The verdict travels WITH the payload: the platform reads one reading rather
290
322
  // than computing a second opinion from the same numbers, which is how two
291
323
  // implementations of the same judgement start disagreeing.
292
- const verdicts = new Map(crosswalk(inventory).map((r) => [
324
+ const verdicts = new Map(crosswalk(merged).map((r) => [
293
325
  r.component.name,
294
326
  { bucket: r.bucket, canonical: r.canonical, because: r.because },
295
327
  ]));
@@ -297,7 +329,7 @@ export async function takeCensus(root) {
297
329
  for (const r of observedRules(inventory)) {
298
330
  laws.set(r.component, [...(laws.get(r.component) ?? []), r.text]);
299
331
  }
300
- const components = inventory.map((c) => ({
332
+ const components = merged.map((c) => ({
301
333
  ...c,
302
334
  ...(verdicts.get(c.name) ?? {}),
303
335
  ...(laws.has(c.name) ? { laws: laws.get(c.name) } : {}),
@@ -321,6 +353,7 @@ export async function takeCensus(root) {
321
353
  : {}),
322
354
  observed: distinctValues(d),
323
355
  ...(components.length > 0 ? { components } : {}),
356
+ ...(defined.length > 0 ? { defined } : {}),
324
357
  totals: {
325
358
  scanned: d.scanned,
326
359
  values: d.findings.length,
@@ -408,6 +441,54 @@ function printComponents(c) {
408
441
  }
409
442
  printCrosswalk(mine);
410
443
  printObserved(mine);
444
+ printDefined(c, mine);
445
+ }
446
+ /**
447
+ * What the project DEFINES, and what crossing that with usage reveals.
448
+ *
449
+ * A library exports components rather than composing them, so a usage-only
450
+ * reading of `packages/ui` found thirteen of its thirty-one (dono, 31/07).
451
+ * This is the other half, and the crossing is the part no toolchain offers.
452
+ */
453
+ function printDefined(census, used) {
454
+ const defined = census.defined ?? [];
455
+ if (defined.length === 0)
456
+ return;
457
+ const withAxes = defined.filter((d) => Object.keys(d.axes).length > 0);
458
+ console.log("");
459
+ console.log(section("What this project defines"));
460
+ console.log(body(`${defined.length} components exported here, ${withAxes.length} with axes their types declare`));
461
+ console.log("");
462
+ for (const d of withAxes.slice(0, 8)) {
463
+ const axes = Object.entries(d.axes)
464
+ .map(([a, o]) => `${a}(${o.join("|")})`)
465
+ .join(" ");
466
+ console.log(body(` ${paint.strong(d.name.padEnd(20))} ${paint.dim(axes)}`));
467
+ }
468
+ if (withAxes.length > 8) {
469
+ console.log(body(paint.faint(` (${withAxes.length - 8} more)`)));
470
+ }
471
+ const crossed = reconcile(defined, used);
472
+ const dead = crossed.filter((r) => r.deadOptions.length > 0);
473
+ const stray = crossed.filter((r) => r.undeclared.length > 0);
474
+ const orphans = crossed.filter((r) => r.orphan);
475
+ if (dead.length === 0 && stray.length === 0 && orphans.length === 0)
476
+ return;
477
+ console.log("");
478
+ console.log(section("What the two readings disagree about"));
479
+ for (const r of stray.slice(0, 4)) {
480
+ for (const u of r.undeclared) {
481
+ console.log(body(` ${paint.strong(r.name)} is passed ${u.axis}="${u.values.join('" | "')}" ${paint.faint("- its own type does not offer that")}`));
482
+ }
483
+ }
484
+ for (const r of dead.slice(0, 4)) {
485
+ for (const d of r.deadOptions) {
486
+ console.log(body(` ${paint.strong(r.name)} declares ${d.axis}="${d.options.join('" | "')}" ${paint.faint("- and nothing ever passes it")}`));
487
+ }
488
+ }
489
+ if (orphans.length > 0) {
490
+ console.log(body(paint.dim(` ${orphans.length} defined here and composed nowhere in this folder - expected in a library, worth a look in an app.`)));
491
+ }
411
492
  }
412
493
  /**
413
494
  * Laws their code already obeys, printed with the evidence that found them.
@@ -455,7 +536,7 @@ function printCrosswalk(mine) {
455
536
  return;
456
537
  console.log(body(paint.strong(title)));
457
538
  for (const r of list.slice(0, limit)) {
458
- console.log(body(` ${r.component.name.padEnd(18)} ${paint.faint(`${r.component.files} files`)} ${paint.dim(r.because)}`));
539
+ console.log(body(` ${r.component.name.padEnd(18)} ${paint.faint(r.component.files === 0 ? "declared" : `${r.component.files} files`)} ${paint.dim(r.because)}`));
459
540
  }
460
541
  if (list.length > limit) {
461
542
  console.log(body(paint.faint(` (${list.length - limit} more)`)));
@@ -0,0 +1,116 @@
1
+ /**
2
+ * WHAT A PROJECT DEFINES, as opposed to what it composes.
3
+ *
4
+ * The inventory reads usage, and that is blind in exactly the place a component
5
+ * LIBRARY lives. Measured on a real one (dono, 31/07): `packages/ui` holds 31
6
+ * component directories and composes almost none of them internally - a library
7
+ * exports components, it does not use them - so a usage reader found thirteen
8
+ * and the other eighteen were invisible.
9
+ *
10
+ * And the definition is the better source anyway. A declared union IS the closed
11
+ * set, verbatim:
12
+ *
13
+ * type ButtonProps = { variant?: "neutral" | "ocean" }
14
+ *
15
+ * Usage can only ever show the options somebody happened to pick; the type shows
16
+ * the ones the author designed, including the one nobody has chosen yet - which
17
+ * is a finding of its own once the two readings are crossed.
18
+ *
19
+ * A SCANNER, NOT A PARSER, like everything else here. It reads the shapes people
20
+ * actually write and stays silent on the rest: a props type built by `Omit<>` or
21
+ * spread from another interface yields no axes rather than wrong ones.
22
+ */
23
+ /** `export function X`, `export default function X`, `export const X =`. */
24
+ const EXPORTED = /export\s+(?:default\s+)?(?:async\s+)?(?:function\s+([A-Z][A-Za-z0-9_]*)|const\s+([A-Z][A-Za-z0-9_]*)\s*[:=])/g;
25
+ /** `type ButtonProps = { … }` or `interface ButtonProps { … }`, to its closing
26
+ * brace, wherever it sits.
27
+ *
28
+ * Two stricter versions failed a spec each: requiring the brace at column zero
29
+ * missed an indented declaration, and requiring it on its own line missed a
30
+ * one-liner. Ending at the first `}` can cut a nested object short, and that is
31
+ * the safe direction - the field reader below simply finds no literal union in
32
+ * the fragment and stays quiet. */
33
+ const PROPS_BLOCK = /(?:type|interface)\s+([A-Z][A-Za-z0-9_]*?)Props\b[^{]*\{([\s\S]*?)\}/g;
34
+ /** One field of a props type: `variant?: "a" | "b"`. Fields are separated by a
35
+ * newline OR a semicolon, and a one-line type uses only the second. */
36
+ const FIELD = /(?:^|[;{])\s*(\w+)\??\s*:\s*([^;\n}]+)/gm;
37
+ /** A union made only of string literals - the shape of a designed axis. */
38
+ const LITERAL_UNION = /^\s*(["'][^"']+["']\s*\|\s*)+["'][^"']+["']\s*$/;
39
+ export function scanDefinitions(file, source) {
40
+ if (!/\.(tsx|jsx|vue|svelte)$/i.test(file))
41
+ return [];
42
+ // Props types first, keyed by the component name they belong to: `ButtonProps`
43
+ // is Button's. Matching by name prefix is the convention essentially every
44
+ // codebase follows, and guessing past it would attach axes to the wrong thing.
45
+ const propsOf = new Map();
46
+ PROPS_BLOCK.lastIndex = 0;
47
+ for (const block of source.matchAll(PROPS_BLOCK)) {
48
+ const owner = block[1];
49
+ const body = block[2];
50
+ const axes = {};
51
+ const flags = [];
52
+ FIELD.lastIndex = 0;
53
+ for (const f of body.matchAll(FIELD)) {
54
+ const prop = f[1];
55
+ const type = f[2].trim().replace(/,$/, "");
56
+ if (type === "boolean") {
57
+ flags.push(prop);
58
+ continue;
59
+ }
60
+ if (!LITERAL_UNION.test(type))
61
+ continue;
62
+ const options = [...type.matchAll(/["']([^"']+)["']/g)].map((m) => m[1]);
63
+ // One option is not a closed set - the same rule the contract writer and
64
+ // the doctor already follow.
65
+ if (options.length >= 2)
66
+ axes[prop] = options;
67
+ }
68
+ if (Object.keys(axes).length > 0 || flags.length > 0) {
69
+ propsOf.set(owner, { axes, flags });
70
+ }
71
+ }
72
+ const out = [];
73
+ const seen = new Set();
74
+ EXPORTED.lastIndex = 0;
75
+ for (const m of source.matchAll(EXPORTED)) {
76
+ const name = m[1] ?? m[2];
77
+ if (!name || seen.has(name))
78
+ continue;
79
+ seen.add(name);
80
+ const props = propsOf.get(name);
81
+ out.push({
82
+ name,
83
+ file,
84
+ axes: props?.axes ?? {},
85
+ flags: props?.flags ?? [],
86
+ });
87
+ }
88
+ return out;
89
+ }
90
+ export function reconcile(defined, used) {
91
+ const usage = new Map(used.filter((u) => !u.from).map((u) => [u.name, u]));
92
+ const out = [];
93
+ for (const d of defined) {
94
+ const u = usage.get(d.name);
95
+ const deadOptions = [];
96
+ const undeclared = [];
97
+ for (const [axis, options] of Object.entries(d.axes)) {
98
+ const passed = new Set((u?.props[axis] ?? []).map((v) => v.trim().toLowerCase()));
99
+ const dead = options.filter((o) => !passed.has(o.toLowerCase()));
100
+ // Every option unused means the axis is untouched, not that each option
101
+ // is dead - reporting the whole set as dead reads as an accusation about
102
+ // options rather than about an axis nobody reaches for.
103
+ if (dead.length > 0 && dead.length < options.length) {
104
+ deadOptions.push({ axis, options: dead });
105
+ }
106
+ const declared = new Set(options.map((o) => o.toLowerCase()));
107
+ const stray = (u?.props[axis] ?? []).filter((v) => v !== "true" && !declared.has(v.trim().toLowerCase()));
108
+ if (stray.length > 0)
109
+ undeclared.push({ axis, values: stray });
110
+ }
111
+ if (deadOptions.length > 0 || undeclared.length > 0 || !u) {
112
+ out.push({ name: d.name, deadOptions, undeclared, orphan: !u });
113
+ }
114
+ }
115
+ return out;
116
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.51",
3
+ "version": "0.16.53",
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": {