synthesisui 0.16.95 → 0.16.96

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.
@@ -100,7 +100,10 @@ export function resolveAnatomy(read, declared, deps,
100
100
  /** Their name → the slug it reaches in this system. See `RefResolver`. */
101
101
  resolve,
102
102
  /** What the component returns, when the skill said and it is not a plain tag. */
103
- root) {
103
+ root,
104
+ /** This component's sketch, so a node can name its classes by INDEX instead
105
+ * of retyping them - see `AnatomyRead.at`. */
106
+ sketch) {
104
107
  const parts = {};
105
108
  const composes = [];
106
109
  const external = [];
@@ -189,9 +192,16 @@ root) {
189
192
  if (key !== wanted) {
190
193
  notes.push(`two parts named \`${wanted}\` - the second is \`${key}\``);
191
194
  }
192
- const classes = typeof node.classes === "string"
193
- ? node.classes.split(/\s+/).filter(Boolean)
194
- : [];
195
+ /**
196
+ * THE CENSUS'S OWN STRING WINS when the node points at it. A retyped
197
+ * list is a copy that can lose a modifier; an index cannot.
198
+ */
199
+ const raw = typeof node.classes === "string"
200
+ ? node.classes
201
+ : typeof node.at === "number"
202
+ ? (sketch?.[node.at]?.classes ?? "")
203
+ : "";
204
+ const classes = raw.split(/\s+/).filter(Boolean);
195
205
  const t = transcribe(classes, declared);
196
206
  parts[key] = { base: t.base, dark: t.dark, states: t.states };
197
207
  /**
@@ -1656,7 +1656,10 @@ async function resolveReadParts(census, root) {
1656
1656
  for (const [component, entry] of Object.entries(read)) {
1657
1657
  const anatomy = entry?.anatomy;
1658
1658
  const resolved = Array.isArray(anatomy) && anatomy.length > 0
1659
- ? resolveAnatomy(anatomy, declared, deps, resolveRef, entry?.root)
1659
+ ? resolveAnatomy(anatomy, declared, deps, resolveRef, entry?.root,
1660
+ // The component's own sketch, so a node naming itself by index reads
1661
+ // the exact class string the census measured (dono, 01/08).
1662
+ looks[component]?.sketch)
1660
1663
  : Array.isArray(entry?.parts) && entry.parts.length > 0
1661
1664
  ? resolveFlatParts(entry.parts, declared)
1662
1665
  : null;
@@ -7,6 +7,7 @@ import { body, section, snippet } from "../output.js";
7
7
  import { reactMajorOf, readInstalledConvention } from "../project-facts.js";
8
8
  import { fetchChangelog, fetchComponent, fetchDesignSystem, RegistryError, } from "../registry.js";
9
9
  import { add } from "./add.js";
10
+ import { doctor } from "./doctor.js";
10
11
  /**
11
12
  * The highest `v<n>` below `installed` among the folder names given, or the one
12
13
  * asked for. Pure, so the version arithmetic can be tested without a disk.
@@ -226,6 +227,26 @@ export async function upgrade(slug, opts) {
226
227
  console.log("");
227
228
  console.log(body(`⚠ Could not regenerate: ${failed.join(", ")} (removed/renamed in v${latest.version}?)`));
228
229
  }
230
+ /**
231
+ * THE CHECK RUNS ITSELF - closing the loop the owner asked for end to end
232
+ * (dono, 01/08).
233
+ *
234
+ * An upgrade that lands a v2 whose whole point is "these literals are tokens
235
+ * now" and then says "ask your agent to migrate" leaves the person with no
236
+ * way to know whether it worked. The doctor is the answer and it is one
237
+ * command, so it is not somebody's job to remember: it runs here, over the
238
+ * same root, and its report IS the receipt.
239
+ *
240
+ * Never strict. A finding is information, not a failure of the upgrade, and
241
+ * exiting non-zero on somebody's own drift would turn a successful install
242
+ * into a red terminal.
243
+ */
244
+ console.log(section("Checking the app against v" + latest.version));
245
+ await doctor({ dir: root }).catch((err) => {
246
+ // A doctor that cannot run is not an upgrade that failed - the artifacts
247
+ // are already on disk and correct.
248
+ console.log(body(`The check could not run (${err instanceof Error ? err.message : "unknown"}). Run \`npx synthesisui doctor\` when you can.`));
249
+ });
229
250
  console.log(section("Migrate the app"));
230
251
  console.log(body(`The migration brief is at _synthesisui/ds/${slug}/UPGRADE.md`));
231
252
  console.log("");
@@ -373,14 +373,14 @@ Open the project and answer the questions below. Then add a \`reading\` object t
373
373
  "components": {
374
374
  "MetricCard": {
375
375
  "anatomy": [
376
- { "as": "icon", "name": "icon", "classes": "size-8 text-ocean-500" },
376
+ { "as": "icon", "name": "icon", "at": 1 },
377
377
  {
378
378
  "as": "stack",
379
379
  "name": "body",
380
- "classes": "flex flex-col gap-1",
380
+ "at": 2,
381
381
  "children": [
382
- { "as": "text", "name": "label", "classes": "text-xs uppercase text-lightgray-500" },
383
- { "as": "heading", "name": "value", "classes": "text-2xl font-bold" }
382
+ { "as": "text", "name": "label", "at": 3 },
383
+ { "as": "heading", "name": "value", "at": 4 }
384
384
  ]
385
385
  }
386
386
  ]
@@ -388,15 +388,15 @@ Open the project and answer the questions below. Then add a \`reading\` object t
388
388
  "ArticleCard": {
389
389
  "root": "Card",
390
390
  "anatomy": [
391
- { "as": "image", "name": "cover", "classes": "aspect-video w-full rounded-t-lg" },
392
- { "as": "heading", "name": "title", "classes": "text-lg font-semibold" },
391
+ { "as": "image", "name": "cover", "at": 1 },
392
+ { "as": "heading", "name": "title", "at": 2 },
393
393
  { "as": "component", "ref": "TextEditor" },
394
394
  {
395
395
  "as": "row",
396
396
  "name": "footer",
397
- "classes": "flex items-center gap-2 border-t p-3",
397
+ "at": 3,
398
398
  "children": [
399
- { "as": "button", "name": "publish", "classes": "btn btn-primary" },
399
+ { "as": "button", "name": "publish", "at": 4 },
400
400
  { "as": "button", "name": "discard", "classes": "btn btn-ghost" }
401
401
  ]
402
402
  }
@@ -902,9 +902,22 @@ file for, minus the one thing that is genuinely yours: the NAMES.
902
902
 
903
903
  Read that and write the anatomy: depth 1 is \`trigger\`, the dot is \`status-dot\`, the
904
904
  capitalised tag is a frontier. **Do not open the component file** - if the sketch is
905
- missing or visibly truncated (60-node cap), say so in not-expressed.md and only then
905
+ missing or visibly truncated (150-node cap), say so in not-expressed.md and only then
906
906
  read, because that gap is the census's to close.
907
907
 
908
+ **NAME THE NODE BY ITS INDEX - never retype the class string.** Each anatomy node carries
909
+ \`"at": <index into this sketch>\`, and the CLI reads the exact string the census measured:
910
+
911
+ \`\`\`json
912
+ { "as": "button", "name": "trigger", "at": 1 }
913
+ \`\`\`
914
+
915
+ Retyping is where a modifier goes missing. Six real components arrived with \`hover:\` and
916
+ \`focus:\` in their source and no state at all in the recipe, and the parser was not the
917
+ problem - the copy was (audit, dono, 01/08). An index cannot lose a class. \`classes\` still
918
+ works and still wins when you send both, which is for the one node the sketch could not
919
+ reach; every other node uses \`at\`.
920
+
908
921
  ### What the CLI now reads for you, so do not spend a turn on it
909
922
 
910
923
  **The rule under all of this: you are given EVIDENCE and asked to DECIDE.** You are never
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synthesisui",
3
- "version": "0.16.95",
3
+ "version": "0.16.96",
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": {