synthesisui 0.16.33 → 0.16.34

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.
@@ -8,7 +8,7 @@ import { checkableName, readRequests, verifyAndCloseRequests, } from "../doctor/
8
8
  import { diagnose, scanSource, siblingTokens, } from "../doctor/scan.js";
9
9
  import { findSelfConflicts, forbiddenProps, isReset, propMatchesLabel, } from "../doctor/self-conflict.js";
10
10
  import { buildTable, EMPTY_TABLE, nearestToken, } from "../doctor/tokens.js";
11
- import { body, section, snippet } from "../output.js";
11
+ import { body, paint, section, snippet } from "../output.js";
12
12
  /**
13
13
  * `synthesisui doctor` - the check nobody else ships.
14
14
  *
@@ -43,7 +43,11 @@ const SKIP = new Set([
43
43
  // Our own installed artifacts are the answer, not the problem.
44
44
  "_synthesisui",
45
45
  ]);
46
- export async function* walk(dir) {
46
+ export async function* walk(dir,
47
+ /** Filled with nested projects that were SKIPPED - each speaks its own
48
+ * system and must not be billed against this one (30/07: the monorepo
49
+ * run charged aluna-landing's tokens against the root's harvest). */
50
+ skippedProjects) {
47
51
  // `readdir`'s overloads infer a Buffer-named Dirent without an explicit
48
52
  // encoding; naming it keeps `e.name` a string.
49
53
  const entries = await readdir(dir, {
@@ -57,7 +61,14 @@ export async function* walk(dir) {
57
61
  if (e.isDirectory()) {
58
62
  if (SKIP.has(e.name))
59
63
  continue;
60
- yield* walk(full);
64
+ // A CHILD directory carrying its own _synthesisui is a separately
65
+ // governed project - its record, its doctor, its numbers.
66
+ const governed = await readdir(join(full, "_synthesisui")).then(() => true, () => false);
67
+ if (governed) {
68
+ skippedProjects?.push(full);
69
+ continue;
70
+ }
71
+ yield* walk(full, skippedProjects);
61
72
  }
62
73
  else if (EXTS.some((x) => e.name.endsWith(x))) {
63
74
  yield full;
@@ -199,7 +210,7 @@ const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
199
210
  /** A bar you can read at a glance, in a font that is always monospace. */
200
211
  function meter(pct, width = 24) {
201
212
  const filled = Math.round((pct / 100) * width);
202
- return `${"█".repeat(filled)}${"░".repeat(width - filled)}`;
213
+ return `${paint.blue("█".repeat(filled))}${paint.faint("░".repeat(width - filled))}`;
203
214
  }
204
215
  function verdict(d, hasSystem, overruled, conflicts) {
205
216
  // A system at war with itself outranks every other number here. The closing
@@ -368,7 +379,10 @@ export async function doctor(opts) {
368
379
  * Nobody debugs from 0%. They conclude the product does not work.
369
380
  */
370
381
  const wiring = await readWiring(root, table.slug);
371
- for await (const file of scopes.length > 0 ? walkAll(scopes) : walk(root)) {
382
+ const skippedProjects = [];
383
+ for await (const file of scopes.length > 0
384
+ ? walkAll(scopes)
385
+ : walk(root, skippedProjects)) {
372
386
  const src = await readFile(file, "utf8").catch(() => "");
373
387
  if (!src)
374
388
  continue;
@@ -442,7 +456,11 @@ export async function doctor(opts) {
442
456
  // `var(--ds-color-semantic-knob, #ffffff)` - is set aside for the opposite
443
457
  // reason: it is already tokenized. The summary said the false half out
444
458
  // loud and hid the true half behind a flag.
445
- console.log(body(`set aside ${plural(asideTotal, "value")} that are not drift (--verbose for why)`));
459
+ console.log(body(`set aside ${plural(asideTotal, "value")} that are not drift` +
460
+ (skippedProjects.length > 0
461
+ ? ` and ${plural(skippedProjects.length, "nested project")} that speak their own systems`
462
+ : "") +
463
+ " (--verbose for why)"));
446
464
  }
447
465
  // 0 of 0 is not a perfect score, it is an empty measurement - printing a
448
466
  // full bar there would be the report's first lie.
@@ -522,8 +540,19 @@ export async function doctor(opts) {
522
540
  }
523
541
  if (hasSystem && measurable) {
524
542
  console.log("");
525
- console.log(body(`Token coverage ${meter(d.coverage)} ${String(d.coverage).padStart(3)}%`));
526
- console.log(body(` ${d.tokenUses} from the system, ${d.findings.length} by hand${d.phantomUses > 0 ? `, ${d.phantomUses} naming nothing` : ""}`));
543
+ console.log(body(`Token coverage ${meter(d.coverage)} ${paint.strong(`${String(d.coverage).padStart(3)}%`)}`));
544
+ console.log(body(paint.dim(` ${d.tokenUses} from the system, ${d.findings.length} by hand${d.phantomUses > 0 ? `, ${d.phantomUses} naming nothing` : ""}`)));
545
+ // The strongest number leads, not trails: it used to sit two screens
546
+ // down, after the phantom list (30/07). For the reader who already owns
547
+ // a system - the ICP - THIS line is the report.
548
+ if (table.source === "yours" && d.findings.length > 0) {
549
+ const named = d.findings.filter((f) => f.token).length;
550
+ if (named > 0) {
551
+ console.log("");
552
+ console.log(body(`${paint.strong(String(named))} of the ${d.findings.length} hand-written values already have a name in YOUR system.`));
553
+ console.log(body(paint.dim("Your agent has no way to know: the tokens exist, the contract does not.")));
554
+ }
555
+ }
527
556
  }
528
557
  /**
529
558
  * THE RECORD - the layer that remembers, printed where the moment is shown.
@@ -635,13 +664,21 @@ export async function doctor(opts) {
635
664
  console.log(body(`${plural(phantoms.size, "name")} written as tokens that your system never declares.`));
636
665
  console.log(body("An undeclared custom property applies nothing at all."));
637
666
  console.log("");
638
- for (const [name, at] of [...phantoms].sort()) {
667
+ // Capped like every other section: 99 lines is a report nobody reads,
668
+ // and it buried the strongest number two screens down (30/07). The full
669
+ // list keeps living behind --verbose.
670
+ const PHANTOM_CAP = 8;
671
+ const sorted = [...phantoms].sort();
672
+ for (const [name, at] of sorted.slice(0, PHANTOM_CAP)) {
639
673
  const where = [...at.files][0];
640
- console.log(body(`${name} ${where}:${at.line}${at.files.size > 1 ? ` +${at.files.size - 1} more` : ""}`));
674
+ console.log(body(`${name} ${paint.dim(`${where}:${at.line}${at.files.size > 1 ? ` +${at.files.size - 1} more` : ""}`)}`));
641
675
  const kin = siblingTokens(name, table);
642
676
  if (kin.length > 0)
643
677
  console.log(snippet([`your system has ${kin.join(", ")}`]));
644
678
  }
679
+ if (sorted.length > PHANTOM_CAP) {
680
+ console.log(body(paint.dim(`and ${sorted.length - PHANTOM_CAP} more · synthesisui doctor --verbose`)));
681
+ }
645
682
  console.log("");
646
683
  console.log(body("Add the name to the system, or use one it has. Do not leave it."));
647
684
  }
@@ -1009,11 +1046,16 @@ export async function doctor(opts) {
1009
1046
  }
1010
1047
  if (d.findings.length > 0 || overrides.length > 0) {
1011
1048
  console.log("");
1012
- console.log(body(`${plural(d.findings.length, "value")} by hand · ${d.named} already ${d.named === 1 ? "has" : "have"} a name`));
1013
- console.log(body(`${plural(overrides.length - lawKeepingCount, "override")} · ${offSystemCount} left the system` +
1014
- (lawKeepingCount > 0
1015
- ? ` · ${lawKeepingCount} more kept a law`
1016
- : "")));
1049
+ if (table.source !== "yours") {
1050
+ console.log(body(`${plural(d.findings.length, "value")} by hand · ${d.named} already ${d.named === 1 ? "has" : "have"} a name`));
1051
+ }
1052
+ // An all-zero status line is filler, not information.
1053
+ if (overrides.length > 0 || offSystemCount > 0) {
1054
+ console.log(body(`${plural(overrides.length - lawKeepingCount, "override")} · ${offSystemCount} left the system` +
1055
+ (lawKeepingCount > 0
1056
+ ? ` · ${lawKeepingCount} more kept a law`
1057
+ : "")));
1058
+ }
1017
1059
  }
1018
1060
  if (plan.length > 0) {
1019
1061
  console.log("");
@@ -1021,7 +1063,7 @@ export async function doctor(opts) {
1021
1063
  const w = Math.min(52, Math.max(...plan.slice(0, 5).map((j) => j.what.length)));
1022
1064
  plan.slice(0, 5).forEach((j, i) => {
1023
1065
  const what = j.what.length > w ? `${j.what.slice(0, w - 1)}…` : j.what.padEnd(w);
1024
- console.log(` ${i + 1}. ${what} ${j.size}${j.cheap ? " (cheap)" : ""}`);
1066
+ console.log(` ${i + 1}. ${what} ${paint.dim(`${j.size}${j.cheap ? " (cheap)" : ""}`)}`);
1025
1067
  });
1026
1068
  }
1027
1069
  console.log("");
@@ -1040,15 +1082,6 @@ export async function doctor(opts) {
1040
1082
  * thread, which is the other job this paragraph does.
1041
1083
  */
1042
1084
  if (table.source === "yours") {
1043
- const named = d.findings.filter((f) => f.token).length;
1044
- const anonymous = d.findings.length - named;
1045
- console.log("");
1046
- console.log(body(named > 0
1047
- ? `${named} of these already have a name in your own system.`
1048
- : "None of these have a name in your system yet."));
1049
- if (anonymous > 0) {
1050
- console.log(body(`The other ${anonymous} do not - and your coding agent has no way to know they should.`));
1051
- }
1052
1085
  console.log("");
1053
1086
  // Careful about what is being offered. This reader ALREADY has a design
1054
1087
  // system, so "install ours" would mean replacing theirs, and a CTA that
@@ -1058,9 +1091,9 @@ export async function doctor(opts) {
1058
1091
  console.log(body("Your tokens exist. What your agent is missing is a contract:"));
1059
1092
  console.log(body("rules it reads BEFORE writing UI, and a manifest of what exists."));
1060
1093
  console.log("");
1061
- console.log(snippet(["npx synthesisui@latest init --ds <slug>"]));
1062
- console.log(body("starts you on a system that ships one - browse them at"));
1063
- console.log(body("https://www.synthesisui.com/gallery"));
1094
+ console.log(paint.blue(snippet(["npx synthesisui@latest init --ds <slug>"])));
1095
+ console.log(body(paint.dim("starts you on a system that ships one - browse them at")));
1096
+ console.log(body(paint.blue("https://www.synthesisui.com/gallery")));
1064
1097
  }
1065
1098
  console.log("");
1066
1099
  }
@@ -231,7 +231,35 @@ function fallbackSpans(line, table) {
231
231
  const DECLARES_TOKEN = /^\s*(--[a-z0-9_-]+)\s*:/i;
232
232
  /** Under this, a radius or spacing value is idiom rather than a decision. */
233
233
  const IDIOM = new Set(["0", "0px", "1px", "9999px", "100%", "50%"]);
234
+ /** Test fixtures speak in example names on purpose - `--ds-nope` inside a
235
+ * spec is the TEST, not drift. Every real project has these; accusing them
236
+ * buried the report's real findings under 99 phantom lines the first time
237
+ * the doctor ran on a repo with its own test suite (30/07). */
238
+ const IS_FIXTURE = /(\.(spec|test)\.[a-z]+$|__tests__\/|\/fixtures?\/)/;
234
239
  export function scanSource(file, source, table) {
240
+ if (IS_FIXTURE.test(file)) {
241
+ const real = scanCore(file, source, table);
242
+ const aside = real.findings.length +
243
+ (real.phantoms?.length ?? 0) +
244
+ (real.setAside?.reduce((n, a) => n + a.count, 0) ?? 0);
245
+ return {
246
+ file,
247
+ findings: [],
248
+ // Neither accused nor credited: fixture var() uses would inflate
249
+ // coverage exactly as unfairly as fixture hexes would deflate it.
250
+ tokenUses: 0,
251
+ ...(aside > 0
252
+ ? {
253
+ setAside: [
254
+ { reason: "test fixtures speak in example names", count: aside },
255
+ ],
256
+ }
257
+ : null),
258
+ };
259
+ }
260
+ return scanCore(file, source, table);
261
+ }
262
+ function scanCore(file, source, table) {
235
263
  const findings = [];
236
264
  const phantoms = [];
237
265
  let tokenUses = 0;
package/dist/index.js CHANGED
File without changes
package/dist/output.js CHANGED
@@ -10,7 +10,7 @@ const WIDTH = 66;
10
10
  export function section(title) {
11
11
  const head = `── ${title} `;
12
12
  const rest = Math.max(4, WIDTH - head.length);
13
- return `\n${head}${"─".repeat(rest)}\n`;
13
+ return `\n${head}${paint.faint("─".repeat(rest))}\n`;
14
14
  }
15
15
  /** Indented, copy-pasteable code block (empty lines stay truly empty). */
16
16
  export function snippet(lines) {
@@ -20,3 +20,27 @@ export function snippet(lines) {
20
20
  export function body(line) {
21
21
  return ` ${line}`;
22
22
  }
23
+ /**
24
+ * Colour, used the way the product uses motion: a quiet base and a few
25
+ * deliberate accents (dono, 30/07: "um cinza mais escuro e um mais claro,
26
+ * e o coverage com um azul bem discreto"). Two grays carry hierarchy -
27
+ * chrome/meta in the darker one, secondary prose in the lighter - and one
28
+ * muted steel blue marks what is ALIVE: the coverage bar, commands, links.
29
+ *
30
+ * Off out of respect wherever colour is not wanted: piped output (no TTY),
31
+ * NO_COLOR, or a dumb terminal - assertions in tests never see a code.
32
+ */
33
+ const COLOR_ON = Boolean(process.stdout.isTTY) &&
34
+ !process.env.NO_COLOR &&
35
+ process.env.TERM !== "dumb";
36
+ const wrap = (code) => (s) => COLOR_ON ? `\x1b[${code}m${s}\x1b[0m` : s;
37
+ export const paint = {
38
+ /** Lighter gray - secondary prose, explanations, counts. */
39
+ dim: wrap("38;5;246"),
40
+ /** Darker gray - chrome: rules, meters' empty track, set-aside notes. */
41
+ faint: wrap("38;5;240"),
42
+ /** Discreet steel blue - the coverage fill, commands, links. */
43
+ blue: wrap("38;5;67"),
44
+ /** Bold - the one or two numbers that carry the report. */
45
+ strong: wrap("1"),
46
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.33",
3
+ "version": "0.16.34",
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": {
@@ -29,7 +29,7 @@
29
29
  "node": ">=18"
30
30
  },
31
31
  "scripts": {
32
- "build": "tsc -p tsconfig.json",
32
+ "build": "tsc -p tsconfig.json && chmod +x dist/index.js",
33
33
  "dev": "tsx src/index.ts",
34
34
  "prepublishOnly": "npm run build",
35
35
  "test": "vitest run"