synthesisui 0.16.92 → 0.16.93

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,7 +4,7 @@ import { resolveAnatomy, resolveFlatParts, safePartName, } from "../anatomy-read
4
4
  import { readCredentials, readToken, resolveRegistry, sameRegistry, } from "../config.js";
5
5
  import { architectureRule, describeArchitecture, describeChoice, detectArchitectures, } from "../doctor/architecture.js";
6
6
  import { findBrokenRefs } from "../doctor/broken-refs.js";
7
- import { nestingRules, propRules, readDefinitionProps, readNesting, } from "../doctor/call-sites.js";
7
+ import { nestingRules, propRules, readDefinitionProps, readNesting, readRuntime, } from "../doctor/call-sites.js";
8
8
  import { asCatalogueTable, describeFallback, fetchCatalogue, } from "../doctor/catalogue-fetch.js";
9
9
  import { describeClassStyle, detectClassStyle, } from "../doctor/class-style.js";
10
10
  import { isNearDuplicate, isNeutral, lightness, } from "../doctor/color-distance.js";
@@ -293,6 +293,10 @@ export async function takeCensus(root, opts) {
293
293
  * filtering them takes the vitrine from 271 to 29 (dono, 01/08).
294
294
  */
295
295
  const screensOf = new Map();
296
+ /** Quem é dono do estado de cada componente, lido da definição. */
297
+ const runtimeOf = new Map();
298
+ /** Em quais projetos (--usage roots) cada componente aparece. */
299
+ const projectsOf = new Map();
296
300
  /** Component → the axes its own `cva` declares. See `variant-read.ts`. */
297
301
  const variantAxes = new Map();
298
302
  /** How much came out of CSS Modules, for the coverage report. */
@@ -376,7 +380,11 @@ export async function takeCensus(root, opts) {
376
380
  });
377
381
  }
378
382
  for (const d of found) {
379
- propsOf.set(d.name, readDefinitionProps(d.name, src));
383
+ const props = readDefinitionProps(d.name, src);
384
+ propsOf.set(d.name, props);
385
+ const runtime = readRuntime(props, src);
386
+ if (Object.keys(runtime).length > 0)
387
+ runtimeOf.set(d.name, runtime);
380
388
  }
381
389
  if (found.length > 0) {
382
390
  countShape(src, found[0].name, shapes);
@@ -598,6 +606,17 @@ export async function takeCensus(root, opts) {
598
606
  usageSeen.add(rel);
599
607
  usageProgress?.step(rel);
600
608
  scanComponentsInto(usageTally, rel, src, [...shared, ...uInternal]);
609
+ // WHERE THE PAIRS LIVE. A library composes almost none of itself inside
610
+ // itself - `Button` goes inside `Modal` in the APP, not in the library
611
+ // source - so measuring nesting only on the defined loop reported zero
612
+ // companions on a 128-component library (e2e, 01/08).
613
+ readNesting(rel, src, nesting);
614
+ // Which PROJECT composes it - the credibility panel's number, per root.
615
+ for (const m of src.matchAll(/<([A-Z][A-Za-z0-9_]*)/g)) {
616
+ const at = projectsOf.get(m[1]) ?? new Set();
617
+ at.add(u.label);
618
+ projectsOf.set(m[1], at);
619
+ }
601
620
  }
602
621
  }
603
622
  usageProgress?.done();
@@ -907,10 +926,30 @@ export async function takeCensus(root, opts) {
907
926
  for (const pair of nestingRules(nesting, systemOwns)) {
908
927
  laws.set(pair.component, [...(laws.get(pair.component) ?? []), pair.text]);
909
928
  }
929
+ /**
930
+ * OS ENCAIXES, ESTRUTURADOS. nestingRules já media os pares e virava só prosa -
931
+ * prosa não é clicável nem consultável (dono, 01/08). O slug segue a mesma
932
+ * kebab-ização que o contrato usa.
933
+ */
934
+ const companionsOf = new Map();
935
+ for (const pair of nestingRules(nesting, systemOwns)) {
936
+ const kebab = (n) => n.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
937
+ const list = companionsOf.get(pair.component) ?? [];
938
+ if (!list.includes(kebab(pair.child)))
939
+ list.push(kebab(pair.child));
940
+ companionsOf.set(pair.component, list);
941
+ }
910
942
  const components = merged.map((c) => ({
911
943
  ...c,
912
944
  ...(verdicts.get(c.name) ?? {}),
913
945
  ...(laws.has(c.name) ? { laws: laws.get(c.name) } : {}),
946
+ ...(runtimeOf.has(c.name) ? { runtime: runtimeOf.get(c.name) } : {}),
947
+ ...(companionsOf.has(c.name)
948
+ ? { companions: companionsOf.get(c.name) }
949
+ : {}),
950
+ ...(projectsOf.has(c.name)
951
+ ? { projects: [...(projectsOf.get(c.name) ?? [])] }
952
+ : {}),
914
953
  }));
915
954
  const pkgRaw = await readFile(join(root, "package.json"), "utf8").catch(() => null);
916
955
  let name = null;
@@ -1,5 +1,5 @@
1
- import { readFile } from "node:fs/promises";
2
- import { relative, resolve } from "node:path";
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import { join, relative, resolve } from "node:path";
3
3
  import { readToken, resolveRegistry } from "../config.js";
4
4
  import { fileRequest } from "../doctor/requests.js";
5
5
  import { diagnose, scanSource } from "../doctor/scan.js";
@@ -340,6 +340,54 @@ async function validateRecipe(name, recipe) {
340
340
  * a published package can be read by anyone, and it goes stale - the crosswalk's own copy
341
341
  * had 26 names against a catalogue of 41.
342
342
  */
343
+ /**
344
+ * O CONTADOR DO AGENTE, fire-and-forget: a tela do UI Kit mostra "vezes que esta receita
345
+ * foi consultada pelo Agente", e este é o único ponto onde a consulta acontece de
346
+ * verdade. Nunca bloqueia a resposta: sem sessão ou sem rede, não conta e não quebra -
347
+ * telemetria que atrasa a ferramenta é telemetria que alguém desliga.
348
+ */
349
+ function countAgentRead(root, component, action) {
350
+ void (async () => {
351
+ try {
352
+ const token = await readToken();
353
+ if (!token || !component)
354
+ return;
355
+ // O slug vem do .lock do sistema instalado - a mesma resolução do sync.
356
+ const dsDir = join(root, "_synthesisui", "ds");
357
+ let slug = null;
358
+ const entries = await readdir(dsDir, { withFileTypes: true }).catch(() => []);
359
+ for (const e of entries) {
360
+ if (!e.isDirectory())
361
+ continue;
362
+ const raw = await readFile(join(dsDir, e.name, ".lock"), "utf8").catch(() => "");
363
+ try {
364
+ const lock = JSON.parse(raw);
365
+ if (lock.slug) {
366
+ slug = lock.slug;
367
+ break;
368
+ }
369
+ }
370
+ catch {
371
+ // lock ilegível - tenta o próximo
372
+ }
373
+ }
374
+ if (!slug)
375
+ return;
376
+ const base = resolveRegistry();
377
+ await fetch(`${base}/api/telemetry/agent-read`, {
378
+ method: "POST",
379
+ headers: {
380
+ Authorization: `Bearer ${token}`,
381
+ "content-type": "application/json",
382
+ },
383
+ body: JSON.stringify({ slug, component, action }),
384
+ });
385
+ }
386
+ catch {
387
+ // offline não conta e não quebra nada
388
+ }
389
+ })();
390
+ }
343
391
  async function askCatalogue(want, post) {
344
392
  const token = await readToken();
345
393
  if (!token) {
@@ -399,6 +447,42 @@ async function describeComponent(root, name) {
399
447
  ? "A molecule in your own ladder - built out of your atoms, so reach for those inside it."
400
448
  : "An organism in your own ladder - a whole region of a screen, so it is more likely to be composed than to be composed into.");
401
449
  }
450
+ /**
451
+ * O ESTADO, ANTES DO CÓDIGO. Um agente que não sabe quem é dono do estado inventa:
452
+ * um Toggle controlado ganha useState interno, um Input não-controlado ganha value
453
+ * sem onChange (dono, 01/08). Isto é a receita dizendo o comportamento.
454
+ */
455
+ if (recipe.runtime) {
456
+ const r = recipe.runtime;
457
+ const lines = [];
458
+ if (r.stateOwner === "caller") {
459
+ lines.push("State: the CALLER owns it - controlled only. Wire the pair below; do not add internal state.");
460
+ }
461
+ else if (r.stateOwner === "self") {
462
+ lines.push("State: the component manages itself. Do not wire external state unless you need to read it.");
463
+ }
464
+ else if (r.stateOwner === "either") {
465
+ lines.push("State: both modes. Pass the controlled pair to own it, or the default* prop to seed it and let the component manage.");
466
+ }
467
+ for (const c of r.controlled ?? []) {
468
+ lines.push(` controlled: ${c.prop} + ${c.event}`);
469
+ }
470
+ if ((r.uncontrolled ?? []).length > 0) {
471
+ lines.push(` uncontrolled seed: ${(r.uncontrolled ?? []).join(", ")}`);
472
+ }
473
+ for (const n of r.notes ?? [])
474
+ lines.push(` ${n}`);
475
+ if (lines.length > 0)
476
+ out.push("", ...lines);
477
+ }
478
+ /** OS ENCAIXES - o que costuma ir junto, medido ou derivado dos layouts. */
479
+ if ((recipe.companions ?? []).length > 0) {
480
+ out.push("", `Goes with: ${(recipe.companions ?? []).join(", ")} - measured pairings; call describe_component on one before composing them together.`);
481
+ }
482
+ if (recipe.usageStats && recipe.usageStats.count > 0) {
483
+ const u = recipe.usageStats;
484
+ out.push("", `In use: ${u.count} instance${u.count === 1 ? "" : "s"} across ${u.files} file${u.files === 1 ? "" : "s"}${u.projects?.length ? ` in ${u.projects.join(", ")}` : ""}.`);
485
+ }
402
486
  // WHAT IT SITS ON. 17 of 23 components in a real library carry no surface of
403
487
  // their own, because the surface belongs to what they return.
404
488
  const p = recipe.preview;
@@ -492,8 +576,10 @@ async function callTool(root, name, args) {
492
576
  case "list_components":
493
577
  return text(await listComponents(root));
494
578
  case "describe_component":
579
+ countAgentRead(root, String(args.name ?? ""), "describe");
495
580
  return text(await describeComponent(root, String(args.name ?? "")));
496
581
  case "add_component":
582
+ countAgentRead(root, String(args.name ?? ""), "add");
497
583
  return text(await addComponent(root, String(args.name ?? "")));
498
584
  case "recipe_vocabulary":
499
585
  return text(await recipeVocabulary());
@@ -37,7 +37,7 @@
37
37
  * version tried to consume the arrow, and `([^)]*)` swallowed the destructure it was
38
38
  * trying to reach, so every arrow component read as declaring no props (spec, 01/08).
39
39
  */
40
- const DESTRUCTURED = /(?:function\s+([A-Z][A-Za-z0-9_]*)\s*\(\s*\{([^}]*)\}|(?:const|let)\s+([A-Z][A-Za-z0-9_]*)\s*(?::[^=]*)?=\s*(?:function\s*)?\(\s*\{([^}]*)\})/g;
40
+ const DESTRUCTURED = /(?:function\s+([A-Z][A-Za-z0-9_]*)\s*\(\s*\{([^}]*)\}|(?:const|let)\s+([A-Z][A-Za-z0-9_]*)\s*(?::[^=]*)?=\s*(?:function\s*)?\(\s*\{([^}]*)\}|(?:const|let)\s+([A-Z][A-Za-z0-9_]*)\s*=\s*(?:React\.)?(?:forwardRef|memo)(?:<[^>]*>)?\s*\(\s*(?:function\s*[A-Za-z0-9_]*\s*)?\(\s*\{([^}]*)\})/g;
41
41
  /** `{...rest}` / `{...props}` on a JSX element - the forwarding, in the markup. */
42
42
  const SPREAD_ONTO = /\{\s*\.\.\.\s*([A-Za-z_$][\w$]*)\s*\}/g;
43
43
  /**
@@ -62,13 +62,16 @@ export function readDefinitionProps(name, source) {
62
62
  */
63
63
  DESTRUCTURED.lastIndex = 0;
64
64
  const matches = [...source.matchAll(DESTRUCTURED)];
65
- const exact = matches.filter((m) => (m[1] ?? m[3]) === name);
65
+ // The third alternative is the forwardRef-inline idiom: a real Toggle is
66
+ // `const Toggle = React.forwardRef<…>(({ checked, onCheckedChange, … }, ref) =>`,
67
+ // and both earlier shapes read past it (test16, 01/08).
68
+ const exact = matches.filter((m) => (m[1] ?? m[3] ?? m[5]) === name);
66
69
  const prefixed = matches.filter((m) => {
67
- const found = m[1] ?? m[3];
70
+ const found = m[1] ?? m[3] ?? m[5];
68
71
  return found !== name && found?.startsWith(name);
69
72
  });
70
73
  for (const m of exact.length > 0 ? exact : prefixed) {
71
- const body = m[2] ?? m[4] ?? "";
74
+ const body = m[2] ?? m[4] ?? m[6] ?? "";
72
75
  for (const raw of body.split(",")) {
73
76
  const part = raw.trim();
74
77
  if (!part)
@@ -187,3 +190,48 @@ export function propRules(name, read) {
187
190
  }
188
191
  return out;
189
192
  }
193
+ /**
194
+ * WHO OWNS THE STATE - read off the definition, deterministically.
195
+ *
196
+ * The recipe said everything about the LOOK and nothing about behaviour, so an agent
197
+ * writing code in the client invented it: a controlled Toggle grew an internal
198
+ * useState, an uncontrolled Input got a value with no onChange (dono, 01/08). The
199
+ * patterns are machine-readable in the props themselves:
200
+ *
201
+ * checked + onChange the CONTROLLED pair - the caller owns the state
202
+ * defaultOpen, defaultValue the UNCONTROLLED seed - the component owns it
203
+ * useState in the body the component manages something itself
204
+ *
205
+ * `either` is both at once, which is exactly how a well-made input is written.
206
+ */
207
+ export function readRuntime(read, source) {
208
+ const props = new Set(read.explicit);
209
+ const controlled = [];
210
+ for (const prop of read.explicit) {
211
+ if (/^(on[A-Z]|default[A-Z])/.test(prop))
212
+ continue;
213
+ const cap = prop[0].toUpperCase() + prop.slice(1);
214
+ // `checked`+`onChange` is the DOM's own spelling; `open`+`onOpenChange` is the
215
+ // primitive libraries'. Both are the same claim.
216
+ const event = [`on${cap}Change`, "onChange", `on${cap}`].find((e) => props.has(e));
217
+ if (event)
218
+ controlled.push({ prop, event });
219
+ }
220
+ const uncontrolled = read.explicit.filter((p) => /^default[A-Z]/.test(p));
221
+ const managesItself = /\buseState\s*[<(]/.test(source);
222
+ const out = {};
223
+ if (controlled.length > 0)
224
+ out.controlled = controlled;
225
+ if (uncontrolled.length > 0)
226
+ out.uncontrolled = uncontrolled;
227
+ if (controlled.length > 0 && (uncontrolled.length > 0 || managesItself)) {
228
+ out.stateOwner = "either";
229
+ }
230
+ else if (controlled.length > 0) {
231
+ out.stateOwner = "caller";
232
+ }
233
+ else if (managesItself || uncontrolled.length > 0) {
234
+ out.stateOwner = "self";
235
+ }
236
+ return out;
237
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.92",
3
+ "version": "0.16.93",
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": {