synthesisui 0.16.77 → 0.16.79

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.
@@ -1,5 +1,6 @@
1
1
  import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { basename, dirname, join, relative } from "node:path";
3
+ import { resolveAnatomy, resolveFlatParts, safePartName, } from "../anatomy-read.js";
3
4
  import { readCredentials, readToken, resolveRegistry, sameRegistry, } from "../config.js";
4
5
  import { findBrokenRefs } from "../doctor/broken-refs.js";
5
6
  import { describeClassStyle, detectClassStyle, } from "../doctor/class-style.js";
@@ -11,9 +12,9 @@ import { describeConvention, describeRemainder, detectConventions, } from "../do
11
12
  import { diagnose, scanSource } from "../doctor/scan.js";
12
13
  import { parseSchemeBlocks } from "../doctor/scheme-blocks.js";
13
14
  import { buildTable } from "../doctor/tokens.js";
14
- import { rootClasses, rootTag, transcribe, transcribeParts, } from "../doctor/transcribe.js";
15
+ import { rootClasses, rootTag, transcribe, } from "../doctor/transcribe.js";
15
16
  import { body, paint, section } from "../output.js";
16
- import { detectStack } from "../stack.js";
17
+ import { detectStack, resolveDeps } from "../stack.js";
17
18
  import { walk, walkAll } from "./doctor.js";
18
19
  /**
19
20
  * How many distinct values travel, PER KIND.
@@ -896,28 +897,79 @@ function sayReach(c) {
896
897
  }
897
898
  }
898
899
  /**
899
- * Fold the skill's named parts into the transcription, in place.
900
+ * Fold the skill's anatomy into the transcription, in place.
901
+ *
902
+ * A CLEAN SPLIT OF WHO KNOWS WHAT. Only the skill can say that a span holds the
903
+ * title and that the thing in the middle is their own `TextEditor`; only this
904
+ * side can say that `text-ocean-500` is `{color.ocean.500}` and that
905
+ * `@tiptap/react` is pinned at `^2.1.0` in their manifest. The platform then
906
+ * turns declarations into roles, because only it has the two palettes.
900
907
  *
901
908
  * Their declared tokens come from the census itself, so a run that only sends a
902
909
  * file it was handed still resolves a `bg-ocean-500` by their own name.
903
910
  */
904
- function resolveReadParts(census) {
911
+ async function resolveReadParts(census, root) {
905
912
  const read = census.reading?.components;
906
913
  if (!read)
907
914
  return;
908
915
  const declared = new Map(Object.entries(census.declared));
909
- const looks = census.looks ?? (census.looks = {});
916
+ // The manifest is read ONCE for the whole run: the version belongs in a rule's
917
+ // EVIDENCE, never in its text, so the rule does not go stale on an upgrade and
918
+ // the number is not lost either.
919
+ const deps = await resolveDeps(root).catch(() => ({}));
920
+ census.looks ??= {};
921
+ const looks = census.looks;
910
922
  let named = 0;
923
+ let shaped = 0;
924
+ let edges = 0;
925
+ const libs = new Set();
926
+ const notes = new Set();
927
+ /**
928
+ * WHAT A NAME IN THEIR CODE REACHES IN THE SYSTEM - from the crosswalk.
929
+ *
930
+ * The census already decided this for every component it read: `Tag` is
931
+ * `nearly`, canonical `badge`; `ToolbarButton` is `nearly`, canonical `button`;
932
+ * `Card` `exists` as `card`. The first version of the frontier kebab-cased the
933
+ * name and looked for `ds-tag`, which does not exist - so a component whose
934
+ * recipe was sitting right there drew as a grey chip (dono, 01/08).
935
+ */
936
+ const crosswalked = new Map();
937
+ for (const c of census.components ?? []) {
938
+ if (c.from)
939
+ continue; // a package's component is not one of theirs
940
+ const target = c.canonical ?? (c.bucket === "exclusive" ? c.name : null);
941
+ if (target)
942
+ crosswalked.set(c.name, safePartName(target));
943
+ }
944
+ const resolveRef = (name) => crosswalked.get(name) ?? null;
911
945
  for (const [component, entry] of Object.entries(read)) {
912
- const parts = entry?.parts;
913
- if (!Array.isArray(parts) || parts.length === 0)
946
+ const anatomy = entry?.anatomy;
947
+ const resolved = Array.isArray(anatomy) && anatomy.length > 0
948
+ ? resolveAnatomy(anatomy, declared, deps, resolveRef, entry?.root)
949
+ : Array.isArray(entry?.parts) && entry.parts.length > 0
950
+ ? resolveFlatParts(entry.parts, declared)
951
+ : null;
952
+ if (!resolved)
914
953
  continue;
915
- const resolved = transcribeParts(parts, declared);
916
- if (Object.keys(resolved).length === 0)
954
+ const hasParts = Object.keys(resolved.parts).length > 0;
955
+ if (!hasParts && resolved.tree.length === 0)
917
956
  continue;
918
957
  const look = looks[component];
958
+ const carried = {
959
+ ...(hasParts ? { parts: resolved.parts } : {}),
960
+ ...(resolved.tree.length > 0 ? { tree: resolved.tree } : {}),
961
+ ...(resolved.composes.length > 0 ? { composes: resolved.composes } : {}),
962
+ ...(resolved.external.length > 0 ? { external: resolved.external } : {}),
963
+ ...(resolved.root ? { root: resolved.root } : {}),
964
+ };
919
965
  looks[component] = look
920
- ? { ...look, parts: { ...(look.parts ?? {}), ...resolved } }
966
+ ? {
967
+ ...look,
968
+ ...carried,
969
+ ...(hasParts
970
+ ? { parts: { ...(look.parts ?? {}), ...resolved.parts } }
971
+ : {}),
972
+ }
921
973
  : {
922
974
  base: {},
923
975
  dark: {},
@@ -926,14 +978,37 @@ function resolveReadParts(census) {
926
978
  literals: [],
927
979
  fromToken: 0,
928
980
  fromLiteral: 0,
929
- parts: resolved,
981
+ ...carried,
930
982
  };
931
- named += Object.keys(resolved).length;
983
+ named += Object.keys(resolved.parts).length;
984
+ if (resolved.tree.length > 0)
985
+ shaped += 1;
986
+ edges += resolved.composes.length;
987
+ for (const e of resolved.external)
988
+ libs.add(e.from);
989
+ for (const n of resolved.notes)
990
+ notes.add(n);
932
991
  }
933
- if (named > 0) {
992
+ const total = Object.keys(read).length;
993
+ if (named > 0 || shaped > 0) {
934
994
  console.log("");
935
- console.log(body(`${named} part${named === 1 ? "" : "s"} your reading named across ${Object.keys(read).length} component${Object.keys(read).length === 1 ? "" : "s"} - each previews as what it IS rather than as a text box`));
995
+ console.log(body(`${named} part${named === 1 ? "" : "s"} your reading named across ${total} component${total === 1 ? "" : "s"} - each previews as what it IS rather than as a text box`));
996
+ }
997
+ if (shaped > 0) {
998
+ console.log(body(`${shaped} of them arrived with a SHAPE - the parts nested as they nest in your code, so the preview is the component and not a row of labels`));
999
+ }
1000
+ // A frontier is worth its own line, because it is the half nobody else has:
1001
+ // the edge is a rule an agent can obey, not a picture.
1002
+ if (edges > 0) {
1003
+ console.log(body(`${edges} place${edges === 1 ? "" : "s"} where one of your components is built out of another - ${edges === 1 ? "it becomes a rule" : "each becomes a rule"} about the two of them, not a guess`));
1004
+ }
1005
+ if (libs.size > 0) {
1006
+ const named_ = [...libs].slice(0, 3).join(", ");
1007
+ console.log(body(`${libs.size} librar${libs.size === 1 ? "y" : "ies"} your components need (${named_}) - ${libs.size === 1 ? "it does" : "they do"} not render here, and what to know about ${libs.size === 1 ? "it" : "them"} travels as rules`));
936
1008
  }
1009
+ // NO SILENT FIX. A correction made quietly is a lie about what they wrote.
1010
+ for (const n of notes)
1011
+ console.log(body(n));
937
1012
  }
938
1013
  export async function runImport(opts) {
939
1014
  const root = opts.root ?? process.cwd();
@@ -981,7 +1056,7 @@ export async function runImport(opts) {
981
1056
  * Resolved here rather than at measure time because the reading arrives
982
1057
  * AFTER the census was taken - this is the first moment both halves exist.
983
1058
  */
984
- resolveReadParts(census);
1059
+ await resolveReadParts(census, root);
985
1060
  }
986
1061
  else {
987
1062
  console.log(section("Reading your project"));
@@ -65,6 +65,20 @@ const TOOLS = [
65
65
  description: "The components this design system defines, with what each is for. Look here BEFORE writing any UI element from scratch - if something covers the purpose, materialize it with add_component instead.",
66
66
  inputSchema: { type: "object", properties: {} },
67
67
  },
68
+ {
69
+ name: "describe_component",
70
+ description: "Everything the system knows about ONE component: what it is made of, what it composes, WHICH LIBRARIES IT NEEDS, and the rules that govern it. Call this before writing code that uses a component - `list_components` gives you a name and a sentence, and a name does not tell you that the editor needs @tiptap/react or that a card is built out of a metric card.",
71
+ inputSchema: {
72
+ type: "object",
73
+ properties: {
74
+ name: {
75
+ type: "string",
76
+ description: "Component name from list_components.",
77
+ },
78
+ },
79
+ required: ["name"],
80
+ },
81
+ },
68
82
  {
69
83
  name: "add_component",
70
84
  description: "Materialize a component from the design system as real typed code in this project, ready to import and extend. Use it yourself - the person who asked for a feature should never have to know component names.",
@@ -191,6 +205,109 @@ async function listComponents(root) {
191
205
  ...rows.sort(),
192
206
  ].join("\n");
193
207
  }
208
+ /**
209
+ * ONE COMPONENT, IN FULL - the round trip that did not exist.
210
+ *
211
+ * `list_components` returns a name and a description, so an agent building with
212
+ * `ds-text-editor` had no way to know it needs `@tiptap/react` before writing the
213
+ * first line. The anatomy, the composition chain and the required libraries are all
214
+ * in the installed contract; nothing was asking for them.
215
+ *
216
+ * It is also the confirmation the skill never had. After an import, reading this
217
+ * back says what the platform UNDERSTOOD - so a reading that came out thin is
218
+ * visible instead of being discovered later in a preview.
219
+ */
220
+ async function describeComponent(root, name) {
221
+ const { documents, requires } = await loadSystem(root);
222
+ let recipe;
223
+ for (const doc of documents) {
224
+ const d = doc;
225
+ recipe = d.components?.[name] ?? d.blocks?.[name];
226
+ if (recipe)
227
+ break;
228
+ }
229
+ if (!recipe) {
230
+ return `This system has no component called "${name}". Run list_components to see what it does have - and if nothing covers what you need, file request_component rather than writing one from scratch.`;
231
+ }
232
+ const out = [
233
+ `${name}${recipe.description ? ` - ${recipe.description}` : ""}`,
234
+ ];
235
+ // WHAT IT SITS ON. 17 of 23 components in a real library carry no surface of
236
+ // their own, because the surface belongs to what they return.
237
+ const p = recipe.preview;
238
+ if (p?.rootRef) {
239
+ out.push("", `Sits on: ${p.rootRef}${p.rootName && p.rootName !== p.rootRef ? ` (your ${p.rootName})` : ""} - its background, border and radius come from there, not from this component.`);
240
+ }
241
+ else if (p?.rootFrom) {
242
+ out.push("", `Sits on: ${p.rootFrom} - a third-party root. Its markup and behaviour are that library's.`);
243
+ }
244
+ // WHAT IT IS MADE OF, and where it ends.
245
+ const composes = [];
246
+ const libs = [];
247
+ const shape = [];
248
+ const walk = (nodes, depth) => {
249
+ for (const raw of nodes) {
250
+ const n = raw;
251
+ const pad = " ".repeat(depth + 1);
252
+ if (n.as === "component" && n.ref) {
253
+ shape.push(`${pad}<${n.ref}>${n.refName && n.refName !== n.ref ? ` (your ${n.refName})` : ""}`);
254
+ if (!composes.includes(n.ref))
255
+ composes.push(n.ref);
256
+ }
257
+ else if (n.as === "external" && n.from) {
258
+ shape.push(`${pad}${n.from} (third party - not ours to render)`);
259
+ if (!libs.includes(n.from))
260
+ libs.push(n.from);
261
+ }
262
+ else {
263
+ shape.push(`${pad}${n.as}${n.part ? ` .${name}-${n.part}` : ""}`);
264
+ }
265
+ if (Array.isArray(n.children))
266
+ walk(n.children, depth + 1);
267
+ }
268
+ };
269
+ if (Array.isArray(p?.parts) && p.parts.length > 0) {
270
+ walk(p.parts, 0);
271
+ out.push("", "Made of:", ...shape);
272
+ }
273
+ else if (recipe.parts && Object.keys(recipe.parts).length > 0) {
274
+ out.push("", `Parts: ${Object.keys(recipe.parts).join(", ")} - compose them inside it.`);
275
+ }
276
+ if (composes.length > 0) {
277
+ out.push("", `Built out of: ${composes.join(", ")}. Change one of those in one place rather than reproducing it here, and call describe_component on it before you do.`);
278
+ }
279
+ /**
280
+ * WHAT IT NEEDS INSTALLED - the whole reason this tool earns its place.
281
+ *
282
+ * From `requires.json`, filtered by the stack at install time, so the answer is
283
+ * about THIS project. Never installs: a dependency has a licence, a bundle cost
284
+ * and a maintainer attached, so the agent reports and the person decides.
285
+ */
286
+ const needed = requires.filter((r) => (r.applies ?? []).includes(name));
287
+ if (needed.length > 0 || libs.length > 0) {
288
+ const named = [
289
+ ...new Set([...needed.map((r) => r.requires), ...libs]),
290
+ ];
291
+ out.push("", `Needs installed: ${named.join(", ")}.`, "Check the manifest before you write against it. If it is missing, say so and ASK - do not install it yourself.");
292
+ for (const r of needed) {
293
+ if (r.pinned)
294
+ out.push(` ${r.requires} ${r.pinned}`);
295
+ }
296
+ }
297
+ // THE RULES, scoped. A relation names both, which is what an agent needs in
298
+ // order not to assemble it wrongly.
299
+ const laws = [...(recipe.usage ?? [])];
300
+ if (laws.length > 0) {
301
+ out.push("", "Rules for this component:", ...laws.map((l) => ` ${l}`));
302
+ }
303
+ if (needed.length > 0) {
304
+ out.push(...needed.map((r) => ` ${r.text}`));
305
+ }
306
+ if (laws.length === 0 && needed.length === 0) {
307
+ out.push("", "No rules govern this component yet. Build with it, and if you find yourself working around it, file request_component with the reasoning.");
308
+ }
309
+ return out.join("\n");
310
+ }
194
311
  async function addComponent(root, name) {
195
312
  const { table } = await loadSystem(root);
196
313
  if (!table.slug)
@@ -207,6 +324,8 @@ async function callTool(root, name, args) {
207
324
  return text(await findToken(root, String(args.value ?? "")));
208
325
  case "list_components":
209
326
  return text(await listComponents(root));
327
+ case "describe_component":
328
+ return text(await describeComponent(root, String(args.name ?? "")));
210
329
  case "add_component":
211
330
  return text(await addComponent(root, String(args.name ?? "")));
212
331
  case "request_component": {
@@ -3,6 +3,7 @@ import { basename, join } from "node:path";
3
3
  import { generateComponentFiles } from "../component-codegen.js";
4
4
  import { readProjectConfig, resolveRegistry } from "../config.js";
5
5
  import { body, section, snippet } from "../output.js";
6
+ import { reactMajorOf, readInstalledConvention } from "../project-facts.js";
6
7
  import { fetchComponent, postRefit, postSaveComponent, RegistryError, } from "../registry.js";
7
8
  /** Slugs INSTALLED under `_synthesisui/ds/` (a `.lock` marks a real install -
8
9
  * a folder holding only refit artifacts doesn't count). */
@@ -132,7 +133,7 @@ export async function refit(file, opts) {
132
133
  if (config.target === "next") {
133
134
  const compDir = join(root, config.componentsDir, res.name);
134
135
  await mkdir(compDir, { recursive: true });
135
- const files = generateComponentFiles(slug, res.name, res.recipe, res.css, saved.version, config.styles);
136
+ const files = generateComponentFiles(slug, res.name, res.recipe, res.css, saved.version, config.styles, await reactMajorOf(root), await readInstalledConvention(root, slug));
136
137
  for (const f of files) {
137
138
  await writeFile(join(compDir, f.filename), f.code, "utf8");
138
139
  }
@@ -4,6 +4,7 @@ import { generateComponentFiles } from "../component-codegen.js";
4
4
  import { readProjectConfig, resolveRegistry } from "../config.js";
5
5
  import { diffLocalDocuments, localChangelogMarkdown, } from "../document-diff.js";
6
6
  import { body, section, snippet } from "../output.js";
7
+ import { reactMajorOf, readInstalledConvention } from "../project-facts.js";
7
8
  import { fetchChangelog, fetchComponent, fetchDesignSystem, RegistryError, } from "../registry.js";
8
9
  import { add } from "./add.js";
9
10
  /**
@@ -151,7 +152,11 @@ export async function upgrade(slug, opts) {
151
152
  continue;
152
153
  try {
153
154
  const res = await fetchComponent(base, slug, entry);
154
- const files = generateComponentFiles(slug, res.name, res.recipe, res.css, res.version, config.styles);
155
+ const files = generateComponentFiles(slug, res.name, res.recipe, res.css, res.version, config.styles,
156
+ // Both were missing here, and `upgrade` is the command that REWRITES
157
+ // components somebody already has: without the convention it would have
158
+ // taken a working component and stripped its styles.
159
+ await reactMajorOf(root), res.classNames ?? (await readInstalledConvention(root, slug)));
155
160
  for (const file of files) {
156
161
  await writeFile(join(componentsRoot, entry, file.filename), file.code, "utf8");
157
162
  }