synthesisui 0.6.0 → 0.7.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/README.md CHANGED
@@ -99,8 +99,28 @@ worse than none.
99
99
  With no system installed it still finds every hand-written value and counts
100
100
  the distinct ones. Runs offline, needs no account, writes nothing.
101
101
 
102
- `--strict` exits 1 when drift is found, for CI. `--all` lists everything
103
- instead of the loudest files.
102
+ ### What your system says
103
+
104
+ ```
105
+ ── What your system says about what you use ──────────────────────
106
+
107
+ ds-badge · 3 places
108
+ Badges whisper status in periwinkle; danger appears only for money at risk.
109
+
110
+ ds-button · 3 places
111
+ One indigo action per view; gold is reserved for moments of ceremony, never buttons.
112
+ Buttons speak quietly - sentence case, no exclamation.
113
+ ```
114
+
115
+ The usage laws your team wrote, for the components this project actually uses,
116
+ ordered by how much you use them. They are prose, so nothing verifies them -
117
+ the value is putting them in front of whoever is touching the component. No
118
+ other tool is positioned to do it, because no other tool knows these laws
119
+ exist.
120
+
121
+ `--strict` exits 1 when drift is found, for CI. `--all` lists every finding
122
+ instead of the loudest files. `--laws` shows every law instead of the busiest
123
+ components.
104
124
 
105
125
  ## What `add` materializes
106
126
 
@@ -1,6 +1,6 @@
1
1
  import { readdir, readFile } from "node:fs/promises";
2
2
  import { join, relative, resolve } from "node:path";
3
- import { bindingsFromDocument, findOverrides, } from "../doctor/overrides.js";
3
+ import { bindingsFromDocument, countComponents, findOverrides, } from "../doctor/overrides.js";
4
4
  import { diagnose, scanSource, } from "../doctor/scan.js";
5
5
  import { buildTable, EMPTY_TABLE } from "../doctor/tokens.js";
6
6
  import { body, section, snippet } from "../output.js";
@@ -132,7 +132,7 @@ function meter(pct, width = 24) {
132
132
  const filled = Math.round((pct / 100) * width);
133
133
  return `${"█".repeat(filled)}${"░".repeat(width - filled)}`;
134
134
  }
135
- function verdict(d, hasSystem) {
135
+ function verdict(d, hasSystem, overruled) {
136
136
  // Nothing found and nothing installed: a utility package, a config folder,
137
137
  // the wrong directory. Selling a design system here would be noise.
138
138
  if (!hasSystem && d.findings.length === 0) {
@@ -152,12 +152,26 @@ function verdict(d, hasSystem) {
152
152
  body("Browse systems at https://www.synthesisui.com/gallery"),
153
153
  ];
154
154
  }
155
- if (d.findings.length === 0) {
155
+ if (d.findings.length === 0 && overruled === 0) {
156
156
  return [
157
157
  body("No drift. Every design value in this project comes from the"),
158
158
  body("system. That is a rarer sentence than it sounds."),
159
159
  ];
160
160
  }
161
+ // A report that accuses a component of being overruled and then closes with
162
+ // "no drift" is a report nobody believes twice.
163
+ if (d.findings.length === 0) {
164
+ return [
165
+ body("No loose values - everything is a token."),
166
+ body(overruled === 1
167
+ ? "But one place takes a component the system defines and"
168
+ : `But ${overruled} places take a component the system defines and`),
169
+ body(overruled === 1
170
+ ? "overrules it locally. Either the recipe should change, or"
171
+ : "overrule it locally. Either the recipe should change, or"),
172
+ body("that override should not be there."),
173
+ ];
174
+ }
161
175
  const lines = [
162
176
  body(`${d.named} of ${d.findings.length} already have a name in your system.`),
163
177
  body("Those are the cheap ones: swap the literal for the token."),
@@ -173,6 +187,8 @@ export async function doctor(opts) {
173
187
  const hasSystem = table.byName.size > 0;
174
188
  const reports = [];
175
189
  const overrides = [];
190
+ const used = new Map();
191
+ const known = new Set(recipes.keys());
176
192
  for await (const file of walk(root)) {
177
193
  const src = await readFile(file, "utf8").catch(() => "");
178
194
  if (!src)
@@ -182,6 +198,9 @@ export async function doctor(opts) {
182
198
  if (recipes.size > 0) {
183
199
  for (const o of findOverrides(src, recipes))
184
200
  overrides.push({ ...o, file: rel });
201
+ for (const [name, n] of countComponents(src, known)) {
202
+ used.set(name, (used.get(name) ?? 0) + n);
203
+ }
185
204
  }
186
205
  }
187
206
  if (reports.length === 0) {
@@ -193,7 +212,10 @@ export async function doctor(opts) {
193
212
  console.log(body(hasSystem
194
213
  ? `${table.name ?? table.slug} v${table.version ?? "?"} - ${table.byName.size} tokens, ${d.scanned} files read`
195
214
  : `No system installed - ${d.scanned} files read`));
196
- if (hasSystem) {
215
+ // 0 of 0 is not a perfect score, it is an empty measurement - printing a
216
+ // full bar there would be the report's first lie.
217
+ const measurable = d.tokenUses + d.findings.length > 0;
218
+ if (hasSystem && measurable) {
197
219
  console.log("");
198
220
  console.log(body(`Token coverage ${meter(d.coverage)} ${String(d.coverage).padStart(3)}%`));
199
221
  console.log(body(` ${d.tokenUses} from the system, ${d.findings.length} by hand`));
@@ -271,8 +293,34 @@ export async function doctor(opts) {
271
293
  console.log(body(`+${byFile.size - shown.length} more files. Run with --all.`));
272
294
  }
273
295
  }
296
+ // The system's own words, for the components this project actually uses.
297
+ // Prose, so nothing verifies it - the value is putting it in front of
298
+ // whoever is touching the component, which no other tool is positioned to
299
+ // do because no other tool knows these laws exist.
300
+ const inUse = [...used.entries()]
301
+ .filter(([name]) => (recipes.get(name)?.usage.length ?? 0) > 0)
302
+ .sort((a, b) => b[1] - a[1]);
303
+ if (inUse.length > 0) {
304
+ console.log(section("What your system says about what you use"));
305
+ const shownComps = opts.laws || opts.all ? inUse : inUse.slice(0, 4);
306
+ for (const [name, count] of shownComps) {
307
+ const laws = recipes.get(name)?.usage ?? [];
308
+ const shown = opts.laws || opts.all ? laws : laws.slice(0, 2);
309
+ console.log(body(`ds-${name} · ${count} place${count === 1 ? "" : "s"}`));
310
+ for (const law of shown)
311
+ console.log(` ${law}`);
312
+ if (laws.length > shown.length) {
313
+ console.log(` +${laws.length - shown.length} more`);
314
+ }
315
+ console.log("");
316
+ }
317
+ if (inUse.length > shownComps.length) {
318
+ console.log(body(`+${inUse.length - shownComps.length} more components carry laws. Run with --laws.`));
319
+ console.log("");
320
+ }
321
+ }
274
322
  console.log(section("What this means"));
275
- for (const line of verdict(d, hasSystem))
323
+ for (const line of verdict(d, hasSystem, overrides.length))
276
324
  console.log(line);
277
325
  console.log("");
278
326
  if (opts.strict && (d.findings.length > 0 || overrides.length > 0))
@@ -46,7 +46,24 @@ export function bindingsOf(recipe) {
46
46
  collect(r.variants, false);
47
47
  collect(r.states, false);
48
48
  collect(r.parts, false);
49
- return { props, baseValues };
49
+ const usage = Array.isArray(r.usage)
50
+ ? r.usage.filter((x) => typeof x === "string")
51
+ : [];
52
+ return { props, baseValues, usage };
53
+ }
54
+ /**
55
+ * How many places each known component is used. An inventory nobody has today:
56
+ * a design system's own report can say "ds-button, 14 places" because it knows
57
+ * which names are components rather than guessing at class strings.
58
+ */
59
+ export function countComponents(source, known) {
60
+ const out = new Map();
61
+ for (const m of source.matchAll(DS_IN_CLASS)) {
62
+ if (!known.has(m[1]))
63
+ continue;
64
+ out.set(m[1], (out.get(m[1]) ?? 0) + 1);
65
+ }
66
+ return out;
50
67
  }
51
68
  /**
52
69
  * Tailwind utilities that plainly set a CSS property. Deliberately short:
package/dist/index.js CHANGED
@@ -27,7 +27,8 @@ Usage - deterministic, FREE:
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
29
  synthesisui doctor [--strict] [--all] audit this repo for DRIFT: every design value written by
30
- hand, and the token your system already has for it
30
+ hand, the token your system already has for it, and the
31
+ laws your system carries for what you use
31
32
 
32
33
  Usage - AI, USES CREDITS (login required):
33
34
  synthesisui generate "<desc>" AI-create a NEW component your DS doesn't have (token-only recipe)
@@ -53,6 +54,7 @@ Options:
53
54
  --force clean: apply the changes (without it, dry run)
54
55
  --strict doctor: exit 1 when drift is found (for CI)
55
56
  --all doctor: list every finding, not just the loudest files
57
+ --laws doctor: show every usage law, not just the busiest components
56
58
  --out <path> output path for the generated template (default: <pagesDir>/<file>)
57
59
  -h, --help this help
58
60
 
@@ -122,6 +124,7 @@ async function main() {
122
124
  dir,
123
125
  strict: flags.strict === true,
124
126
  all: flags.all === true,
127
+ laws: flags.laws === true,
125
128
  });
126
129
  break;
127
130
  case "list":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.6.0",
3
+ "version": "0.7.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": {